shufflecom-calculations 3.3.19 → 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/lib/lottery/lottery.d.ts +16 -10
- package/lib/lottery/lottery.js +27 -37
- package/lib/lottery/lottery.js.map +1 -1
- package/lib/sports/sports-bet.types.d.ts +1 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/lottery/lottery.spec.ts +118 -100
- package/src/lottery/lottery.ts +34 -38
- package/src/sports/sports-bet.types.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shufflecom-calculations",
|
|
3
|
-
"version": "
|
|
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": "
|
|
17
|
+
"gitHead": "3980abe5ea8b7672f7abecb880e65504d68375bf"
|
|
18
18
|
}
|
|
@@ -1,119 +1,137 @@
|
|
|
1
1
|
import { createHmac, randomBytes } from 'crypto';
|
|
2
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
expect(
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
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
|
-
|
|
89
|
-
|
|
108
|
+
it(`(${version}) lottery random ball generator check`, () => {
|
|
109
|
+
const config = Lottery.getConfig(version);
|
|
90
110
|
|
|
91
|
-
|
|
111
|
+
const checkBalls = (ticketData: number[][]) => {
|
|
112
|
+
const endIndex = config.primaryBallCount;
|
|
113
|
+
const betterSet = new Uint8Array(config.maxPrimaryBall + 1);
|
|
92
114
|
|
|
93
|
-
|
|
94
|
-
|
|
115
|
+
for (let i = 0; i < ticketData.length; i++) {
|
|
116
|
+
const ticket = ticketData[i];
|
|
95
117
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
if (
|
|
100
|
-
|
|
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
|
-
|
|
104
|
-
};
|
|
130
|
+
return 'VALID';
|
|
131
|
+
};
|
|
105
132
|
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
}
|
package/src/lottery/lottery.ts
CHANGED
|
@@ -1,67 +1,63 @@
|
|
|
1
1
|
import { fisherYatesShuffle } from '../utils/fisher-yates-shuffle';
|
|
2
2
|
|
|
3
|
-
export
|
|
4
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
25
|
-
return fisherYatesShuffle(balls, hashBuffer);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
static generateRandomBalls() {
|
|
43
|
+
static generateRandomBalls(version: LotteryVersion) {
|
|
29
44
|
const primaryBalls: Set<number> = new Set();
|
|
30
|
-
|
|
31
|
-
|
|
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() *
|
|
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
|
|
45
|
-
|
|
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
|
}
|