@kasufinance/kasu-sdk 2.6.0 → 2.7.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 +128 -3
- package/dist/bundle.cjs.js +1119 -254
- package/dist/bundle.esm.js +1109 -255
- package/dist/domain/index.d.ts +3 -2
- package/dist/domain/index.js +2 -2
- package/dist/domain/index.js.map +1 -1
- package/dist/domain/requests.d.ts +42 -4
- package/dist/domain/requests.js +20 -4
- package/dist/domain/requests.js.map +1 -1
- package/dist/domain/wallet-errors.d.ts +63 -8
- package/dist/domain/wallet-errors.js +115 -16
- package/dist/domain/wallet-errors.js.map +1 -1
- package/dist/facade/deposits.js +2 -1
- package/dist/facade/deposits.js.map +1 -1
- package/dist/facade/flows.d.ts +95 -0
- package/dist/facade/flows.js +116 -0
- package/dist/facade/flows.js.map +1 -0
- package/dist/facade/index.d.ts +3 -0
- package/dist/facade/index.js +3 -0
- package/dist/facade/index.js.map +1 -1
- package/dist/facade/kasu.d.ts +6 -2
- package/dist/facade/kasu.js +28 -3
- package/dist/facade/kasu.js.map +1 -1
- package/dist/facade/read-only.d.ts +12 -0
- package/dist/facade/read-only.js +13 -0
- package/dist/facade/read-only.js.map +1 -0
- package/dist/flows/deposit-flow.d.ts +301 -0
- package/dist/flows/deposit-flow.js +358 -0
- package/dist/flows/deposit-flow.js.map +1 -0
- package/dist/flows/flow.d.ts +72 -0
- package/dist/flows/flow.js +110 -0
- package/dist/flows/flow.js.map +1 -0
- package/dist/flows/index.d.ts +19 -0
- package/dist/flows/index.js +17 -0
- package/dist/flows/index.js.map +1 -0
- package/dist/flows/observable.d.ts +59 -0
- package/dist/flows/observable.js +81 -0
- package/dist/flows/observable.js.map +1 -0
- package/dist/flows/withdraw-flow.d.ts +99 -0
- package/dist/flows/withdraw-flow.js +83 -0
- package/dist/flows/withdraw-flow.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/services/DataService/data-service.js +0 -1
- package/dist/services/DataService/data-service.js.map +1 -1
- package/package.json +8 -2
- package/src/domain/index.ts +7 -1
- package/src/domain/requests.ts +57 -4
- package/src/domain/wallet-errors.ts +135 -18
- package/src/facade/deposits.ts +2 -3
- package/src/facade/flows.ts +172 -0
- package/src/facade/index.ts +8 -0
- package/src/facade/kasu.ts +43 -3
- package/src/facade/read-only.ts +13 -0
- package/src/flows/deposit-flow.ts +775 -0
- package/src/flows/flow.ts +108 -0
- package/src/flows/index.ts +45 -0
- package/src/flows/observable.ts +97 -0
- package/src/flows/withdraw-flow.ts +210 -0
- package/src/index.ts +7 -0
- package/src/services/DataService/data-service.ts +0 -1
- package/dist/domain/au-minimum.test.d.ts +0 -1
- package/dist/domain/au-minimum.test.js +0 -202
- package/dist/domain/au-minimum.test.js.map +0 -1
- package/dist/domain/deposit-bounds.test.d.ts +0 -1
- package/dist/domain/deposit-bounds.test.js +0 -135
- package/dist/domain/deposit-bounds.test.js.map +0 -1
- package/dist/domain/loan-contract.test.d.ts +0 -1
- package/dist/domain/loan-contract.test.js +0 -255
- package/dist/domain/loan-contract.test.js.map +0 -1
- package/dist/domain/partners.test.d.ts +0 -1
- package/dist/domain/partners.test.js +0 -53
- package/dist/domain/partners.test.js.map +0 -1
- package/dist/domain/pools.test.d.ts +0 -1
- package/dist/domain/pools.test.js +0 -184
- package/dist/domain/pools.test.js.map +0 -1
- package/dist/domain/rates.test.d.ts +0 -1
- package/dist/domain/rates.test.js +0 -181
- package/dist/domain/rates.test.js.map +0 -1
- package/dist/domain/requests.test.d.ts +0 -1
- package/dist/domain/requests.test.js +0 -470
- package/dist/domain/requests.test.js.map +0 -1
- package/dist/domain/settlement.test.d.ts +0 -1
- package/dist/domain/settlement.test.js +0 -152
- package/dist/domain/settlement.test.js.map +0 -1
- package/dist/domain/tranche-display-name.test.d.ts +0 -1
- package/dist/domain/tranche-display-name.test.js +0 -58
- package/dist/domain/tranche-display-name.test.js.map +0 -1
- package/dist/domain/tranches.test.d.ts +0 -1
- package/dist/domain/tranches.test.js +0 -206
- package/dist/domain/tranches.test.js.map +0 -1
- package/dist/domain/wallet-errors.test.d.ts +0 -1
- package/dist/domain/wallet-errors.test.js +0 -71
- package/dist/domain/wallet-errors.test.js.map +0 -1
- package/dist/facade/config.test.d.ts +0 -1
- package/dist/facade/config.test.js +0 -216
- package/dist/facade/config.test.js.map +0 -1
- package/dist/facade/facade.test.d.ts +0 -1
- package/dist/facade/facade.test.js +0 -278
- package/dist/facade/facade.test.js.map +0 -1
- package/dist/services/Locking/calculate-apy.test.d.ts +0 -1
- package/dist/services/Locking/calculate-apy.test.js +0 -41
- package/dist/services/Locking/calculate-apy.test.js.map +0 -1
- package/dist/tests/sample.test.d.ts +0 -1
- package/dist/tests/sample.test.js +0 -59
- package/dist/tests/sample.test.js.map +0 -1
- package/src/domain/au-minimum.test.ts +0 -371
- package/src/domain/deposit-bounds.test.ts +0 -200
- package/src/domain/loan-contract.test.ts +0 -343
- package/src/domain/partners.test.ts +0 -83
- package/src/domain/pools.test.ts +0 -260
- package/src/domain/rates.test.ts +0 -254
- package/src/domain/requests.test.ts +0 -653
- package/src/domain/settlement.test.ts +0 -198
- package/src/domain/tranche-display-name.test.ts +0 -96
- package/src/domain/tranches.test.ts +0 -300
- package/src/domain/wallet-errors.test.ts +0 -100
- package/src/facade/config.test.ts +0 -265
- package/src/facade/facade.test.ts +0 -374
- package/src/services/Locking/calculate-apy.test.ts +0 -52
- package/src/tests/sample.test.ts +0 -63
|
@@ -8,37 +8,125 @@
|
|
|
8
8
|
* enumerated once here.
|
|
9
9
|
*
|
|
10
10
|
* Lifted from kasu-ui's `src/lib/web3/is-user-rejected.ts` and kasu-mobile's
|
|
11
|
-
* `src/features/lending/lib/errors.ts
|
|
11
|
+
* `src/features/lending/lib/errors.ts` — and now the UNION of the two, so
|
|
12
|
+
* neither app has to keep a wrapper on top of this one.
|
|
12
13
|
*/
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Did the lender reject the request in their wallet?
|
|
16
17
|
*
|
|
17
|
-
* Providers surface a rejection in different shapes
|
|
18
|
+
* Providers surface a rejection in different shapes, and the union of them is
|
|
19
|
+
* the point of this function existing once:
|
|
18
20
|
* - MetaMask and most EIP-1193 wallets: `code: 4001`
|
|
21
|
+
* - Coinbase Wallet and ethers v5: `code: 'ACTION_REJECTED'`
|
|
22
|
+
* - viem: a `UserRejectedRequestError` name
|
|
19
23
|
* - WalletConnect and some Privy paths: `Error('User rejected the request')`
|
|
20
|
-
* -
|
|
21
|
-
*
|
|
24
|
+
* - a provider error WRAPPED by another layer, carrying the real code and
|
|
25
|
+
* message on a nested `error` — the shape Privy's embedded wallet
|
|
26
|
+
* surfaces on Expo, where the outer object says nothing useful
|
|
27
|
+
* - ethers' own `reason` field, which is often the only place the text lands
|
|
28
|
+
* - a PLAIN OBJECT carrying `message`, which `String(err)` would read as
|
|
29
|
+
* `"[object Object]"`
|
|
22
30
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
31
|
+
* Previously kasu-ui's implementation verbatim, with kasu-mobile keeping its
|
|
32
|
+
* own superset on top. That is precisely the drift this layer exists to stop —
|
|
33
|
+
* a rejection kasu-mobile recognised and kasu-ui did not was reported to the
|
|
34
|
+
* same lender as a failure on one app and a cancellation on the other. The
|
|
35
|
+
* union lives here; the mobile wrapper goes.
|
|
36
|
+
*
|
|
37
|
+
* ## The text is read for a SUBJECT, not for a keyword
|
|
38
|
+
*
|
|
39
|
+
* The words alone are not the signal. "Declined" and "request rejected" are
|
|
40
|
+
* also what a rate limiter, a risk engine and a KYC decision say, and those
|
|
41
|
+
* arrive wrapped in exactly the same envelope a wallet error does — ethers'
|
|
42
|
+
* `SERVER_ERROR` around `-32603` carries the upstream body on a nested
|
|
43
|
+
* `error.message`, so `{ error: { message: 'request rejected: rate limit
|
|
44
|
+
* exceeded' } }` is an RPC refusing to serve us, not a lender refusing to
|
|
45
|
+
* sign. Reporting one as the other tells a lender they cancelled something
|
|
46
|
+
* they never saw, and DROPS the real error on the floor.
|
|
47
|
+
*
|
|
48
|
+
* So a rejection is a machine-readable wallet code, or a sentence that names
|
|
49
|
+
* the party who did it: "user rejected", "declined by the user", "cancelled by
|
|
50
|
+
* the wallet". A bare "declined" is not a rejection, whatever else is on the
|
|
51
|
+
* envelope.
|
|
52
|
+
*
|
|
53
|
+
* Within that rule, being generous is the safe direction: calling a genuine
|
|
54
|
+
* fault a cancellation costs a lender one retry, while calling a deliberate
|
|
55
|
+
* rejection a failure sends them to support to report a bug that does not
|
|
56
|
+
* exist.
|
|
27
57
|
*/
|
|
28
58
|
export function isUserRejected(err: unknown): boolean {
|
|
29
59
|
if (!err) return false;
|
|
30
60
|
if (typeof err === 'object') {
|
|
31
|
-
const
|
|
32
|
-
|
|
61
|
+
const e = err as {
|
|
62
|
+
code?: unknown;
|
|
63
|
+
error?: { code?: unknown } | null;
|
|
64
|
+
};
|
|
65
|
+
if (isRejectionCode(e.code)) return true;
|
|
66
|
+
// A wrapped provider error: the outer layer's code is its own, the
|
|
67
|
+
// inner one is the wallet's.
|
|
68
|
+
if (isRejectionCode(e.error?.code)) return true;
|
|
33
69
|
}
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
70
|
+
const lower = rejectionText(err).toLowerCase();
|
|
71
|
+
return USER_REJECTION_PATTERNS.some((pattern) => pattern.test(lower));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isRejectionCode(code: unknown): boolean {
|
|
75
|
+
return code === 4001 || code === 'ACTION_REJECTED';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The wordings that name the wallet or the person at it.
|
|
80
|
+
*
|
|
81
|
+
* Each one carries a SUBJECT — the identifier a wallet library uses, or the
|
|
82
|
+
* verb with the party who performed it. That is what separates a lender
|
|
83
|
+
* pressing Reject from a server declining to answer, and it is why none of
|
|
84
|
+
* these is a bare "declined" or "rejected".
|
|
85
|
+
*/
|
|
86
|
+
const USER_REJECTION_PATTERNS: readonly RegExp[] = [
|
|
87
|
+
// Machine-readable markers that only a wallet layer emits, arriving as
|
|
88
|
+
// text because something in between stringified the error.
|
|
89
|
+
/action_rejected/,
|
|
90
|
+
/user_rejected/,
|
|
91
|
+
/userrejectedrequest/,
|
|
92
|
+
// "the user did it", in the orders the wallets write it.
|
|
93
|
+
/user\s+(?:has\s+)?(?:rejected|denied|declined|refused|cancell?ed)/,
|
|
94
|
+
/(?:rejected|denied|declined|refused|cancell?ed)\s+by\s+(?:the\s+)?(?:user|wallet|signer|owner)/,
|
|
95
|
+
// The wallet as the subject, which is how some embedded wallets word it.
|
|
96
|
+
/(?:wallet|signer)\s+(?:rejected|denied|declined|refused)/,
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Every place a wallet might have put the words: the message, ethers' `reason`,
|
|
101
|
+
* the error's `name` (viem puts the whole signal there), and a wrapped error's
|
|
102
|
+
* own three. Joined rather than picked, because which one carries the text
|
|
103
|
+
* depends on how many layers wrapped it.
|
|
104
|
+
*
|
|
105
|
+
* A separator is used rather than a bare space: two fields must not be able to
|
|
106
|
+
* form a phrase across the join that neither of them said.
|
|
107
|
+
*/
|
|
108
|
+
function rejectionText(err: unknown): string {
|
|
109
|
+
if (typeof err !== 'object' || err === null) return String(err);
|
|
110
|
+
const e = err as {
|
|
111
|
+
message?: unknown;
|
|
112
|
+
reason?: unknown;
|
|
113
|
+
name?: unknown;
|
|
114
|
+
error?: {
|
|
115
|
+
message?: unknown;
|
|
116
|
+
reason?: unknown;
|
|
117
|
+
name?: unknown;
|
|
118
|
+
} | null;
|
|
119
|
+
};
|
|
120
|
+
return [
|
|
121
|
+
e.message,
|
|
122
|
+
e.reason,
|
|
123
|
+
e.name,
|
|
124
|
+
e.error?.message,
|
|
125
|
+
e.error?.reason,
|
|
126
|
+
e.error?.name,
|
|
127
|
+
]
|
|
128
|
+
.filter((part): part is string => typeof part === 'string')
|
|
129
|
+
.join(' | ');
|
|
42
130
|
}
|
|
43
131
|
|
|
44
132
|
/**
|
|
@@ -54,3 +142,32 @@ export function isUnpredictableGas(err: unknown): boolean {
|
|
|
54
142
|
if (!err || typeof err !== 'object') return false;
|
|
55
143
|
return (err as { code?: unknown }).code === 'UNPREDICTABLE_GAS_LIMIT';
|
|
56
144
|
}
|
|
145
|
+
|
|
146
|
+
/** A step that failed, as a code plus whether the lender chose it. */
|
|
147
|
+
export type WalletFailure<Step> =
|
|
148
|
+
| { step: Step; reason: 'cancelled' }
|
|
149
|
+
| { step: Step; reason: 'failed'; error: unknown };
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The `cancelled` / `failed` split, for a WALLET call on any step of any flow.
|
|
153
|
+
*
|
|
154
|
+
* A lender who pressed Reject is not a fault. Reporting one as the other is
|
|
155
|
+
* how a support queue fills with people who did exactly what they meant to.
|
|
156
|
+
*
|
|
157
|
+
* Generic in the step so both flows share one implementation — it was
|
|
158
|
+
* duplicated byte-for-byte in each of them, which is the same drift this
|
|
159
|
+
* layer exists to stop, one level up.
|
|
160
|
+
*
|
|
161
|
+
* Only pass it an error a WALLET produced. An HTTP port's throw is always a
|
|
162
|
+
* `failed`: the lender's wallet was not involved in it, so a backend that
|
|
163
|
+
* happens to word a refusal "declined" must never be shown to them as
|
|
164
|
+
* something they did.
|
|
165
|
+
*/
|
|
166
|
+
export function classifyWalletFailure<Step>(
|
|
167
|
+
step: Step,
|
|
168
|
+
err: unknown,
|
|
169
|
+
): WalletFailure<Step> {
|
|
170
|
+
return isUserRejected(err)
|
|
171
|
+
? { step, reason: 'cancelled' }
|
|
172
|
+
: { step, reason: 'failed', error: err };
|
|
173
|
+
}
|
package/src/facade/deposits.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { ContractTransaction } from 'ethers';
|
|
|
2
2
|
|
|
3
3
|
import { UserLending } from '../services/UserLending/user-lending';
|
|
4
4
|
|
|
5
|
+
import { READ_ONLY_MESSAGE } from './read-only';
|
|
5
6
|
import { DepositParams, KycParams, WithdrawParams } from './types';
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -32,9 +33,7 @@ export class DepositsFacade {
|
|
|
32
33
|
*/
|
|
33
34
|
private assertWritable(): void {
|
|
34
35
|
if (this._isReadOnly) {
|
|
35
|
-
throw new Error(
|
|
36
|
-
'Kasu: this instance is read-only; call kasu.connect(signer) first',
|
|
37
|
-
);
|
|
36
|
+
throw new Error(READ_ONLY_MESSAGE);
|
|
38
37
|
}
|
|
39
38
|
}
|
|
40
39
|
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { BigNumber } from 'ethers';
|
|
2
|
+
|
|
3
|
+
import type { IERC20MetadataAbi } from '../contracts';
|
|
4
|
+
import {
|
|
5
|
+
DepositFlow,
|
|
6
|
+
DepositFlowOptions,
|
|
7
|
+
DepositPorts,
|
|
8
|
+
WaitableTransaction,
|
|
9
|
+
WithdrawFlow,
|
|
10
|
+
WithdrawPorts,
|
|
11
|
+
} from '../flows';
|
|
12
|
+
|
|
13
|
+
import { DepositsFacade } from './deposits';
|
|
14
|
+
import { READ_ONLY_MESSAGE } from './read-only';
|
|
15
|
+
import { KycParams } from './types';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The ports a consumer MUST supply for a deposit, plus optional overrides for
|
|
19
|
+
* the ones the SDK can serve itself.
|
|
20
|
+
*
|
|
21
|
+
* The three required ones all reach the application's own backend or wallet:
|
|
22
|
+
* the SDK has no opinion on how a lender signs, which proxy the agreements
|
|
23
|
+
* service sits behind, or where the KYC signature is fetched — and it must not
|
|
24
|
+
* learn any of them, because it is a public package.
|
|
25
|
+
*/
|
|
26
|
+
export type DepositFlowPortOverrides = Pick<
|
|
27
|
+
DepositPorts,
|
|
28
|
+
'signMessage' | 'generateContract' | 'getKycSignature'
|
|
29
|
+
> &
|
|
30
|
+
Partial<DepositPorts>;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Every withdraw port has an SDK default except `getKycSignature`, which has
|
|
34
|
+
* no default at all and which turns the KYC pre-check on by being supplied.
|
|
35
|
+
*/
|
|
36
|
+
export type WithdrawFlowPortOverrides = Partial<WithdrawPorts>;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Builds `DepositFlow` / `WithdrawFlow` instances wired to THIS Kasu instance:
|
|
40
|
+
* its chain's stable token, its `LendingPoolManager`, its signer.
|
|
41
|
+
*
|
|
42
|
+
* ```ts
|
|
43
|
+
* const flow = kasu.connect(signer).flows.deposit({
|
|
44
|
+
* signMessage: (m) => signer.signMessage(m),
|
|
45
|
+
* generateContract: (req) => postToMyProxy(req),
|
|
46
|
+
* getKycSignature: (p) => postToMyBackend(p),
|
|
47
|
+
* });
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* A flow built from a read-only instance constructs fine and reads fine — the
|
|
51
|
+
* write ports throw `READ_ONLY_MESSAGE` when the run reaches them, exactly as
|
|
52
|
+
* `kasu.deposits.deposit` does. Constructing is not the mistake; submitting is.
|
|
53
|
+
*
|
|
54
|
+
* It holds the `DepositsFacade` and NOTHING the facade already owns: no
|
|
55
|
+
* `UserLending`, no chain id, no signer. Two paths to one behaviour is how the
|
|
56
|
+
* KYC params a flow built came to differ from the ones `kasu.deposits`
|
|
57
|
+
* built — the same class of drift the flows themselves exist to end.
|
|
58
|
+
*/
|
|
59
|
+
export class FlowsFacade {
|
|
60
|
+
constructor(
|
|
61
|
+
private readonly _deposits: DepositsFacade,
|
|
62
|
+
/**
|
|
63
|
+
* The chain's stable token, bound to whatever the Kasu instance holds.
|
|
64
|
+
* A factory rather than a contract: it is one `new Contract`, and a
|
|
65
|
+
* cached binding would outlive the config it was built from.
|
|
66
|
+
*/
|
|
67
|
+
private readonly _erc20: () => IERC20MetadataAbi,
|
|
68
|
+
/** The read-only flag `Kasu` already computed — never re-derived here. */
|
|
69
|
+
private readonly _isReadOnly: boolean,
|
|
70
|
+
/**
|
|
71
|
+
* The ERC-20 spender every deposit run approves: this chain's
|
|
72
|
+
* `LendingPoolManager`, which is the only contract the default deposit
|
|
73
|
+
* port calls. Passed to the flow so a consumer never has to hand-wire
|
|
74
|
+
* an address whose only wrong value grants an approval to the wrong
|
|
75
|
+
* contract.
|
|
76
|
+
*/
|
|
77
|
+
private readonly _spender: string,
|
|
78
|
+
) {}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* A deposit pipeline. `readAllowance`, `approve`, `deposit` and
|
|
82
|
+
* `buildKycParams` default to the SDK's own implementations; pass any of
|
|
83
|
+
* them to override (kasu-ui approves through its sponsored-gas path, for
|
|
84
|
+
* one).
|
|
85
|
+
*
|
|
86
|
+
* Each default is applied per key with `??`, not by spreading `ports` over
|
|
87
|
+
* them: `{ ...defaults, ...ports }` lets an EXPLICITLY undefined value
|
|
88
|
+
* delete the default it was meant to keep, and
|
|
89
|
+
* `approve: sponsoredGas ? sponsoredApprove : undefined` is exactly how a
|
|
90
|
+
* consumer writes a conditional override.
|
|
91
|
+
*/
|
|
92
|
+
deposit(
|
|
93
|
+
ports: DepositFlowPortOverrides,
|
|
94
|
+
opts?: DepositFlowOptions,
|
|
95
|
+
): DepositFlow {
|
|
96
|
+
return new DepositFlow(
|
|
97
|
+
{
|
|
98
|
+
signMessage: ports.signMessage,
|
|
99
|
+
generateContract: ports.generateContract,
|
|
100
|
+
getKycSignature: ports.getKycSignature,
|
|
101
|
+
buildKycParams:
|
|
102
|
+
ports.buildKycParams ??
|
|
103
|
+
((userAddress: `0x${string}`): KycParams =>
|
|
104
|
+
this._deposits.buildKycParams(userAddress)),
|
|
105
|
+
readAllowance:
|
|
106
|
+
ports.readAllowance ??
|
|
107
|
+
((owner: string, spender: string): Promise<BigNumber> =>
|
|
108
|
+
this._erc20().allowance(owner, spender)),
|
|
109
|
+
approve:
|
|
110
|
+
ports.approve ??
|
|
111
|
+
(async (
|
|
112
|
+
spender: string,
|
|
113
|
+
amount: BigNumber,
|
|
114
|
+
): Promise<WaitableTransaction> => {
|
|
115
|
+
this._assertWritable();
|
|
116
|
+
// The EXACT amount the flow asked for. Nothing here
|
|
117
|
+
// rounds it up, and nothing here substitutes
|
|
118
|
+
// MaxUint256.
|
|
119
|
+
return await this._erc20().approve(spender, amount);
|
|
120
|
+
}),
|
|
121
|
+
deposit:
|
|
122
|
+
ports.deposit ??
|
|
123
|
+
((params): Promise<WaitableTransaction> =>
|
|
124
|
+
this._deposits.deposit(params)),
|
|
125
|
+
now: ports.now,
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
contractTtlMs: opts?.contractTtlMs,
|
|
129
|
+
// Per key here too, for the same reason the ports are: an
|
|
130
|
+
// explicit `spender: undefined` must not delete the default.
|
|
131
|
+
spender: opts?.spender ?? this._spender,
|
|
132
|
+
},
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* A withdrawal pipeline. Both write ports and `buildKycParams` default to
|
|
138
|
+
* this instance; supplying `getKycSignature` turns the KYC pre-check on.
|
|
139
|
+
* Defaults are applied per key, for the reason `deposit()` gives.
|
|
140
|
+
*/
|
|
141
|
+
withdraw(ports: WithdrawFlowPortOverrides = {}): WithdrawFlow {
|
|
142
|
+
return new WithdrawFlow({
|
|
143
|
+
buildKycParams:
|
|
144
|
+
ports.buildKycParams ??
|
|
145
|
+
((userAddress: `0x${string}`): KycParams =>
|
|
146
|
+
this._deposits.buildKycParams(userAddress)),
|
|
147
|
+
getKycSignature: ports.getKycSignature,
|
|
148
|
+
withdraw:
|
|
149
|
+
ports.withdraw ??
|
|
150
|
+
((params): Promise<WaitableTransaction> =>
|
|
151
|
+
this._deposits.withdraw({
|
|
152
|
+
poolId: params.poolId,
|
|
153
|
+
trancheId: params.trancheId,
|
|
154
|
+
amount: params.amount,
|
|
155
|
+
})),
|
|
156
|
+
withdrawMax:
|
|
157
|
+
ports.withdrawMax ??
|
|
158
|
+
((
|
|
159
|
+
poolId: string,
|
|
160
|
+
trancheId: string,
|
|
161
|
+
userAddress: string,
|
|
162
|
+
): Promise<WaitableTransaction> =>
|
|
163
|
+
this._deposits.withdrawMax(poolId, trancheId, userAddress)),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private _assertWritable(): void {
|
|
168
|
+
if (this._isReadOnly) {
|
|
169
|
+
throw new Error(READ_ONLY_MESSAGE);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
package/src/facade/index.ts
CHANGED
|
@@ -5,6 +5,14 @@ export { Kasu } from './kasu';
|
|
|
5
5
|
export { StrategiesFacade } from './strategies';
|
|
6
6
|
export { DepositsFacade } from './deposits';
|
|
7
7
|
export { PortfolioFacade } from './user-portfolio';
|
|
8
|
+
export { FlowsFacade } from './flows';
|
|
9
|
+
export type {
|
|
10
|
+
DepositFlowPortOverrides,
|
|
11
|
+
WithdrawFlowPortOverrides,
|
|
12
|
+
} from './flows';
|
|
13
|
+
|
|
14
|
+
// The one refusal every write path shares
|
|
15
|
+
export { READ_ONLY_MESSAGE } from './read-only';
|
|
8
16
|
|
|
9
17
|
// Chain configurations
|
|
10
18
|
export { CHAIN_CONFIGS } from './chain-configs';
|
package/src/facade/kasu.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { Provider } from '@ethersproject/providers';
|
|
2
2
|
import { providers, Signer } from 'ethers';
|
|
3
3
|
|
|
4
|
+
import { IERC20MetadataAbi__factory } from '../contracts';
|
|
5
|
+
import type { IERC20MetadataAbi } from '../contracts';
|
|
4
6
|
import { KasuSdk } from '../kasu-sdk';
|
|
5
7
|
import { SdkConfig, SdkConfigOptions } from '../sdk-config';
|
|
6
8
|
|
|
7
9
|
import { CHAIN_CONFIGS } from './chain-configs';
|
|
8
10
|
import { DepositsFacade } from './deposits';
|
|
11
|
+
import { FlowsFacade } from './flows';
|
|
9
12
|
import { StrategiesFacade } from './strategies';
|
|
10
13
|
import {
|
|
11
14
|
ChainConfigEntry,
|
|
@@ -18,8 +21,9 @@ import { PortfolioFacade } from './user-portfolio';
|
|
|
18
21
|
/**
|
|
19
22
|
* High-level entry point for external integrators.
|
|
20
23
|
*
|
|
21
|
-
* Provides
|
|
22
|
-
* exposes the underlying `KasuSdk` services via `.services` for
|
|
24
|
+
* Provides four domain facades — `strategies`, `deposits`, `portfolio`,
|
|
25
|
+
* `flows` — and exposes the underlying `KasuSdk` services via `.services` for
|
|
26
|
+
* power-users.
|
|
23
27
|
*
|
|
24
28
|
* ```ts
|
|
25
29
|
* import { Kasu } from '@kasufinance/kasu-sdk';
|
|
@@ -43,6 +47,8 @@ export class Kasu {
|
|
|
43
47
|
public readonly deposits: DepositsFacade;
|
|
44
48
|
/** User positions, yields, transaction history. */
|
|
45
49
|
public readonly portfolio: PortfolioFacade;
|
|
50
|
+
/** Headless deposit / withdraw state machines. */
|
|
51
|
+
public readonly flows: FlowsFacade;
|
|
46
52
|
|
|
47
53
|
private readonly _sdk: KasuSdk;
|
|
48
54
|
private readonly _chainConfig: ChainConfigEntry;
|
|
@@ -70,10 +76,15 @@ export class Kasu {
|
|
|
70
76
|
sdk.UserLending,
|
|
71
77
|
);
|
|
72
78
|
|
|
79
|
+
// Derived ONCE and handed to every facade that needs it. Each of them
|
|
80
|
+
// re-deriving `Signer.isSigner` is how two facades come to disagree
|
|
81
|
+
// about whether the same instance can write.
|
|
82
|
+
const isReadOnly = !Signer.isSigner(signerOrProvider);
|
|
83
|
+
|
|
73
84
|
this.deposits = new DepositsFacade(
|
|
74
85
|
sdk.UserLending,
|
|
75
86
|
chainConfig.chainId.toString(),
|
|
76
|
-
|
|
87
|
+
isReadOnly,
|
|
77
88
|
);
|
|
78
89
|
|
|
79
90
|
this.portfolio = new PortfolioFacade(
|
|
@@ -81,6 +92,13 @@ export class Kasu {
|
|
|
81
92
|
sdk.UserLending,
|
|
82
93
|
sdk.Portfolio,
|
|
83
94
|
);
|
|
95
|
+
|
|
96
|
+
this.flows = new FlowsFacade(
|
|
97
|
+
this.deposits,
|
|
98
|
+
() => erc20Of(chainConfig, signerOrProvider),
|
|
99
|
+
isReadOnly,
|
|
100
|
+
chainConfig.contracts.LendingPoolManager,
|
|
101
|
+
);
|
|
84
102
|
}
|
|
85
103
|
|
|
86
104
|
/**
|
|
@@ -241,6 +259,28 @@ function rpcUrlsOf(chainConfig: ChainConfigEntry): string[] {
|
|
|
241
259
|
return (chainConfig as { rpcUrls?: string[] }).rpcUrls ?? [];
|
|
242
260
|
}
|
|
243
261
|
|
|
262
|
+
/**
|
|
263
|
+
* The chain's stable token, bound to whatever this instance holds.
|
|
264
|
+
*
|
|
265
|
+
* Built per call rather than cached: it is one `new Contract`, and a cached
|
|
266
|
+
* binding would outlive the signer a `connect()` replaced. A config with no
|
|
267
|
+
* `stableAsset` (a hand-written entry from before 2.5.0) throws here rather
|
|
268
|
+
* than inside ethers, naming the two ports that make the flow work without
|
|
269
|
+
* one.
|
|
270
|
+
*/
|
|
271
|
+
function erc20Of(
|
|
272
|
+
chainConfig: ChainConfigEntry,
|
|
273
|
+
signerOrProvider: Provider | Signer,
|
|
274
|
+
): IERC20MetadataAbi {
|
|
275
|
+
const address = stableAssetOf(chainConfig)?.address;
|
|
276
|
+
if (!address) {
|
|
277
|
+
throw new Error(
|
|
278
|
+
'Kasu: this chain config has no stableAsset; pass readAllowance and approve ports explicitly',
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
return IERC20MetadataAbi__factory.connect(address, signerOrProvider);
|
|
282
|
+
}
|
|
283
|
+
|
|
244
284
|
/**
|
|
245
285
|
* The read-only provider used when the caller passes no `signerOrProvider`.
|
|
246
286
|
*
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The refusal every write path shares when the instance holds a Provider
|
|
3
|
+
* rather than a Signer.
|
|
4
|
+
*
|
|
5
|
+
* One constant, because the message's job is to name the fix — `kasu.connect`
|
|
6
|
+
* — and three near-identical wordings across the facades would be three
|
|
7
|
+
* different pieces of advice for one problem. ethers' own
|
|
8
|
+
* "sending a transaction requires a signer" arrives several frames deeper,
|
|
9
|
+
* after the params have been encoded, and says nothing about how to get a
|
|
10
|
+
* signer onto THIS object.
|
|
11
|
+
*/
|
|
12
|
+
export const READ_ONLY_MESSAGE =
|
|
13
|
+
'Kasu: this instance is read-only; call kasu.connect(signer) first';
|