Skip to content

Commit

Permalink
feat: Use local storage
Browse files Browse the repository at this point in the history
URL parameters were limited in size, which put a limit on the maximum
program size that could be encoded. This commit moves away from stateful
URLs and uses the browser's local storage instead. Every time the Run
button is clicked, the storage is updated. Every time the app is
restarted, it reads from the storage. If the storage is empty, then
there are sane defaults, as before.

As a side effect, this commit disables the Share button. I plan to let
users copy and paste a large blob of characters which will encode the
app's state. This will have to wait until a later commit.
  • Loading branch information
uncomputable committed Nov 24, 2024
1 parent 4081252 commit 714cafc
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 104 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ leptos_router = { version = "0.6.15", features = ["csr"] }
console_error_panic_hook = "0.1.7"
hex-conservative = "0.2.1"
js-sys = "0.3.70"
web-sys = { version = "0.3.70", features = ["Navigator", "Clipboard"] }
web-sys = { version = "0.3.70", features = ["Navigator", "Clipboard", "Storage"] }
wasm-bindgen-futures = "0.4.43"
gloo-timers = { version = "0.3.0", features = ["futures"] }
lz-str = "0.2.1"
Expand Down
16 changes: 7 additions & 9 deletions src/components/app.rs
Original file line number Diff line number Diff line change
@@ -1,28 +1,26 @@
use leptos::{component, provide_context, view, IntoView, RwSignal, SignalGetUntracked};
use leptos_router::use_query_map;
use leptos::{component, provide_context, view, IntoView, RwSignal};

use super::program_window::{Program, ProgramWindow, Runtime};
use crate::components::footer::Footer;
use crate::components::run_window::{HashedData, RunWindow, SignedData, SigningKeys, TxEnv};
use crate::components::state::FromParams;
use crate::components::state::LocalStorage;
use crate::transaction::TxParams;

#[derive(Copy, Clone, Debug, Default)]
pub struct ActiveRunTab(pub RwSignal<&'static str>);

#[component]
pub fn App() -> impl IntoView {
let url_params = use_query_map().get_untracked();

let signing_keys = SigningKeys::from_map(&url_params).unwrap_or_default();
let signing_keys = SigningKeys::load_from_storage().unwrap_or_default();
provide_context(signing_keys);
let program = Program::new(signing_keys.first_public_key());
let program = Program::load_from_storage()
.unwrap_or_else(|| Program::new_p2pk(signing_keys.first_public_key()));
provide_context(program);
let tx_params = TxParams::from_map(&url_params).unwrap_or_default();
let tx_params = TxParams::load_from_storage().unwrap_or_default();
let tx_env = TxEnv::new(program, tx_params);
provide_context(tx_env);
provide_context(SignedData::new(tx_env.lazy_env));
provide_context(HashedData::from_map(&url_params).unwrap_or_default());
provide_context(HashedData::load_from_storage().unwrap_or_default());
provide_context(Runtime::new(program, tx_env.lazy_env));
provide_context(ActiveRunTab::default());

Expand Down
22 changes: 13 additions & 9 deletions src/components/program_window/program_tab.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,18 @@ pub struct Program {
}

impl Program {
pub fn new(key: secp256k1::XOnlyPublicKey) -> Self {
pub fn new(text: String) -> Self {
let program = Self {
text: create_rw_signal(text),
cached_text: create_rw_signal("".to_string()),
lazy_cmr: create_rw_signal(Err("".to_string())),
lazy_satisfied: create_rw_signal(Err("".to_string())),
};
program.update_on_read();
program
}

pub fn new_p2pk(key: secp256k1::XOnlyPublicKey) -> Self {
let text = format!(
r#"mod witness {{
const SIG: Signature = 0x1d7d93f350e2db564f90da49fb00ee47294bb6d8f061929818b26065a3e50fdd87e0e8ab45eecd04df0b92b427e6d49a5c96810c23706566e9093c992e075dc5; // TODO: update this
Expand All @@ -39,14 +50,7 @@ fn main() {{
key.serialize().as_hex()
);

let program = Self {
text: create_rw_signal(text),
cached_text: create_rw_signal("".to_string()),
lazy_cmr: create_rw_signal(Err("".to_string())),
lazy_satisfied: create_rw_signal(Err("".to_string())),
};
program.update_on_read();
program
Self::new(text)
}

pub fn cmr(self) -> Result<simplicity::Cmr, String> {
Expand Down
6 changes: 5 additions & 1 deletion src/components/program_window/run_button.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
use leptos::{component, ev, use_context, view, IntoView, SignalGet};

use crate::components::program_window::Runtime;
use crate::components::state::update_local_storage;

#[component]
pub fn RunButton() -> impl IntoView {
let runtime = use_context::<Runtime>().expect("runtime should exist in context");
let audio_ref = runtime.alarm_audio_ref;

let run_program = move |_event: ev::MouseEvent| runtime.run();
let run_program = move |_event: ev::MouseEvent| {
update_local_storage();
runtime.run();
};
let button_class = move || match runtime.run_succeeded.get() {
None => "button run-button",
Some(false) => "button run-button failure",
Expand Down
3 changes: 1 addition & 2 deletions src/components/program_window/share_button.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
use leptos::{component, view, IntoView};

use crate::components::copy_to_clipboard::CopyToClipboard;
use crate::components::state::stateful_url;

#[component]
pub fn ShareButton() -> impl IntoView {
let url = move || stateful_url().unwrap_or_default();
let url = move || "Sharing is temporarily disabled".to_string();
view! {
<CopyToClipboard content=url class="button" tooltip_below=true>
<i class="fa-solid fa-share-nodes"></i>
Expand Down
211 changes: 129 additions & 82 deletions src/components/state.rs
Original file line number Diff line number Diff line change
@@ -1,64 +1,142 @@
use std::num::NonZeroU32;

use leptos::{use_context, SignalGetUntracked};
use leptos_router::ParamsMap;
use leptos::{use_context, SignalGetUntracked, SignalWithUntracked};
use web_sys::window;

use crate::components::program_window::Program;
use crate::components::run_window::{HashedData, SigningKeys, TxEnv};
use crate::transaction::TxParams;

/// [`leptos_router::Params`] with simpler error handling via [`Option`].
pub trait FromParams: Sized {
/// [`leptos_router::Params::from_map`] that returns `Option<Self>`
/// instead of `Result<Self, ParamsError>`.
fn from_map(map: &ParamsMap) -> Option<Self>;
/// Get the browser's local storage.
fn local_storage() -> Option<web_sys::Storage> {
let window = window()?;
window.local_storage().ok().flatten()
}

pub trait ToParams {
/// Convert the value into route parameters and route values.
fn to_params(&self) -> impl Iterator<Item = (&'static str, String)>;
/// Read / write an object to / from the browser's local storage.
pub trait LocalStorage: Sized {
/// Iterate over the keys that make up the object.
fn keys() -> impl Iterator<Item = &'static str>;

/// Construct an object from the values of the corresponding keys.
///
/// Return `None` if there are not enough values or
/// if there is an ill-formatted value.
fn from_values(values: impl Iterator<Item = String>) -> Option<Self>;

/// Convert an object into its underlying values, in the order of keys.
fn to_values(&self) -> impl Iterator<Item = String>;

/// Load an object from the browser's local storage.
fn load_from_storage() -> Option<Self> {
let storage = local_storage()?;
let values = Self::keys().filter_map(|key| storage.get_item(key).ok().flatten());
Self::from_values(values)
}

/// Store an object in the browser's local storage.
///
/// Replaces any existing value.
fn store_in_storage(&self) {
let storage = match local_storage() {
Some(storage) => storage,
_ => return,
};
for (key, value) in Self::keys().zip(self.to_values()) {
let _result = storage.set_item(key, value.as_str());
}
}
}

/// Store the app's entire state in the browser's local storage.
pub fn update_local_storage() {
use_context::<Program>()
.expect("program should exist in context")
.store_in_storage();
use_context::<TxEnv>()
.expect("transaction environment should exist in context")
.params
.with_untracked(LocalStorage::store_in_storage);
use_context::<SigningKeys>()
.expect("signing keys should exist in context")
.store_in_storage();
use_context::<HashedData>()
.expect("hashed data should exist in context")
.store_in_storage();
leptos::logging::log!("Update storage");
}

impl FromParams for SigningKeys {
fn from_map(map: &ParamsMap) -> Option<Self> {
let key_offset = map.get("seed").and_then(|s| s.parse::<u32>().ok())?;
let key_count = map.get("keys").and_then(|s| s.parse::<NonZeroU32>().ok())?;
Some(Self::new(key_offset, key_count))
impl LocalStorage for Program {
fn keys() -> impl Iterator<Item = &'static str> {
["program"].into_iter()
}

fn from_values(mut values: impl Iterator<Item = String>) -> Option<Self> {
values.next().map(Self::new)
}

fn to_values(&self) -> impl Iterator<Item = String> {
[self.text.get_untracked()].into_iter()
}
}

impl ToParams for SigningKeys {
fn to_params(&self) -> impl Iterator<Item = (&'static str, String)> {
impl LocalStorage for SigningKeys {
fn keys() -> impl Iterator<Item = &'static str> {
["seed", "key_count"].into_iter()
}

fn from_values(mut values: impl Iterator<Item = String>) -> Option<Self> {
let seed = values.next().and_then(|s| s.parse::<u32>().ok())?;
let key_count = values.next().and_then(|s| s.parse::<NonZeroU32>().ok())?;
Some(Self::new(seed, key_count))
}

fn to_values(&self) -> impl Iterator<Item = String> {
[
("seed", self.key_offset.get_untracked().to_string()),
("keys", self.key_count.get_untracked().to_string()),
self.key_offset.get_untracked().to_string(),
self.key_count.get_untracked().to_string(),
]
.into_iter()
}
}

impl FromParams for HashedData {
fn from_map(map: &ParamsMap) -> Option<Self> {
map.get("hashes")
.and_then(|s| s.parse::<u32>().ok())
.map(Self::new)
impl LocalStorage for HashedData {
fn keys() -> impl Iterator<Item = &'static str> {
["hash_count"].into_iter()
}

fn from_values(mut values: impl Iterator<Item = String>) -> Option<Self> {
let hash_count = values.next().and_then(|s| s.parse::<u32>().ok())?;
Some(Self::new(hash_count))
}
}

impl ToParams for HashedData {
fn to_params(&self) -> impl Iterator<Item = (&'static str, String)> {
[("hashes", self.hash_count.get_untracked().to_string())].into_iter()
fn to_values(&self) -> impl Iterator<Item = String> {
[self.hash_count.get_untracked().to_string()].into_iter()
}
}

impl FromParams for TxParams {
fn from_map(map: &ParamsMap) -> Option<Self> {
let txid = map.get("txid").and_then(|s| s.parse().ok())?;
let vout = map.get("vout").and_then(|s| s.parse().ok())?;
let value_in = map.get("value").and_then(|s| s.parse().ok())?;
let recipient_address = map.get("recipient").and_then(|s| s.parse().ok());
let fee = map.get("fee").and_then(|s| s.parse().ok())?;
let lock_time = map.get("lock_time").and_then(|s| s.parse().ok())?;
let sequence = map.get("sequence").and_then(|s| s.parse().ok())?;
impl LocalStorage for TxParams {
fn keys() -> impl Iterator<Item = &'static str> {
[
"txid",
"vout",
"value",
"recipient",
"fee",
"lock_time",
"sequence",
]
.into_iter()
}

fn from_values(mut values: impl Iterator<Item = String>) -> Option<Self> {
let txid = values.next().and_then(|s| s.parse().ok())?;
let vout = values.next().and_then(|s| s.parse().ok())?;
let value_in = values.next().and_then(|s| s.parse().ok())?;
let recipient_address = values.next().and_then(|s| s.parse().ok());
let fee = values.next().and_then(|s| s.parse().ok())?;
let lock_time = values.next().and_then(|s| s.parse().ok())?;
let sequence = values.next().and_then(|s| s.parse().ok())?;

Some(Self {
txid,
Expand All @@ -70,51 +148,20 @@ impl FromParams for TxParams {
sequence,
})
}
}

impl ToParams for TxParams {
fn to_params(&self) -> impl Iterator<Item = (&'static str, String)> {
let mut params = vec![
("txid", self.txid.to_string()),
("vout", self.vout.to_string()),
("value", self.value_in.to_string()),
("fee", self.fee.to_string()),
("lock_time", self.lock_time.to_string()),
("sequence", self.sequence.to_string()),
];
if let Some(address) = &self.recipient_address {
params.push(("recipient", address.to_string()));
}
params.into_iter()
fn to_values(&self) -> impl Iterator<Item = String> {
[
self.txid.to_string(),
self.vout.to_string(),
self.value_in.to_string(),
self.recipient_address
.as_ref()
.map(ToString::to_string)
.unwrap_or_default(),
self.fee.to_string(),
self.lock_time.to_string(),
self.sequence.to_string(),
]
.into_iter()
}
}

pub fn stateful_url() -> Option<String> {
web_sys::window().map(|window| {
let location = window.location();
let origin = location.origin().unwrap_or_default();
let pathname = location.pathname().unwrap_or_default();
let mut url = format!("{}{}", origin, pathname);

let tx_params = use_context::<TxEnv>()
.expect("transaction environment should exist in context")
.params
.get_untracked();
let signing_keys =
use_context::<SigningKeys>().expect("signing keys should exist in context");
let hashed_data = use_context::<HashedData>().expect("hashed data should exist in context");

for (param, value) in tx_params
.to_params()
.chain(signing_keys.to_params())
.chain(hashed_data.to_params())
{
url.push('?');
url.push_str(param);
url.push('=');
url.push_str(value.as_str());
}

url
})
}

0 comments on commit 714cafc

Please sign in to comment.