Skip to content

Commit

Permalink
Merge pull request #1677 from Emurgo/mnemonic-duplicate-words
Browse files Browse the repository at this point in the history
Mnemonic-duplicate-words
  • Loading branch information
vsubhuman committed Oct 13, 2021
2 parents 5586ba7 + d9a27fe commit b6ef6c8
Show file tree
Hide file tree
Showing 3 changed files with 180 additions and 90 deletions.
26 changes: 26 additions & 0 deletions scripts/create_mnemonic.sh
@@ -0,0 +1,26 @@
#!/bin/env node

const {generateMnemonic} = require('bip39');
const crypto = require('crypto');

const MNEMONIC_STRENGTH = 160;
const DUPLICATED_WORDS = /(\b\S+\b)\s+\b\1\b/

let mnemonics = []

function createMnemonicWithDuplicatedWords() {
do {
const mnemonic = generateMnemonic(MNEMONIC_STRENGTH, crypto.randomBytes);
if (mnemonic.match(DUPLICATED_WORDS)) {
return mnemonic;
}
} while (true)
}

// It creates 10 mnemonics with duplicated words
do {
mnemonics.push(createMnemonicWithDuplicatedWords());
if (mnemonics.length >= 10) break;
} while (true)

console.log(mnemonics);
180 changes: 108 additions & 72 deletions src/components/WalletInit/CreateWallet/MnemonicCheckScreen.js
@@ -1,64 +1,52 @@
// @flow

import {useNavigation, useRoute} from '@react-navigation/native'
import {useNavigation} from '@react-navigation/native'
import React from 'react'
import {defineMessages, useIntl} from 'react-intl'
import {ScrollView, TouchableOpacity, View} from 'react-native'
import {SafeAreaView} from 'react-native-safe-area-context'
import {useDispatch} from 'react-redux'

import {createWallet} from '../../../actions'
import type {NetworkId, WalletImplementationId, YoroiProvider} from '../../../config/types'
import {useParams} from '../../../navigation'
import {ROOT_ROUTES, WALLET_ROOT_ROUTES} from '../../../RoutesList'
import assert from '../../../utils/assert'
import {ignoreConcurrentAsyncHandler} from '../../../utils/utils'
import {Button, Spacer, StatusBar, Text} from '../../UiKit'
import styles from './styles/MnemonicCheckScreen.style'

const messages = defineMessages({
instructions: {
id: 'components.walletinit.createwallet.mnemoniccheckscreen.instructions',
defaultMessage: '!!!Tap each word in the correct order to verify your recovery phrase',
},
clearButton: {
id: 'components.walletinit.createwallet.mnemoniccheckscreen.clearButton',
defaultMessage: '!!!Clear',
},
confirmButton: {
id: 'components.walletinit.createwallet.mnemoniccheckscreen.confirmButton',
defaultMessage: '!!!Confirm',
},
mnemonicWordsInputLabel: {
id: 'components.walletinit.createwallet.mnemoniccheckscreen.mnemonicWordsInputLabel',
defaultMessage: '!!!Recovery phrase',
},
mnemonicWordsInputInvalidPhrase: {
id: 'components.walletinit.createwallet.mnemoniccheckscreen.mnemonicWordsInputInvalidPhrase',
defaultMessage: '!!!Recovery phrase does not match',
},
})
export type Params = {
mnemonic: string,
password: string,
name: string,
networkId: NetworkId,
walletImplementationId: WalletImplementationId,
provider: YoroiProvider,
}

type Entry = {id: number, word: string}

const MnemonicCheckScreen = () => {
const intl = useIntl()
const navigation = useNavigation()
const route = (useRoute(): any)
const mnemonic: string = route.params.mnemonic
const sortedWords = mnemonic.split(' ').sort()
const [partialPhrase, setPartialPhrase] = React.useState<Array<string>>([])
const deselectWord = (removeWord: string) => setPartialPhrase(partialPhrase.filter((word) => word !== removeWord))
const selectWord = (addWord: string) => setPartialPhrase([...partialPhrase, addWord])
const {mnemonic, password, name, networkId, walletImplementationId, provider} = useParams<Params>()

const isPhraseComplete = partialPhrase.length === sortedWords.length
const isPhraseValid = mnemonic === partialPhrase.join(' ')
const mnemonicEntries: Array<Entry> = mnemonic
.split(' ')
.sort()
.map((word, id) => ({word, id}))

const [userEntries, setUserEntries] = React.useState<Array<Entry>>([])
const appendEntry = (entry: Entry) => setUserEntries([...userEntries, entry])
const removeLastEntry = () => setUserEntries((entries) => entries.slice(0, -1))

const isPhraseComplete = userEntries.length === mnemonicEntries.length
const isPhraseValid = userEntries.map((entry) => entry.word).join(' ') === mnemonic

const dispatch = useDispatch()
const handleWalletConfirmation = async () => {
const {mnemonic, password, name, networkId, walletImplementationId, provider} = route.params

assert.assert(!!mnemonic, 'handleWalletConfirmation:: mnemonic')
assert.assert(!!password, 'handleWalletConfirmation:: password')
assert.assert(!!name, 'handleWalletConfirmation:: name')
assert.assert(networkId != null, 'handleWalletConfirmation:: networkId')
assert.assert(!!walletImplementationId, 'handleWalletConfirmation:: implementationId')
assertions({mnemonic, password, name, networkId, walletImplementationId})

await dispatch(createWallet(name, mnemonic, password, networkId, walletImplementationId, provider))

Expand Down Expand Up @@ -88,14 +76,14 @@ const MnemonicCheckScreen = () => {

<Spacer height={24} />

<MnemonicInput onPress={deselectWord} partialPhrase={partialPhrase} error={!isPhraseValid && isPhraseComplete} />
<MnemonicInput onPress={removeLastEntry} userEntries={userEntries} error={isPhraseComplete && !isPhraseValid} />

<Spacer height={8} />

<ErrorMessage visible={!(isPhraseValid || !isPhraseComplete)} />

<ScrollView bounces={false} contentContainerStyle={styles.scrollViewContentContainer}>
<WordBadges words={sortedWords} partialPhrase={partialPhrase} onSelect={selectWord} />
<WordBadges mnemonicEntries={mnemonicEntries} userEntries={userEntries} onPress={appendEntry} />
</ScrollView>

<View style={styles.buttons}>
Expand All @@ -114,24 +102,21 @@ const MnemonicCheckScreen = () => {

export default MnemonicCheckScreen

const MnemonicInput = ({
partialPhrase,
error,
onPress,
}: {
partialPhrase: Array<string>,
type MnemonicInputProps = {
userEntries: Array<Entry>,
error: boolean,
onPress: (word: string) => any,
}) => {
onPress: () => any,
}
const MnemonicInput = ({userEntries, error, onPress}: MnemonicInputProps) => {
return (
<View style={styles.recoveryPhrase}>
<View style={[styles.recoveryPhraseOutline, error && styles.recoveryPhraseError]}>
{partialPhrase.map((word, index, array) => {
{userEntries.map((entry, index, array) => {
const isLast = index === array.length - 1

return (
<View key={word} style={[styles.wordBadgeContainer, !isLast && styles.selected]}>
<WordBadge word={word} disabled={!isLast} onPress={isLast ? () => onPress(word) : undefined} />
<View key={entry.id} style={[styles.wordBadgeContainer, !isLast && styles.selected]}>
<WordBadge word={`${entry.word} x`} disabled={!isLast} onPress={onPress} />
</View>
)
})}
Expand Down Expand Up @@ -160,33 +145,84 @@ const ErrorMessage = ({visible}: {visible: boolean}) => {
)
}

const WordBadges = ({
words,
partialPhrase,
onSelect,
}: {
words: Array<string>,
partialPhrase: Array<string>,
onSelect: (word: string) => any,
}) => {
type WordBadgesProps = {
mnemonicEntries: Array<Entry>,
userEntries: Array<Entry>,
onPress: (wordEntry: Entry) => any,
}
const WordBadges = ({mnemonicEntries, userEntries, onPress}: WordBadgesProps) => {
const isWordUsed = (entryId: number) => userEntries.some((entry) => entry.id === entryId)

return (
<View style={styles.words}>
{words.map((word) => (
<View key={word} style={[styles.wordBadgeContainer, partialPhrase.includes(word) && styles.hidden]}>
<WordBadge
word={word}
onPress={() => onSelect(word)}
disabled={partialPhrase.includes(word)}
testID={partialPhrase.includes(word) ? `wordBadgeTapped-${word}` : `wordBadgeNonTapped-${word}`}
/>
</View>
))}
{mnemonicEntries.map((entry) => {
const isUsed = isWordUsed(entry.id)

return (
<View key={entry.id} style={[styles.wordBadgeContainer, isUsed && styles.hidden]}>
<WordBadge
word={entry.word}
onPress={() => onPress(entry)}
disabled={isUsed}
testID={isUsed ? `wordBadgeTapped-${entry.word}` : `wordBadgeNonTapped-${entry.word}`}
/>
</View>
)
})}
</View>
)
}

const WordBadge = ({word, onPress, disabled}: {word: string, disabled?: boolean, onPress?: () => any}) => (
type WordBadgeProps = {
word: string,
disabled?: boolean,
onPress?: () => any,
}
const WordBadge = ({word, onPress, disabled}: WordBadgeProps) => (
<TouchableOpacity activeOpacity={0.5} onPress={onPress} disabled={disabled} style={styles.wordBadge}>
<Text style={styles.wordBadgeText}>{word} x</Text>
<Text style={styles.wordBadgeText}>{word}</Text>
</TouchableOpacity>
)

const assertions = ({
mnemonic,
password,
name,
networkId,
walletImplementationId,
}: {
mnemonic: string,
name: string,
password: string,
networkId: NetworkId,
walletImplementationId: WalletImplementationId,
}) => {
assert.assert(!!mnemonic, 'handleWalletConfirmation:: mnemonic')
assert.assert(!!password, 'handleWalletConfirmation:: password')
assert.assert(!!name, 'handleWalletConfirmation:: name')
assert.assert(networkId != null, 'handleWalletConfirmation:: networkId')
assert.assert(!!walletImplementationId, 'handleWalletConfirmation:: implementationId')
}

const messages = defineMessages({
instructions: {
id: 'components.walletinit.createwallet.mnemoniccheckscreen.instructions',
defaultMessage: '!!!Tap each word in the correct order to verify your recovery phrase',
},
clearButton: {
id: 'components.walletinit.createwallet.mnemoniccheckscreen.clearButton',
defaultMessage: '!!!Clear',
},
confirmButton: {
id: 'components.walletinit.createwallet.mnemoniccheckscreen.confirmButton',
defaultMessage: '!!!Confirm',
},
mnemonicWordsInputLabel: {
id: 'components.walletinit.createwallet.mnemoniccheckscreen.mnemonicWordsInputLabel',
defaultMessage: '!!!Recovery phrase',
},
mnemonicWordsInputInvalidPhrase: {
id: 'components.walletinit.createwallet.mnemoniccheckscreen.mnemonicWordsInputInvalidPhrase',
defaultMessage: '!!!Recovery phrase does not match',
},
})
Expand Up @@ -7,22 +7,50 @@ import React from 'react'
import {CONFIG} from '../../../config/config'
import MnemonicCheckScreen from './MnemonicCheckScreen'

storiesOf('MnemonicCheckScreen', module).add('Default', () => {
const route = {
key: 'key',
name: 'name',
params: {
mnemonic: CONFIG.DEBUG.MNEMONIC1,
name: CONFIG.DEBUG.WALLET_NAME,
password: CONFIG.DEBUG.PASSWORD,
networkId: CONFIG.NETWORKS.HASKELL_SHELLEY.NETWORK_ID,
walletImplementationId: 'haskell-shelley',
},
}
storiesOf('MnemonicCheckScreen', module)
.add('Default', () => {
const route = {
key: 'key',
name: 'name',
params: {
mnemonic: CONFIG.DEBUG.MNEMONIC1,
name: CONFIG.DEBUG.WALLET_NAME,
password: CONFIG.DEBUG.PASSWORD,
networkId: CONFIG.NETWORKS.HASKELL_SHELLEY.NETWORK_ID,
walletImplementationId: 'haskell-shelley',
},
}

return (
<NavigationRouteContext.Provider value={route}>
<MnemonicCheckScreen />
</NavigationRouteContext.Provider>
)
})
return (
<NavigationRouteContext.Provider value={route}>
<MnemonicCheckScreen />
</NavigationRouteContext.Provider>
)
})
.add('with duplicates', () => {
const mnemonicWithDuplicates = [
'scrap scrap song',
'radar lemon parade',
'repeat parade media',
'shrimp live benefit',
'people room spider',
].join(' ')

const route = {
key: 'key',
name: 'name',
params: {
mnemonic: mnemonicWithDuplicates,
name: 'wallet name',
password: 'password',
networkId: 1,
walletImplementationId: 'haskell-shelley',
},
}

return (
<NavigationRouteContext.Provider value={route}>
<MnemonicCheckScreen />
</NavigationRouteContext.Provider>
)
})

0 comments on commit b6ef6c8

Please sign in to comment.