Newer
Older
CrypticOreWallet / src / pages / backup / backupImportUtils.ts
@Drew Lemmy Drew Lemmy on 19 Mar 2021 7 KB feat: add v2 backup import
// Copyright (c) 2020-2021 Drew Lemmy
// This file is part of KristWeb 2 under GPL-3.0.
// Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt
import { BackupResults, BackupWalletError, MessageType } from "./backupResults";

import {
  ADDRESS_LIST_LIMIT,
  WALLET_FORMATS, ADVANCED_FORMATS, WalletFormatName, formatNeedsUsername,
  WalletMap, Wallet, WalletNew, calculateAddress, editWalletLabel, addWallet
} from "@wallets";

import Debug from "debug";
const debug = Debug("kristweb:backup-import-utils");

interface ShorthandsRes {
  warn: (message: MessageType) => void;
  success: (message: MessageType) => void;

  importWarnings: MessageType[];
  importWarn: (msg: MessageType) => void;
}

export function getShorthands(
  results: BackupResults,
  uuid: string,
  version: string
): ShorthandsRes {
  const warn = results.addWarningMessage.bind(results, "wallets", uuid);
  const success = results.addSuccessMessage.bind(results, "wallets", uuid);

  // Warnings to only be added if the wallet was actually added
  const importWarnings: MessageType[] = [];
  const importWarn = (msg: MessageType) => {
    debug("%s wallet %s added import warning:", version, uuid, msg);

    // Prepend the i18n key if it was just a string
    importWarnings.push(typeof msg === "string"
      ? "import.walletMessages." + msg
      : msg);
  };

  return { warn, success, importWarnings, importWarn };
}

/** Asserts that `val` is a truthy string */
export const str = (val: any): val is string => val && typeof val === "string";

// -----------------------------------------------------------------------------
// REQUIRED PROPERTY VALIDATION
// -----------------------------------------------------------------------------
/** Converts a v1 format name to a v2 format name. */
const _upgradeFormatName = (name: string): WalletFormatName =>
  name === "krist" ? "api" : name as WalletFormatName;

/** Verifies that the wallet's format is valid, upgrade it if necessary,
 * and check if it's an advanced format. */
export function checkFormat(
  { importWarn }: ShorthandsRes,
  wallet: { format?: string; username?: string }
): {
  format: WalletFormatName;
  username?: string;
} {
  // Check if the wallet format is supported (converting the old format name
  // `krist` to `api` if necessary)
  const format = _upgradeFormatName(wallet.format || "kristwallet");
  if (!WALLET_FORMATS[format]) throw new BackupWalletError("errorUnknownFormat");

  // Check if the wallet is using an advanced (unsupported) format
  if (ADVANCED_FORMATS.includes(format))
    importWarn({ key: "import.walletMessages.warningAdvancedFormat", args: { format }});

  // If the wallet format requires a username, check that the username is
  // actually present
  const { username } = wallet;
  if (formatNeedsUsername(format) && !str(username))
    throw new BackupWalletError("errorUsernameMissing");

  return { format, username };
}

// -----------------------------------------------------------------------------
// OPTIONAL PROPERTY VALIDATION
// -----------------------------------------------------------------------------
export const isLabelValid = (label?: string): boolean =>
  str(label) && label.trim().length < 32;
export function checkLabelValid({ importWarn }: ShorthandsRes, label?: string): void {
  const labelValid = isLabelValid(label);
  if (label && !labelValid) importWarn("warningLabelInvalid");
}

export const isCategoryValid = isLabelValid;
export function checkCategoryValid({ importWarn }: ShorthandsRes, category?: string): void {
  const categoryValid = isCategoryValid(category);
  if (category && !categoryValid) importWarn("warningCategoryInvalid");
}

// -------------------------- ---------------------------------------------------
// WALLET IMPORT PREPARATION/VALIDATION
// -----------------------------------------------------------------------------
interface CheckAddressRes {
  address: string;
  privatekey: string;
  existingWallet?: Wallet;
  existingImportWallet?: Wallet;
}

export async function checkAddress(
  addressPrefix: string,
  existingWallets: WalletMap,
  results: BackupResults,

  oldPrivatekey: string,
  privatekeyMismatchErrorKey:
    "errorMasterKeyMismatch" | "errorPrivateKeyMismatch",

  format: WalletFormatName | undefined,
  password: string,
  username?: string
): Promise<CheckAddressRes> {
  // Calculate the address in advance, to check for existing wallets
  const { privatekey, address } = await calculateAddress(
    addressPrefix,
    format || "kristwallet",
    password,
    username
  );

  // Check that our calculated private key is actually equal to the stored
  // private key. In practice these should never be different.
  if (privatekey !== oldPrivatekey)
    throw new BackupWalletError(privatekeyMismatchErrorKey);

  // Check if a wallet already exists, either in the Redux store, or our list of
  // imported wallets during this backup import
  const existingWallet = Object.values(existingWallets)
    .find(w => w.address === address);
  const existingImportWallet = results.importedWallets
    .find(w => w.address === address);

  return { address, privatekey, existingWallet, existingImportWallet };
}

// -----------------------------------------------------------------------------
// WALLET IMPORT
// -----------------------------------------------------------------------------
export async function finalWalletImport(
  existingWallets: WalletMap,
  appMasterPassword: string,
  addressPrefix: string,

  shorthands: ShorthandsRes,
  results: BackupResults,
  noOverwrite: boolean,

  existingWallet: Wallet | undefined,
  address: string,
  password: string,
  newWalletData: WalletNew
): Promise<void> {
  const { warn, success, importWarnings } = shorthands;

  const { label } = newWalletData;
  const labelValid = isLabelValid(label);

  // Handle duplicate wallets
  if (existingWallet) {
    // The wallet already exists in the Redux store, so determine if we need to
    // update its label (only if it was determined to be valid)
    if (labelValid && existingWallet.label !== label!.trim()) {
      if (noOverwrite) {
        // The user didn't want to overwrite the wallet, so skip it
        results.skippedWallets++;
        return success({ key: "import.walletMessages.successSkippedNoOverwrite", args: { address }});
      } else {
        const newLabel = label!.trim();
        debug(
          "changing existing wallet %s (%s) label from %s to %s",
          existingWallet.id, existingWallet.address,
          existingWallet.label, newLabel
        );

        await editWalletLabel(existingWallet, newLabel);

        return success({ key: "import.walletMessages.successUpdated", args: { address, label: newLabel }});
      }
    } else {
      results.skippedWallets++;
      return success({ key: "import.walletMessages.successSkipped", args: { address }});
    }
  }

  // No existing wallet to update/skip, so now check if we can add it without
  // going over the wallet limit
  const currentWalletCount =
    Object.keys(existingWallets).length + results.importedWallets.length;
  if (currentWalletCount >= ADDRESS_LIST_LIMIT)
    throw new BackupWalletError("errorLimitReached");

  // Now that we're actually importing the wallet, push any warnings that may
  // have been generated
  importWarnings.forEach(warn);

  debug("adding new wallet %s", address);
  const newWallet = await addWallet(
    addressPrefix, appMasterPassword,
    newWalletData, password,
    true
  );
  debug("new wallet %s (%s)", newWallet.id, newWallet.address);

  // Add it to the results
  results.newWallets++;
  results.importedWallets.push(newWallet); // To keep track of limits
  return success("import.walletMessages.success");
}