shufflecom-calculations 3.3.20 → 4.0.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.0.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": "3980abe5ea8b7672f7abecb880e65504d68375bf"
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
  }