Newer
Older
CrypticOreWallet / src / krist / wallets / walletStorage.ts
@Drew Lemmy Drew Lemmy on 30 Mar 2021 3 KB chore: more error handling
// Copyright (c) 2020-2021 Drew Lemmy
// This file is part of KristWeb 2 under AGPL-3.0.
// Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt
import { store } from "@app";
import * as actions from "@actions/WalletsActions";

import { TranslatedError } from "@utils/i18n";

import { Wallet, WalletMap } from ".";
import { broadcastDeleteWallet } from "@global/StorageBroadcast";

import * as Sentry from "@sentry/react";
import Debug from "debug";
const debug = Debug("kristweb:wallet-storage");

/** The limit provided by the Krist server for a single address lookup. In the
 * future I may implement batching for these, but for now, this seems like a
 * reasonable compromise to limit wallet storage. It should also give us a fair
 * upper bound for potential performance issues. */
export const ADDRESS_LIST_LIMIT = 128;

/** Get the local storage key for a given wallet. */
export function getWalletKey(wallet: Wallet | string): string {
  const id = typeof wallet === "string" ? wallet : wallet.id;
  return `wallet2-${id}`;
}

/** Extract a wallet ID from a local storage key. */
const walletKeyRegex = /^wallet2-([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})$/;
export function extractWalletKey(key: string): [string, string] | undefined {
  const [, id] = walletKeyRegex.exec(key) || [];
  return id ? [key, id] : undefined;
}

export function parseWallet(id: string, data: string | null): Wallet {
  if (data === null) // localStorage key was missing
    throw new TranslatedError("masterPassword.walletStorageCorrupt");

  try {
    const wallet: Wallet = JSON.parse(data);

    // Validate the wallet data actually makes sense
    if (!wallet || !wallet.id || wallet.id !== id)
      throw new TranslatedError("masterPassword.walletStorageCorrupt");

    return wallet;
  } catch (e) {
    Sentry.withScope(scope => {
      scope.setTag("wallet-id", id);
      scope.setTag("wallet-data", data);

      Sentry.captureException(e);
      console.error(e);
    });

    if (e.name === "SyntaxError") // Invalid JSON
      throw new TranslatedError("masterPassword.errorStorageCorrupt");
    else throw e; // Unknown error
  }
}

/** Loads all available wallets from local storage. */
export function loadWallets(): WalletMap {
  const walletMap: WalletMap = {};

  const lsKeys = Object.keys(localStorage);
  for (const lsKey of lsKeys) {
    // Find all 'wallet2' keys from local storage
    const extracted = extractWalletKey(lsKey);
    if (!extracted) continue;

    // Parse the wallet from the stored string
    const [key, id] = extracted;
    const wallet = parseWallet(id, localStorage.getItem(key));

    walletMap[wallet.id] = wallet;
  }

  return walletMap;
}

/** Saves a wallet to local storage, unless it has `dontSave` set. */
export function saveWallet(wallet: Wallet): void {
  if (wallet.dontSave) return;

  const key = getWalletKey(wallet);
  debug("saving wallet key %s", key);

  const serialised = JSON.stringify(wallet);
  localStorage.setItem(key, serialised);
}

/** Deletes a wallet, removing it from local storage and dispatching the change
 * to the Redux store. */
export function deleteWallet(wallet: Wallet): void {
  const key = getWalletKey(wallet);
  localStorage.removeItem(key);

  broadcastDeleteWallet(wallet.id); // Broadcast changes to other tabs

  store.dispatch(actions.removeWallet(wallet.id));
}