View on GitHub
File Changes
      // Note: we only restore for 0th account
      const accountIndex = HARD_DERIVATION_START + 0;
      const rootPk = generateWalletRootKey(recoveryPhrase);
-
      const accountKey = rootPk.bip44_account(
-
        RustModule.Wallet.AccountIndex.new(accountIndex)
-
      );

                      
      const wallet = await createStandardBip44Wallet({
        db: request.db,
        }),
        rootPk,
        password: walletPassword,
-
        accountPublicKey: accountKey.public(),
        accountIndex,
        walletName,
        accountName: '', // set account name empty now
}

                      
export async function getSelectedExplorer(): Promise<ExplorerType> {
-
  const explorer = await _getFromStorage(storageKeys.SELECTED_EXPLORER_KEY);
+
  const explorer = await _getFromStorage<ExplorerType>(storageKeys.SELECTED_EXPLORER_KEY);
  return explorer || Explorer.SEIZA;
}

                      
/* Util functions */
-
async function _saveInStorage(key: string, toSave: any): Promise<void> {
+
export async function _saveInStorage(key: string, toSave: any): Promise<void> {
  await setLocalItem(key, JSON.stringify(toSave));
}

                      
-
async function _getFromStorage(key: string): any | void {
+
export async function _getFromStorage<T>(key: string): Promise<T | void> {
  return await getLocalItem(key).then((result) => {
    if (result == null) {
      return result;
import {
  resetLegacy,
  getLegacyAddressesList,
+
  legacyGetLastReceiveAddressIndex,
+
  legacySaveLastReceiveAddressIndex,
+
  legacyGetLocalStorageWallet,
+
  getCurrentCryptoAccount,
+
  clearStorageV1,
} from './database/legacy';
import LocalStorageApi from '../../../localStorage/index';
import {
import {
  OPEN_TAB_ID_KEY,
} from '../../../../utils/tabManager';
+
import { migrateFromStorageV1 } from './bridge/walletHelper';
+
import { RustModule } from '../cardanoCrypto/rustLoader';
+
import type { ConfigType } from '../../../../../config/config-types';
+

                      
+
declare var CONFIG: ConfigType;
+
const protocolMagic = CONFIG.network.protocolMagic;

                      
export async function migrateToLatest(
  localStorageApi: LocalStorageApi,

                      
  // if we had more than one address, then the WALLET key must exist in localstorage
  try {
-
    await saveLastReceiveAddressIndex(maxIndex);
+
    await legacySaveLastReceiveAddressIndex(maxIndex);
  } catch (_err) {
    // no wallet in storage
    return false;
  persistentDb: lf$Database,
): Promise<boolean> {
  // all information in the v1 indexdb can be inferred from the blockchain
-
  await reset(persistentDb);
+
  await resetLegacy();

                      
+
  const wallet = await legacyGetLocalStorageWallet();
+
  const account = await getCurrentCryptoAccount();
+
  if (wallet != null && account != null) {
+
    const lastReceiveIndex = await legacyGetLastReceiveAddressIndex();
+

                      
+
    const settings = RustModule.Wallet.BlockchainSettings.from_json({
+
      protocol_magic: protocolMagic
+
    });
+
    const migratedWallet = await migrateFromStorageV1({
+
      db: persistentDb,
+
      accountPubKey: account.root_cached_key,
+
      displayCutoff: lastReceiveIndex,
+
      encryptedPk: wallet.masterKey == null
+
        ? undefined
+
        : {
+
          Hash: wallet.masterKey,
+
          IsEncrypted: true,
+
          PasswordLastUpdate: wallet.adaWallet.cwPassphraseLU == null
+
            ? null
+
            : wallet.adaWallet.cwPassphraseLU,
+
        },
+
      hwWalletMetaInsert: wallet.adaWallet.cwHardwareInfo == null
+
        ? undefined
+
        : {
+
          Vendor: wallet.adaWallet.cwHardwareInfo.vendor,
+
          Model: wallet.adaWallet.cwHardwareInfo.model,
+
          Label: wallet.adaWallet.cwHardwareInfo.deviceId,
+
          DeviceId: wallet.adaWallet.cwHardwareInfo.label,
+
          Language: wallet.adaWallet.cwHardwareInfo.language,
+
          MajorVersion: wallet.adaWallet.cwHardwareInfo.majorVersion,
+
          MinorVersion: wallet.adaWallet.cwHardwareInfo.minorVersion,
+
          PatchVersion: wallet.adaWallet.cwHardwareInfo.patchVersion,
+
        },
+
      settings,
+
      walletName: wallet.adaWallet.cwMeta.cwName,
+
    });
+
  }

                      
+
  await clearStorageV1();
  return true;
}
  const entropy = RustModule.Wallet.Entropy.from_english_mnemonics(walletMnemonic);
  const rootPk = RustModule.Wallet.Bip44RootPrivateKey.recover(entropy, '');

                      
-
  const firstAccountIndex = 0 + HARD_DERIVATION_START;
-
  const firstAccountPk = rootPk.bip44_account(
-
    RustModule.Wallet.AccountIndex.new(firstAccountIndex)
-
  );
-

                      
  const state = await createStandardBip44Wallet({
    db,
    settings,
    rootPk,
    password: privateDeriverPassword,
-
    accountPublicKey: firstAccountPk.public(),
    accountIndex: HARD_DERIVATION_START + 0,
    walletName: 'My Test Wallet',
    accountName: '',
  CARDANO_COINTYPE,
  BIP44_PURPOSE,
  BIP44_SCAN_SIZE,
+
  EXTERNAL,
} from '../../../../../config/numbersConfig';

                      
import type {
import {
  GetOrAddAddress,
} from '../database/uncategorized/api/write';
+
import type { KeyInsert } from '../database/uncategorized/tables';
import type { HWFeatures, } from '../database/wallet/tables';

                      
import { WalletBuilder } from './walletBuilder';
  settings: RustModule.Wallet.BlockchainSettings,
  rootPk: RustModule.Wallet.Bip44RootPrivateKey,
  password: string,
-
  // TODO: remove since we know root PK and index
-
  accountPublicKey: RustModule.Wallet.Bip44AccountPublic,
  accountIndex: number,
  walletName: string,
  accountName: string,
    Buffer.from(request.rootPk.key().to_hex(), 'hex'),
  );

                      
+
  const accountPublicKey = request.rootPk.bip44_account(
+
    RustModule.Wallet.AccountIndex.new(request.accountIndex)
+
  ).public();
+

                      
  // Note: we generate initial addresses in a separate database query
  // from creation of the actual wallet
  const initialDerivations = await raii(

                      
      return await getAccountDefaultDerivations(
        request.settings,
-
        request.accountPublicKey,
+
        accountPublicKey,
        hashToIdFunc,
      );
    }
            Name: request.accountName,
            LastSyncInfoId: ids.lastSyncInfoId,
          }),
+
          parentDerivationId: null,
          pathToPublic: [
            {
              index: BIP44_PURPOSE,

                      
  return state;
}
+

                      
+
export async function migrateFromStorageV1(request: {
+
  db: lf$Database,
+
  settings: RustModule.Wallet.BlockchainSettings,
+
  encryptedPk: void | KeyInsert,
+
  accountPubKey: string,
+
  displayCutoff: number,
+
  walletName: string,
+
  hwWalletMetaInsert: void | HWFeatures,
+
}): Promise<HasConceptualWallet & HasBip44Wrapper & HasPublicDeriver<mixed>> {
+

                      
+
  const accountIndex = HARD_DERIVATION_START + 0;
+
  const accountName = '';
+

                      
+
  const accountPublicKey = RustModule.Wallet.Bip44AccountPublic.new(
+
    RustModule.Wallet.PublicKey.from_hex(request.accountPubKey),
+
    RustModule.Wallet.DerivationScheme.v2()
+
  );
+

                      
+
  // Note: we generate initial addresses in a separate database query
+
  // from creation of the actual wallet
+
  const initialDerivations = await raii(
+
    request.db,
+
    getAllSchemaTables(request.db, GetOrAddAddress),
+
    async tx => {
+
      const hashToIdFunc = async (
+
        addressHash: Array<string>
+
      ): Promise<Array<number>> => {
+
        const rows = await GetOrAddAddress.addByHash(
+
          request.db, tx,
+
          addressHash
+
        );
+
        return rows.map(row => row.AddressId);
+
      };
+

                      
+
      const insert = await getAccountDefaultDerivations(
+
        request.settings,
+
        accountPublicKey,
+
        hashToIdFunc,
+
      );
+
      // replace default display cutoff
+
      const external = insert.find(chain => chain.index === EXTERNAL);
+
      if (external == null) {
+
        throw new Error('migrateFromStorageV1 cannot find external chain. Should never happen');
+
      }
+
      external.insert.DisplayCutoff = request.displayCutoff;
+

                      
+
      return insert;
+
    }
+
  );
+

                      
+
  let state;
+
  {
+
    let builder = WalletBuilder
+
      .start(request.db)
+
      .addConceptualWallet(
+
        _finalState => ({
+
          CoinType: CARDANO_COINTYPE,
+
          Name: request.walletName,
+
        })
+
      )
+
      .addBip44Wrapper(
+
        finalState => ({
+
          ConceptualWalletId: finalState.conceptualWalletRow.ConceptualWalletId,
+
          IsBundled: false,
+
          SignerLevel: DerivationLevels.ROOT.level,
+
          PublicDeriverLevel: DerivationLevels.ACCOUNT.level,
+
          Version: 2,
+
        })
+
      );
+
    if (request.encryptedPk != null) {
+
      const encryptedPk = request.encryptedPk;
+
      builder = builder.addPrivateDeriver(
+
        finalState => ({
+
          // private deriver level === root level
+
          pathToPrivate: [],
+
          addLevelRequest: parent => ({
+
            privateKeyInfo: encryptedPk,
+
            publicKeyInfo: null,
+
            derivationInfo: keys => ({
+
              PublicKeyId: keys.public,
+
              PrivateKeyId: keys.private,
+
              Parent: parent,
+
              Index: null,
+
            }),
+
            levelInfo: id => ({
+
              KeyDerivationId: id,
+
            })
+
          }),
+
          addPrivateDeriverRequest: derivationId => ({
+
            Bip44WrapperId: finalState.bip44WrapperRow.Bip44WrapperId,
+
            KeyDerivationId: derivationId,
+
            Level: DerivationLevels.ROOT.level,
+
          }),
+
        })
+
      );
+
    }
+
    builder = builder
+
      .addAdhocPublicDeriver(
+
        finalState => ({
+
          bip44WrapperId: finalState.bip44WrapperRow.Bip44WrapperId,
+
          publicKey: {
+
            Hash: accountPublicKey.key().to_hex(),
+
            IsEncrypted: false,
+
            PasswordLastUpdate: null,
+
          },
+
          publicDeriverInsert: ids => ({
+
            Bip44WrapperId: ids.wrapperId,
+
            KeyDerivationId: ids.derivationId,
+
            Name: accountName,
+
            LastSyncInfoId: ids.lastSyncInfoId,
+
          }),
+
          parentDerivationId: finalState.privateDeriver == null
+
            ? null
+
            : finalState.privateDeriver.privateDeriverResult.KeyDerivationId,
+
          pathToPublic: [
+
            {
+
              index: BIP44_PURPOSE,
+
              insert: {},
+
            },
+
            {
+
              index: CARDANO_COINTYPE,
+
              insert: {},
+
            },
+
            {
+
              index: accountIndex,
+
              insert: {},
+
            },
+
          ],
+
          initialDerivations,
+
          hwWalletMetaInsert: request.hwWalletMetaInsert == null
+
            ? undefined
+
            : {
+
              ConceptualWalletId: finalState.conceptualWalletRow.ConceptualWalletId,
+
              ...request.hwWalletMetaInsert
+
            },
+
        })
+
      );
+
    state = builder.commit();
+
  }
+

                      
+
  return state;
+
}
export type AddAdhocPublicDeriverRequest = {|
  bip44WrapperId: number,
  publicKey: KeyInsert,
+
  parentDerivationId: null | number,
  pathToPublic: InsertPath,
  publicDeriverInsert: {
    derivationId: number,
    tx: lf$Transaction,
    request: AddAdhocPublicDeriverRequest,
  ): Promise<AddAdhocPublicDeriverResponse<Row>> {
-
    let parentId: number | null = null;
+
    let parentId: number | null = request.parentDerivationId;
    for (let i = 0; i < request.pathToPublic.length - 1; i++) {
      const levelResult = await AddPrivateDeriver.depTables.AddDerivation.add(
        db, tx,
const populateAndCreate = async (
  storeType: $Values<typeof schema.DataStoreType>
): Promise<lf$Database> => {
-
  const schemaBuilder = schema.create('yoroi-schema', 7);
+
  const schemaBuilder = schema.create('yoroi-schema', 2);

                      
  populateUncategorizedDb(schemaBuilder);
  populateBip44Db(schemaBuilder);

                      
import { dumpByVersion } from './index';
import { RustModule } from '../../cardanoCrypto/rustLoader';
+
import {
+
  _getFromStorage,
+
  _saveInStorage,
+
} from '../adaLocalStorage';
+
import {
+
  removeLocalItem,
+
} from '../../../../localStorage/primitives';

                      
/**
 * This file contains methods used to extract information
} & LegacyAddressingInfo;

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

                      
-
export type LegacyCryptoAccount = {
-
  account: number,
-
  // master public key
-
  root_cached_key: RustModule.Wallet.Bip44AccountPublic,
-
  derivation_scheme: string
-
}
-

                      
type LegacyLocalStorageCryptoAccount = {
  account: number,
  root_cached_key: string, // MasterPublicKey
  derivation_scheme: string
}

                      
+
export type LegacyAdaWallet = {
+
  cwAmount: LegacyAdaAmount,
+
  cwId: string,
+
  cwMeta: LegacyAdaWalletMetaParams,
+
  cwType: LegacyAdaWalletType,
+
  cwPassphraseLU?: Date,
+
  cwHardwareInfo?: LegacyAdaWalletHardwareInfo,
+
};
+
export type LegacyAdaWalletMetaParams = {
+
  cwName: string,
+
  cwAssurance: LegacyAdaAssurance,
+
  // This was never used but is supposed to represent 0 = (bitcoin, ada); 1 = (satoshi, lovelace)
+
  cwUnit: number
+
};
+
export type LegacyAdaAssurance = 'CWANormal' | 'CWAStrict';
+
export type LegacyAdaWalletType = 'CWTWeb' | 'CWTHardware';
+
export type LegacyAdaWalletHardwareInfo = {
+
  vendor : string,
+
  model: string,
+
  deviceId: string,
+
  label: string,
+
  majorVersion: number,
+
  minorVersion: number,
+
  patchVersion: number,
+
  language: string,
+
  publicMasterKey: string,
+
};
+

                      
+
const legacyStorageKeys = {
+
  ACCOUNT_KEY: 'ACCOUNT', // Note: only a single account
+
  WALLET_KEY: 'WALLET',
+
  LAST_BLOCK_NUMBER_KEY: 'LAST_BLOCK_NUMBER',
+
};
+

                      
export const getLegacyAddressesList = (): Array<LegacyAdaAddress> => {
  if (dumpByVersion.Addresses) {
    return dumpByVersion.Addresses;
    delete dumpByVersion[prop];
  }
};
+

                      
+
export async function legacySaveLastReceiveAddressIndex(index: number): Promise<void> {
+
  const stored = await _getFromStorage<LegacyLocalStorageWallet>(legacyStorageKeys.WALLET_KEY);
+
  if (!stored) {
+
    throw new Error('Need to create a wallet before saving wallet metadata');
+
  }
+
  stored.lastReceiveAddressIndex = index;
+
  await _saveInStorage(legacyStorageKeys.WALLET_KEY, stored);
+
}
+
export async function legacyGetLastReceiveAddressIndex(): Promise<number> {
+
  const stored  = await _getFromStorage<LegacyLocalStorageWallet>(legacyStorageKeys.WALLET_KEY);
+
  return stored ? stored.lastReceiveAddressIndex : 0;
+
}
+

                      
+
export async function legacyGetLocalStorageWallet(): Promise<void | LegacyLocalStorageWallet> {
+
  const stored = await _getFromStorage<LegacyLocalStorageWallet>(legacyStorageKeys.WALLET_KEY);
+
  return stored ? stored : undefined;
+
}
+
export async function getCurrentCryptoAccount(): Promise<null | LegacyLocalStorageCryptoAccount> {
+
  const localAccount = await _getFromStorage<LegacyLocalStorageCryptoAccount>(
+
    legacyStorageKeys.ACCOUNT_KEY
+
  );
+
  if (!localAccount) {
+
    return null;
+
  }
+
  if (localAccount.derivation_scheme !== 'V2') {
+
    throw Error('Sanity check');
+
  }
+
  return localAccount;
+
}
+

                      
+
export async function clearStorageV1(): Promise<void> {
+
  await removeLocalItem(legacyStorageKeys.ACCOUNT_KEY);
+
  await removeLocalItem(legacyStorageKeys.WALLET_KEY);
+
  await removeLocalItem(legacyStorageKeys.LAST_BLOCK_NUMBER_KEY);
+
}
// @flow

                      
-
import { migrateToLatest } from '../adaMigration';
+
import { RustModule } from '../../cardanoCrypto/rustLoader';
+

                      
+
beforeAll(async () => {
+
  await RustModule.load();
+
});
+

                      
+
test('Migrate storage v1 to storage v2', async (done) => {
+

                      
+
  // TODO
+
  done();
+
});
    settings,
    rootPk,
    password: privateDeriverPassword,
-
    accountPublicKey: firstAccountPk.public(),
    accountIndex: HARD_DERIVATION_START + 0,
    walletName: 'My Test Wallet',
    accountName: '',
  description: '[staging] Cardano ADA wallet',
  defaultTitle: '[staging] Yoroi',
  contentSecurityPolicy: `default-src 'self'; frame-src ${SEIZA_FOR_YOROI_URL} ${SEIZA_URL} https://connect.trezor.io/ https://emurgo.github.io/yoroi-extension-ledger-bridge; script-src 'self' 'unsafe-eval' blob:; connect-src wss://stg-yoroi-backend.yoroiwallet.com:443 https://stg-yoroi-backend.yoroiwallet.com; style-src * 'unsafe-inline' 'self' blob:; img-src 'self' data:;`,
-
  version_name: 'st-1.9.0',
+
  version_name: 'st-1.10.0',
});
  versionName,
  extensionKey
}) => ({
-
  version: '1.9.0',
+
  version: '1.10.0',
  name: 'yoroi',
  manifest_version: 2,
  ...(versionName ? { version_name: versionName } : {}),
  description: '[testnet] Cardano ADA wallet',
  defaultTitle: '[testnet] Yoroi',
  contentSecurityPolicy: `default-src 'self'; frame-src ${SEIZA_FOR_YOROI_URL} ${SEIZA_URL} https://connect.trezor.io/ https://emurgo.github.io/yoroi-extension-ledger-bridge; script-src 'self' 'unsafe-eval'; connect-src wss://testnet-yoroi-backend.yoroiwallet.com:443 https://testnet-yoroi-backend.yoroiwallet.com; style-src * 'unsafe-inline' 'self' blob:; img-src 'self' data:;`,
-
  versionName: 'tn-1.9.0',
+
  versionName: 'tn-1.10.0',
});