shufflecom-calculations 3.3.20 → 4.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shufflecom-calculations",
3
- "version": "3.3.20",
3
+ "version": "4.1.0",
4
4
  "description": "",
5
5
  "types": "lib/index.d.ts",
6
6
  "main": "lib/index.js",
@@ -14,5 +14,5 @@
14
14
  },
15
15
  "author": "",
16
16
  "license": "ISC",
17
- "gitHead": "680a93d4430f249efb6b00ac6a79421a9dcf71a1"
17
+ "gitHead": "12f611596255710c364326a119bf766ee459e0a5"
18
18
  }
@@ -1,119 +1,137 @@
1
1
  import { createHmac, randomBytes } from 'crypto';
2
- import { LOTTERY_POWER_BALL_MAX_NUMBER, LOTTERY_PRIMARY_BALL_COUNT, LOTTERY_PRIMARY_BALL_MAX_NUMBER, Lottery } from './lottery';
2
+ import { Lottery, LotteryVersion } from './lottery';
3
+
4
+ function generateSeedAndHashes(version: LotteryVersion) {
5
+ const seed = randomBytes(32).toString('hex');
6
+ const btcBlockHash = randomBytes(32).toString('hex');
7
+ return {
8
+ seed,
9
+ btcBlockHash,
10
+ results: Lottery.generateBalls(version, createHmac('sha256', seed).update(btcBlockHash).digest('hex')),
11
+ };
12
+ }
3
13
 
4
14
  describe('Lottery', () => {
5
15
  it('lottery provably fair', () => {
6
- const lotterySeeds = [
7
- {
8
- seed: '7068df16f5ed60bd856c956df8437504ddc8a14e301201f08cb2d9c8457f57dd',
9
- btcBlockHash: '90d543f6352a959285472a3b5a259513c38bb0baa5af3a28e59a759983da1cb2',
10
- results: [51, 48, 37, 29, 49, 7],
11
- },
12
- {
13
- seed: '9b437ae3c72373f0d07db77fa8784ba9df669b9aaf6fb2867683d1ca6aba149b',
14
- btcBlockHash: '780f71593352440989e65a4a752ae8a6ee73c940206a5608ff5a0fcd719db437',
15
- results: [43, 46, 35, 44, 1, 13],
16
- },
17
- {
18
- seed: '1b42b3477981fc3bfbcedc4fbe598b2938aeb5564371be115911f18b2f781922',
19
- btcBlockHash: 'c66271152aa3869660c109a16ddf2e3cb45379a237d2f8d4e3cc0d888a833b79',
20
- results: [49, 28, 12, 10, 30, 6],
21
- },
22
- {
23
- seed: 'f77706a9561669febcf626adc0cbeb8ed0bffce0a26918deb4d5bacbd840545f',
24
- btcBlockHash: '2178204156e99463104b3fdaa5149156939cf8c168b354407d804173b2c64c76',
25
- results: [2, 40, 32, 9, 18, 10],
26
- },
27
- {
28
- seed: '559ac610caf66e960f8c3396a85cf9258bb14015e40b0649920ab772d4748d5b',
29
- btcBlockHash: 'e99de92f4da4c38d9978324ddbf3ab4134ea9f1e3ae288d6fa69fdfbc8bef894',
30
- results: [44, 23, 18, 10, 6, 17],
31
- },
32
- {
33
- seed: '6fbe8a32f2c2f5c67af615c2f50e85cfea5a304fd7d5eaef882e994cdad23d1c',
34
- btcBlockHash: '18b761483323262626ed5cca932db1f9e268b93654204b189757d8cde888f2b6',
35
- results: [51, 12, 17, 11, 24, 1],
36
- },
37
- {
38
- seed: '44ca0c4d384b60fb593d63ca59ee85222a33dfa10da7847d241860fee32bed35',
39
- btcBlockHash: '5b15290eda6e2a880f8c8797161887055b13f608aeb7cfc8cba4d44e1167f516',
40
- results: [47, 45, 33, 43, 29, 8],
41
- },
42
- {
43
- seed: '2762358b55a0e7a9b29cbf69c1578d9e5697d95f605d583f2d05b71e25cf5523',
44
- btcBlockHash: 'cba1653303bf96ac3986f5a956f56b029af1950a30cce2407f0e2dca89898433',
45
- results: [10, 38, 5, 18, 30, 12],
46
- },
47
- {
48
- seed: '279340242134b1b5f4fac74b9334ddbacba0c9fc2a48cab4ebc40378193a2def',
49
- btcBlockHash: 'd99ba049066e67852ccbbd4f78144b8c2f93326a8da707a596ba3ea87df67389',
50
- results: [11, 21, 17, 2, 40, 8],
51
- },
52
- {
53
- seed: '7b5912894dd7d4cae1dc0d20ae326b6d66dfe3a1b5654c76c3569cbb92d378d6',
54
- btcBlockHash: '0a0984225605f3756e8f510476389e0bb9d61afefc4bd58438763dcb53b5e7dc',
55
- results: [41, 31, 47, 40, 55, 15],
56
- },
57
- ];
16
+ const lotterySeeds: Record<LotteryVersion, { seed: string; btcBlockHash: string; results: number[] }[]> = {
17
+ [LotteryVersion.V1]: [
18
+ {
19
+ seed: '7068df16f5ed60bd856c956df8437504ddc8a14e301201f08cb2d9c8457f57dd',
20
+ btcBlockHash: '90d543f6352a959285472a3b5a259513c38bb0baa5af3a28e59a759983da1cb2',
21
+ results: [51, 48, 37, 29, 49, 7],
22
+ },
23
+ {
24
+ seed: 'f77706a9561669febcf626adc0cbeb8ed0bffce0a26918deb4d5bacbd840545f',
25
+ btcBlockHash: '2178204156e99463104b3fdaa5149156939cf8c168b354407d804173b2c64c76',
26
+ results: [2, 40, 32, 9, 18, 10],
27
+ },
28
+ {
29
+ seed: '44ca0c4d384b60fb593d63ca59ee85222a33dfa10da7847d241860fee32bed35',
30
+ btcBlockHash: '5b15290eda6e2a880f8c8797161887055b13f608aeb7cfc8cba4d44e1167f516',
31
+ results: [47, 45, 33, 43, 29, 8],
32
+ },
33
+ {
34
+ seed: '7b5912894dd7d4cae1dc0d20ae326b6d66dfe3a1b5654c76c3569cbb92d378d6',
35
+ btcBlockHash: '0a0984225605f3756e8f510476389e0bb9d61afefc4bd58438763dcb53b5e7dc',
36
+ results: [41, 31, 47, 40, 55, 15],
37
+ },
38
+ ],
39
+ [LotteryVersion.V2]: [
40
+ {
41
+ seed: '7068df16f5ed60bd856c956df8437504ddc8a14e301201f08cb2d9c8457f57dd',
42
+ btcBlockHash: '90d543f6352a959285472a3b5a259513c38bb0baa5af3a28e59a759983da1cb2',
43
+ results: [71, 37, 55, 66, 56, 7],
44
+ },
45
+ {
46
+ seed: '9b437ae3c72373f0d07db77fa8784ba9df669b9aaf6fb2867683d1ca6aba149b',
47
+ btcBlockHash: '780f71593352440989e65a4a752ae8a6ee73c940206a5608ff5a0fcd719db437',
48
+ results: [51, 50, 65, 56, 10, 13],
49
+ },
50
+ {
51
+ seed: '1b42b3477981fc3bfbcedc4fbe598b2938aeb5564371be115911f18b2f781922',
52
+ btcBlockHash: 'c66271152aa3869660c109a16ddf2e3cb45379a237d2f8d4e3cc0d888a833b79',
53
+ results: [58, 69, 28, 27, 2, 6],
54
+ },
55
+ {
56
+ seed: 'f77706a9561669febcf626adc0cbeb8ed0bffce0a26918deb4d5bacbd840545f',
57
+ btcBlockHash: '2178204156e99463104b3fdaa5149156939cf8c168b354407d804173b2c64c76',
58
+ results: [34, 63, 58, 19, 41, 10],
59
+ },
60
+ ],
61
+ };
58
62
 
59
- lotterySeeds.forEach(e => {
60
- const hexToStr = createHmac('sha256', e.seed).update(e.btcBlockHash).digest('hex');
61
- const results = Lottery.generateBalls(hexToStr);
62
- expect(results).toEqual(e.results);
63
+ Object.values(LotteryVersion).forEach(ver => {
64
+ lotterySeeds[ver].forEach(e => {
65
+ const hexToStr = createHmac('sha256', e.seed).update(e.btcBlockHash).digest('hex');
66
+ const results = Lottery.generateBalls(ver, hexToStr);
67
+ expect(results).toEqual(e.results);
68
+ });
63
69
  });
64
70
  });
65
71
 
66
- it('lottery balls check', () => {
67
- const allResults: number[][] = [];
68
- for (let i = 0; i < 5; i++) {
69
- const { results } = generateSeedAndHashes();
70
- allResults.push(results);
71
- }
72
-
73
- allResults.forEach(results => {
74
- const primaryBalls = results.slice(0, 5);
75
- const powerBall = results[5];
72
+ it('lottery config check', () => {
73
+ // DO NOT CHANGE THIS NUMBERS
74
+ const lotteryConfigs: Record<LotteryVersion, { maxPrimaryBall: number; maxPowerBall: number; primaryBallCount: number }> = {
75
+ [LotteryVersion.V1]: { maxPrimaryBall: 55, maxPowerBall: 18, primaryBallCount: 5 },
76
+ [LotteryVersion.V2]: { maxPrimaryBall: 74, maxPowerBall: 18, primaryBallCount: 5 },
77
+ };
76
78
 
77
- expect(primaryBalls.filter(ball => ball < 1 || ball > LOTTERY_PRIMARY_BALL_MAX_NUMBER).length).toBe(0);
78
- expect(new Set(primaryBalls).size).toBe(primaryBalls.length);
79
- expect(powerBall <= LOTTERY_POWER_BALL_MAX_NUMBER && powerBall > 0).toBe(true);
79
+ Object.values(LotteryVersion).forEach(ver => {
80
+ const config = lotteryConfigs[ver];
81
+ expect(Lottery.getConfig(ver)).toMatchObject({
82
+ maxPrimaryBall: config.maxPrimaryBall,
83
+ maxPowerBall: config.maxPowerBall,
84
+ primaryBallCount: config.primaryBallCount,
85
+ });
80
86
  });
81
87
  });
82
88
 
83
- it('lottery random ball generator check', () => {
84
- const checkBalls = (ticketData: number[][]) => {
85
- const endIndex = LOTTERY_PRIMARY_BALL_COUNT;
86
- const betterSet = new Uint8Array(LOTTERY_PRIMARY_BALL_MAX_NUMBER + 1);
89
+ for (const version of Object.values(LotteryVersion)) {
90
+ it(`(${version}) lottery balls check`, () => {
91
+ const allResults: number[][] = [];
92
+ for (let i = 0; i < 5; i++) {
93
+ const { results } = generateSeedAndHashes(version);
94
+ allResults.push(results);
95
+ }
96
+
97
+ const config = Lottery.getConfig(version);
98
+ allResults.forEach(results => {
99
+ const primaryBalls = results.slice(0, config.primaryBallCount);
100
+ const powerBall = results[config.primaryBallCount];
101
+
102
+ expect(primaryBalls.filter(ball => ball < 1 || ball > config.maxPrimaryBall).length).toBe(0);
103
+ expect(new Set(primaryBalls).size).toBe(primaryBalls.length);
104
+ expect(powerBall <= config.maxPowerBall && powerBall > 0).toBe(true);
105
+ });
106
+ });
87
107
 
88
- for (let i = 0; i < ticketData.length; i++) {
89
- const ticket = ticketData[i];
108
+ it(`(${version}) lottery random ball generator check`, () => {
109
+ const config = Lottery.getConfig(version);
90
110
 
91
- if (ticket.length !== LOTTERY_PRIMARY_BALL_COUNT + 1) return 'INVALID_AMOUNT_OF_BALLS';
111
+ const checkBalls = (ticketData: number[][]) => {
112
+ const endIndex = config.primaryBallCount;
113
+ const betterSet = new Uint8Array(config.maxPrimaryBall + 1);
92
114
 
93
- const powerball = ticket[endIndex];
94
- if (powerball < 1 || powerball > LOTTERY_POWER_BALL_MAX_NUMBER) return 'INVALID_POWERBALL';
115
+ for (let i = 0; i < ticketData.length; i++) {
116
+ const ticket = ticketData[i];
95
117
 
96
- betterSet.fill(0);
97
- for (let j = 0; j < endIndex; j++) {
98
- const ball = ticket[j];
99
- if (ball < 1 || ball > LOTTERY_PRIMARY_BALL_MAX_NUMBER || betterSet[ball]) return 'INVALID_PRIMARY_BALL';
100
- betterSet[ball] = 1;
118
+ if (ticket.length !== config.primaryBallCount + 1) return 'INVALID_AMOUNT_OF_BALLS';
119
+
120
+ const powerball = ticket[endIndex];
121
+ if (powerball < 1 || powerball > config.maxPowerBall) return 'INVALID_POWERBALL';
122
+
123
+ betterSet.fill(0);
124
+ for (let j = 0; j < endIndex; j++) {
125
+ const ball = ticket[j];
126
+ if (ball < 1 || ball > config.maxPrimaryBall || betterSet[ball]) return 'INVALID_PRIMARY_BALL';
127
+ betterSet[ball] = 1;
128
+ }
101
129
  }
102
- }
103
- return 'VALID';
104
- };
130
+ return 'VALID';
131
+ };
105
132
 
106
- const ticketData = Lottery.batchGenerateTicketNumbers(1000_000);
107
- expect(checkBalls(ticketData)).toEqual('VALID');
108
- });
133
+ const ticketData = Lottery.batchGenerateTicketNumbers(version, 1000_000);
134
+ expect(checkBalls(ticketData)).toEqual('VALID');
135
+ });
136
+ }
109
137
  });
110
-
111
- function generateSeedAndHashes() {
112
- const seed = randomBytes(32).toString('hex');
113
- const btcBlockHash = randomBytes(32).toString('hex');
114
- return {
115
- seed,
116
- btcBlockHash,
117
- results: Lottery.generateBalls(createHmac('sha256', seed).update(btcBlockHash).digest('hex')),
118
- };
119
- }
@@ -1,67 +1,63 @@
1
1
  import { fisherYatesShuffle } from '../utils/fisher-yates-shuffle';
2
2
 
3
- export const LOTTERY_PRIMARY_BALL_MAX_NUMBER = 55;
4
- export const LOTTERY_POWER_BALL_MAX_NUMBER = 18;
5
- export const LOTTERY_PRIMARY_BALL_COUNT = 5;
3
+ export enum LotteryVersion {
4
+ V1 = 'V1',
5
+ V2 = 'V2',
6
+ }
7
+
8
+ type LotteryConfig = {
9
+ maxPrimaryBall: number;
10
+ maxPowerBall: number;
11
+ primaryBallCount: number;
12
+ };
13
+
14
+ export const LOTTERY_VERSION_RECORD: Record<LotteryVersion, LotteryConfig> = {
15
+ [LotteryVersion.V1]: { maxPrimaryBall: 55, maxPowerBall: 18, primaryBallCount: 5 },
16
+ [LotteryVersion.V2]: { maxPrimaryBall: 74, maxPowerBall: 18, primaryBallCount: 5 },
17
+ };
6
18
 
7
19
  export class Lottery {
8
- static generateBalls(hexStr: string) {
20
+ static readonly version = LotteryVersion.V2;
21
+
22
+ static getConfig(version: LotteryVersion): LotteryConfig {
23
+ return LOTTERY_VERSION_RECORD[version];
24
+ }
25
+
26
+ static generateBalls(version: LotteryVersion, hexStr: string) {
9
27
  const hashed = Buffer.from(hexStr, 'hex');
10
28
 
29
+ const config = this.getConfig(version);
11
30
  const shuffledPrimaryBalls = this.shuffleBalls(
12
- Array.from({ length: LOTTERY_PRIMARY_BALL_MAX_NUMBER }, (_, i) => i + 1),
31
+ Array.from({ length: config.maxPrimaryBall }, (_, i) => i + 1),
13
32
  hashed,
14
33
  );
15
34
 
16
35
  const shuffledPowerBalls = this.shuffleBalls(
17
- Array.from({ length: LOTTERY_POWER_BALL_MAX_NUMBER }, (_, i) => i + 1),
36
+ Array.from({ length: config.maxPowerBall }, (_, i) => i + 1),
18
37
  hashed,
19
38
  );
20
39
 
21
40
  return [...shuffledPrimaryBalls.slice(0, 5), shuffledPowerBalls[0]];
22
41
  }
23
42
 
24
- static shuffleBalls(balls: number[], hashBuffer: Buffer) {
25
- return fisherYatesShuffle(balls, hashBuffer);
26
- }
27
-
28
- static generateRandomBalls() {
43
+ static generateRandomBalls(version: LotteryVersion) {
29
44
  const primaryBalls: Set<number> = new Set();
30
- while (primaryBalls.size < LOTTERY_PRIMARY_BALL_COUNT) {
31
- const randomNumber = Math.floor(Math.random() * LOTTERY_PRIMARY_BALL_MAX_NUMBER) + 1;
45
+ const config = this.getConfig(version);
46
+ while (primaryBalls.size < config.primaryBallCount) {
47
+ const randomNumber = Math.floor(Math.random() * config.maxPrimaryBall) + 1;
32
48
  primaryBalls.add(randomNumber);
33
49
  }
34
50
 
35
- const powerball = Math.floor(Math.random() * LOTTERY_POWER_BALL_MAX_NUMBER) + 1;
51
+ const powerball = Math.floor(Math.random() * config.maxPowerBall) + 1;
36
52
 
37
53
  return [...Array.from(primaryBalls), powerball];
38
54
  }
39
55
 
40
- static batchGenerateTicketNumbers(count: number) {
41
- return Array.from({ length: count }, () => this.generateRandomBalls());
56
+ static batchGenerateTicketNumbers(version: LotteryVersion, count: number) {
57
+ return Array.from({ length: count }, () => this.generateRandomBalls(version));
42
58
  }
43
59
 
44
- static validateTicketNumbers({ ticketData, hasPowerBall }: { ticketData: number[][]; hasPowerBall: boolean }) {
45
- const endIndex = LOTTERY_PRIMARY_BALL_COUNT;
46
- const betterSet = new Uint8Array(LOTTERY_PRIMARY_BALL_MAX_NUMBER + 1);
47
-
48
- for (let i = 0; i < ticketData.length; i++) {
49
- const ticket = ticketData[i];
50
-
51
- if (ticket.length !== LOTTERY_PRIMARY_BALL_COUNT + (hasPowerBall ? 1 : 0)) return false;
52
-
53
- if (hasPowerBall) {
54
- const powerball = ticket[endIndex];
55
- if (powerball < 1 || powerball > LOTTERY_POWER_BALL_MAX_NUMBER) return false;
56
- }
57
-
58
- betterSet.fill(0);
59
- for (let j = 0; j < endIndex; j++) {
60
- const ball = ticket[j];
61
- if (ball < 1 || ball > LOTTERY_PRIMARY_BALL_MAX_NUMBER || betterSet[ball]) return false;
62
- betterSet[ball] = 1;
63
- }
64
- }
65
- return true;
60
+ static shuffleBalls(balls: number[], hashBuffer: Buffer) {
61
+ return fisherYatesShuffle(balls, hashBuffer);
66
62
  }
67
63
  }
@@ -23,3 +23,12 @@ export const VALID_NAME_REGEX = /^[^0-9!@#$%^&*`~()+=[{\]};:"\\|<>\/?]*$/;
23
23
  // U+1160 Hangul Jungseong Filler, U+3164 Hangul Filler
24
24
  export const NO_INVISIBLE_CHAR_REGEX = /^[^\u2800\uFFA0\u115F\u1160\u3164]*$/;
25
25
  export const US_PHONE_NUMBER_REGEX = /^\+1\d{10}$/;
26
+ // E.164 international phone number: leading +, first digit 1-9, total 7-15 digits
27
+ export const E164_PHONE_NUMBER_REGEX = /^\+[1-9]\d{6,14}$/;
28
+ // Brazilian CPF (digits only, no formatting)
29
+ export const CPF_REGEX = /^\d{11}$/;
30
+ // Mexican CURP (18 alphanumeric chars)
31
+ export const CURP_REGEX = /^[A-Z0-9]{18}$/i;
32
+ // Argentine CUIL/CUIT: 11 digits, no formatting, leading with a valid person/entity
33
+ // prefix (20-27 individuals, 30/33/34 legal entities)
34
+ export const CUIL_CUIT_REGEX = /^(20|23|24|25|26|27|30|33|34)\d{9}$/;
@@ -1,16 +1,36 @@
1
1
  import {
2
+ ADDRESS_URL_REGEX,
3
+ CUIL_CUIT_REGEX,
2
4
  HEX_COLOR_CODE_REGEX,
5
+ NO_INVISIBLE_CHAR_REGEX,
3
6
  PROMOTION_CODE_REGEX,
4
7
  PWD_REGEX,
5
- USERNAME_REGEX,
6
8
  TXN_URL_REGEX,
7
- ADDRESS_URL_REGEX,
8
- VALID_NAME_REGEX,
9
+ USERNAME_REGEX,
9
10
  US_PHONE_NUMBER_REGEX,
10
- NO_INVISIBLE_CHAR_REGEX,
11
+ VALID_NAME_REGEX,
11
12
  } from '.';
12
13
 
13
14
  describe('regex', () => {
15
+ describe('CUIL_CUIT_REGEX', () => {
16
+ it('accepts 11 digits with a valid individual or entity prefix', () => {
17
+ expect(CUIL_CUIT_REGEX.test('20123456789')).toBe(true);
18
+ expect(CUIL_CUIT_REGEX.test('27123456789')).toBe(true);
19
+ expect(CUIL_CUIT_REGEX.test('30123456789')).toBe(true);
20
+ });
21
+
22
+ it('rejects an invalid leading prefix', () => {
23
+ expect(CUIL_CUIT_REGEX.test('19123456789')).toBe(false);
24
+ expect(CUIL_CUIT_REGEX.test('28123456789')).toBe(false);
25
+ });
26
+
27
+ it('rejects wrong length or non-digits', () => {
28
+ expect(CUIL_CUIT_REGEX.test('2012345678')).toBe(false); // 10 digits
29
+ expect(CUIL_CUIT_REGEX.test('201234567890')).toBe(false); // 12 digits
30
+ expect(CUIL_CUIT_REGEX.test('20-12345678-9')).toBe(false); // formatted
31
+ });
32
+ });
33
+
14
34
  describe('PWD_REGEX', () => {
15
35
  it('should not allow passwords with less than 8 characters', () => {
16
36
  expect(PWD_REGEX.test('abcdef1')).toBe(false);