Skip to content

Commit

Permalink
feat(wasm)!: refactor transaction planner and view (#2973)
Browse files Browse the repository at this point in the history
Collects several improvements to the WASM interface for
building transactions via the planner:

  * added wasm planner which allows more flexible creation of TransactionPlan on TS side
  * added logic of storing and reading advice when scanning blocks
  * added function for generating ephemeral address
  * planner reads data from indexedDB directly from rust

Co-authored-by: Conor Schaefer <[email protected]>
Co-authored-by: Valentine <[email protected]>
  • Loading branch information
3 people authored Sep 21, 2023
1 parent 081fe52 commit cb57996
Show file tree
Hide file tree
Showing 12 changed files with 1,135 additions and 853 deletions.
3 changes: 3 additions & 0 deletions Cargo.lock

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

4 changes: 4 additions & 0 deletions crates/wasm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,19 @@ penumbra-tct = { path = "../crypto/tct" }
penumbra-transaction = { path = "../core/transaction", default-features = false }

anyhow = "1.0.75"
ark-ff = { version = "0.4.2", features = ["std"] }
base64 = "0.21.2"
console_error_panic_hook = { version = "0.1.7", optional = true }
decaf377 = { version = "0.5", features = ["r1cs"] }
hex = "0.4.3"
indexed_db_futures = "0.3.0"
rand_core = { version = "0.6.4", features = ["getrandom"] }
serde = { version = "1.0.186", features = ["derive"] }
serde-wasm-bindgen = "0.5.0"
thiserror = "1.0"
wasm-bindgen = "0.2.87"
wasm-bindgen-futures = "0.4.37"
wasm-bindgen-test = "0.3.0"
web-sys = { version = "0.3.64", features = ["console"] }

[dev-dependencies]
Expand Down
69 changes: 69 additions & 0 deletions crates/wasm/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
use base64::DecodeError;
use hex::FromHexError;
use penumbra_tct::error::{InsertBlockError, InsertEpochError, InsertError};
use serde_wasm_bindgen::Error;
use std::convert::Infallible;
use thiserror::Error;
use wasm_bindgen::{JsError, JsValue};
use web_sys::DomException;

pub type WasmResult<T> = Result<T, WasmError>;

#[derive(Error, Debug)]
pub enum WasmError {
#[error("{0}")]
Anyhow(#[from] anyhow::Error),

#[error("{0}")]
DecodeError(#[from] DecodeError),

#[error("{0}")]
Dom(#[from] DomError),

#[error("{0}")]
FromHexError(#[from] FromHexError),

#[error("{0}")]
Infallible(#[from] Infallible),

#[error("{0}")]
InsertBlockError(#[from] InsertBlockError),

#[error("{0}")]
InsertEpochError(#[from] InsertEpochError),

#[error("{0}")]
InsertError(#[from] InsertError),

#[error("{0}")]
Wasm(#[from] serde_wasm_bindgen::Error),
}

impl From<WasmError> for serde_wasm_bindgen::Error {
fn from(wasm_err: WasmError) -> Self {
Error::new(wasm_err.to_string())
}
}

impl From<WasmError> for JsValue {
fn from(error: WasmError) -> Self {
JsError::from(error).into()
}
}

#[derive(Debug)]
pub struct DomError(DomException);

impl std::fmt::Display for DomError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "DOM Exception: {:?}", self.0)
}
}

impl std::error::Error for DomError {}

impl From<DomException> for WasmError {
fn from(dom_exception: DomException) -> Self {
WasmError::Dom(DomError(dom_exception))
}
}
106 changes: 106 additions & 0 deletions crates/wasm/src/keys.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
use crate::error::WasmResult;
use penumbra_keys::keys::{SeedPhrase, SpendKey};
use penumbra_keys::{Address, FullViewingKey};
use penumbra_proto::{core::crypto::v1alpha1 as pb, serializers::bech32str, DomainType};
use rand_core::OsRng;
use std::str::FromStr;
use wasm_bindgen::prelude::*;

/// generate a spend key from a seed phrase
/// Arguments:
/// seed_phrase: `string`
/// Returns: `bech32 string`
#[wasm_bindgen]
pub fn generate_spend_key(seed_phrase: &str) -> WasmResult<JsValue> {
let seed = SeedPhrase::from_str(seed_phrase)?;
let spend_key = SpendKey::from_seed_phrase_bip39(seed, 0);

let proto = spend_key.to_proto();

let spend_key_str = bech32str::encode(
&proto.inner,
bech32str::spend_key::BECH32_PREFIX,
bech32str::Bech32m,
);

Ok(JsValue::from_str(&spend_key_str))
}

/// get full viewing key from spend key
/// Arguments:
/// spend_key_str: `bech32 string`
/// Returns: `bech32 string`
#[wasm_bindgen]
pub fn get_full_viewing_key(spend_key: &str) -> WasmResult<JsValue> {
let spend_key = SpendKey::from_str(spend_key)?;

let fvk: &FullViewingKey = spend_key.full_viewing_key();

let proto = fvk.to_proto();

let fvk_bech32 = bech32str::encode(
&proto.inner,
bech32str::full_viewing_key::BECH32_PREFIX,
bech32str::Bech32m,
);
Ok(JsValue::from_str(&fvk_bech32))
}

/// get address by index using FVK
/// Arguments:
/// full_viewing_key: `bech32 string`
/// index: `u32`
/// Returns: `pb::Address`
#[wasm_bindgen]
pub fn get_address_by_index(full_viewing_key: &str, index: u32) -> WasmResult<JsValue> {
let fvk = FullViewingKey::from_str(full_viewing_key)?;
let (address, _dtk) = fvk.incoming().payment_address(index.into());
let proto = address.to_proto();
let result = serde_wasm_bindgen::to_value(&proto)?;
Ok(result)
}

/// get ephemeral (randomizer) address using FVK
/// The derivation tree is like "spend key / address index / ephemeral address" so we must also pass index as an argument
/// Arguments:
/// full_viewing_key: `bech32 string`
/// index: `u32`
/// Returns: `pb::Address`
#[wasm_bindgen]
pub fn get_ephemeral_address(full_viewing_key: &str, index: u32) -> WasmResult<JsValue> {
let fvk = FullViewingKey::from_str(full_viewing_key)?;
let (address, _dtk) = fvk.ephemeral_address(OsRng, index.into());
let proto = address.to_proto();
let result = serde_wasm_bindgen::to_value(&proto)?;
Ok(result)
}

/// Check if the address is FVK controlled
/// Arguments:
/// full_viewing_key: `bech32 String`
/// address: `bech32 String`
/// Returns: `Option<pb::AddressIndex>`
#[wasm_bindgen]
pub fn is_controlled_address(full_viewing_key: &str, address: &str) -> WasmResult<JsValue> {
let fvk = FullViewingKey::from_str(full_viewing_key)?;
let index: Option<pb::AddressIndex> = fvk
.address_index(&Address::from_str(address)?)
.map(Into::into);
let result = serde_wasm_bindgen::to_value(&index)?;
Ok(result)
}

/// Get canonical short form address by index
/// This feature is probably redundant and will be removed from wasm in the future
/// Arguments:
/// full_viewing_key: `bech32 string`
/// index: `u32`
/// Returns: `String`
#[wasm_bindgen]
pub fn get_short_address_by_index(full_viewing_key: &str, index: u32) -> WasmResult<JsValue> {
let fvk = FullViewingKey::from_str(full_viewing_key)?;

let (address, _dtk) = fvk.incoming().payment_address(index.into());
let short_address = address.display_short_form();
Ok(JsValue::from_str(&short_address))
}
124 changes: 4 additions & 120 deletions crates/wasm/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,131 +1,15 @@
#![deny(clippy::unwrap_used)]
#![allow(dead_code)]
extern crate core;

mod error;
mod keys;
mod note_record;
mod planner;
mod storage;
mod swap_record;
mod tx;
mod utils;
mod view_server;
use penumbra_proto::{core::crypto::v1alpha1 as pb, serializers::bech32str, DomainType};
mod wasm_planner;

use penumbra_keys::{Address, FullViewingKey};
use std::convert::TryFrom;
use std::str::FromStr;

use penumbra_keys::keys::{SeedPhrase, SpendKey};
use wasm_bindgen::prelude::*;

use penumbra_transaction::Transaction;

pub use tx::send_plan;
pub use view_server::ViewServer;

#[wasm_bindgen]
pub fn generate_spend_key(seed_phrase: &str) -> JsValue {
utils::set_panic_hook();
let seed =
SeedPhrase::from_str(seed_phrase).expect("the provided string is a valid seed phrase");
let spend_key = SpendKey::from_seed_phrase_bip39(seed, 0);

let proto = spend_key.to_proto();
let spend_key_str = &bech32str::encode(
&proto.inner,
bech32str::spend_key::BECH32_PREFIX,
bech32str::Bech32m,
);

serde_wasm_bindgen::to_value(&spend_key_str).expect("able to serialize spend key")
}

#[wasm_bindgen]
pub fn get_full_viewing_key(spend_key_str: &str) -> JsValue {
utils::set_panic_hook();
let spend_key =
SpendKey::from_str(spend_key_str).expect("the provided string is a valid spend key");

let fvk: &FullViewingKey = spend_key.full_viewing_key();

let proto = pb::FullViewingKey::from(fvk.to_proto());

let fvk_str = &bech32str::encode(
&proto.inner,
bech32str::full_viewing_key::BECH32_PREFIX,
bech32str::Bech32m,
);
serde_wasm_bindgen::to_value(&fvk_str).expect("able to serialize full viewing key")
}

#[wasm_bindgen]
pub fn get_address_by_index(full_viewing_key: &str, index: u32) -> JsValue {
utils::set_panic_hook();
let fvk = FullViewingKey::from_str(full_viewing_key.as_ref())
.expect("the provided string is a valid FullViewingKey");

let (address, _dtk) = fvk.incoming().payment_address(index.into());

let proto = address.to_proto();
let address_str = &bech32str::encode(
&proto.inner,
bech32str::address::BECH32_PREFIX,
bech32str::Bech32m,
);

serde_wasm_bindgen::to_value(&address_str).expect("able to serialize address")
}

#[wasm_bindgen]
pub fn base64_to_bech32(prefix: &str, base64_str: &str) -> JsValue {
utils::set_panic_hook();

let bech32 = &bech32str::encode(
&base64::Engine::decode(&base64::engine::general_purpose::STANDARD, base64_str)
.expect("the provided string is a valid base64 string"),
prefix,
bech32str::Bech32m,
);
serde_wasm_bindgen::to_value(bech32).expect("able to serialize bech32 string")
}
#[wasm_bindgen]
pub fn is_controlled_address(full_viewing_key: &str, address: &str) -> JsValue {
utils::set_panic_hook();
let fvk = FullViewingKey::from_str(full_viewing_key.as_ref())
.expect("the provided string is a valid FullViewingKey");

let index = fvk.address_index(&Address::from_str(address.as_ref()).expect("valid address"));

serde_wasm_bindgen::to_value(&index).expect("able to serialize address index")
}

#[wasm_bindgen]
pub fn get_short_address_by_index(full_viewing_key: &str, index: u32) -> JsValue {
utils::set_panic_hook();
let fvk = FullViewingKey::from_str(full_viewing_key.as_ref())
.expect("The provided string is not a valid FullViewingKey");

let (address, _dtk) = fvk.incoming().payment_address(index.into());
let short_address = address.display_short_form();
serde_wasm_bindgen::to_value(&short_address).expect("able to serialize address")
}

#[wasm_bindgen]
pub fn decode_transaction(tx_bytes: &str) -> JsValue {
utils::set_panic_hook();
let tx_vec: Vec<u8> =
base64::Engine::decode(&base64::engine::general_purpose::STANDARD, tx_bytes)
.expect("the provided tx string is a valid base64 string");
let transaction: Transaction =
Transaction::try_from(tx_vec).expect("the provided tx string is a valid transaction");
serde_wasm_bindgen::to_value(&transaction).expect("able to serialize transaction")
}

#[wasm_bindgen]
pub fn decode_nct_root(tx_bytes: &str) -> JsValue {
utils::set_panic_hook();
let tx_vec: Vec<u8> =
hex::decode(tx_bytes).expect("the provided tx string is a valid hex string");
let root = penumbra_tct::Root::decode(tx_vec.as_slice())
.expect("the provided tx string is a valid nct root");
serde_wasm_bindgen::to_value(&root).expect("able to serialize nct root")
}
3 changes: 1 addition & 2 deletions crates/wasm/src/note_record.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ use penumbra_proto::{view::v1alpha1 as pb, DomainType, TypeUrl};
use penumbra_sct::Nullifier;
use penumbra_shielded_pool::{note, Note};
use penumbra_tct as tct;
use std::convert::{TryFrom, TryInto};

use serde::{Deserialize, Serialize};
use std::convert::{TryFrom, TryInto};

/// Corresponds to the SpendableNoteRecord proto
#[derive(Serialize, Deserialize, Debug, Clone)]
Expand Down
Loading

0 comments on commit cb57996

Please sign in to comment.