shufflecom-calculations 1.2.4 → 1.3.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.
@@ -0,0 +1,521 @@
1
+ import BigNumber from 'bignumber.js';
2
+ import { hexToBytes } from '../utils/hex-to-bytes';
3
+ const BYTE_TOTAL = BigNumber(256);
4
+ const TOTAL_TILES = BigNumber(37);
5
+
6
+ // Outside Bets
7
+ export enum RouletteParity {
8
+ EVEN = 'EVEN',
9
+ ODD = 'ODD',
10
+ }
11
+
12
+ interface RouletteParityInput {
13
+ parity: RouletteParity;
14
+ amount: BigNumber; // check to be placed for negative numbers
15
+ }
16
+
17
+ export enum RouletteColor {
18
+ RED = 'RED',
19
+ BLACK = 'BLACK',
20
+ }
21
+
22
+ interface ColorInput {
23
+ color: RouletteColor;
24
+ amount: BigNumber;
25
+ }
26
+
27
+ export enum RouletteColumn {
28
+ TOP = 'TOP',
29
+ MIDDLE = 'MIDDLE',
30
+ BOTTOM = 'BOTTOM',
31
+ }
32
+
33
+ interface ColumnInput {
34
+ column: RouletteColumn;
35
+ amount: BigNumber;
36
+ }
37
+
38
+ export enum RouletteDozen {
39
+ FIRST = 'FIRST',
40
+ SECOND = 'SECOND',
41
+ THIRD = 'THIRD',
42
+ }
43
+
44
+ interface DozenInput {
45
+ dozen: RouletteDozen;
46
+ amount: BigNumber;
47
+ }
48
+
49
+ export enum RouletteHalf {
50
+ LOW = 'LOW',
51
+ HIGH = 'HIGH',
52
+ }
53
+
54
+ interface HalfInput {
55
+ half: RouletteHalf;
56
+ amount: BigNumber;
57
+ }
58
+
59
+ // Inside bets involving 0
60
+ // Straight 0
61
+ // Split 0,1/0,2/0,3
62
+ // Street 0,1,2/0,2,3 (0,1,3 is not included as it's not physically on the board)
63
+ // Corner 0,1,2,3
64
+ // No double street
65
+
66
+ export const STRAIGHT_VALUES = [
67
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36,
68
+ ] as const;
69
+ export const STREET_VALUES = [
70
+ // these 2 are the only street that are not allowed in a double street as it's not physically possible on the board
71
+ [0, 1, 2],
72
+ [0, 2, 3],
73
+ // every other adjacent street below is a valid double street pair
74
+ [1, 2, 3],
75
+ [4, 5, 6],
76
+ [7, 8, 9],
77
+ [10, 11, 12],
78
+ [13, 14, 15],
79
+ [16, 17, 18],
80
+ [19, 20, 21],
81
+ [22, 23, 24],
82
+ [25, 26, 27],
83
+ [28, 29, 30],
84
+ [31, 32, 33],
85
+ [34, 35, 36],
86
+ ] as const;
87
+
88
+ export type straightValue = (typeof STRAIGHT_VALUES)[number];
89
+
90
+ export type streetValue = (typeof STREET_VALUES)[number];
91
+
92
+ export interface StraightInput {
93
+ straightNumber: straightValue;
94
+ amount: BigNumber;
95
+ }
96
+
97
+ export interface SplitInput {
98
+ firstNumber: straightValue;
99
+ secondNumber: straightValue;
100
+ amount: BigNumber;
101
+ }
102
+
103
+ export interface CornerInput {
104
+ firstNumber: straightValue;
105
+ secondNumber: straightValue;
106
+ thirdNumber: straightValue;
107
+ fourthNumber: straightValue;
108
+
109
+ amount: BigNumber;
110
+ }
111
+
112
+ export interface StreetInput {
113
+ street: streetValue;
114
+ amount: BigNumber;
115
+ }
116
+
117
+ export interface DoubleStreetInput {
118
+ firstStreet: streetValue;
119
+ secondStreet: streetValue;
120
+ amount: BigNumber;
121
+ }
122
+
123
+ // !: Needs to match RoulettePlayInput input type
124
+ export interface RouletteInput {
125
+ // outside bets
126
+ parityValues: RouletteParityInput[];
127
+ colorValues: ColorInput[];
128
+ halfValues: HalfInput[];
129
+ columnValues: ColumnInput[];
130
+ dozenValues: DozenInput[];
131
+
132
+ // inside bets
133
+ straightValues: StraightInput[];
134
+ splitValues: SplitInput[];
135
+ streetValues: StreetInput[]; // number is the smallest number in the column
136
+ cornerValues: CornerInput[]; // check to be done that it's a valid corner
137
+ doubleStreetValues: DoubleStreetInput[]; // firstNumber is the smallest number in the column, secondNumber is the smallest number in the adjacent column
138
+ }
139
+
140
+ const isValidStraight = (num: number): boolean => {
141
+ return STRAIGHT_VALUES.includes(num as straightValue);
142
+ };
143
+
144
+ export const isValidStreet = (street: streetValue): boolean => {
145
+ const sortedStreet = (street as unknown as number[]).sort((a, b) => a - b);
146
+
147
+ return sortedStreet.length === 3 && STREET_VALUES.some((val) => val[0] === sortedStreet[0] && val[1] === sortedStreet[1] && val[2] === sortedStreet[2]);
148
+ };
149
+
150
+ // for corners (up, down, left, right, and diagonally adjacent)
151
+ export const cornerNumbers = (num: straightValue): straightValue[] => {
152
+ if (!isValidStraight(num)) {
153
+ return [];
154
+ }
155
+
156
+ // need to hardcode as 0 is a special case
157
+ if (num === 0) {
158
+ return [1, 2, 3];
159
+ } else if (num === 1) {
160
+ return [0, 2, 3, 4, 5];
161
+ } else if (num === 2) {
162
+ return [0, 1, 3, 4, 5, 6];
163
+ } else if (num === 3) {
164
+ return [0, 1, 2, 5, 6];
165
+ }
166
+
167
+ // when num is 0
168
+ let possibleCornerNumbers: number[] = [];
169
+ if (num % 3 === 1) {
170
+ possibleCornerNumbers = [num - 3, num - 2, num + 1, num + 3, num + 4];
171
+ } else if (num % 3 === 2) {
172
+ possibleCornerNumbers = [num - 4, num - 3, num - 2, num - 1, num + 1, num + 2, num + 3, num + 4];
173
+ } else {
174
+ possibleCornerNumbers = [num - 4, num - 3, num - 1, num + 2, num + 3];
175
+ }
176
+
177
+ return possibleCornerNumbers.filter((num) => isValidStraight(num)) as straightValue[];
178
+ };
179
+
180
+ // up, down, left, right
181
+ export const isValidSplit = (firstNumber: straightValue, secondNumber: straightValue): boolean => {
182
+ if (!isValidStraight(firstNumber) || !isValidStraight(secondNumber)) {
183
+ return false;
184
+ }
185
+
186
+ let possibleSplitNumbers: number[] = [];
187
+
188
+ if (firstNumber % 3 === 1) {
189
+ possibleSplitNumbers = [firstNumber - 3, firstNumber + 1, firstNumber + 3];
190
+ } else if (firstNumber % 3 === 2) {
191
+ possibleSplitNumbers = [firstNumber - 3, firstNumber - 1, firstNumber + 1, firstNumber + 3];
192
+ } else {
193
+ possibleSplitNumbers = [firstNumber - 3, firstNumber - 1, firstNumber + 3];
194
+ }
195
+
196
+ const filteredPossibleSplitNumbers = possibleSplitNumbers.filter((num) => isValidStraight(num)) as straightValue[];
197
+
198
+ if (firstNumber !== 0 && secondNumber !== 0) {
199
+ return filteredPossibleSplitNumbers.includes(secondNumber);
200
+ } else if (firstNumber === 0) {
201
+ return secondNumber === 1 || secondNumber === 2 || secondNumber === 3;
202
+ } else if (secondNumber === 0) {
203
+ return firstNumber === 1 || firstNumber === 2 || firstNumber === 3;
204
+ }
205
+
206
+ return false; // should never reach here
207
+ };
208
+
209
+ export const isValidCorner = (firstNumber: straightValue, secondNumber: straightValue, thirdNumber: straightValue, fourthNumber: straightValue): boolean => {
210
+ // all 4 numbers MUST be adjacent to every other number
211
+ const numbersAdjacentToFirstNumber = cornerNumbers(firstNumber);
212
+ const numbersAdjacentToSecondNumber = cornerNumbers(secondNumber);
213
+ const numbersAdjacentToThirdNumber = cornerNumbers(thirdNumber);
214
+ const numbersAdjacentToFourthNumber = cornerNumbers(fourthNumber);
215
+
216
+ const allNumsAdjacentToFirstNumber =
217
+ numbersAdjacentToFirstNumber.includes(secondNumber) &&
218
+ numbersAdjacentToFirstNumber.includes(thirdNumber) &&
219
+ numbersAdjacentToFirstNumber.includes(fourthNumber);
220
+
221
+ const allNumsAdjacentToSecondNumber =
222
+ numbersAdjacentToSecondNumber.includes(firstNumber) &&
223
+ numbersAdjacentToSecondNumber.includes(thirdNumber) &&
224
+ numbersAdjacentToSecondNumber.includes(fourthNumber);
225
+
226
+ const allNumsAdjacentToThirdNumber =
227
+ numbersAdjacentToThirdNumber.includes(firstNumber) &&
228
+ numbersAdjacentToThirdNumber.includes(secondNumber) &&
229
+ numbersAdjacentToThirdNumber.includes(fourthNumber);
230
+
231
+ const allNumsAdjacentToFourthNumber =
232
+ numbersAdjacentToFourthNumber.includes(firstNumber) &&
233
+ numbersAdjacentToFourthNumber.includes(secondNumber) &&
234
+ numbersAdjacentToFourthNumber.includes(thirdNumber);
235
+
236
+ return allNumsAdjacentToFirstNumber && allNumsAdjacentToSecondNumber && allNumsAdjacentToThirdNumber && allNumsAdjacentToFourthNumber;
237
+ };
238
+
239
+ export const isValidDoubleStreet = (firstStreet: streetValue, secondStreet: streetValue): boolean => {
240
+ if (!isValidStreet(firstStreet) || !isValidStreet(secondStreet)) {
241
+ return false;
242
+ }
243
+
244
+ const firstNumOfFirstStreet = firstStreet[0];
245
+ const firstNumOfSecondStreet = secondStreet[0];
246
+
247
+ // 0 is not in any valid double streets
248
+ if (firstNumOfFirstStreet === 0 || firstNumOfSecondStreet === 0) {
249
+ return false;
250
+ }
251
+
252
+ return firstNumOfFirstStreet + 3 === firstNumOfSecondStreet || firstNumOfFirstStreet - 3 === firstNumOfSecondStreet;
253
+ };
254
+
255
+ export enum BetType {
256
+ // outside bets
257
+ PARITY = 'PARITY',
258
+ COLOR = 'COLOR',
259
+ HALF = 'HALF',
260
+ COLUMN = 'COLUMN',
261
+ DOZEN = 'DOZEN',
262
+
263
+ // inside bets
264
+ STRAIGHT = 'STRAIGHT',
265
+ SPLIT = 'SPLIT',
266
+ STREET = 'STREET',
267
+ CORNER = 'CORNER',
268
+ DOUBLE_STREET = 'DOUBLE_STREET',
269
+ }
270
+
271
+ // payout, not profit
272
+ const PAYOUT_MAP: Record<BetType, BigNumber> = {
273
+ [BetType.PARITY]: BigNumber(2),
274
+ [BetType.COLOR]: BigNumber(2),
275
+ [BetType.HALF]: BigNumber(2),
276
+ [BetType.COLUMN]: BigNumber(3),
277
+ [BetType.DOZEN]: BigNumber(3),
278
+ [BetType.STRAIGHT]: BigNumber(36),
279
+ [BetType.SPLIT]: BigNumber(18),
280
+ [BetType.STREET]: BigNumber(12),
281
+ [BetType.CORNER]: BigNumber(9),
282
+ [BetType.DOUBLE_STREET]: BigNumber(6),
283
+ };
284
+
285
+ export const RED_NUMBERS = [1, 3, 5, 7, 9, 12, 14, 16, 18, 19, 21, 23, 25, 27, 30, 32, 34, 36];
286
+ export const BLACK_NUMBERS = [2, 4, 6, 8, 10, 11, 13, 15, 17, 20, 22, 24, 26, 28, 29, 31, 33, 35];
287
+
288
+ // Payout stuff
289
+ export const parityPayout = ({ parity, amount }: RouletteParityInput, result: straightValue): BigNumber => {
290
+ if (result === 0) return BigNumber(0);
291
+
292
+ const resultParity = result % 2 === 0 ? RouletteParity.EVEN : RouletteParity.ODD;
293
+ const isWin = parity === resultParity;
294
+
295
+ if (isWin) {
296
+ return amount.multipliedBy(PAYOUT_MAP[BetType.PARITY]);
297
+ }
298
+ return BigNumber(0);
299
+ };
300
+
301
+ export const colorPayout = ({ color, amount }: ColorInput, result: straightValue): BigNumber => {
302
+ const isRed = RED_NUMBERS.includes(result);
303
+ const isBlack = BLACK_NUMBERS.includes(result);
304
+
305
+ if ((isRed && color === RouletteColor.RED) || (isBlack && color === RouletteColor.BLACK)) {
306
+ return amount.multipliedBy(PAYOUT_MAP[BetType.COLOR]);
307
+ }
308
+
309
+ return BigNumber(0);
310
+ };
311
+
312
+ export const halfsPayout = ({ half, amount }: HalfInput, result: straightValue): BigNumber => {
313
+ const isLow = result > 0 && result <= 18;
314
+ const isHigh = result > 18 && result <= 36;
315
+
316
+ if ((isLow && half === RouletteHalf.LOW) || (isHigh && half === RouletteHalf.HIGH)) {
317
+ return amount.multipliedBy(PAYOUT_MAP[BetType.HALF]);
318
+ }
319
+
320
+ return BigNumber(0);
321
+ };
322
+
323
+ export const columnPayout = ({ column, amount }: ColumnInput, result: straightValue): BigNumber => {
324
+ const topColumn = [3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36];
325
+ const middleColumn = [2, 5, 8, 11, 14, 17, 20, 23, 26, 29, 32, 35];
326
+ const bottomColumn = [1, 4, 7, 10, 13, 16, 19, 22, 25, 28, 31, 34];
327
+
328
+ if (
329
+ (column === RouletteColumn.TOP && topColumn.includes(result)) ||
330
+ (column === RouletteColumn.MIDDLE && middleColumn.includes(result)) ||
331
+ (column === RouletteColumn.BOTTOM && bottomColumn.includes(result))
332
+ ) {
333
+ return amount.multipliedBy(PAYOUT_MAP[BetType.COLUMN]);
334
+ }
335
+
336
+ return BigNumber(0);
337
+ };
338
+
339
+ export const dozenPayout = ({ dozen, amount }: DozenInput, result: straightValue): BigNumber => {
340
+ const firstDozen = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
341
+ const secondDozen = [13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24];
342
+ const thirdDozen = [25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36];
343
+
344
+ if (
345
+ (dozen === RouletteDozen.FIRST && firstDozen.includes(result)) ||
346
+ (dozen === RouletteDozen.SECOND && secondDozen.includes(result)) ||
347
+ (dozen === RouletteDozen.THIRD && thirdDozen.includes(result))
348
+ ) {
349
+ return amount.multipliedBy(PAYOUT_MAP[BetType.DOZEN]);
350
+ }
351
+
352
+ return BigNumber(0);
353
+ };
354
+
355
+ export const straightPayout = ({ straightNumber, amount }: StraightInput, result: straightValue): BigNumber => {
356
+ if (result === straightNumber) {
357
+ return amount.multipliedBy(PAYOUT_MAP[BetType.STRAIGHT]);
358
+ }
359
+
360
+ return BigNumber(0);
361
+ };
362
+
363
+ export const splitPayout = ({ firstNumber, secondNumber, amount }: SplitInput, result: straightValue): BigNumber => {
364
+ if (result === firstNumber || result === secondNumber) {
365
+ return amount.multipliedBy(PAYOUT_MAP[BetType.SPLIT]);
366
+ }
367
+
368
+ return BigNumber(0);
369
+ };
370
+
371
+ export const streetPayout = ({ street, amount }: StreetInput, result: straightValue): BigNumber => {
372
+ if ((street as readonly number[]).includes(result)) {
373
+ return amount.multipliedBy(PAYOUT_MAP[BetType.STREET]);
374
+ }
375
+
376
+ return BigNumber(0);
377
+ };
378
+
379
+ export const cornerPayout = ({ firstNumber, secondNumber, thirdNumber, fourthNumber, amount }: CornerInput, result: straightValue): BigNumber => {
380
+ const cornerNumbers = [firstNumber, secondNumber, thirdNumber, fourthNumber];
381
+
382
+ if (cornerNumbers.includes(result)) {
383
+ return amount.multipliedBy(PAYOUT_MAP[BetType.CORNER]);
384
+ }
385
+
386
+ return BigNumber(0);
387
+ };
388
+
389
+ export const doubleStreetPayout = ({ firstStreet, secondStreet, amount }: DoubleStreetInput, result: straightValue): BigNumber => {
390
+ if ((firstStreet as readonly number[]).includes(result) || (secondStreet as readonly number[]).includes(result)) {
391
+ return amount.multipliedBy(PAYOUT_MAP[BetType.DOUBLE_STREET]);
392
+ }
393
+
394
+ return BigNumber(0);
395
+ };
396
+
397
+ export class Roulette {
398
+ // get a whole number from 0-36 inclusive
399
+ static getResult(hexStr: string): straightValue {
400
+ const gameResultBytes = hexToBytes(hexStr);
401
+
402
+ let result = BigNumber(0);
403
+
404
+ // Only use the first 4
405
+ for (let i = 0; i < 4; i++) {
406
+ const value = gameResultBytes[i];
407
+ result = result.plus(BigNumber(value).dividedBy(BYTE_TOTAL.pow(i + 1)));
408
+ }
409
+
410
+ const resultNum = result.multipliedBy(TOTAL_TILES).integerValue(BigNumber.ROUND_DOWN).toNumber() as straightValue;
411
+
412
+ if (!isValidStraight(resultNum)) {
413
+ throw new Error('Invalid result');
414
+ }
415
+
416
+ return resultNum;
417
+ }
418
+
419
+ static getPayout(inputs: RouletteInput, result: straightValue): BigNumber {
420
+ if (!this.validateInputs(inputs)) {
421
+ throw new Error('Invalid inputs');
422
+ }
423
+
424
+ const {
425
+ // outside bets
426
+ parityValues,
427
+ colorValues,
428
+ halfValues,
429
+ columnValues,
430
+ dozenValues,
431
+
432
+ // inside bets
433
+ straightValues,
434
+ splitValues,
435
+ streetValues,
436
+ cornerValues,
437
+ doubleStreetValues,
438
+ } = inputs;
439
+
440
+ const totalParityPayout = parityValues.reduce((totalPayout, input) => {
441
+ return totalPayout.plus(parityPayout(input, result));
442
+ }, BigNumber(0));
443
+
444
+ const totalColorPayout = colorValues.reduce((totalPayout, input) => {
445
+ return totalPayout.plus(colorPayout(input, result));
446
+ }, BigNumber(0));
447
+
448
+ const totalHalfsPayout = halfValues.reduce((totalPayout, input) => {
449
+ return totalPayout.plus(halfsPayout(input, result));
450
+ }, BigNumber(0));
451
+
452
+ const totalColumnsPayout = columnValues.reduce((totalPayout, input) => {
453
+ return totalPayout.plus(columnPayout(input, result));
454
+ }, BigNumber(0));
455
+
456
+ const totalDozensPayout = dozenValues.reduce((totalPayout, input) => {
457
+ return totalPayout.plus(dozenPayout(input, result));
458
+ }, BigNumber(0));
459
+
460
+ const totalStraightsPayout = straightValues.reduce((totalPayout, input) => {
461
+ return totalPayout.plus(straightPayout(input, result));
462
+ }, BigNumber(0));
463
+
464
+ const totalSplitPayout = splitValues.reduce((totalPayout, input) => {
465
+ return totalPayout.plus(splitPayout(input, result));
466
+ }, BigNumber(0));
467
+
468
+ const totalStreetPayout = streetValues.reduce((totalPayout, input) => {
469
+ return totalPayout.plus(streetPayout(input, result));
470
+ }, BigNumber(0));
471
+
472
+ const totalCornerPayout = cornerValues.reduce((totalPayout, input) => {
473
+ return totalPayout.plus(cornerPayout(input, result));
474
+ }, BigNumber(0));
475
+
476
+ const totalDoubleStreetPayout = doubleStreetValues.reduce((totalPayout, input) => {
477
+ return totalPayout.plus(doubleStreetPayout(input, result));
478
+ }, BigNumber(0));
479
+
480
+ const totalPayout = totalParityPayout
481
+ .plus(totalColorPayout)
482
+ .plus(totalHalfsPayout)
483
+ .plus(totalColumnsPayout)
484
+ .plus(totalDozensPayout)
485
+ .plus(totalStraightsPayout)
486
+ .plus(totalSplitPayout)
487
+ .plus(totalStreetPayout)
488
+ .plus(totalCornerPayout)
489
+ .plus(totalDoubleStreetPayout);
490
+
491
+ return totalPayout;
492
+ }
493
+
494
+ static validateInputs(inputs: RouletteInput): boolean {
495
+ const { straightValues, splitValues, streetValues, cornerValues, doubleStreetValues, parityValues, colorValues, columnValues, dozenValues, halfValues } =
496
+ inputs;
497
+ // user must bet on at least one thing
498
+ if (
499
+ straightValues.length === 0 &&
500
+ splitValues.length === 0 &&
501
+ streetValues.length === 0 &&
502
+ cornerValues.length === 0 &&
503
+ doubleStreetValues.length === 0 &&
504
+ parityValues.length === 0 &&
505
+ colorValues.length === 0 &&
506
+ columnValues.length === 0 &&
507
+ dozenValues.length === 0 &&
508
+ halfValues.length === 0
509
+ ) {
510
+ return false;
511
+ }
512
+
513
+ const straightsValid = straightValues.every((input) => isValidStraight(input.straightNumber));
514
+ const splitsValid = splitValues.every((input) => isValidSplit(input.firstNumber, input.secondNumber));
515
+ const streetsValid = streetValues.every((input) => isValidStreet(input.street));
516
+ const cornersValid = cornerValues.every((input) => isValidCorner(input.firstNumber, input.secondNumber, input.thirdNumber, input.fourthNumber));
517
+ const doubleStreetsValid = doubleStreetValues.every((input) => isValidDoubleStreet(input.firstStreet, input.secondStreet));
518
+
519
+ return straightsValid && splitsValid && streetsValid && cornersValid && doubleStreetsValid;
520
+ }
521
+ }
package/src/index.ts CHANGED
@@ -9,6 +9,7 @@ export { VipBonusIssuanceStatus } from './utils/vip-bonus.type';
9
9
  export { deriveBonusIssuances } from './utils/derive-vip-bonus';
10
10
  export { convertBs58ToUuid, convertUuidToBs58 } from './utils/uuid-converter';
11
11
  export { Dice, DiceDirection } from './games/dice';
12
+ export { Roulette } from './games/roulette';
12
13
  export { Mines } from './games/mines';
13
14
  export { Plinko, PlinkoRiskLevel } from './games/plinko';
14
15
  export { Crash } from './games/crash';
@@ -6,7 +6,7 @@ export class DatesCalculator {
6
6
  // Most recent thursday at 11:00 UTC
7
7
  static startOfWeeklyBonus = (utcDate: Dayjs) => {
8
8
  if (!utcDate.isUTC()) {
9
- throw 'utcDate must be in UTC';
9
+ throw new Error('utcDate must be in UTC');
10
10
  }
11
11
 
12
12
  const thisThursday = utcDate.startOf('week').set('day', 4).set('hour', 11);
@@ -21,7 +21,7 @@ export class DatesCalculator {
21
21
  // Most recent first friday of a month at 00:00 UTC
22
22
  static startOfMonthlyBonus = (utcDate: Dayjs): Date => {
23
23
  if (!utcDate.isUTC()) {
24
- throw 'utcDate must be in UTC';
24
+ throw new Error('utcDate must be in UTC');
25
25
  }
26
26
 
27
27
  const startOfMonth = utcDate.startOf('month');