@strkfarm/sdk 1.0.15 → 1.0.16

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.
@@ -25,11 +25,13 @@ export class Web3Number extends BigNumber {
25
25
  }
26
26
 
27
27
  multipliedBy(value: string | number) {
28
- return new Web3Number(this.mul(value).toString(), this.decimals);
28
+ let _value = Number(value).toFixed(6);
29
+ return new Web3Number(this.mul(_value).toString(), this.decimals);
29
30
  }
30
31
 
31
32
  dividedBy(value: string | number) {
32
- return new Web3Number(this.div(value).toString(), this.decimals);
33
+ let _value = Number(value).toFixed(6);
34
+ return new Web3Number(this.div(_value).toString(), this.decimals);
33
35
  }
34
36
 
35
37
  plus(value: string | number) {
package/src/global.ts CHANGED
@@ -45,7 +45,13 @@ export class FatalError extends Error {
45
45
  }
46
46
  }
47
47
 
48
- const tokens: TokenInfo[] = [];
48
+ const tokens: TokenInfo[] = [{
49
+ name: 'Starknet',
50
+ symbol: 'STRK',
51
+ address: '0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d',
52
+ decimals: 18,
53
+ coingeckId: 'starknet'
54
+ }];
49
55
 
50
56
  /** Contains globally useful functions.
51
57
  * - fatalError: Things to do when a fatal error occurs
@@ -64,6 +70,10 @@ export class Global {
64
70
  console.error(err);
65
71
  }
66
72
 
73
+ static getDefaultTokens() {
74
+ return tokens;
75
+ }
76
+
67
77
  static async getTokens(): Promise<TokenInfo[]> {
68
78
  if (tokens.length) return tokens;
69
79
 
package/src/index.ts CHANGED
@@ -6,4 +6,7 @@ export * from './global';
6
6
  export * from './strategies';
7
7
  export * from './notifs';
8
8
  export * from './utils';
9
- export * from './node';
9
+ export * from './node';
10
+
11
+ export * from './utils/store';
12
+ export * from './utils/encrypt';
@@ -1,3 +1,4 @@
1
+ import { ContractAddr } from "@/dataTypes"
1
2
  import { BlockIdentifier, RpcProvider } from "starknet"
2
3
 
3
4
  export interface TokenInfo {
@@ -21,6 +22,20 @@ export interface IConfig {
21
22
  heartbeatUrl?: string
22
23
  }
23
24
 
25
+ export interface IProtocol {
26
+ name: string,
27
+ logo: string,
28
+ }
29
+
30
+ export interface IStrategyMetadata {
31
+ name: string,
32
+ description: string,
33
+ address: ContractAddr,
34
+ type: 'ERC4626' | 'ERC721' | 'Other',
35
+ depositTokens: TokenInfo[],
36
+ protocols: IProtocol[]
37
+ }
38
+
24
39
  export function getMainnetConfig(rpcUrl = "https://starknet-mainnet.public.blastapi.io", blockIdentifier: BlockIdentifier = 'pending'): IConfig {
25
40
  return {
26
41
  provider: new RpcProvider({
@@ -9,7 +9,14 @@ export interface PriceInfo {
9
9
  price: number,
10
10
  timestamp: Date
11
11
  }
12
- export class Pricer {
12
+
13
+ export abstract class PricerBase {
14
+ async getPrice(tokenSymbol: string): Promise<PriceInfo> {
15
+ throw new Error('Method not implemented');
16
+ }
17
+ }
18
+
19
+ export class Pricer implements PricerBase{
13
20
  readonly config: IConfig;
14
21
  readonly tokens: TokenInfo[] = [];
15
22
  protected prices: {
@@ -80,10 +87,10 @@ export class Pricer {
80
87
  Global.assert(!this.isStale(timestamp, tokenName), `Price of ${tokenName} is stale`);
81
88
 
82
89
  }
83
- async getPrice(tokenName: string) {
84
- Global.assert(this.prices[tokenName], `Price of ${tokenName} not found`);
85
- this.assertNotStale(this.prices[tokenName].timestamp, tokenName);
86
- return this.prices[tokenName];
90
+ async getPrice(tokenSymbol: string) {
91
+ Global.assert(this.prices[tokenSymbol], `Price of ${tokenSymbol} not found`);
92
+ this.assertNotStale(this.prices[tokenSymbol].timestamp, tokenSymbol);
93
+ return this.prices[tokenSymbol];
87
94
  }
88
95
 
89
96
  protected _loadPrices(onUpdate: (tokenSymbol: string) => void = () => {}) {
@@ -1 +1,2 @@
1
- export * from './autoCompounderStrk';
1
+ export * from './autoCompounderStrk';
2
+ export * from './vesu-rebalance';
@@ -0,0 +1,429 @@
1
+ import { ContractAddr, Web3Number } from "@/dataTypes";
2
+ import { IConfig, IProtocol, IStrategyMetadata } from "@/interfaces";
3
+ import { Pricer, PricerBase } from "@/modules";
4
+ import { CairoCustomEnum, Contract, num, uint256 } from "starknet";
5
+ import VesuRebalanceAbi from '@/data/vesu-rebalance.abi.json';
6
+ import { Global } from "@/global";
7
+ import { assert } from "@/utils";
8
+ import axios from "axios";
9
+
10
+ interface PoolProps {
11
+ pool_id: ContractAddr;
12
+ max_weight: number;
13
+ v_token: ContractAddr;
14
+ }
15
+
16
+ interface Change {
17
+ pool_id: ContractAddr;
18
+ changeAmt: Web3Number;
19
+ finalAmt: Web3Number;
20
+ isDeposit: boolean;
21
+ }
22
+
23
+ interface PoolInfoFull {
24
+ pool_id: ContractAddr;
25
+ pool_name: string | undefined;
26
+ max_weight: number;
27
+ current_weight: number;
28
+ v_token: ContractAddr;
29
+ amount: Web3Number;
30
+ usdValue: Web3Number;
31
+ APY: {
32
+ baseApy: number;
33
+ defiSpringApy: number;
34
+ netApy: number;
35
+ };
36
+ currentUtilization: number;
37
+ maxUtilization: number;
38
+ }
39
+ /**
40
+ * Represents a VesuRebalance strategy.
41
+ * This class implements an automated rebalancing strategy for Vesu pools,
42
+ * managing deposits and withdrawals while optimizing yield through STRK rewards.
43
+ */
44
+ export class VesuRebalance {
45
+ /** Configuration object for the strategy */
46
+ readonly config: IConfig;
47
+ /** Contract address of the strategy */
48
+ readonly address: ContractAddr;
49
+ /** Pricer instance for token price calculations */
50
+ readonly pricer: PricerBase;
51
+ /** Metadata containing strategy information */
52
+ readonly metadata: IStrategyMetadata;
53
+ /** Contract instance for interacting with the strategy */
54
+ readonly contract: Contract;
55
+ readonly BASE_WEIGHT = 10000; // 10000 bps = 100%
56
+
57
+ /**
58
+ * Creates a new VesuRebalance strategy instance.
59
+ * @param config - Configuration object containing provider and other settings
60
+ * @param pricer - Pricer instance for token price calculations
61
+ * @param metadata - Strategy metadata including deposit tokens and address
62
+ * @throws {Error} If more than one deposit token is specified
63
+ */
64
+ constructor(config: IConfig, pricer: PricerBase, metadata: IStrategyMetadata) {
65
+ this.config = config;
66
+ this.pricer = pricer;
67
+
68
+ assert(metadata.depositTokens.length === 1, 'VesuRebalance only supports 1 deposit token');
69
+ this.metadata = metadata;
70
+ this.address = metadata.address;
71
+
72
+ this.contract = new Contract(VesuRebalanceAbi, this.address.address, this.config.provider);
73
+ }
74
+
75
+ /**
76
+ * Creates a deposit call to the strategy contract.
77
+ * @param assets - Amount of assets to deposit
78
+ * @param receiver - Address that will receive the strategy tokens
79
+ * @returns Populated contract call for deposit
80
+ */
81
+ depositCall(assets: Web3Number, receiver: ContractAddr) {
82
+ // Technically its not erc4626 abi, but we just need approve call
83
+ // so, its ok to use it
84
+ const assetContract = new Contract(VesuRebalanceAbi, this.metadata.depositTokens[0].address, this.config.provider);
85
+ const call1 = assetContract.populate('approve', [this.address.address, uint256.bnToUint256(assets.toWei())]);
86
+ const call2 = this.contract.populate('deposit', [uint256.bnToUint256(assets.toWei()), receiver.address]);
87
+ return [call1, call2];
88
+ }
89
+
90
+ /**
91
+ * Creates a withdrawal call to the strategy contract.
92
+ * @param assets - Amount of assets to withdraw
93
+ * @param receiver - Address that will receive the withdrawn assets
94
+ * @param owner - Address that owns the strategy tokens
95
+ * @returns Populated contract call for withdrawal
96
+ */
97
+ withdrawCall(assets: Web3Number, receiver: ContractAddr, owner: ContractAddr) {
98
+ return [this.contract.populate('withdraw', [uint256.bnToUint256(assets.toWei()), receiver.address, owner.address])];
99
+ }
100
+
101
+ /**
102
+ * Returns the underlying asset token of the strategy.
103
+ * @returns The deposit token supported by this strategy
104
+ */
105
+ asset() {
106
+ return this.metadata.depositTokens[0];
107
+ }
108
+
109
+ /**
110
+ * Returns the number of decimals used by the strategy token.
111
+ * @returns Number of decimals (same as the underlying token)
112
+ */
113
+ decimals() {
114
+ return this.metadata.depositTokens[0].decimals; // same as underlying token
115
+ }
116
+
117
+ /**
118
+ * Calculates the Total Value Locked (TVL) for a specific user.
119
+ * @param user - Address of the user
120
+ * @returns Object containing the amount in token units and USD value
121
+ */
122
+ async getUserTVL(user: ContractAddr) {
123
+ const shares = await this.contract.balanceOf(user.address);
124
+ const assets = await this.contract.convert_to_assets(uint256.bnToUint256(shares));
125
+ const amount = Web3Number.fromWei(assets.toString(), this.metadata.depositTokens[0].decimals);
126
+ let price = await this.pricer.getPrice(this.metadata.depositTokens[0].symbol);
127
+ const usdValue = Number(amount.toFixed(6)) * price.price;
128
+ return {
129
+ amount,
130
+ usdValue
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Calculates the total TVL of the strategy.
136
+ * @returns Object containing the total amount in token units and USD value
137
+ */
138
+ async getTVL() {
139
+ const assets = await this.contract.total_assets();
140
+ const amount = Web3Number.fromWei(assets.toString(), this.metadata.depositTokens[0].decimals);
141
+ let price = await this.pricer.getPrice(this.metadata.depositTokens[0].symbol);
142
+ const usdValue = Number(amount.toFixed(6)) * price.price;
143
+ return {
144
+ amount,
145
+ usdValue
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Retrieves the list of allowed pools and their detailed information from multiple sources:
151
+ * 1. Contract's allowed pools
152
+ * 2. Vesu positions API for current positions
153
+ * 3. Vesu pools API for APY and utilization data
154
+ *
155
+ * @returns {Promise<{
156
+ * data: Array<PoolInfoFull>,
157
+ * isErrorPositionsAPI: boolean
158
+ * }>} Object containing:
159
+ * - data: Array of pool information including IDs, weights, amounts, APYs and utilization
160
+ * - isErrorPositionsAPI: Boolean indicating if there was an error fetching position data
161
+ */
162
+ async getPools() {
163
+ const allowedPools: PoolProps[] = (await this.contract.get_allowed_pools()).map((p: any) => ({
164
+ pool_id: ContractAddr.from(p.pool_id),
165
+ max_weight: Number(p.max_weight) / this.BASE_WEIGHT,
166
+ v_token: ContractAddr.from(p.v_token),
167
+ }));
168
+
169
+ /*
170
+ Vesu Positions API Response Schema (/positions?walletAddress=):
171
+ {
172
+ "data": [{
173
+ "pool": {
174
+ "id": string, // Pool identifier
175
+ "name": string // Pool name
176
+ },
177
+ "collateral": {
178
+ "value": string, // Amount of collateral in wei
179
+ "usdPrice": {
180
+ "value": string, // USD value in wei
181
+ "decimals": number // Decimals for USD value
182
+ }
183
+ }
184
+ }]
185
+ }
186
+
187
+ Vesu Pools API Response Schema (/pools):
188
+ {
189
+ "data": [{
190
+ "id": string,
191
+ "assets": [{
192
+ "stats": {
193
+ "supplyApy": {
194
+ "value": string,
195
+ "decimals": number
196
+ },
197
+ "defiSpringSupplyApr": {
198
+ "value": string,
199
+ "decimals": number
200
+ },
201
+ "currentUtilization": {
202
+ "value": string,
203
+ "decimals": number
204
+ }
205
+ },
206
+ "config": {
207
+ "maxUtilization": {
208
+ "value": string,
209
+ "decimals": number
210
+ }
211
+ }
212
+ }],
213
+ }]
214
+ }
215
+ */
216
+ let isErrorPositionsAPI = false;
217
+ let vesuPositions: any[] = [];
218
+ try {
219
+ const res = await axios.get(`https://api.vesu.xyz/positions?walletAddress=${this.address.address}`)
220
+ const data = await res.data;
221
+ vesuPositions = data.data;
222
+ } catch (e) {
223
+ console.error(`${VesuRebalance.name}: Error fetching pools for ${this.address.address}`, e);
224
+ isErrorPositionsAPI = true;
225
+ }
226
+
227
+
228
+ let isErrorPoolsAPI = false;
229
+ let pools: any[] = [];
230
+ try {
231
+ const res = await axios.get(`https://api.vesu.xyz/pools`);
232
+ const data = await res.data;
233
+ pools = data.data;
234
+ } catch (e) {
235
+ console.error(`${VesuRebalance.name}: Error fetching pools for ${this.address.address}`, e);
236
+ isErrorPoolsAPI = true;
237
+ }
238
+
239
+ const totalAssets = (await this.getTVL()).amount;
240
+
241
+ const info = allowedPools.map(async (p) => {
242
+ const vesuPosition = vesuPositions.find((d: any) => d.pool.id.toString() === num.getDecimalString(p.pool_id.address.toString()));
243
+ const pool = pools.find((d: any) => d.id == num.getDecimalString(p.pool_id.address));
244
+ const assetInfo = pool?.assets.find((d: any) => ContractAddr.from(this.asset().address).eqString(d.address));
245
+ let vTokenContract = new Contract(VesuRebalanceAbi, p.v_token.address, this.config.provider);
246
+ const bal = await vTokenContract.balanceOf(this.address.address);
247
+ const assets = await vTokenContract.convert_to_assets(uint256.bnToUint256(bal.toString()));
248
+ const item = {
249
+ pool_id: p.pool_id,
250
+ pool_name: vesuPosition?.pool.name,
251
+ max_weight: p.max_weight,
252
+ current_weight: isErrorPositionsAPI || !vesuPosition ? 0 : Number(Web3Number.fromWei(vesuPosition.collateral.value, this.decimals()).dividedBy(totalAssets.toString()).toFixed(6)),
253
+ v_token: p.v_token,
254
+ amount: Web3Number.fromWei(assets.toString(), this.decimals()),
255
+ usdValue: isErrorPositionsAPI || !vesuPosition ? Web3Number.fromWei("0", this.decimals()) : Web3Number.fromWei(vesuPosition.collateral.usdPrice.value, vesuPosition.collateral.usdPrice.decimals),
256
+ APY: isErrorPoolsAPI || !assetInfo ? {
257
+ baseApy: 0,
258
+ defiSpringApy: 0,
259
+ netApy: 0,
260
+ } : {
261
+ baseApy: Number(Web3Number.fromWei(assetInfo.stats.supplyApy.value, assetInfo.stats.supplyApy.decimals).toFixed(6)),
262
+ defiSpringApy: Number(Web3Number.fromWei(assetInfo.stats.defiSpringSupplyApr.value, assetInfo.stats.defiSpringSupplyApr.decimals).toFixed(6)),
263
+ netApy: 0,
264
+ },
265
+ currentUtilization: isErrorPoolsAPI || !assetInfo ? 0 : Number(Web3Number.fromWei(assetInfo.stats.currentUtilization.value, assetInfo.stats.currentUtilization.decimals).toFixed(6)),
266
+ maxUtilization: isErrorPoolsAPI || !assetInfo ? 0 : Number(Web3Number.fromWei(assetInfo.config.maxUtilization.value, assetInfo.config.maxUtilization.decimals).toFixed(6)),
267
+ }
268
+ item.APY.netApy = item.APY.baseApy + item.APY.defiSpringApy;
269
+ return item;
270
+ });
271
+ const data = await Promise.all(info);
272
+ return {
273
+ data,
274
+ isErrorPositionsAPI,
275
+ isErrorPoolsAPI,
276
+ isError: isErrorPositionsAPI || isErrorPoolsAPI,
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Calculates the weighted average APY across all pools based on USD value.
282
+ * @returns {Promise<number>} The weighted average APY across all pools
283
+ */
284
+ async netAPY(): Promise<number> {
285
+ const { data: pools } = await this.getPools();
286
+ return this.netAPYGivenPools(pools);
287
+ }
288
+
289
+ /**
290
+ * Calculates the weighted average APY across all pools based on USD value.
291
+ * @returns {Promise<number>} The weighted average APY across all pools
292
+ */
293
+ netAPYGivenPools(pools: PoolInfoFull[]): number {
294
+ const weightedApy = pools.reduce((acc: number, curr) => {
295
+ const weight = curr.current_weight;
296
+ return acc + (curr.APY.netApy * weight);
297
+ }, 0);
298
+
299
+ return weightedApy;
300
+ }
301
+
302
+ /**
303
+ * Calculates optimal position changes to maximize APY while respecting max weights.
304
+ * The algorithm:
305
+ * 1. Sorts pools by APY (highest first)
306
+ * 2. Calculates target amounts based on max weights
307
+ * 3. For each pool that needs more funds:
308
+ * - Takes funds from lowest APY pools that are over their target
309
+ * 4. Validates that total assets remain constant
310
+ *
311
+ * @returns {Promise<{
312
+ * changes: Change[],
313
+ * finalPools: PoolInfoFull[],
314
+ * isAnyPoolOverMaxWeight: boolean
315
+ * }>} Object containing:
316
+ * - changes: Array of position changes
317
+ * - finalPools: Array of pool information after rebalance
318
+ * @throws Error if rebalance is not possible while maintaining constraints
319
+ */
320
+ async getRebalancedPositions() {
321
+ const { data: pools } = await this.getPools();
322
+ const totalAssets = (await this.getTVL()).amount;
323
+ if (totalAssets.eq(0)) return {
324
+ changes: [],
325
+ finalPools: [],
326
+ }
327
+
328
+ // assert sum of pools.amount <= totalAssets
329
+ const sumPools = pools.reduce((acc, curr) => acc.plus(curr.amount.toString()), Web3Number.fromWei("0", this.decimals()));
330
+ assert(sumPools.lte(totalAssets), 'Sum of pools.amount must be less than or equal to totalAssets');
331
+
332
+ // Sort pools by APY and calculate target amounts
333
+ const sortedPools = [...pools].sort((a, b) => b.APY.netApy - a.APY.netApy);
334
+ const targetAmounts: Record<string, Web3Number> = {};
335
+ let remainingAssets = totalAssets;
336
+
337
+ // First pass: Allocate to high APY pools up to their max weight
338
+ let isAnyPoolOverMaxWeight = false;
339
+ for (const pool of sortedPools) {
340
+ const maxAmount = totalAssets.multipliedBy(pool.max_weight * 0.9); // 10% tolerance
341
+ const targetAmount = remainingAssets.gte(maxAmount) ? maxAmount : remainingAssets;
342
+ targetAmounts[pool.pool_id.address.toString()] = targetAmount;
343
+ remainingAssets = remainingAssets.minus(targetAmount.toString());
344
+ if (pool.current_weight > pool.max_weight) {
345
+ isAnyPoolOverMaxWeight = true;
346
+ }
347
+ }
348
+
349
+ assert(remainingAssets.lt(0.00001), 'Remaining assets must be 0');
350
+
351
+ // Calculate required changes
352
+ const changes: Change[] = sortedPools.map(pool => {
353
+ const target = targetAmounts[pool.pool_id.address.toString()] || Web3Number.fromWei("0", this.decimals());
354
+ const change = Web3Number.fromWei(target.minus(pool.amount.toString()).toWei(), this.decimals());
355
+ return {
356
+ pool_id: pool.pool_id,
357
+ changeAmt: change,
358
+ finalAmt: target,
359
+ isDeposit: change.gt(0)
360
+ };
361
+ });
362
+
363
+ // Validate changes
364
+ const sumChanges = changes.reduce((sum, c) => sum.plus(c.changeAmt.toString()), Web3Number.fromWei("0", this.decimals()));
365
+ const sumFinal = changes.reduce((sum, c) => sum.plus(c.finalAmt.toString()), Web3Number.fromWei("0", this.decimals()));
366
+ const hasChanges = changes.some(c => !c.changeAmt.eq(0));
367
+
368
+ if (!sumChanges.eq(0)) throw new Error('Sum of changes must be zero');
369
+ if (!sumFinal.eq(totalAssets)) throw new Error('Sum of final amounts must equal total assets');
370
+ if (!hasChanges) throw new Error('No changes required');
371
+
372
+ const finalPools: PoolInfoFull[] = pools.map((p) => {
373
+ const target = targetAmounts[p.pool_id.address.toString()] || Web3Number.fromWei("0", this.decimals());
374
+ return {
375
+ ...p,
376
+ amount: target,
377
+ usdValue: Web3Number.fromWei("0", this.decimals()),
378
+ }
379
+ });
380
+ return {
381
+ changes,
382
+ finalPools,
383
+ isAnyPoolOverMaxWeight,
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Creates a rebalance Call object for the strategy contract
389
+ * @param pools - Array of pool information including IDs, weights, amounts, APYs and utilization
390
+ * @returns Populated contract call for rebalance
391
+ */
392
+ async getRebalanceCall(
393
+ pools: Awaited<ReturnType<typeof this.getRebalancedPositions>>['changes'],
394
+ isOverWeightAdjustment: boolean // here, yield increase doesnt matter
395
+ ) {
396
+ const actions: any[] = [];
397
+ // sort to put withdrawals first
398
+ pools.sort((a, b) => b.isDeposit ? -1 : 1);
399
+ console.log('pools', pools);
400
+ pools.forEach((p) => {
401
+ if (p.changeAmt.eq(0)) return null;
402
+ actions.push({
403
+ pool_id: p.pool_id.address,
404
+ feature: new CairoCustomEnum(p.isDeposit ? {DEPOSIT: {}} : {WITHDRAW: {}}),
405
+ token: this.asset().address,
406
+ amount: uint256.bnToUint256(p.changeAmt.multipliedBy(p.isDeposit ? 1 : -1).toWei()),
407
+ });
408
+ });
409
+ if (actions.length === 0) return null;
410
+ if (isOverWeightAdjustment) {
411
+ return this.contract.populate('rebalance_weights', [actions]);
412
+ }
413
+ return this.contract.populate('rebalance', [actions]);
414
+ }
415
+ }
416
+
417
+ const _description = 'Automatically diversify {{TOKEN}} holdings into different Vesu pools while reducing risk and maximizing yield. Defi spring STRK Rewards are auto-compounded as well.'
418
+ const _protocol: IProtocol = {name: 'Vesu', logo: 'https://static-assets-8zct.onrender.com/integrations/vesu/logo.png'}
419
+ /**
420
+ * Represents the Vesu Rebalance Strategies.
421
+ */
422
+ export const VesuRebalanceStrategies: IStrategyMetadata[] = [{
423
+ name: 'Vesu STRK',
424
+ description: _description.replace('{{TOKEN}}', 'STRK'),
425
+ address: ContractAddr.from('0xeeb729d554ae486387147b13a9c8871bc7991d454e8b5ff570d4bf94de71e1'),
426
+ type: 'ERC4626',
427
+ depositTokens: [Global.getDefaultTokens().find(t => t.symbol === 'STRK')!],
428
+ protocols: [_protocol]
429
+ }]
@@ -1,6 +1,3 @@
1
- export * from './store';
2
- export * from './encrypt';
3
-
4
1
  // Utility type to make all optional properties required
5
2
  export type RequiredFields<T> = {
6
3
  [K in keyof T]-?: T[K]
@@ -9,4 +6,10 @@ export type RequiredFields<T> = {
9
6
  // Utility type to get only the required fields of a type
10
7
  export type RequiredKeys<T> = {
11
8
  [K in keyof T]-?: {} extends Pick<T, K> ? never : K
12
- }[keyof T]
9
+ }[keyof T]
10
+
11
+ export function assert(condition: boolean, message: string) {
12
+ if (!condition) {
13
+ throw new Error(message);
14
+ }
15
+ }