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/lib/lottery/lottery.d.ts +16 -10
- package/lib/lottery/lottery.js +27 -37
- package/lib/lottery/lottery.js.map +1 -1
- package/lib/regex/index.d.ts +4 -0
- package/lib/regex/index.js +5 -1
- package/lib/regex/index.js.map +1 -1
- 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/regex/index.ts +9 -0
- package/src/regex/regex.spec.ts +24 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shufflecom-calculations",
|
|
3
|
-
"version": "
|
|
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": "
|
|
17
|
+
"gitHead": "12f611596255710c364326a119bf766ee459e0a5"
|
|
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
|
}
|
package/src/regex/index.ts
CHANGED
|
@@ -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}$/;
|
package/src/regex/regex.spec.ts
CHANGED
|
@@ -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
|
-
|
|
8
|
-
VALID_NAME_REGEX,
|
|
9
|
+
USERNAME_REGEX,
|
|
9
10
|
US_PHONE_NUMBER_REGEX,
|
|
10
|
-
|
|
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);
|