Source: db.js

// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Copyright (c) DUSK NETWORK. All rights reserved.

import { Dexie, indexedDB } from "../deps.js";
import { unspentSpentNotes } from "./crypto.js";
import { request, responseBytes } from "./node.js";
import { getNullifiersRkyvSerialized } from "./rkyv.js";

// Set Polyfill
if (globalThis.indexedDB === undefined) {
  Dexie.dependencies.indexedDB = indexedDB;
}

/**
 * @class NoteData
 * @type {Object}
 * @property {UInt8Array} note The rkyv serialized note.
 * @property {UInt8Array} nullifier The rkyv serialized BlsScalar.
 * @property {number} pos The position of the node
 * @property {number} block_height The block height of the note
 * @property {string} psk The bs58 encoded public spend key of the note
 */
export function NoteData(note, psk, pos, nullifier, block_height) {
  this.note = note;
  this.psk = psk;
  this.pos = pos;
  this.nullifier = nullifier;
  this.block_height = block_height;
}

/**
 * @class HistoryData
 * @type {Object}
 * @property {string} psk The bs58 encoded public spend key of the note
 * @property {Array<TxData>} history the tx data
 */
export function HistoryData(psk, history) {
  this.psk = psk;
  this.history = history;
}

/**
 * Persist the state of unspent_notes and spent_notes in the indexedDB
 * This is called by the sync function
 *
 * @param {Array<NoteData>} unspent_notes Notes which are not spent
 * @param {Array<NoteData>} spent_notes
 * @param {number} pos The position we are at right now
 * @param {number} blockHeight The block height we are at right now
 * @ignore Only called by the sync function
 */
export async function insertSpentUnspentNotes(
  unspentNotes,
  spentNotes,
  pos,
  blockHeight,
) {
  try {
    if (localStorage.getItem("lastPos") == null) {
    }

    localStorage.setItem("lastPos", Math.max(pos, getLastPos()));
    localStorage.setItem(
      "blockHeight",
      Math.max(blockHeight, getLastBlockHeight()),
    );
  } catch (e) {
    console.warn("Cannot set pos in local storage, the wallet will be slow");
  }

  const db = initializeState();

  await db.unspentNotes
    .bulkPut(unspentNotes)
    .then(() => db.spentNotes.bulkPut(spentNotes))
    .finally(() => db.close());
}

/**
 * Fetch unspent notes from the IndexedDB if there are any
 *
 * @param {string} psk - bs58 encoded public spend key to fetch the unspent notes of
 * @returns {Promise<Array<NoteData>>} notes - unspent notes belonging to the psk
 * @ignore Only called by the sync function
 */
export async function getUnspentNotes(psk) {
  const db = initializeState();

  const myTable = db.table("unspentNotes");

  if (myTable) {
    const notes = myTable.filter((note) => note.psk == psk);
    const result = await notes.toArray();

    return result;
  }
}

/**
 * Fetch spent notes from the IndexedDB if there are any
 *
 * @param {string} psk bs58 encoded public spend key to fetch the unspent notes of
 * @returns {Array<NoteData>}  spent notes of the psk
 * @ignore Only called by the sync function
 */
export async function getSpentNotes(psk) {
  const db = initializeState();

  const myTable = db.table("spentNotes");

  if (myTable) {
    const notes = myTable.filter((note) => note.psk == psk);
    const result = await notes.toArray();

    return result;
  }
}

/**
 * Fetch lastPos from the localStorage if there is any 0 by default
 *
 * @returns {number} lastPos the position where to fetch from
 * @ignore Only called by the sync function
 */
export const getLastPos = () => parseInt(localStorage.getItem("lastPos")) || 0;
/**
 * Fetch last blockheight from the localStorage if there is any 0 by default
 *
 * @returns {number} blockHeight the last block height
 * @ignore Only called by the sync function
 */
export const getLastBlockHeight = () =>
  parseInt(localStorage.getItem("blockHeight")) || 0;

/**
 * Increment the lastPos by 1 if non zero
 * @returns {number} lastPos the position where to fetch from
 */
export function getNextPos() {
  const pos = getLastPos();

  return pos === 0 ? pos : pos + 1;
}

/**
 * Set the lastPos in the localStorage
 * @param {number} position the position to set
 */
export function setLastPos(position) {
  localStorage.setItem("lastPos", position);
}

/**
 * Check if the last position exists in the localstorage
 */
export const lastPosExists = () => localStorage.getItem("lastPos") !== null;

/**
 * Given bs58 encoded psk, fetch all the spent and unspent notes for that psk
 *
 * @param {string} psk
 * @returns {Array<NoteData>} spent and unspent notes of the psk contactinated
 * @ignore Only called by the sync function
 */
export async function getAllNotes(psk) {
  const db = initializeState();

  const unspentNotesTable = db
    .table("unspentNotes")
    .filter((note) => note.psk == psk);

  const spentNotesTable = db
    .table("spentNotes")
    .filter((note) => note.psk == psk);

  const unspent = await unspentNotesTable.toArray();
  const spent = await spentNotesTable.toArray();

  const concat = spent.concat(unspent);

  return concat;
}

/**
 * Read all the unspent notes and check if they are spent from the node
 * If they are spent then move from unspent to spent
 *
 * @param {WebAssembly.Exports} wasm
 *
 * @returns {Promise} Promise object which resolves after the corrected notes are inserted
 * @ignore Only called by the sync function
 */
export async function correctNotes(wasm) {
  // Move the unspent notes to spent notes if they were spent
  const unspentNotesNullifiers = [];
  const unspentNotesTemp = [];
  const unspentNotesPsks = [];
  const unspentNotesPos = [];
  const unspentNotesBlockHeights = [];

  // grab all the unspent notes and put the data of those unspent notes in arrays
  const allUnspentNotes = await getAllUnpsentNotes();

  for (const unspentNote of await allUnspentNotes) {
    unspentNotesNullifiers.push(unspentNote.nullifier);
    unspentNotesTemp.push(unspentNote.note);
    unspentNotesPsks.push(unspentNote.psk);
    unspentNotesPos.push(unspentNote.pos);
    unspentNotesBlockHeights.push(unspentNote.block_height);
  }

  // start the correction of the notes
  // get the nullifiers
  const unspentNotesNullifiersSerialized = await getNullifiersRkyvSerialized(
    wasm,
    unspentNotesNullifiers,
  );

  // Fetch existing nullifiers from the node
  const unspentNotesExistingNullifiersBytes = await responseBytes(
    await request(
      unspentNotesNullifiersSerialized,
      "existing_nullifiers",
      false,
    ),
  );

  // calculate the unspent and spent notes
  // from all the unspent note in the db
  // their nullifiers
  const correctedNotes = await unspentSpentNotes(
    wasm,
    unspentNotesTemp,
    unspentNotesNullifiers,
    unspentNotesBlockHeights,
    unspentNotesExistingNullifiersBytes,
    unspentNotesPsks,
  );

  // These are the spent notes which were unspent before
  const correctedSpentNotes = Array.from(correctedNotes.spent_notes);
  const posToRemove = correctedSpentNotes.map((noteData) => noteData.pos);

  return deleteUnspentNotesInsertSpentNotes(posToRemove, correctedSpentNotes);
}

/**
 * Insert history data
 * @param {HistoryData} historyData
 */
export async function insertHistory(historyData) {
  const db = initializeHistory();

  const existingHistory = await getHistory(historyData.psk);

  // remove duplicates
  historyData.history = existingHistory.history
    .concat(historyData.history)
    .filter(
      (v, i, a) =>
        a.findIndex((v2) => v2.block_height === v.block_height) === i,
    );

  await db.cache.put(historyData);
}

/**
 *
 * @param {string} psk
 * @returns {HistoryData}
 */
export async function getHistory(psk) {
  const db = initializeHistory();

  const historyData = (await db.cache.get(psk)) ?? new HistoryData(psk, []);

  return historyData;
}

/**
 * Clears all localstorage inserts and IndexedDB inserts
 */
export async function clearDB() {
  localStorage.removeItem("lastPos");
  localStorage.removeItem("lastPsk");
  localStorage.removeItem("blockHeight");

  await Dexie.delete("history");

  return Dexie.delete("state");
}

/**
 * Fetch all unspent notes from the IndexedDB if there are any
 * @returns {Promise<Array<NoteData>>} unspent notes of the psk
 * @ignore Only called by the sync function
 */
async function getAllUnpsentNotes() {
  const db = initializeState();

  const myTable = db.table("unspentNotes");

  if (myTable) {
    const result = await myTable.toArray();

    return result;
  }
}

/**
 * Delete unspent notes given their Pos and insert spent notes given data
 * We want to move notes from unspent to spent when they are used in a tx
 *
 * @param {Array<number>} unspentNotesPos - ids of the unspent notes to delete
 * @param {Array<NoteData>} spentNotes - spent notes to insert
 * @ignore Only called by the sync function
 */
async function deleteUnspentNotesInsertSpentNotes(unspentNotesPos, spentNotes) {
  const db = initializeState();

  const unspentNotesTable = db.table("unspentNotes");
  if (unspentNotesTable) {
    await unspentNotesTable.bulkDelete(unspentNotesPos);
  }

  const spentNotesTable = db.table("spentNotes");

  if (spentNotesTable) {
    return spentNotesTable.bulkPut(spentNotes);
  }
}

function initializeState() {
  const db = new Dexie("state");

  db.version(1).stores({
    // Added a autoincremented id for good practice
    // if we need to index it in future
    unspentNotes: "pos,psk,nullifier",
    spentNotes: "pos,psk,nullifier",
  });

  return db;
}

function initializeHistory() {
  const db = new Dexie("history");

  db.version(1).stores({
    cache: "&psk",
  });

  return db;
}