Merge pull request #6397 from BlueWallet/entropy-ts

feat: Entropy refactor
This commit is contained in:
GLaDOS 2024-06-09 21:41:07 +00:00 committed by GitHub
commit 3f52a597a4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 275 additions and 183 deletions

View File

@ -159,9 +159,10 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
}
async generateFromEntropy(user: Buffer) {
const random = await randomBytes(user.length < 32 ? 32 - user.length : 0);
const buf = Buffer.concat([user, random], 32);
this.secret = bip39.entropyToMnemonic(buf.toString('hex'));
if (user.length !== 32 && user.length !== 16) {
throw new Error('Entropy has to be 16 or 32 bytes long');
}
this.secret = bip39.entropyToMnemonic(user.toString('hex'));
}
_getExternalWIFByIndex(index: number): string | false {

View File

@ -63,18 +63,10 @@ export class LegacyWallet extends AbstractWallet {
}
async generateFromEntropy(user: Buffer): Promise<void> {
let i = 0;
do {
i += 1;
const random = await randomBytes(user.length < 32 ? 32 - user.length : 0);
const buf = Buffer.concat([user, random], 32);
try {
this.secret = ECPair.fromPrivateKey(buf).toWIF();
return;
} catch (e) {
if (i === 5) throw e;
}
} while (true);
if (user.length !== 32) {
throw new Error('Entropy should be 32 bytes');
}
this.secret = ECPair.fromPrivateKey(user).toWIF();
}
getAddress(): string | false {

View File

@ -131,7 +131,7 @@ interface FButtonProps {
last?: boolean;
disabled?: boolean;
onPress: () => void;
onLongPress: () => void;
onLongPress?: () => void;
}
export const FButton = ({ text, icon, width, first, last, ...props }: FButtonProps) => {

View File

@ -43,7 +43,8 @@
"entropy": {
"save": "Save",
"title": "Entropy",
"undo": "Undo"
"undo": "Undo",
"amountOfEntropy": "{bits} of {limit} bits"
},
"errors": {
"broadcast": "Broadcast failed.",
@ -385,6 +386,8 @@
"add_bitcoin": "Bitcoin",
"add_bitcoin_explain": "Simple and powerful Bitcoin wallet",
"add_create": "Create",
"add_entropy": "Entropy",
"add_entropy_bytes": "{bytes} bytes of entropy",
"add_entropy_generated": "{gen} bytes of generated entropy",
"add_entropy_provide": "Provide entropy via dice rolls",
"add_entropy_remain": "{gen} bytes of generated entropy. Remaining {rem} bytes will be obtained from the System random number generator.",
@ -398,6 +401,10 @@
"add_title": "Add Wallet",
"add_wallet_name": "Name",
"add_wallet_type": "Type",
"add_wallet_seed_length": "Seed Length",
"add_wallet_seed_length_message": "Choose the length of the seed phrase you wish to use for this wallet.",
"add_wallet_seed_length_12": "12 words",
"add_wallet_seed_length_24": "24 words",
"clipboard_bitcoin": "You have a Bitcoin address on your clipboard. Would you like to use it for a transaction?",
"clipboard_lightning": "You have a Lightning invoice on your clipboard. Would you like to use it for a transaction?",
"details_address": "Address",

View File

@ -11,7 +11,7 @@ const ImportWallet = lazy(() => import('../screen/wallets/import'));
const PleaseBackup = lazy(() => import('../screen/wallets/PleaseBackup'));
const PleaseBackupLNDHub = lazy(() => import('../screen/wallets/pleaseBackupLNDHub'));
const PleaseBackupLdk = lazy(() => import('../screen/wallets/pleaseBackupLdk'));
const ProvideEntropy = lazy(() => import('../screen/wallets/provideEntropy'));
const ProvideEntropy = lazy(() => import('../screen/wallets/ProvideEntropy'));
const WalletsAddMultisig = lazy(() => import('../screen/wallets/addMultisig'));
const WalletsAddMultisigStep2 = lazy(() => import('../screen/wallets/addMultisigStep2'));
const WalletsAddMultisigHelp = lazy(() => import('../screen/wallets/addMultisigHelp'));

View File

@ -3,6 +3,7 @@ import { useNavigation } from '@react-navigation/native';
import React, { useEffect, useReducer } from 'react';
import {
ActivityIndicator,
Alert,
Keyboard,
KeyboardAvoidingView,
LayoutAnimation,
@ -53,7 +54,7 @@ interface State {
label: string;
selectedWalletType: ButtonSelected;
backdoorPressed: number;
entropy: string | any[] | undefined;
entropy: Buffer | undefined;
entropyButtonText: string;
}
@ -161,18 +162,13 @@ const WalletsAdd: React.FC = () => {
});
}, [colorScheme, setOptions]);
const entropyGenerated = (newEntropy: string | any[]) => {
const entropyGenerated = (newEntropy: Buffer) => {
let entropyTitle;
if (!newEntropy) {
entropyTitle = loc.wallets.add_entropy_provide;
} else if (newEntropy.length < 32) {
entropyTitle = loc.formatString(loc.wallets.add_entropy_remain, {
gen: newEntropy.length,
rem: 32 - newEntropy.length,
});
} else {
entropyTitle = loc.formatString(loc.wallets.add_entropy_generated, {
gen: newEntropy.length,
entropyTitle = loc.formatString(loc.wallets.add_entropy_bytes, {
bytes: newEntropy.length,
});
}
setEntropy(newEntropy);
@ -203,7 +199,7 @@ const WalletsAdd: React.FC = () => {
dispatch({ type: 'INCREMENT_BACKDOOR_PRESSED', payload: value });
};
const setEntropy = (value: string | any[]) => {
const setEntropy = (value: Buffer) => {
dispatch({ type: 'SET_ENTROPY', payload: value });
};
@ -236,7 +232,6 @@ const WalletsAdd: React.FC = () => {
if (selectedWalletType === ButtonSelected.ONCHAIN) {
if (entropy) {
try {
// @ts-ignore: Return later to update
await w.generateFromEntropy(entropy);
} catch (e: any) {
console.log(e.toString());
@ -334,8 +329,34 @@ const WalletsAdd: React.FC = () => {
};
const navigateToEntropy = () => {
// @ts-ignore: Return later to update
navigate('ProvideEntropy', { onGenerated: entropyGenerated });
Alert.alert(
loc.wallets.add_wallet_seed_length,
loc.wallets.add_wallet_seed_length_message,
[
{
text: loc._.cancel,
onPress: () => {},
style: 'default',
},
{
text: loc.wallets.add_wallet_seed_length_12,
onPress: () => {
// @ts-ignore: Return later to update
navigate('ProvideEntropy', { onGenerated: entropyGenerated, words: 12 });
},
style: 'default',
},
{
text: loc.wallets.add_wallet_seed_length_24,
onPress: () => {
// @ts-ignore: Return later to update
navigate('ProvideEntropy', { onGenerated: entropyGenerated, words: 24 });
},
style: 'default',
},
],
{ cancelable: true },
);
};
const navigateToImportWallet = () => {

View File

@ -1,8 +1,18 @@
import { useNavigation, useRoute } from '@react-navigation/native';
import BN from 'bignumber.js';
import PropTypes from 'prop-types';
import React, { useReducer, useState } from 'react';
import { Dimensions, Image, PixelRatio, ScrollView, StyleSheet, Text, TouchableOpacity, useWindowDimensions, View } from 'react-native';
import React, { useEffect, useReducer, useState } from 'react';
import {
Alert,
Dimensions,
Image,
PixelRatio,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
useWindowDimensions,
} from 'react-native';
import { Icon } from 'react-native-elements';
import { BlueSpacing20 } from '../../BlueComponents';
@ -11,48 +21,81 @@ import SafeArea from '../../components/SafeArea';
import { Tabs } from '../../components/Tabs';
import { BlueCurrentTheme, useTheme } from '../../components/themes';
import loc from '../../loc';
import { randomBytes } from '../../class/rng';
const ENTROPY_LIMIT = 256;
export enum EActionType {
push = 'push',
pop = 'pop',
limit = 'limit',
noop = 'noop',
}
const shiftLeft = (value, places) => value.multipliedBy(2 ** places);
const shiftRight = (value, places) => value.div(2 ** places).dp(0, BN.ROUND_DOWN);
type TEntropy = { value: number; bits: number };
type TState = { entropy: BN; bits: number; items: number[]; limit: number };
type TAction =
| ({ type: EActionType.push } & TEntropy)
| { type: EActionType.pop }
| { type: EActionType.limit; limit: number }
| { type: EActionType.noop };
type TPush = (v: TEntropy | null) => void;
type TPop = () => void;
const initialState = { entropy: BN(0), bits: 0, items: [] };
export const eReducer = (state = initialState, action) => {
const initialState: TState = {
entropy: BN(0),
bits: 0,
items: [],
limit: 256,
};
const shiftLeft = (value: BN, places: number) => value.multipliedBy(2 ** places);
const shiftRight = (value: BN, places: number) => value.div(2 ** places).dp(0, BN.ROUND_DOWN);
export const eReducer = (state: TState = initialState, action: TAction): TState => {
switch (action.type) {
case 'push': {
let { value, bits } = action;
case EActionType.noop:
return state;
case EActionType.push: {
let value: number | BN = action.value;
let bits: number = action.bits;
if (value >= 2 ** bits) {
throw new TypeError("Can't push value exceeding size in bits");
}
if (state.bits === ENTROPY_LIMIT) return state;
if (state.bits + bits > ENTROPY_LIMIT) {
value = shiftRight(BN(value), bits + state.bits - ENTROPY_LIMIT);
bits = ENTROPY_LIMIT - state.bits;
if (state.bits === state.limit) return state;
if (state.bits + bits > state.limit) {
value = shiftRight(BN(value), bits + state.bits - state.limit);
bits = state.limit - state.bits;
}
const entropy = shiftLeft(state.entropy, bits).plus(value);
const items = [...state.items, bits];
return { entropy, bits: state.bits + bits, items };
return { ...state, entropy, bits: state.bits + bits, items };
}
case 'pop': {
case EActionType.pop: {
if (state.bits === 0) return state;
const bits = state.items.pop();
const bits = state.items.pop()!;
const entropy = shiftRight(state.entropy, bits);
return { entropy, bits: state.bits - bits, items: [...state.items] };
return { ...state, entropy, bits: state.bits - bits, items: [...state.items] };
}
case EActionType.limit: {
return { ...state, limit: action.limit };
}
default:
return state;
}
};
export const entropyToHex = ({ entropy, bits }) => {
export const entropyToHex = ({ entropy, bits }: { entropy: BN; bits: number }): string => {
if (bits === 0) return '0x';
const hex = entropy.toString(16);
const hexSize = Math.floor((bits - 1) / 4) + 1;
return '0x' + '0'.repeat(hexSize - hex.length) + hex;
};
export const getEntropy = (number, base) => {
export const getEntropy = (number: number, base: number): TEntropy | null => {
if (base === 1) return null;
let maxPow = 1;
while (2 ** (maxPow + 1) <= base) {
@ -77,12 +120,12 @@ export const getEntropy = (number, base) => {
};
// cut entropy to bytes, convert to Buffer
export const convertToBuffer = ({ entropy, bits }) => {
export const convertToBuffer = ({ entropy, bits }: { entropy: BN; bits: number }): Buffer => {
if (bits < 8) return Buffer.from([]);
const bytes = Math.floor(bits / 8);
// convert to byte array
let arr = [];
const arr: string[] = [];
const ent = entropy.toString(16).split('').reverse();
ent.forEach((v, index) => {
if (index % 2 === 1) {
@ -91,18 +134,19 @@ export const convertToBuffer = ({ entropy, bits }) => {
arr.unshift(v);
}
});
arr = arr.map(i => parseInt(i, 16));
let arr2 = arr.map(i => parseInt(i, 16));
if (arr.length > bytes) {
arr.shift();
} else if (arr.length < bytes) {
const zeros = [...Array(bytes - arr.length)].map(() => 0);
arr = [...zeros, ...arr];
arr2.shift();
} else if (arr2.length < bytes) {
const zeros = [...Array(bytes - arr2.length)].map(() => 0);
arr2 = [...zeros, ...arr2];
}
return Buffer.from(arr);
return Buffer.from(arr2);
};
const Coin = ({ push }) => (
const Coin = ({ push }: { push: TPush }) => (
<View style={styles.coinRoot}>
<TouchableOpacity accessibilityRole="button" onPress={() => push(getEntropy(0, 2))} style={styles.coinBody}>
<Image style={styles.coinImage} source={require('../../img/coin1.png')} />
@ -113,11 +157,24 @@ const Coin = ({ push }) => (
</View>
);
Coin.propTypes = {
push: PropTypes.func.isRequired,
const diceIcon = (i: number): string => {
switch (i) {
case 1:
return 'dice-one';
case 2:
return 'dice-two';
case 3:
return 'dice-three';
case 4:
return 'dice-four';
case 5:
return 'dice-five';
default:
return 'dice-six';
}
};
const Dice = ({ push, sides }) => {
const Dice = ({ push, sides }: { push: TPush; sides: number }) => {
const { width } = useWindowDimensions();
const { colors } = useTheme();
const diceWidth = width / 4;
@ -132,22 +189,6 @@ const Dice = ({ push, sides }) => {
backgroundColor: colors.elevated,
},
});
const diceIcon = i => {
switch (i) {
case 1:
return 'dice-one';
case 2:
return 'dice-two';
case 3:
return 'dice-three';
case 4:
return 'dice-four';
case 5:
return 'dice-five';
default:
return 'dice-six';
}
};
return (
<ScrollView contentContainerStyle={[styles.diceContainer, stylesHook.diceContainer]}>
@ -168,48 +209,53 @@ const Dice = ({ push, sides }) => {
);
};
Dice.propTypes = {
sides: PropTypes.number.isRequired,
push: PropTypes.func.isRequired,
};
const buttonFontSize =
PixelRatio.roundToNearestPixel(Dimensions.get('window').width / 26) > 22
? 22
: PixelRatio.roundToNearestPixel(Dimensions.get('window').width / 26);
const Buttons = ({ pop, save, colors }) => (
const Buttons = ({ pop, save, colors }: { pop: TPop; save: () => void; colors: any }) => (
<FContainer>
<FButton
onPress={pop}
text={loc.entropy.undo}
icon={
<View style={styles.buttonsIcon}>
<Icon name="undo" size={buttonFontSize} type="font-awesome" color={colors.buttonAlternativeTextColor} />
</View>
}
text={loc.entropy.undo}
/>
<FButton
onPress={save}
text={loc.entropy.save}
icon={
<View style={styles.buttonsIcon}>
<Icon name="arrow-down" size={buttonFontSize} type="font-awesome" color={colors.buttonAlternativeTextColor} />
</View>
}
text={loc.entropy.save}
/>
</FContainer>
);
Buttons.propTypes = {
pop: PropTypes.func.isRequired,
save: PropTypes.func.isRequired,
colors: PropTypes.shape.isRequired,
const TollTab = ({ active }: { active: boolean }) => {
const { colors } = useTheme();
return <Icon name="toll" type="material" color={active ? colors.buttonAlternativeTextColor : colors.buttonBackgroundColor} />;
};
const D6Tab = ({ active }: { active: boolean }) => {
const { colors } = useTheme();
return <Icon name="dice" type="font-awesome-5" color={active ? colors.buttonAlternativeTextColor : colors.buttonBackgroundColor} />;
};
const D20Tab = ({ active }: { active: boolean }) => {
const { colors } = useTheme();
return <Icon name="dice-d20" type="font-awesome-5" color={active ? colors.buttonAlternativeTextColor : colors.buttonBackgroundColor} />;
};
const Entropy = () => {
const [entropy, dispatch] = useReducer(eReducer, initialState);
const { onGenerated } = useRoute().params;
// @ts-ignore: navigation is not typed yet
const { onGenerated, words } = useRoute().params;
const navigation = useNavigation();
const [tab, setTab] = useState(1);
const [show, setShow] = useState(false);
@ -223,12 +269,65 @@ const Entropy = () => {
},
});
const push = v => v && dispatch({ type: 'push', value: v.value, bits: v.bits });
const pop = () => dispatch({ type: 'pop' });
const save = () => {
navigation.pop();
const buf = convertToBuffer(entropy);
onGenerated(buf);
useEffect(() => {
dispatch({ type: EActionType.limit, limit: words === 24 ? 256 : 128 });
}, [dispatch, words]);
const handlePush: TPush = v => {
if (v === null) {
dispatch({ type: EActionType.noop });
} else {
dispatch({ type: EActionType.push, value: v.value, bits: v.bits });
}
};
const handlePop: TPop = () => {
dispatch({ type: EActionType.pop });
};
const handleSave = async () => {
let buf = convertToBuffer(entropy);
const bufLength = words === 24 ? 32 : 16;
const remaining = bufLength - buf.length;
let entropyTitle = '';
if (buf.length < bufLength) {
entropyTitle = loc.formatString(loc.wallets.add_entropy_remain, {
gen: buf.length,
rem: remaining,
});
} else {
entropyTitle = loc.formatString(loc.wallets.add_entropy_generated, {
gen: buf.length,
});
}
Alert.alert(
loc.wallets.add_entropy,
entropyTitle,
[
{
text: loc._.ok,
onPress: async () => {
if (remaining > 0) {
const random = await randomBytes(remaining);
buf = Buffer.concat([buf, random], bufLength);
}
// @ts-ignore: navigation is not typed yet
navigation.pop();
onGenerated(buf);
},
style: 'default',
},
{
text: loc._.cancel,
onPress: () => {},
style: 'default',
},
],
{ cancelable: true },
);
};
const hex = entropyToHex(entropy);
@ -240,34 +339,19 @@ const Entropy = () => {
<BlueSpacing20 />
<TouchableOpacity accessibilityRole="button" onPress={() => setShow(!show)}>
<View style={[styles.entropy, stylesHook.entropy]}>
<Text style={[styles.entropyText, stylesHook.entropyText]}>{show ? hex : `${bits} of 256 bits`}</Text>
<Text style={[styles.entropyText, stylesHook.entropyText]}>
{show ? hex : loc.formatString(loc.entropy.amountOfEntropy, { bits, limit: entropy.limit })}
</Text>
</View>
</TouchableOpacity>
<Tabs
active={tab}
onSwitch={setTab}
tabs={[
// eslint-disable-next-line react/no-unstable-nested-components
({ active }) => (
<Icon name="toll" type="material" color={active ? colors.buttonAlternativeTextColor : colors.buttonBackgroundColor} />
),
// eslint-disable-next-line react/no-unstable-nested-components
({ active }) => (
<Icon name="dice" type="font-awesome-5" color={active ? colors.buttonAlternativeTextColor : colors.buttonBackgroundColor} />
),
// eslint-disable-next-line react/no-unstable-nested-components
({ active }) => (
<Icon name="dice-d20" type="font-awesome-5" color={active ? colors.buttonAlternativeTextColor : colors.buttonBackgroundColor} />
),
]}
/>
<Tabs active={tab} onSwitch={setTab} tabs={[TollTab, D6Tab, D20Tab]} />
{tab === 0 && <Coin push={push} />}
{tab === 1 && <Dice sides={6} push={push} />}
{tab === 2 && <Dice sides={20} push={push} />}
{tab === 0 && <Coin push={handlePush} />}
{tab === 1 && <Dice sides={6} push={handlePush} />}
{tab === 2 && <Dice sides={20} push={handlePush} />}
<Buttons pop={pop} save={save} colors={colors} />
<Buttons pop={handlePop} save={handleSave} colors={colors} />
</SafeArea>
);
};

View File

@ -140,31 +140,26 @@ describe('P2SH Segwit HD (BIP49)', () => {
});
it('can consume user generated entropy', async () => {
const hd = new HDSegwitP2SHWallet();
const zeroes = [...Array(32)].map(() => 0);
await hd.generateFromEntropy(Buffer.from(zeroes));
const hd1 = new HDSegwitP2SHWallet();
const zeroes16 = [...Array(16)].map(() => 0);
await hd1.generateFromEntropy(Buffer.from(zeroes16));
assert.strictEqual(hd1.getSecret(), 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about');
const hd2 = new HDSegwitP2SHWallet();
const zeroes32 = [...Array(32)].map(() => 0);
await hd2.generateFromEntropy(Buffer.from(zeroes32));
assert.strictEqual(
hd.getSecret(),
hd2.getSecret(),
'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art',
);
});
it('can fullfill user generated entropy if less than 32 bytes provided', async () => {
it('throws an error if not 32 bytes provided', async () => {
const hd = new HDSegwitP2SHWallet();
const zeroes = [...Array(16)].map(() => 0);
await hd.generateFromEntropy(Buffer.from(zeroes));
const secret = hd.getSecret();
assert.strictEqual(secret.startsWith('abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon'), true);
const secretWithoutChecksum = secret.split(' ');
secretWithoutChecksum.pop();
const secretWithoutChecksumString = secretWithoutChecksum.join(' ');
assert.strictEqual(
secretWithoutChecksumString.endsWith('abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon'),
false,
);
assert.ok(secret.split(' ').length === 12 || secret.split(' ').length === 24);
const values = [...Array(31)].map(() => 1);
await assert.rejects(async () => await hd.generateFromEntropy(Buffer.from(values)), {
message: 'Entropy has to be 16 or 32 bytes long',
});
});
it('can sign and verify messages', async () => {

View File

@ -1,10 +1,7 @@
import assert from 'assert';
import * as bitcoin from 'bitcoinjs-lib';
import { ECPairFactory } from 'ecpair';
import ecc from '../../blue_modules/noble_ecc';
import { LegacyWallet } from '../../class';
const ECPair = ECPairFactory(ecc);
describe('Legacy wallet', () => {
it('can validate addresses', () => {
@ -146,17 +143,12 @@ describe('Legacy wallet', () => {
assert.strictEqual(l.getSecret(), 'KwFfNUhSDaASSAwtG7ssQM1uVX8RgX5GHWnnLfhfiQDigjioWXHH');
});
it('can fullfill user generated entropy if less than 32 bytes provided', async () => {
it('throws an error if not 32 bytes provided', async () => {
const l = new LegacyWallet();
const values = [...Array(16)].map(() => 1);
await l.generateFromEntropy(Buffer.from(values));
assert.strictEqual(l.getSecret().startsWith('KwFfNUhSDaASSAwtG7ssQM'), true);
assert.strictEqual(l.getSecret().endsWith('GHWnnLfhfiQDigjioWXHH'), false);
const keyPair = ECPair.fromWIF(l.getSecret());
assert.strictEqual(keyPair.privateKey.toString('hex').startsWith('01010101'), true);
assert.strictEqual(keyPair.privateKey.toString('hex').endsWith('01010101'), false);
assert.strictEqual(keyPair.privateKey.toString('hex').endsWith('00000000'), false);
assert.strictEqual(keyPair.privateKey.toString('hex').endsWith('ffffffff'), false);
const values = [...Array(31)].map(() => 1);
await assert.rejects(async () => await l.generateFromEntropy(Buffer.from(values)), {
message: 'Entropy should be 32 bytes',
});
});
it('can sign and verify messages', async () => {

View File

@ -1,69 +1,69 @@
import assert from 'assert';
import { convertToBuffer, entropyToHex, eReducer, getEntropy } from '../../screen/wallets/provideEntropy';
import { convertToBuffer, EActionType, entropyToHex, eReducer, getEntropy } from '../../screen/wallets/ProvideEntropy';
describe('Entropy reducer and format', () => {
it('handles push and pop correctly', () => {
let state = eReducer(undefined, { type: null });
let state = eReducer(undefined, { type: EActionType.noop });
assert.equal(entropyToHex(state), '0x');
state = eReducer(state, { type: 'push', value: 0, bits: 1 });
state = eReducer(state, { type: EActionType.push, value: 0, bits: 1 });
assert.equal(entropyToHex(state), '0x0');
state = eReducer(state, { type: 'push', value: 0, bits: 1 });
state = eReducer(state, { type: EActionType.push, value: 0, bits: 1 });
assert.equal(entropyToHex(state), '0x0');
state = eReducer(state, { type: 'push', value: 0, bits: 3 });
state = eReducer(state, { type: EActionType.push, value: 0, bits: 3 });
assert.equal(entropyToHex(state), '0x00');
state = eReducer(state, { type: 'pop' });
state = eReducer(state, { type: EActionType.pop });
assert.equal(entropyToHex(state), '0x0');
state = eReducer(state, { type: 'pop' });
state = eReducer(state, { type: 'pop' });
state = eReducer(state, { type: EActionType.pop });
state = eReducer(state, { type: EActionType.pop });
assert.equal(entropyToHex(state), '0x');
state = eReducer(state, { type: 'push', value: 1, bits: 1 });
state = eReducer(state, { type: EActionType.push, value: 1, bits: 1 });
assert.equal(entropyToHex(state), '0x1'); // 0b1
state = eReducer(state, { type: 'push', value: 0, bits: 1 });
state = eReducer(state, { type: EActionType.push, value: 0, bits: 1 });
assert.equal(entropyToHex(state), '0x2'); // 0b10
state = eReducer(state, { type: 'push', value: 0b01, bits: 2 });
state = eReducer(state, { type: EActionType.push, value: 0b01, bits: 2 });
assert.equal(entropyToHex(state), '0x9'); // 0b1001
state = eReducer(state, { type: 'push', value: 0b10, bits: 2 });
state = eReducer(state, { type: EActionType.push, value: 0b10, bits: 2 });
assert.equal(entropyToHex(state), '0x26'); // 0b100110
});
it('handles 128 bits correctly', () => {
const state = eReducer(undefined, { type: 'push', value: 0, bits: 128 });
const state = eReducer(undefined, { type: EActionType.push, value: 0, bits: 128 });
assert.equal(entropyToHex(state), '0x00000000000000000000000000000000');
});
it('handles 256 bits correctly', () => {
let state = eReducer(undefined, { type: null }); // get init state
let state = eReducer(undefined, { type: EActionType.noop }); // get init state
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for (const i of [...Array(256)]) {
state = eReducer(state, { type: 'push', value: 1, bits: 1 });
state = eReducer(state, { type: EActionType.push, value: 1, bits: 1 });
}
assert.equal(entropyToHex(state), '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff');
});
it('handles pop when empty without error', () => {
const state = eReducer(undefined, { type: 'pop' });
const state = eReducer(undefined, { type: EActionType.pop });
assert.equal(entropyToHex(state), '0x');
});
it('handles 256 bits limit', () => {
let state = eReducer(undefined, { type: 'push', value: 0, bits: 254 });
state = eReducer(state, { type: 'push', value: 0b101, bits: 3 });
let state = eReducer(undefined, { type: EActionType.push, value: 0, bits: 254 });
state = eReducer(state, { type: EActionType.push, value: 0b101, bits: 3 });
assert.equal(entropyToHex(state), '0x0000000000000000000000000000000000000000000000000000000000000002');
});
it('Throws error if you try to push value exceeding size in bits', () => {
assert.throws(() => eReducer(undefined, { type: 'push', value: 8, bits: 3 }), {
assert.throws(() => eReducer(undefined, { type: EActionType.push, value: 8, bits: 3 }), {
name: 'TypeError',
message: "Can't push value exceeding size in bits",
});
@ -104,48 +104,48 @@ describe('getEntropy function', () => {
describe('convertToBuffer function', () => {
it('zero bits', () => {
const state = eReducer(undefined, { type: null });
const state = eReducer(undefined, { type: EActionType.noop });
assert.deepEqual(convertToBuffer(state), Buffer.from([]));
});
it('8 zero bits', () => {
const state = eReducer(undefined, { type: 'push', value: 0, bits: 8 });
const state = eReducer(undefined, { type: EActionType.push, value: 0, bits: 8 });
assert.deepEqual(convertToBuffer(state), Buffer.from([0]));
});
it('8 filled bits', () => {
const state = eReducer(undefined, { type: 'push', value: 0b11111111, bits: 8 });
const state = eReducer(undefined, { type: EActionType.push, value: 0b11111111, bits: 8 });
assert.deepEqual(convertToBuffer(state), Buffer.from([0b11111111]));
});
it('9 zero bits', () => {
const state = eReducer(undefined, { type: 'push', value: 0, bits: 9 });
const state = eReducer(undefined, { type: EActionType.push, value: 0, bits: 9 });
assert.deepEqual(convertToBuffer(state), Buffer.from([0]));
});
it('9 filled bits', () => {
const state = eReducer(undefined, { type: 'push', value: 0b111111111, bits: 9 });
const state = eReducer(undefined, { type: EActionType.push, value: 0b111111111, bits: 9 });
assert.deepEqual(convertToBuffer(state), Buffer.from([0b11111111]));
});
it('9 bits', () => {
const state = eReducer(undefined, { type: 'push', value: 0b111100111, bits: 9 });
const state = eReducer(undefined, { type: EActionType.push, value: 0b111100111, bits: 9 });
assert.deepEqual(convertToBuffer(state), Buffer.from([0b11100111]));
});
it('3 bytes', () => {
let state = eReducer(undefined, { type: 'push', value: 1, bits: 8 });
state = eReducer(state, { type: 'push', value: 2, bits: 8 });
state = eReducer(state, { type: 'push', value: 3, bits: 8 });
let state = eReducer(undefined, { type: EActionType.push, value: 1, bits: 8 });
state = eReducer(state, { type: EActionType.push, value: 2, bits: 8 });
state = eReducer(state, { type: EActionType.push, value: 3, bits: 8 });
assert.deepEqual(convertToBuffer(state), Buffer.from([1, 2, 3]));
});
it('256 bits or 32bytes', () => {
let state = eReducer(undefined, { type: null }); // get init state
let state = eReducer(undefined, { type: EActionType.noop }); // get init state
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for (const i of [...Array(256)]) {
state = eReducer(state, { type: 'push', value: 1, bits: 1 });
state = eReducer(state, { type: EActionType.push, value: 1, bits: 1 });
}
const bytes = [...Array(32)].map(() => 255);