@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
|
@@ -1,371 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
AU_ALPHA3,
|
|
3
|
-
AU_MIN_CUMULATIVE_BY_STABLE,
|
|
4
|
-
auMinimumRemaining,
|
|
5
|
-
auThresholdFor,
|
|
6
|
-
isAuMinimumExempt,
|
|
7
|
-
isAustralianKyc,
|
|
8
|
-
parseMinorUnits,
|
|
9
|
-
} from './au-minimum';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Ported from kasu-ui
|
|
13
|
-
* `src/features/lending/lib/au-lending-restriction.test.ts`, minus the two
|
|
14
|
-
* cases that pin toast copy — those strings stay in the applications.
|
|
15
|
-
*
|
|
16
|
-
* What these pin is the arithmetic: integer bigint minor units and a
|
|
17
|
-
* truncating parse, so no consumer can disagree with another at the
|
|
18
|
-
* 360,000 / 500,000 boundary.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
// 6dp on every current deployment (USDC on Base/XDC, AUDD on XDC).
|
|
22
|
-
const DP = 6;
|
|
23
|
-
const n = (v: string): bigint => BigInt(v);
|
|
24
|
-
|
|
25
|
-
describe('isAustralianKyc', () => {
|
|
26
|
-
it('accepts the alpha-3 code in any casing, with surrounding whitespace', () => {
|
|
27
|
-
expect(isAustralianKyc('AUS')).toBe(true);
|
|
28
|
-
expect(isAustralianKyc('aus')).toBe(true);
|
|
29
|
-
expect(isAustralianKyc(' AUS ')).toBe(true);
|
|
30
|
-
expect(isAustralianKyc('Aus')).toBe(true);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('rejects the alpha-2 code — the collapsed KYC field is alpha-3', () => {
|
|
34
|
-
// Deliberate: a bare 'AU' must NOT restrict. The KYC claim is
|
|
35
|
-
// normalised to alpha-3 before it ever reaches this rule.
|
|
36
|
-
expect(isAustralianKyc('AU')).toBe(false);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('rejects near-misses and absent countries (fail-open)', () => {
|
|
40
|
-
expect(isAustralianKyc('AUT')).toBe(false);
|
|
41
|
-
expect(isAustralianKyc('')).toBe(false);
|
|
42
|
-
expect(isAustralianKyc(undefined)).toBe(false);
|
|
43
|
-
expect(isAustralianKyc(null)).toBe(false);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('exports the alpha-3 constant it compares against', () => {
|
|
47
|
-
expect(AU_ALPHA3).toBe('AUS');
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
describe('auThresholdFor', () => {
|
|
52
|
-
it('returns the per-stable whole-unit minimum, case-insensitively', () => {
|
|
53
|
-
expect(auThresholdFor('USDC')).toBe(360_000);
|
|
54
|
-
expect(auThresholdFor('usdc')).toBe(360_000);
|
|
55
|
-
expect(auThresholdFor(' USDC ')).toBe(360_000);
|
|
56
|
-
expect(auThresholdFor('AUDD')).toBe(500_000);
|
|
57
|
-
expect(auThresholdFor('audd')).toBe(500_000);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('fails open for an unlisted stable or a missing symbol', () => {
|
|
61
|
-
expect(auThresholdFor('DAI')).toBeUndefined();
|
|
62
|
-
expect(auThresholdFor(undefined)).toBeUndefined();
|
|
63
|
-
expect(auThresholdFor(null)).toBeUndefined();
|
|
64
|
-
expect(auThresholdFor('')).toBeUndefined();
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('exposes the table kasu-backend’s minimums must match', () => {
|
|
68
|
-
expect(AU_MIN_CUMULATIVE_BY_STABLE).toEqual({
|
|
69
|
-
USDC: 360_000,
|
|
70
|
-
AUDD: 500_000,
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
describe('parseMinorUnits (6dp)', () => {
|
|
76
|
-
it('parses whole and partial decimal input', () => {
|
|
77
|
-
expect(parseMinorUnits('0', DP)).toBe(n('0'));
|
|
78
|
-
expect(parseMinorUnits('1', DP)).toBe(n('1000000'));
|
|
79
|
-
// Mid-typing states an amount field really produces.
|
|
80
|
-
expect(parseMinorUnits('10.', DP)).toBe(n('10000000'));
|
|
81
|
-
expect(parseMinorUnits('.5', DP)).toBe(n('500000'));
|
|
82
|
-
expect(parseMinorUnits('1.5', DP)).toBe(n('1500000'));
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('TRUNCATES excess precision — a position can never be inflated into passing', () => {
|
|
86
|
-
expect(parseMinorUnits('1.23456789', DP)).toBe(n('1234567'));
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('returns null for empty, non-numeric, negative and absent input', () => {
|
|
90
|
-
expect(parseMinorUnits('', DP)).toBeNull();
|
|
91
|
-
expect(parseMinorUnits(' ', DP)).toBeNull();
|
|
92
|
-
expect(parseMinorUnits('abc', DP)).toBeNull();
|
|
93
|
-
expect(parseMinorUnits('-5', DP)).toBeNull();
|
|
94
|
-
// A lone separator matches the shape but carries no digits.
|
|
95
|
-
expect(parseMinorUnits('.', DP)).toBeNull();
|
|
96
|
-
expect(parseMinorUnits(null, DP)).toBeNull();
|
|
97
|
-
expect(parseMinorUnits(undefined, DP)).toBeNull();
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('accepts numbers as well as strings', () => {
|
|
101
|
-
expect(parseMinorUnits(360_000, DP)).toBe(n('360000000000'));
|
|
102
|
-
expect(parseMinorUnits(0, DP)).toBe(n('0'));
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
// `address` and `exemptAddresses` are required keys on AuMinimumInput (a
|
|
107
|
-
// refactor must not be able to drop either silently), so the shared fixture
|
|
108
|
-
// supplies the un-exempted defaults and the exemption cases override them.
|
|
109
|
-
const BASE = {
|
|
110
|
-
stableSymbol: 'USDC',
|
|
111
|
-
decimals: DP,
|
|
112
|
-
address: undefined,
|
|
113
|
-
exemptAddresses: undefined,
|
|
114
|
-
} as const;
|
|
115
|
-
|
|
116
|
-
describe('auMinimumRemaining — fail-open cases return 0 (no raised minimum)', () => {
|
|
117
|
-
it('is 0 when the KYC country is absent', () => {
|
|
118
|
-
expect(
|
|
119
|
-
auMinimumRemaining({
|
|
120
|
-
...BASE,
|
|
121
|
-
country: undefined,
|
|
122
|
-
existingDeposited: '0',
|
|
123
|
-
}),
|
|
124
|
-
).toBe(0);
|
|
125
|
-
expect(
|
|
126
|
-
auMinimumRemaining({
|
|
127
|
-
...BASE,
|
|
128
|
-
country: null,
|
|
129
|
-
existingDeposited: '0',
|
|
130
|
-
}),
|
|
131
|
-
).toBe(0);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it('is 0 for a non-Australian lender', () => {
|
|
135
|
-
expect(
|
|
136
|
-
auMinimumRemaining({
|
|
137
|
-
...BASE,
|
|
138
|
-
country: 'USA',
|
|
139
|
-
existingDeposited: '0',
|
|
140
|
-
}),
|
|
141
|
-
).toBe(0);
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it('is 0 on a deployment whose stable has no configured threshold', () => {
|
|
145
|
-
expect(
|
|
146
|
-
auMinimumRemaining({
|
|
147
|
-
...BASE,
|
|
148
|
-
stableSymbol: 'DAI',
|
|
149
|
-
country: 'AUS',
|
|
150
|
-
existingDeposited: '0',
|
|
151
|
-
}),
|
|
152
|
-
).toBe(0);
|
|
153
|
-
});
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
describe('auMinimumRemaining — Australian lender', () => {
|
|
157
|
-
it('demands the FULL threshold while the position is unknown (strict-until-loaded)', () => {
|
|
158
|
-
// `undefined` is not a zero position — it is an unread one. Treating
|
|
159
|
-
// it as 0 keeps the floor at its strictest until the read lands, so
|
|
160
|
-
// the field never advertises a minimum lower than the backend accepts.
|
|
161
|
-
expect(
|
|
162
|
-
auMinimumRemaining({
|
|
163
|
-
...BASE,
|
|
164
|
-
country: 'AUS',
|
|
165
|
-
existingDeposited: undefined,
|
|
166
|
-
}),
|
|
167
|
-
).toBe(360_000);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it('is 0 once the existing position exactly meets the threshold (inclusive)', () => {
|
|
171
|
-
expect(
|
|
172
|
-
auMinimumRemaining({
|
|
173
|
-
...BASE,
|
|
174
|
-
country: 'AUS',
|
|
175
|
-
existingDeposited: '360000',
|
|
176
|
-
}),
|
|
177
|
-
).toBe(0);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it('is 0 once the existing position exceeds the threshold', () => {
|
|
181
|
-
expect(
|
|
182
|
-
auMinimumRemaining({
|
|
183
|
-
...BASE,
|
|
184
|
-
country: 'AUS',
|
|
185
|
-
existingDeposited: '400000',
|
|
186
|
-
}),
|
|
187
|
-
).toBe(0);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it('returns the remainder for a partial position', () => {
|
|
191
|
-
expect(
|
|
192
|
-
auMinimumRemaining({
|
|
193
|
-
...BASE,
|
|
194
|
-
country: 'AUS',
|
|
195
|
-
existingDeposited: '120000',
|
|
196
|
-
}),
|
|
197
|
-
).toBe(240_000);
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it('keeps sub-unit precision — one minor unit short still demands one minor unit', () => {
|
|
201
|
-
// 359,999.999999 lent → 0.000001 USDC left to reach 360,000. The
|
|
202
|
-
// bigint arithmetic must not round that away into a 0 (unrestricted).
|
|
203
|
-
expect(
|
|
204
|
-
auMinimumRemaining({
|
|
205
|
-
...BASE,
|
|
206
|
-
country: 'AUS',
|
|
207
|
-
existingDeposited: '359999.999999',
|
|
208
|
-
}),
|
|
209
|
-
).toBe(0.000001);
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
it('accepts a numeric existing position, not just a string', () => {
|
|
213
|
-
expect(
|
|
214
|
-
auMinimumRemaining({
|
|
215
|
-
...BASE,
|
|
216
|
-
country: 'AUS',
|
|
217
|
-
existingDeposited: 120_000,
|
|
218
|
-
}),
|
|
219
|
-
).toBe(240_000);
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it('uses the 500,000 threshold on AUDD deployments', () => {
|
|
223
|
-
expect(
|
|
224
|
-
auMinimumRemaining({
|
|
225
|
-
...BASE,
|
|
226
|
-
stableSymbol: 'AUDD',
|
|
227
|
-
country: 'AUS',
|
|
228
|
-
existingDeposited: 0,
|
|
229
|
-
}),
|
|
230
|
-
).toBe(500_000);
|
|
231
|
-
});
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
// Deployment configuration the caller supplies — see `isAuMinimumExempt`. The
|
|
235
|
-
// real list is never shipped in this public package; these are stand-ins.
|
|
236
|
-
const EXEMPT_CHECKSUMMED = '0x1111111111111111111111111111111111111111';
|
|
237
|
-
const EXEMPT_LOWER = EXEMPT_CHECKSUMMED.toLowerCase();
|
|
238
|
-
const OTHER_EXEMPT = '0x2222222222222222222222222222222222222222';
|
|
239
|
-
const NOT_EXEMPT = '0x3333333333333333333333333333333333333333';
|
|
240
|
-
const LIST: readonly string[] = [EXEMPT_LOWER, OTHER_EXEMPT.toLowerCase()];
|
|
241
|
-
|
|
242
|
-
describe('isAuMinimumExempt', () => {
|
|
243
|
-
it('matches a listed wallet in any casing, with surrounding whitespace', () => {
|
|
244
|
-
expect(isAuMinimumExempt(EXEMPT_CHECKSUMMED, LIST)).toBe(true);
|
|
245
|
-
expect(isAuMinimumExempt(EXEMPT_LOWER, LIST)).toBe(true);
|
|
246
|
-
expect(isAuMinimumExempt(EXEMPT_LOWER.toUpperCase(), LIST)).toBe(true);
|
|
247
|
-
expect(isAuMinimumExempt(` ${OTHER_EXEMPT} `, LIST)).toBe(true);
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
it('matches a list entry stored in mixed case too', () => {
|
|
251
|
-
// The rule normalises both sides, so a caller that reads its list
|
|
252
|
-
// straight out of configuration cannot be caught out by casing.
|
|
253
|
-
expect(
|
|
254
|
-
isAuMinimumExempt(EXEMPT_LOWER, [EXEMPT_CHECKSUMMED.toUpperCase()]),
|
|
255
|
-
).toBe(true);
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
it('fails CLOSED on anything unlisted or not a string', () => {
|
|
259
|
-
expect(isAuMinimumExempt(NOT_EXEMPT, LIST)).toBe(false);
|
|
260
|
-
expect(isAuMinimumExempt('', LIST)).toBe(false);
|
|
261
|
-
expect(isAuMinimumExempt(' ', LIST)).toBe(false);
|
|
262
|
-
expect(isAuMinimumExempt(undefined, LIST)).toBe(false);
|
|
263
|
-
expect(isAuMinimumExempt(null, LIST)).toBe(false);
|
|
264
|
-
// A prefix of, or an extension to, a listed address is another wallet.
|
|
265
|
-
expect(isAuMinimumExempt(EXEMPT_LOWER.slice(0, -1), LIST)).toBe(false);
|
|
266
|
-
expect(isAuMinimumExempt(`${EXEMPT_LOWER}00`, LIST)).toBe(false);
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
it('fails CLOSED when no list was supplied', () => {
|
|
270
|
-
expect(isAuMinimumExempt(EXEMPT_LOWER, undefined)).toBe(false);
|
|
271
|
-
expect(isAuMinimumExempt(EXEMPT_LOWER, null)).toBe(false);
|
|
272
|
-
expect(isAuMinimumExempt(EXEMPT_LOWER, [])).toBe(false);
|
|
273
|
-
});
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
describe('auMinimumRemaining — named-address exemption', () => {
|
|
277
|
-
it('is 0 for an exempt wallet that would otherwise owe the full threshold', () => {
|
|
278
|
-
for (const address of [
|
|
279
|
-
EXEMPT_CHECKSUMMED,
|
|
280
|
-
EXEMPT_LOWER,
|
|
281
|
-
OTHER_EXEMPT,
|
|
282
|
-
]) {
|
|
283
|
-
expect(
|
|
284
|
-
auMinimumRemaining({
|
|
285
|
-
...BASE,
|
|
286
|
-
country: 'AUS',
|
|
287
|
-
address,
|
|
288
|
-
exemptAddresses: LIST,
|
|
289
|
-
existingDeposited: 0,
|
|
290
|
-
}),
|
|
291
|
-
).toBe(0);
|
|
292
|
-
}
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
it('is 0 on AUDD too, where the threshold is the larger 500,000', () => {
|
|
296
|
-
expect(
|
|
297
|
-
auMinimumRemaining({
|
|
298
|
-
...BASE,
|
|
299
|
-
stableSymbol: 'AUDD',
|
|
300
|
-
country: 'AUS',
|
|
301
|
-
address: EXEMPT_LOWER,
|
|
302
|
-
exemptAddresses: LIST,
|
|
303
|
-
existingDeposited: 0,
|
|
304
|
-
}),
|
|
305
|
-
).toBe(0);
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
it('overrides an UNKNOWN position — the strict full-threshold case', () => {
|
|
309
|
-
// `undefined` is what a caller passes while the on-chain read is in
|
|
310
|
-
// flight, and it normally yields the whole threshold. An exemption
|
|
311
|
-
// outranks it, so an exempt lender never sees a floor that later drops.
|
|
312
|
-
expect(
|
|
313
|
-
auMinimumRemaining({
|
|
314
|
-
...BASE,
|
|
315
|
-
country: 'AUS',
|
|
316
|
-
address: EXEMPT_LOWER,
|
|
317
|
-
exemptAddresses: LIST,
|
|
318
|
-
existingDeposited: undefined,
|
|
319
|
-
}),
|
|
320
|
-
).toBe(0);
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
it('leaves a non-exempt Australian on the full threshold', () => {
|
|
324
|
-
expect(
|
|
325
|
-
auMinimumRemaining({
|
|
326
|
-
...BASE,
|
|
327
|
-
country: 'AUS',
|
|
328
|
-
address: NOT_EXEMPT,
|
|
329
|
-
exemptAddresses: LIST,
|
|
330
|
-
existingDeposited: 0,
|
|
331
|
-
}),
|
|
332
|
-
).toBe(360_000);
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
it('treats an absent address, or an absent list, as NOT exempt', () => {
|
|
336
|
-
// Both keys are required; either VALUE may be absent, and when it is
|
|
337
|
-
// the rule applies exactly as it did before exemptions existed.
|
|
338
|
-
for (const address of [undefined, null]) {
|
|
339
|
-
expect(
|
|
340
|
-
auMinimumRemaining({
|
|
341
|
-
...BASE,
|
|
342
|
-
country: 'AUS',
|
|
343
|
-
address,
|
|
344
|
-
exemptAddresses: LIST,
|
|
345
|
-
existingDeposited: 0,
|
|
346
|
-
}),
|
|
347
|
-
).toBe(360_000);
|
|
348
|
-
}
|
|
349
|
-
expect(
|
|
350
|
-
auMinimumRemaining({
|
|
351
|
-
...BASE,
|
|
352
|
-
country: 'AUS',
|
|
353
|
-
address: EXEMPT_LOWER,
|
|
354
|
-
exemptAddresses: undefined,
|
|
355
|
-
existingDeposited: 0,
|
|
356
|
-
}),
|
|
357
|
-
).toBe(360_000);
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
it('is 0 for a non-Australian at an exempt address, as it always was', () => {
|
|
361
|
-
expect(
|
|
362
|
-
auMinimumRemaining({
|
|
363
|
-
...BASE,
|
|
364
|
-
country: 'FRA',
|
|
365
|
-
address: EXEMPT_LOWER,
|
|
366
|
-
exemptAddresses: LIST,
|
|
367
|
-
existingDeposited: 0,
|
|
368
|
-
}),
|
|
369
|
-
).toBe(0);
|
|
370
|
-
});
|
|
371
|
-
});
|
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
import * as fc from 'fast-check';
|
|
2
|
-
|
|
3
|
-
import { TrancheData } from '../services/DataService/types';
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
ceilToCents,
|
|
7
|
-
floorToCents,
|
|
8
|
-
isBelowMinimumCapacity,
|
|
9
|
-
MAX_LENDING_AMOUNT_FALLBACK,
|
|
10
|
-
MIN_LENDING_AMOUNT_FALLBACK,
|
|
11
|
-
parseTrancheBound,
|
|
12
|
-
resolveBoundShortcuts,
|
|
13
|
-
resolveDepositBounds,
|
|
14
|
-
} from './deposit-bounds';
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Ported from kasu-ui `src/features/lending/lib/tranche-bounds.property.test.ts`,
|
|
18
|
-
* with the `resolveDepositBounds` / `parseTrancheBound` cases the property file
|
|
19
|
-
* did not cover.
|
|
20
|
-
*
|
|
21
|
-
* The Min/Max shortcuts (clickable bound labels on a lend form) must never
|
|
22
|
-
* write an amount the form rejects, so the snapping is checked as invariants
|
|
23
|
-
* over the whole input domain rather than at a few hand-picked values.
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
function tranche(over: Partial<TrancheData>): TrancheData {
|
|
27
|
-
return {
|
|
28
|
-
minimumDeposit: '500',
|
|
29
|
-
maximumDeposit: '500000',
|
|
30
|
-
poolCapacity: '250000',
|
|
31
|
-
...over,
|
|
32
|
-
} as unknown as TrancheData;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const amountArb = fc.double({
|
|
36
|
-
min: 0,
|
|
37
|
-
max: 1e12,
|
|
38
|
-
noNaN: true,
|
|
39
|
-
noDefaultInfinity: true,
|
|
40
|
-
});
|
|
41
|
-
/** `String(k / 100)` prints at most two decimals for any integer `k` in range. */
|
|
42
|
-
const AT_MOST_TWO_DP = /^\d+(\.\d{1,2})?$/;
|
|
43
|
-
|
|
44
|
-
describe('parseTrancheBound', () => {
|
|
45
|
-
it('accepts a positive finite bound from a string or a number', () => {
|
|
46
|
-
expect(parseTrancheBound('500')).toBe(500);
|
|
47
|
-
expect(parseTrancheBound(500)).toBe(500);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('rejects anything that is not a usable bound', () => {
|
|
51
|
-
expect(parseTrancheBound(null)).toBeNull();
|
|
52
|
-
expect(parseTrancheBound(undefined)).toBeNull();
|
|
53
|
-
expect(parseTrancheBound('')).toBeNull();
|
|
54
|
-
expect(parseTrancheBound('0')).toBeNull();
|
|
55
|
-
expect(parseTrancheBound('-1')).toBeNull();
|
|
56
|
-
expect(parseTrancheBound('not-a-number')).toBeNull();
|
|
57
|
-
expect(parseTrancheBound(Infinity)).toBeNull();
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
describe('resolveDepositBounds', () => {
|
|
62
|
-
it('clamps the max by the tranche’s REMAINING capacity', () => {
|
|
63
|
-
// A tranche can fill mid-epoch while `maximumDeposit` stays put.
|
|
64
|
-
expect(
|
|
65
|
-
resolveDepositBounds(
|
|
66
|
-
tranche({ maximumDeposit: '500000', poolCapacity: '25285.86' }),
|
|
67
|
-
),
|
|
68
|
-
).toEqual({ minDeposit: 500, maxDeposit: 25285.86 });
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('keeps the configured max when capacity is larger', () => {
|
|
72
|
-
expect(
|
|
73
|
-
resolveDepositBounds(
|
|
74
|
-
tranche({ maximumDeposit: '500000', poolCapacity: '900000' }),
|
|
75
|
-
),
|
|
76
|
-
).toEqual({ minDeposit: 500, maxDeposit: 500000 });
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('falls back when a "coming soon" pool has no bounds configured', () => {
|
|
80
|
-
expect(
|
|
81
|
-
resolveDepositBounds(
|
|
82
|
-
tranche({
|
|
83
|
-
minimumDeposit: '0',
|
|
84
|
-
maximumDeposit: '0',
|
|
85
|
-
poolCapacity: '900000',
|
|
86
|
-
}),
|
|
87
|
-
),
|
|
88
|
-
).toEqual({
|
|
89
|
-
minDeposit: MIN_LENDING_AMOUNT_FALLBACK,
|
|
90
|
-
maxDeposit: MAX_LENDING_AMOUNT_FALLBACK,
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it('uses both fallbacks and skips the capacity clamp with no tranche at all', () => {
|
|
95
|
-
expect(resolveDepositBounds(undefined)).toEqual({
|
|
96
|
-
minDeposit: MIN_LENDING_AMOUNT_FALLBACK,
|
|
97
|
-
maxDeposit: MAX_LENDING_AMOUNT_FALLBACK,
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('treats unparseable or negative capacity as zero', () => {
|
|
102
|
-
expect(
|
|
103
|
-
resolveDepositBounds(tranche({ poolCapacity: 'not-a-number' }))
|
|
104
|
-
.maxDeposit,
|
|
105
|
-
).toBe(0);
|
|
106
|
-
expect(
|
|
107
|
-
resolveDepositBounds(tranche({ poolCapacity: '-5' })).maxDeposit,
|
|
108
|
-
).toBe(0);
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
describe('cent snapping', () => {
|
|
113
|
-
it('floorToCents never exceeds its input, stays within a cent of it, and has ≤ 2dp', () => {
|
|
114
|
-
fc.assert(
|
|
115
|
-
fc.property(amountArb, (n) => {
|
|
116
|
-
const snapped = floorToCents(n);
|
|
117
|
-
expect(snapped).toBeLessThanOrEqual(n);
|
|
118
|
-
expect(n - snapped).toBeLessThan(0.02);
|
|
119
|
-
expect(String(snapped)).toMatch(AT_MOST_TWO_DP);
|
|
120
|
-
}),
|
|
121
|
-
);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('ceilToCents never undershoots its input, stays within a cent of it, and has ≤ 2dp', () => {
|
|
125
|
-
fc.assert(
|
|
126
|
-
fc.property(amountArb, (n) => {
|
|
127
|
-
const snapped = ceilToCents(n);
|
|
128
|
-
expect(snapped).toBeGreaterThanOrEqual(n);
|
|
129
|
-
expect(snapped - n).toBeLessThan(0.02);
|
|
130
|
-
expect(String(snapped)).toMatch(AT_MOST_TWO_DP);
|
|
131
|
-
}),
|
|
132
|
-
);
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
describe('resolveBoundShortcuts', () => {
|
|
137
|
-
it('keeps both shortcuts inside the validated range whenever they are enabled', () => {
|
|
138
|
-
fc.assert(
|
|
139
|
-
fc.property(amountArb, amountArb, (minDeposit, maxDeposit) => {
|
|
140
|
-
const { min, max, enabled } = resolveBoundShortcuts(
|
|
141
|
-
minDeposit,
|
|
142
|
-
maxDeposit,
|
|
143
|
-
);
|
|
144
|
-
if (!enabled) return;
|
|
145
|
-
expect(min).toBeGreaterThanOrEqual(minDeposit);
|
|
146
|
-
expect(max).toBeLessThanOrEqual(maxDeposit);
|
|
147
|
-
expect(max).toBeGreaterThanOrEqual(min);
|
|
148
|
-
expect(max).toBeGreaterThan(0);
|
|
149
|
-
}),
|
|
150
|
-
);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('disables the pair whenever the snapped range is empty', () => {
|
|
154
|
-
// Tranche at capacity (max clamps to 0), sub-cent dust the SDK's float
|
|
155
|
-
// subtraction leaves behind, remaining capacity below the tranche
|
|
156
|
-
// minimum, and an AU floor above the capacity.
|
|
157
|
-
expect(resolveBoundShortcuts(500, 0).enabled).toBe(false);
|
|
158
|
-
expect(
|
|
159
|
-
resolveBoundShortcuts(500, 1.4551915228366852e-11).enabled,
|
|
160
|
-
).toBe(false);
|
|
161
|
-
expect(resolveBoundShortcuts(500, 137).enabled).toBe(false);
|
|
162
|
-
expect(resolveBoundShortcuts(240_000, 100_000).enabled).toBe(false);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it('snaps SDK float noise onto cents without crossing the bound', () => {
|
|
166
|
-
expect(resolveBoundShortcuts(500, 249999.99999999997)).toEqual({
|
|
167
|
-
min: 500,
|
|
168
|
-
max: 249999.99,
|
|
169
|
-
enabled: true,
|
|
170
|
-
});
|
|
171
|
-
expect(resolveBoundShortcuts(500, 25285.86)).toEqual({
|
|
172
|
-
min: 500,
|
|
173
|
-
max: 25285.86,
|
|
174
|
-
enabled: true,
|
|
175
|
-
});
|
|
176
|
-
// An AU remainder with more precision than the field shows rounds UP.
|
|
177
|
-
expect(resolveBoundShortcuts(358765.432109, 500_000).min).toBe(
|
|
178
|
-
358765.44,
|
|
179
|
-
);
|
|
180
|
-
});
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
describe('isBelowMinimumCapacity', () => {
|
|
184
|
-
it('is exactly the negation of the shortcuts being enabled', () => {
|
|
185
|
-
fc.assert(
|
|
186
|
-
fc.property(amountArb, amountArb, (minDeposit, maxDeposit) => {
|
|
187
|
-
// The "nothing to click" and "nothing to type" states must
|
|
188
|
-
// never be able to disagree.
|
|
189
|
-
expect(isBelowMinimumCapacity(minDeposit, maxDeposit)).toBe(
|
|
190
|
-
!resolveBoundShortcuts(minDeposit, maxDeposit).enabled,
|
|
191
|
-
);
|
|
192
|
-
}),
|
|
193
|
-
);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it('flags a capacity that has fallen under the tranche minimum', () => {
|
|
197
|
-
expect(isBelowMinimumCapacity(500, 137)).toBe(true);
|
|
198
|
-
expect(isBelowMinimumCapacity(500, 25285.86)).toBe(false);
|
|
199
|
-
});
|
|
200
|
-
});
|