Skip to content

Commit

Permalink
Merge pull request #40 from ChainSafe/willem/persistance-api
Browse files Browse the repository at this point in the history
Persistance API
  • Loading branch information
willemolding authored Oct 16, 2024
2 parents ab44d54 + c640ef6 commit 5a0ef28
Show file tree
Hide file tree
Showing 12 changed files with 169 additions and 44 deletions.
1 change: 1 addition & 0 deletions packages/demo-wallet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"@types/react-dom": "^18.3.0",
"@webzjs/webz-core": "workspace:^",
"bootstrap": "^5.3.3",
"idb-keyval": "^6.2.1",
"react": "^18.2.0",
"react-bootstrap": "^2.10.4",
"react-dom": "^18.2.0",
Expand Down
38 changes: 36 additions & 2 deletions packages/demo-wallet/src/App/Actions.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,35 @@
import initWasm, { initThreadPool, WebWallet } from "@webzjs/webz-core";
import { get, set } from 'idb-keyval';

import { State, Action } from "./App";
import { MAINNET_LIGHTWALLETD_PROXY } from "./Constants";

export async function init(dispatch: React.Dispatch<Action>) {
export async function init(state: State, dispatch: React.Dispatch<Action>) {
await initWasm();
await initThreadPool(10);

let bytes = await get("wallet");
let wallet;
if (bytes) {
console.info("Saved wallet detected. Restoring wallet from storage");
wallet = new WebWallet("main", MAINNET_LIGHTWALLETD_PROXY, 1, bytes);
} else {
wallet = new WebWallet("main", MAINNET_LIGHTWALLETD_PROXY, 1);
}

dispatch({
type: "set-web-wallet",
payload: new WebWallet("main", MAINNET_LIGHTWALLETD_PROXY, 1),
payload: wallet,
});
let summary = await wallet.get_wallet_summary();
if (summary) {
dispatch({ type: "set-summary", payload: summary });
}
let chainHeight = await wallet.get_latest_block();
if (chainHeight) {
dispatch({ type: "set-chain-height", payload: chainHeight });
}
dispatch({ type: "set-active-account", payload: summary?.account_balances[0][0] });
}

export async function addNewAccount(state: State, dispatch: React.Dispatch<Action>, seedPhrase: string, birthdayHeight: number) {
Expand Down Expand Up @@ -45,6 +65,7 @@ export async function triggerRescan(
}
await state.webWallet?.sync();
await syncStateWithWallet(state, dispatch);
await flushDbToStore(state, dispatch);
}

export async function triggerTransfer(
Expand All @@ -70,3 +91,16 @@ export async function triggerTransfer(

await state.webWallet.send_authorized_transactions(txids);
}

export async function flushDbToStore(
state: State,
dispatch: React.Dispatch<Action>
) {
if (!state.webWallet) {
throw new Error("Wallet not initialized");
}
console.info("Serializing wallet and dumpling to IndexDb store");
let bytes = await state.webWallet.db_to_bytes();
await set("wallet", bytes);
console.info("Wallet saved to storage");
}
2 changes: 1 addition & 1 deletion packages/demo-wallet/src/App/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export function App() {
const [state, dispatch] = useReducer(reducer, initialState);

useEffect(() => {
init(dispatch);
init(state, dispatch);
}, [dispatch]);

return (
Expand Down
2 changes: 1 addition & 1 deletion packages/demo-wallet/src/App/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Card from "react-bootstrap/Card";
import Stack from "react-bootstrap/Stack";

import { WalletContext } from "../App";
import { syncStateWithWallet, triggerRescan } from "../Actions";
import { syncStateWithWallet, triggerRescan, flushDbToStore } from "../Actions";
import { Button } from "react-bootstrap";

import { zatsToZec } from "../../utils";
Expand Down
3 changes: 2 additions & 1 deletion packages/demo-wallet/src/App/components/ImportAccount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Form from "react-bootstrap/Form";
import { ToastContainer, toast } from "react-toastify";

import { WalletContext } from "../App";
import { addNewAccount } from "../Actions";
import { addNewAccount, flushDbToStore } from "../Actions";

export function ImportAccount() {
let {state, dispatch} = useContext(WalletContext);
Expand All @@ -22,6 +22,7 @@ export function ImportAccount() {
});
setBirthdayHeight(0);
setSeedPhrase("");
flushDbToStore(state, dispatch)
};

return (
Expand Down
7 changes: 7 additions & 0 deletions packages/e2e-tests/e2e/e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,10 @@ test('Account was added', async ({ page }) => {
});
expect(result).toBe(1);
});

test('Wallet can be serialized', async ({ page }) => {
let result = await page.evaluate(async () => {
let bytes = await window.webWallet.db_to_bytes();
return bytes;
});
});
26 changes: 16 additions & 10 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

56 changes: 35 additions & 21 deletions src/bindgen/wallet.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::num::NonZeroU32;
use std::str::FromStr;

use nonempty::NonEmpty;
use serde::{Deserialize, Serialize};
Expand All @@ -8,7 +9,8 @@ use tonic_web_wasm_client::Client;

use crate::error::Error;
use crate::wallet::usk_from_seed_str;
use crate::{bindgen::proposal::Proposal, BlockRange, Wallet, PRUNING_DEPTH};
use crate::Network;
use crate::{bindgen::proposal::Proposal, Wallet, PRUNING_DEPTH};
use wasm_thread as thread;
use zcash_address::ZcashAddress;
use zcash_client_backend::data_api::{InputSource, WalletRead};
Expand All @@ -17,13 +19,12 @@ use zcash_client_backend::proto::service::{
};
use zcash_client_memory::MemoryWalletDb;
use zcash_keys::keys::UnifiedFullViewingKey;
use zcash_primitives::consensus;
use zcash_primitives::consensus::{self, Parameters, TestNetwork};
use zcash_primitives::transaction::TxId;

pub type MemoryWallet<T> = Wallet<MemoryWalletDb<consensus::Network>, T>;
pub type AccountId =
<MemoryWalletDb<zcash_primitives::consensus::Network> as WalletRead>::AccountId;
pub type NoteRef = <MemoryWalletDb<zcash_primitives::consensus::Network> as InputSource>::NoteRef;
pub type MemoryWallet<T> = Wallet<MemoryWalletDb<Network>, T>;
pub type AccountId = <MemoryWalletDb<Network> as WalletRead>::AccountId;
pub type NoteRef = <MemoryWalletDb<Network> as InputSource>::NoteRef;

/// # A Zcash wallet
///
Expand Down Expand Up @@ -93,14 +94,6 @@ pub struct WebWallet {
}

impl WebWallet {
fn network_from_str(network: &str) -> Result<consensus::Network, Error> {
match network {
"main" => Ok(consensus::Network::MainNetwork),
"test" => Ok(consensus::Network::TestNetwork),
_ => Err(Error::InvalidNetwork(network.to_string())),
}
}

pub fn client(&self) -> CompactTxStreamerClient<tonic_web_wasm_client::Client> {
self.inner.client.clone()
}
Expand All @@ -119,6 +112,7 @@ impl WebWallet {
/// * `network` - Must be one of "main" or "test"
/// * `lightwalletd_url` - Url of the lightwalletd instance to connect to (e.g. https://zcash-mainnet.chainsafe.dev)
/// * `min_confirmations` - Number of confirmations required before a transaction is considered final
/// * `db_bytes` - (Optional) UInt8Array of a serialized wallet database. This can be used to restore a wallet from a previous session that was serialized by `db_to_bytes`
///
/// # Examples
///
Expand All @@ -130,19 +124,25 @@ impl WebWallet {
network: &str,
lightwalletd_url: &str,
min_confirmations: u32,
db_bytes: Option<Box<[u8]>>,
) -> Result<WebWallet, Error> {
let network = Self::network_from_str(network)?;
let network = Network::from_str(network)?;
let min_confirmations = NonZeroU32::try_from(min_confirmations)
.map_err(|_| Error::InvalidMinConformations(min_confirmations))?;
let client = Client::new(lightwalletd_url.to_string());

let db = match db_bytes {
Some(bytes) => {
tracing::info!(
"Serialized db was provided to constructor. Attempting to deserialize"
);
postcard::from_bytes(&bytes)?
}
None => MemoryWalletDb::new(network, PRUNING_DEPTH),
};

Ok(Self {
inner: Wallet::new(
MemoryWalletDb::new(network, PRUNING_DEPTH),
client,
network,
min_confirmations,
)?,
inner: Wallet::new(db, client, network, min_confirmations)?,
})
}

Expand Down Expand Up @@ -295,6 +295,20 @@ impl WebWallet {
Ok(serde_wasm_bindgen::to_value(&txids).unwrap())
}

/// Serialize the internal wallet database to bytes
///
/// This should be used for persisting the wallet between sessions. The resulting byte array can be used to construct a new wallet instance.
/// Note this method is async and will block until a read-lock can be acquired on the wallet database
///
/// # Returns
///
/// A postcard encoded byte array of the wallet database
///
pub async fn db_to_bytes(&self) -> Result<Box<[u8]>, Error> {
let bytes = self.inner.db_to_bytes().await?;
Ok(bytes.into_boxed_slice())
}

/// Send a list of authorized transactions to the network to be included in the blockchain
///
/// These will be sent via the connected lightwalletd instance
Expand Down
2 changes: 2 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ pub enum Error {
InvalidSeedPhrase,
#[error("Failed when creating transaction")]
FailedToCreateTransaction,
#[error("Failed to serialize db using postcard: {0}")]
FailedSerialization(#[from] postcard::Error),
}

impl From<Error> for JsValue {
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ pub use bindgen::wallet::WebWallet;

pub mod error;
pub mod init;
pub mod network;
pub use network::Network;

pub mod wallet;
pub use wallet::Wallet;
Expand Down
Loading

0 comments on commit 5a0ef28

Please sign in to comment.