BlueWallet/screen/wallets/ProvideEntropy.tsx
Marcos Rodriguez Velez c52bda10d7
REF: Update package
2024-06-12 12:46:44 -04:00

436 lines
12 KiB
TypeScript

import { useNavigation, useRoute } from '@react-navigation/native';
import BN from 'bignumber.js';
import React, { useEffect, useReducer, useState } from 'react';
import {
Alert,
Dimensions,
Image,
PixelRatio,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
useWindowDimensions,
} from 'react-native';
import { Icon } from '@rneui/themed';
import { BlueSpacing20 } from '../../BlueComponents';
import { FButton, FContainer } from '../../components/FloatButtons';
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';
export enum EActionType {
push = 'push',
pop = 'pop',
limit = 'limit',
noop = 'noop',
}
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: 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 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 === 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 { ...state, entropy, bits: state.bits + bits, items };
}
case EActionType.pop: {
if (state.bits === 0) return state;
const bits = state.items.pop()!;
const entropy = shiftRight(state.entropy, bits);
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 }: { 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: number, base: number): TEntropy | null => {
if (base === 1) return null;
let maxPow = 1;
while (2 ** (maxPow + 1) <= base) {
maxPow += 1;
}
let bits = maxPow;
let summ = 0;
while (bits >= 1) {
const block = 2 ** bits;
if (summ + block > base) {
bits -= 1;
continue;
}
if (number < summ + block) {
return { value: number - summ, bits };
}
summ += block;
bits -= 1;
}
return null;
};
// cut entropy to bytes, convert to Buffer
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
const arr: string[] = [];
const ent = entropy.toString(16).split('').reverse();
ent.forEach((v, index) => {
if (index % 2 === 1) {
arr[0] = v + arr[0];
} else {
arr.unshift(v);
}
});
let arr2 = arr.map(i => parseInt(i, 16));
if (arr.length > bytes) {
arr2.shift();
} else if (arr2.length < bytes) {
const zeros = [...Array(bytes - arr2.length)].map(() => 0);
arr2 = [...zeros, ...arr2];
}
return Buffer.from(arr2);
};
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')} />
</TouchableOpacity>
<TouchableOpacity accessibilityRole="button" onPress={() => push(getEntropy(1, 2))} style={styles.coinBody}>
<Image style={styles.coinImage} source={require('../../img/coin2.png')} />
</TouchableOpacity>
</View>
);
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 }: { push: TPush; sides: number }) => {
const { width } = useWindowDimensions();
const { colors } = useTheme();
const diceWidth = width / 4;
const stylesHook = StyleSheet.create({
dice: {
borderColor: colors.buttonBackgroundColor,
},
diceText: {
color: colors.foregroundColor,
},
diceContainer: {
backgroundColor: colors.elevated,
},
});
return (
<ScrollView contentContainerStyle={[styles.diceContainer, stylesHook.diceContainer]}>
{[...Array(sides)].map((_, i) => (
<TouchableOpacity accessibilityRole="button" key={i} onPress={() => push(getEntropy(i, sides))}>
<View style={[styles.diceRoot, { width: diceWidth }]}>
{sides === 6 ? (
<Icon style={styles.diceIcon} name={diceIcon(i + 1)} size={70} color="grey" type="font-awesome-5" />
) : (
<View style={[styles.dice, stylesHook.dice]}>
<Text style={stylesHook.diceText}>{i + 1}</Text>
</View>
)}
</View>
</TouchableOpacity>
))}
</ScrollView>
);
};
const buttonFontSize =
PixelRatio.roundToNearestPixel(Dimensions.get('window').width / 26) > 22
? 22
: PixelRatio.roundToNearestPixel(Dimensions.get('window').width / 26);
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>
}
/>
<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>
}
/>
</FContainer>
);
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);
// @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);
const { colors } = useTheme();
const stylesHook = StyleSheet.create({
entropy: {
backgroundColor: colors.inputBackgroundColor,
},
entropyText: {
color: colors.foregroundColor,
},
});
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);
let bits = entropy.bits.toString();
bits = ' '.repeat(bits.length < 3 ? 3 - bits.length : 0) + bits;
return (
<SafeArea>
<BlueSpacing20 />
<TouchableOpacity accessibilityRole="button" onPress={() => setShow(!show)}>
<View style={[styles.entropy, stylesHook.entropy]}>
<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={[TollTab, D6Tab, D20Tab]} />
{tab === 0 && <Coin push={handlePush} />}
{tab === 1 && <Dice sides={6} push={handlePush} />}
{tab === 2 && <Dice sides={20} push={handlePush} />}
<Buttons pop={handlePop} save={handleSave} colors={colors} />
</SafeArea>
);
};
const styles = StyleSheet.create({
entropy: {
padding: 5,
marginLeft: 10,
marginRight: 10,
borderRadius: 9,
minHeight: 49,
paddingHorizontal: 8,
justifyContent: 'center',
flexDirection: 'row',
alignItems: 'center',
},
entropyText: {
fontSize: 15,
fontFamily: 'Courier',
},
coinRoot: {
flex: 1,
justifyContent: 'center',
flexDirection: 'row',
flexWrap: 'wrap',
},
coinBody: {
flex: 0.33,
justifyContent: 'center',
alignItems: 'center',
aspectRatio: 1,
borderWidth: 1,
borderRadius: 5,
borderColor: BlueCurrentTheme.colors.lightButton,
margin: 10,
padding: 10,
maxWidth: 100,
maxHeight: 100,
},
coinImage: {
aspectRatio: 1,
width: '100%',
height: '100%',
borderRadius: 75,
},
diceContainer: {
alignItems: 'flex-start',
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'center',
paddingBottom: 100,
},
diceRoot: {
aspectRatio: 1,
maxWidth: 100,
maxHeight: 100,
},
dice: {
margin: 3,
borderWidth: 1,
borderRadius: 5,
justifyContent: 'center',
alignItems: 'center',
aspectRatio: 1,
borderColor: BlueCurrentTheme.colors.buttonBackgroundColor,
},
diceIcon: {
margin: 3,
justifyContent: 'center',
alignItems: 'center',
aspectRatio: 1,
color: 'grey',
},
buttonsIcon: {
backgroundColor: 'transparent',
transform: [{ rotate: '-45deg' }],
alignItems: 'center',
},
});
export default Entropy;