Skip to content

Commit

Permalink
Merge pull request #370 from Emurgo/develop
Browse files Browse the repository at this point in the history
Yoroi 1.4.0 Release - Ledger + Desync fix
  • Loading branch information
nicarq authored Mar 27, 2019
2 parents 673fe6a + 43abb1d commit 3ff6e2d
Show file tree
Hide file tree
Showing 145 changed files with 2,472 additions and 1,725 deletions.
2 changes: 1 addition & 1 deletion .babelrc
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"react-intl",
{
"messagesDir": "./translations/messages/",
"enforceDescriptions": true,
"enforceDescriptions": false,
"extractSourceLocation": true
}
],
Expand Down
15 changes: 15 additions & 0 deletions app/actions/ada/hw-verify-address-actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// @flow
import Action from '../lib/Action';

import type {
BIP32Path
} from '@cardano-foundation/ledgerjs-hw-app-cardano';
import Wallet from '../../domain/Wallet';

// ======= ADDRESSES ACTIONS =======

export default class HWVerifyAddressActions {
closeAddressDetailDialog: Action<void> = new Action();
selectAddress: Action<{ address: string, path: BIP32Path }> = new Action();
verifyAddress: Action<{ wallet: Wallet }> = new Action();
}
3 changes: 3 additions & 0 deletions app/actions/ada/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import HWConnectActions from './hw-connect-actions';
import TrezorSendActions from './trezor-send-actions';
import AdaRedemptionActions from './ada-redemption-actions';
import LedgerSendActions from './ledger-send-actions';
import HWVerifyAddressActions from './hw-verify-address-actions';

export type AdaActionsMap = {
adaRedemption: AdaRedemptionActions,
Expand All @@ -20,6 +21,7 @@ export type AdaActionsMap = {
trezorSend: TrezorSendActions,
ledgerConnect: HWConnectActions,
ledgerSend: LedgerSendActions,
hwVerifyAddress: HWVerifyAddressActions,
};

const adaActionsMap: AdaActionsMap = {
Expand All @@ -33,6 +35,7 @@ const adaActionsMap: AdaActionsMap = {
trezorSend: new TrezorSendActions(),
ledgerConnect: new HWConnectActions(),
ledgerSend: new LedgerSendActions(),
hwVerifyAddress: new HWVerifyAddressActions(),
};

export default adaActionsMap;
94 changes: 43 additions & 51 deletions app/api/ada/adaAddress.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

import { Wallet } from 'rust-cardano-crypto';
import _ from 'lodash';
import config from '../../config';
import {
toAdaAddress
} from './lib/cardanoCrypto/cryptoToModel';
Expand All @@ -15,16 +14,20 @@ import {
getAddresses,
getAddressesList,
getAddressesListByType,
deleteAddress
} from './lib/lovefieldDatabase';
import type {
AddressesTableRow
} from './lib/lovefieldDatabase';
import {
getLastReceiveAddressIndex,
saveLastReceiveAddressIndex
} from './adaLocalStorage';
import {
UnusedAddressesError,
} from '../common';
import {
getAddressInHex
getAddressInHex,
getLatestUsedIndex
} from './lib/utils';
import type {
AdaAddresses,
Expand All @@ -35,8 +38,6 @@ import {
stringifyError
} from '../../utils/logging';

const { MAX_ALLOWED_UNUSED_ADDRESSES } = config.wallets;

export function isValidAdaAddress(address: string): Promise<boolean> {
try {
const result: boolean = getResultOrFail(Wallet.checkAddress(getAddressInHex(address)));
Expand Down Expand Up @@ -75,72 +76,63 @@ export function getAdaAddressesByType(addressType: AddressType): Promise<AdaAddr
return getAddressesListByType(addressType);
}

export async function newExternalAdaAddress(
cryptoAccount: CryptoAccount
): Promise<AdaAddress> {
return await newAdaAddress(cryptoAccount, 'External', addresses => {
// We use the isUsed status to now find the next unused address
const lastUsedAddressIndex = _.findLastIndex(addresses, address => address.cadIsUsed) + 1;
// TODO Move this to a config file
const unusedSpan = addresses.length - lastUsedAddressIndex;
if (unusedSpan >= MAX_ALLOWED_UNUSED_ADDRESSES) {
throw new UnusedAddressesError();
}
});
/**
* With bip44, we keep a buffer of unused addresses
* This means when we need a new address, we just use one of the unused ones in our buffer
*
* Once this address appears in a transaction, it will be marked as "used" and a new address
* will be generated by a different process to maintain bip44 compliance
*/
export async function popBip44Address(type: AddressType): Promise<AdaAddress> {
return type === 'Internal'
? popBip44InternalAddress()
: popBip44ExternalAddress();
}

/** Create and save the next address for the given account */
export async function newAdaAddress(
cryptoAccount: CryptoAccount,
addressType: AddressType,
addrValidation?: (AdaAddresses => void)
): Promise<AdaAddress> {
const address: AdaAddress = await createAdaAddress(cryptoAccount, addressType, addrValidation);
await saveAdaAddress(address, addressType);
return address;
async function popBip44InternalAddress(): Promise<AdaAddress> {
const existingAddresses = await getAdaAddressesByType('Internal');
const nextAddressIndex = getLatestUsedIndex(existingAddresses) + 1;
if (nextAddressIndex === existingAddresses.length) {
throw new UnusedAddressesError();
}
const poppedAddress = existingAddresses[nextAddressIndex];

return poppedAddress;
}

/** Create new wallet address based off bip44 and then convert it to an AdaAddress */
export async function createAdaAddress(
cryptoAccount: CryptoAccount,
addressType: AddressType,
addrValidation?: (AdaAddresses => void)
): Promise<AdaAddress> {
// Note this function doesn't just get the addresses but also calculates their isUsed status
const filteredAddresses = await getAdaAddressesByType(addressType);
if (addrValidation) {
addrValidation(filteredAddresses);
async function popBip44ExternalAddress(): Promise<AdaAddress> {
const existingAddresses = await getAdaAddressesByType('External');
const nextAddressIndex = getLastReceiveAddressIndex() + 1;
if (nextAddressIndex === existingAddresses.length) {
throw new UnusedAddressesError();
}
const addressIndex = filteredAddresses.length;
const [address]: Array<string> = getResultOrFail(
Wallet.generateAddresses(cryptoAccount, addressType, [addressIndex])
);
return toAdaAddress(cryptoAccount.account, addressType, addressIndex, address);
const poppedAddress = existingAddresses[nextAddressIndex];
saveLastReceiveAddressIndex(nextAddressIndex);

return poppedAddress;
}

/** Wrapper function to save addresses to LovefieldDB */
/** Wrapper function to save addresses to LovefieldDB
* Note: does NOT update lastReceiveAddressIndex
*/
export function saveAdaAddress(
address: AdaAddress,
addressType: AddressType
): Promise<Array<AddressesTableRow>> {
return saveAddresses([address], addressType);
}

/** Wrapper function to remove an addresse from LovefieldDB */
export function removeAdaAddress(
address: AdaAddress
): Promise<Array<void>> {
return deleteAddress(address.cadId);
}

/** Save list of addresses from lovefieldDB */
/** Save list of addresses to lovefieldDB
* Note: does NOT update lastReceiveAddressIndex
*/
export async function saveAsAdaAddresses(
cryptoAccount: CryptoAccount,
addresses: Array<string>,
offset: number,
addressType: AddressType
): Promise<Array<AddressesTableRow>> {
const mappedAddresses: Array<AdaAddress> = addresses.map((hash, index) => (
toAdaAddress(cryptoAccount.account, addressType, index, hash)
toAdaAddress(cryptoAccount.account, addressType, index + offset, hash)
));
return saveAddresses(mappedAddresses, addressType);
}
Expand Down
46 changes: 40 additions & 6 deletions app/api/ada/adaLocalStorage.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,18 @@

import type { AdaWallet } from './adaTypes';

export type LocalStorageWallet = {
adaWallet: AdaWallet,
masterKey?: string, // unused in hardware wallets
// this is a per-account setting but we only have 1 account per wallet in Yoroi
lastReceiveAddressIndex: number
}

// Use constant keys to store/load localstorage
const storageKeys = {
ACCOUNT_KEY: 'ACCOUNT', // Note: only a single account
WALLET_KEY: 'WALLET',
LAST_BLOCK_NUMBER_KEY: 'LAST_BLOCK_NUMBER'
LAST_BLOCK_NUMBER_KEY: 'LAST_BLOCK_NUMBER',
};

/* Account storage */
Expand All @@ -25,26 +32,40 @@ export function getSingleCryptoAccount(): CryptoAccount {

/* Wallet storage */

/** @param AdaWallet cache wallet information
* @param masterKey decrypt to send funds */
export function saveAdaWallet(
export function createStoredWallet(
adaWallet: AdaWallet,
masterKey?: string
): void {
_saveInStorage(storageKeys.WALLET_KEY, { adaWallet, masterKey });
_saveInStorage(storageKeys.WALLET_KEY, ({
adaWallet,
masterKey,
lastReceiveAddressIndex: 0 // always start by showing one address
}: LocalStorageWallet));
}

export function getAdaWallet(): ?AdaWallet {
const stored = _getFromStorage(storageKeys.WALLET_KEY);
return stored ? stored.adaWallet : null;
}

export function saveAdaWallet(adaWallet: AdaWallet): void {
const stored: LocalStorageWallet = _getFromStorage(storageKeys.WALLET_KEY);
stored.adaWallet = adaWallet;
_saveInStorage(storageKeys.WALLET_KEY, stored);
}

export function getWalletMasterKey(): string {
const stored = _getFromStorage(storageKeys.WALLET_KEY);
return stored.masterKey;
}

/* Last block Nunmber storage */
export function saveWalletMasterKey(masterKey: string): void {
const stored: LocalStorageWallet = _getFromStorage(storageKeys.WALLET_KEY);
stored.masterKey = masterKey;
_saveInStorage(storageKeys.WALLET_KEY, stored);
}

/* Last block number storage */

export function saveLastBlockNumber(blockNumber: number): void {
_saveInStorage(storageKeys.LAST_BLOCK_NUMBER_KEY, blockNumber);
Expand All @@ -56,6 +77,19 @@ export function getLastBlockNumber(): number {
return Number(lastBlockNum);
}

/* Last block number storage */

export function saveLastReceiveAddressIndex(index: number): void {
const stored: LocalStorageWallet = _getFromStorage(storageKeys.WALLET_KEY);
stored.lastReceiveAddressIndex = index;
_saveInStorage(storageKeys.WALLET_KEY, stored);
}

export function getLastReceiveAddressIndex(): number {
const stored = _getFromStorage(storageKeys.WALLET_KEY);
return stored.lastReceiveAddressIndex;
}

/* Util functions */

function _saveInStorage(key: string, toSave: any): void {
Expand Down
94 changes: 94 additions & 0 deletions app/api/ada/adaMigration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// @flow

// Handle migration to newer versions of Yoroi

import {
reset,
getAddressesList
} from './lib/lovefieldDatabase';
import {
saveLastReceiveAddressIndex,
} from './adaLocalStorage';
import LocalStorageApi from '../localStorage/index';
import {
Logger,
} from '../../utils/logging';

const semver = require('semver');

export async function migrateToLatest(localStorageApi: LocalStorageApi) {
const lastLaunchVersion = await localStorageApi.getLastLaunchVersion();
Logger.info(`Starting migration for ${lastLaunchVersion}`);
/**
* Note: Although we don't start migration if the user is running a fresh installation
* We still cannot be certain any key exists in localstorage
*
* For example, somebody may have downloaded Yoroi a long time ago
* but only completed the language select before closing the application
*
* Therefore, you need to always check that data exists before migrating it
*/

/**
* Note: Be careful about the kinds of migrations you do here.
* You are essentially swapping the app state under the hood
* Therefore mobx may not notice the change as expected
*/

const migrationMap: { [ver: string]: (() => Promise<void>) } = {
'=0.0.0': async () => await testMigration(localStorageApi),
'<1.4.0': bip44Migration
};

for (const key of Object.keys(migrationMap)) {
if (semver.satisfies(lastLaunchVersion, key)) {
Logger.info(`Migration started for ${key}`);
await migrationMap[key]();
}
}
}

/**
* We use this as a dummy migration so that our tests can verify migration is working correctly
*/
async function testMigration(localStorageApi: LocalStorageApi): Promise<void> {
// changing the locale is something we can easily detect from our tests
Logger.info(`Starting testMigration`);
// Note: mobx will not notice this change until you refresh
await localStorageApi.setUserLocale('ja-JP');
}

/**
* Previous version of Yoroi were not BIP44 compliant
* Notably, it didnt scan 20 addresses ahead of the last used address.
* This causes desyncs when you use Yoroi either on multiple computers with the same wallet
* or you use the same wallet on Chrome + mobile.
*/
async function bip44Migration(): Promise<void> {
Logger.info(`Starting bip44Migration`);
const addresses = await getAddressesList();
if (!addresses || addresses.length === 0) {
return;
}
/**
* We used to consider all addresses in the DB as explicitly generated by the user
* However, BIP44 requires us to also store 20 addresses after the last used address
* Therefore the highest index in the old format is the heightest generated for new format
*/
const maxIndex = Math.max(
...addresses
.filter(address => address.change === 0)
.map(address => address.index),
0
);

// if we had more than one address, then the WALLET key must exist in localstorage
saveLastReceiveAddressIndex(maxIndex);

/**
* Once we've saved the receive address, we dump the DB entirely
* We need to do this since old wallets may have incorrect history
* Due to desync issue caused by the incorrect bip44 implementation
*/
reset();
}
Loading

0 comments on commit 3ff6e2d

Please sign in to comment.