Skip to content

Commit

Permalink
add bip44Validators and more UI additions
Browse files Browse the repository at this point in the history
  • Loading branch information
v-almonacid committed Nov 24, 2020
1 parent 5a534f8 commit e0a19e7
Show file tree
Hide file tree
Showing 8 changed files with 180 additions and 60 deletions.
Expand Up @@ -6,14 +6,17 @@ import {View, ScrollView, Dimensions} from 'react-native'
import {withHandlers} from 'recompose'
import {injectIntl, defineMessages, type intlShape} from 'react-intl'
import QRCodeScanner from 'react-native-qrcode-scanner'
import {Bip32PublicKey} from 'react-native-haskell-shelley'

import {Text, BulletPointItem} from '../../UiKit'
import {WALLET_INIT_ROUTES} from '../../../RoutesList'
import {withNavigationTitle} from '../../../utils/renderUtils'
import {Logger} from '../../../utils/logging'
import {errorMessages} from '../../../i18n/global-messages'
import {showErrorDialog} from '../../../actions'
import {
isValidPublicKey,
isCIP1852AccountPath,
} from '../../../utils/bip44Validators'

import styles from './styles/ImportReadOnlyWalletScreen.style'

Expand Down Expand Up @@ -45,28 +48,6 @@ const messages = defineMessages({

let scannerRef // reference to QR code sanner to re-activate if required

// TODO(v-almonacid): move these validators somewhere else

const isString = (s) => typeof s === 'string' || s instanceof String

const isValidPath = (path: Array<any>) => {
for (const i of path) {
if (!(Number.isInteger(i) && i >= 0)) {
return false
}
}
return true
}

const canParsePublicKey = async (publicKeyHex: string): Promise<boolean> => {
try {
await Bip32PublicKey.from_bytes(Buffer.from(publicKeyHex, 'hex'))
return true
} catch (_e) {
return false
}
}

const handleOnRead = async (
event: Object,
navigation: Navigation,
Expand All @@ -76,14 +57,7 @@ const handleOnRead = async (
Logger.debug('ImportReadOnlyWalletScreen::handleOnRead::data', event.data)
const dataObj = JSON.parse(event.data)
const {publicKeyHex, path} = dataObj
if (
publicKeyHex != null &&
isString(publicKeyHex) &&
(await canParsePublicKey(publicKeyHex)) &&
path != null &&
Array.isArray(path) &&
isValidPath(path)
) {
if (isCIP1852AccountPath(path) && (await isValidPublicKey(publicKeyHex))) {
Logger.debug('ImportReadOnlyWalletScreen::publicKeyHex', publicKeyHex)
Logger.debug('ImportReadOnlyWalletScreen::path', path)
navigation.navigate(WALLET_INIT_ROUTES.SAVE_READ_ONLY_WALLET, {
Expand Down
79 changes: 58 additions & 21 deletions src/components/WalletInit/RestoreWallet/SaveReadOnlyWalletScreen.js
Expand Up @@ -8,15 +8,16 @@ import {injectIntl, intlShape, defineMessages} from 'react-intl'
import {connect} from 'react-redux'
import {compose} from 'redux'
import {withHandlers} from 'recompose'
import {withNavigationTitle} from '../../../utils/renderUtils'

import {withNavigationTitle} from '../../../utils/renderUtils'
import {Text, StatusBar} from '../../UiKit'
import WalletNameForm from '../WalletNameForm'
import {
createWalletWithBip44Account,
handleGeneralError,
} from '../../../actions'
import {generateShelleyPlateFromKey} from '../../../crypto/shelley/plate'
import {formatPath} from '../../../crypto/commonUtils'
import {WALLET_ROOT_ROUTES} from '../../../RoutesList'
import {CONFIG} from '../../../config/config'
import assert from '../../../utils/assert'
Expand Down Expand Up @@ -47,10 +48,18 @@ const messages = defineMessages({
id: 'components.walletinit.verifyrestoredwallet.walletAddressLabel',
defaultMessage: '!!!Wallet Address(es):',
},
key: {
id: 'components.walletinit.savereadonlywalletscreen.key',
defaultMessage: '!!!Key:',
},
derivationPath: {
id: 'components.walletinit.savereadonlywalletscreen.derivationPath',
defaultMessage: '!!!Derivation path:',
},
})

const CheckSumView = ({icon, checksum}) => (
<View style={styles.checkSumView}>
<View style={styles.checksumView}>
<WalletAccountIcon iconSeed={icon} />
<Text style={styles.checksumText}>{checksum}</Text>
</View>
Expand All @@ -66,7 +75,13 @@ const SaveReadOnlyWalletScreen = ({onSubmit, route, intl}) => {
})

const {formatMessage} = intl
const {publicKeyHex} = route.params
const {publicKeyHex, path} = route.params

const normalizedPath = path.map((i) => {
if (i >= CONFIG.NUMBERS.HARD_DERIVATION_START) {
return i - CONFIG.NUMBERS.HARD_DERIVATION_START
}
})

const generatePlates = async () => {
const {addresses, accountPlate} = await generateShelleyPlateFromKey(
Expand All @@ -84,26 +99,48 @@ const SaveReadOnlyWalletScreen = ({onSubmit, route, intl}) => {
<>
<SafeAreaView style={styles.safeAreaView}>
<StatusBar type="dark" />
<Text>{formatMessage(messages.checksumLabel)}</Text>
{!!plate.accountPlate.ImagePart && (
<CheckSumView
icon={plate.accountPlate.ImagePart}
checksum={plate.accountPlate.TextPart}
/>
)}

<View style={styles.addressesContainer}>
<Text>{formatMessage(messages.walletAddressLabel)}</Text>
<FlatList
data={plate.addresses}
keyExtractor={(item) => item}
renderItem={({item}) => (
<WalletAddress
addressHash={item}
networkId={CONFIG.NETWORKS.HASKELL_SHELLEY.NETWORK_ID}
<View style={styles.scrollView}>
<View style={styles.checksumContainer}>
<Text>{formatMessage(messages.checksumLabel)}</Text>
{!!plate.accountPlate.ImagePart && (
<CheckSumView
icon={plate.accountPlate.ImagePart}
checksum={plate.accountPlate.TextPart}
/>
)}
/>
</View>

<View style={styles.addressesContainer}>
<Text>{formatMessage(messages.walletAddressLabel)}</Text>
<FlatList
data={plate.addresses}
keyExtractor={(item) => item}
renderItem={({item}) => (
<WalletAddress
addressHash={item}
networkId={CONFIG.NETWORKS.HASKELL_SHELLEY.NETWORK_ID}
/>
)}
/>
</View>

<View style={styles.keyAttributesContainer}>
<Text style={styles.label}>{formatMessage(messages.key)}</Text>
<View style={styles.keyView}>
<Text secondary monospace>
{publicKeyHex}
</Text>
</View>

<Text style={styles.label}>
{formatMessage(messages.derivationPath)}
</Text>
<Text secondary monospace>
{`m/${normalizedPath[0]}'/${normalizedPath[1]}'/${
normalizedPath[2]
}`}
</Text>
</View>
</View>
</SafeAreaView>

Expand Down
@@ -1,27 +1,47 @@
// @flow
import {StyleSheet} from 'react-native'

import {COLORS} from '../../../../styles/config'
import {THEME} from '../../../../styles/config'

const SECTION_MARGIN = 22
const LABEL_MARGIN = 6

export default StyleSheet.create({
safeAreaView: {
backgroundColor: COLORS.WHITE,
backgroundColor: THEME.COLORS.BACKGROUND,
paddingHorizontal: 16,
paddingTop: 30,
},
checkSumView: {
scrollView: {
paddingRight: 10,
},
label: {
marginBottom: LABEL_MARGIN,
},
checksumContainer: {
marginBottom: SECTION_MARGIN,
},
checksumView: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 12,
marginBottom: 32,
borderColor: 'red',
flexWrap: 'wrap',
},
checksumText: {
fontSize: 18,
fontWeight: 'bold',
paddingLeft: 12,
},
addressesContainer: {
marginBottom: 32,
marginBottom: SECTION_MARGIN,
},
keyAttributesContainer: {
marginBottom: SECTION_MARGIN,
},
keyView: {
padding: 4,
backgroundColor: THEME.COLORS.CODE_STYLE_BACKGROUND,
marginBottom: 10,
},
})
5 changes: 3 additions & 2 deletions src/components/WalletInit/WalletInitScreen.js
Expand Up @@ -65,8 +65,9 @@ const messages = defineMessages({
id:
'components.walletinit.walletinitscreen.importReadOnlyWalletExplanation',
defaultMessage:
"!!!If you have a QR code containing a wallet's public key," +
', choose this option to import this wallet in read-only mode.',
"!!!The Yoroi extension allows you to export any of your wallets' " +
'public keys in a QR code. Choose this option to import a wallet from ' +
' a QR code in read-only mode.',
},
createWalletWithLedgerButton: {
id: 'components.walletinit.walletinitscreen.createWalletWithLedgerButton',
Expand Down
4 changes: 3 additions & 1 deletion src/i18n/locales/en-US.json
Expand Up @@ -313,6 +313,8 @@
"components.walletinit.restorewallet.walletverifyscreen.verifyInstructions1": "Make sure the Byron wallet address matches your old wallet.",
"components.walletinit.restorewallet.walletverifyscreen.verifyInstructions2": "If you entered the wrong recovery phrase, you will get an empty wallet with different addresses from your old one.",
"components.walletinit.savereadonlywalletscreen.defaultWalletName": "My read-only wallet",
"components.walletinit.savereadonlywalletscreen.derivationPath": "Derivation path:",
"components.walletinit.savereadonlywalletscreen.key": "Key:",
"components.walletinit.savereadonlywalletscreen.title": "Verify read-only wallet",
"components.walletinit.walletdescription.byEmurgo": "By",
"components.walletinit.walletform.continueButton": "Continue",
Expand All @@ -324,7 +326,7 @@
"components.walletinit.walletfreshinitscreen.addWalletOnShelleyButton": "Add wallet (Jormungandr ITN)",
"components.walletinit.walletinitscreen.createWalletButton": "Create wallet",
"components.walletinit.walletinitscreen.createWalletWithLedgerButton": "Connect to Ledger Nano",
"components.walletinit.walletinitscreen.importReadOnlyWalletExplanation": "If you have a QR code containing a wallet's public key, choose this option to import this wallet in read-only mode.",
"components.walletinit.walletinitscreen.importReadOnlyWalletExplanation": "The Yoroi extension allows you to export any of your wallets' public keys in a QR code. Choose this option to import a wallet from a QR code in read-only mode.",
"components.walletinit.walletinitscreen.importReadOnlyWalletLabel": "Read-only wallet",
"components.walletinit.walletinitscreen.restoreNormalWalletExplanation": "If you have a Yoroi recovery phrase consisting of 15 words generated when you created a Yoroi Wallet, choose this option to restore your wallet.",
"components.walletinit.walletinitscreen.restoreNormalWalletLabel": "15-word Wallet",
Expand Down
1 change: 1 addition & 0 deletions src/styles/config.js
Expand Up @@ -41,6 +41,7 @@ export const DEFAULT_THEME_COLORS = {
NAVIGATION_ACTIVE: COLORS.LIGHT_POSITIVE_GREEN,
NAVIGATION_INACTIVE: COLORS.SECONDARY_TEXT,
BACKGROUND: COLORS.WHITE,
CODE_STYLE_BACKGROUND: COLORS.LIGHT_GRAY,
}

export const THEME = {
Expand Down
45 changes: 45 additions & 0 deletions src/utils/bip44Validators.js
@@ -0,0 +1,45 @@
// @flow
import {Bip32PublicKey} from 'react-native-haskell-shelley'

import {NUMBERS} from '../config/numbers'
import assert from './assert'

const isString = (s) => typeof s === 'string' || s instanceof String

const isUInt32 = (i) => Number.isInteger(i) && i >= 0 && i < 4294967296

export const isValidPath = (path: any): boolean => {
if (!(Array.isArray(path) && path.length > 0 && path.length <= 5)) {
return false
}
for (const i of path) {
if (!isUInt32(i)) {
return false
}
}
return true
}

export const isCIP1852AccountPath = (path: Array<number>): boolean => {
assert.preconditionCheck(isValidPath(path), 'invalid bip44 path')
// note: allows non-zero accounts
return (
path.length === 3 &&
(path[0] === NUMBERS.WALLET_TYPE_PURPOSE.CIP1852 || path[0] === 1852) &&
(path[1] === NUMBERS.COIN_TYPES.CARDANO || path[1] === 1815)
)
}

export const canParsePublicKey = async (
publicKeyHex: string,
): Promise<boolean> => {
try {
await Bip32PublicKey.from_bytes(Buffer.from(publicKeyHex, 'hex'))
return true
} catch (_e) {
return false
}
}

export const isValidPublicKey = async (key: any): Promise<boolean> =>
key != null && isString(key) && (await canParsePublicKey(key))
40 changes: 40 additions & 0 deletions src/utils/bip44Validators.test.js
@@ -0,0 +1,40 @@
// @flow
import jestSetup from '../jestSetup'

import {isCIP1852AccountPath, isValidPath} from './bip44Validators'

jestSetup.setup()

describe('Check BIP44/CIP1852 paths', () => {
it('valid CIP1852 paths', () => {
const account0 = [1852, 1815, 0]
expect(isCIP1852AccountPath(account0)).toEqual(true)

const account0Hardened = [2147485500, 2147485463, 2147483648]
expect(isCIP1852AccountPath(account0Hardened)).toEqual(true)

const lastAccount = [2147485500, 2147485463, 4294967295]
expect(isCIP1852AccountPath(lastAccount)).toEqual(true)
})

it('invalid CIP1852 paths', () => {
const bip44Path = [2147483692, 2147485463, 2147483648]
expect(isCIP1852AccountPath(bip44Path)).toEqual(false)

const nonHardened = [2147483692, 2147485463, 0]
expect(isCIP1852AccountPath(nonHardened)).toEqual(false)
})

it('invalid BIP44 paths', () => {
const paths = [
undefined,
'',
[],
{},
[2147483692, 2147485463, 2147483648, 2147483692, 2147483692, 2147483692],
]
for (const path of paths) {
expect(isValidPath(path)).toEqual(false)
}
})
})

0 comments on commit e0a19e7

Please sign in to comment.