diff --git a/Cargo.lock b/Cargo.lock index 2a931b96f..e58cdecdd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,6 +27,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + [[package]] name = "adler32" version = "1.2.0" @@ -1637,7 +1643,7 @@ dependencies = [ "cc", "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.7.4", "object", "rustc-demangle", ] @@ -1711,6 +1717,15 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bit-set" version = "0.5.3" @@ -3405,12 +3420,12 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.31" +version = "1.0.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f211bbe8e69bbd0cfdea405084f128ae8b4aaa6b0b522fc8f2b009084797920" +checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.8.0", ] [[package]] @@ -5210,6 +5225,15 @@ dependencies = [ "adler", ] +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + [[package]] name = "mio" version = "1.0.2" @@ -6424,7 +6448,10 @@ name = "rain_orderbook_js_api" version = "0.0.0-alpha.0" dependencies = [ "alloy", + "base64 0.22.1", + "bincode", "cynic", + "flate2", "js-sys", "rain_orderbook_app_settings", "rain_orderbook_bindings", @@ -6433,6 +6460,7 @@ dependencies = [ "reqwest 0.12.5", "serde", "serde-wasm-bindgen 0.6.5", + "sha2", "thiserror", "tokio", "tsify", @@ -7513,9 +7541,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.125" +version = "1.0.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" dependencies = [ "indexmap", "itoa", diff --git a/crates/js_api/Cargo.toml b/crates/js_api/Cargo.toml index 48a2fa80c..56c9e5539 100644 --- a/crates/js_api/Cargo.toml +++ b/crates/js_api/Cargo.toml @@ -25,4 +25,8 @@ serde-wasm-bindgen = { version = "0.6.5" } wasm-bindgen-futures = { version = "0.4.42" } tsify = { version = "0.4.5", default-features = false, features = ["js", "wasm-bindgen"] } tokio = { workspace = true, features = ["sync", "macros", "io-util", "rt", "time"] } -alloy = { workspace = true, features = [ "dyn-abi" ] } \ No newline at end of file +alloy = { workspace = true, features = [ "dyn-abi" ] } +flate2 = "1.0.34" +base64 = "0.22.1" +bincode = "1.3.3" +sha2 = "0.10.8" \ No newline at end of file diff --git a/crates/js_api/src/gui/mod.rs b/crates/js_api/src/gui/mod.rs index 76cb38d50..06533f362 100644 --- a/crates/js_api/src/gui/mod.rs +++ b/crates/js_api/src/gui/mod.rs @@ -1,4 +1,6 @@ use alloy::primitives::Address; +use base64::{engine::general_purpose::URL_SAFE, Engine}; +use flate2::{read::GzDecoder, write::GzEncoder, Compression}; use rain_orderbook_app_settings::gui::{ Gui, GuiDeployment, GuiFieldDefinition, ParseGuiConfigSourceError, }; @@ -6,7 +8,8 @@ use rain_orderbook_bindings::impl_wasm_traits; use rain_orderbook_common::dotrain_order::{DotrainOrder, DotrainOrderError}; use serde::{Deserialize, Serialize}; use serde_wasm_bindgen::{from_value, to_value}; -use std::collections::HashMap; +use std::collections::BTreeMap; +use std::io::prelude::*; use thiserror::Error; use tsify::Tsify; use wasm_bindgen::{ @@ -18,6 +21,8 @@ use wasm_bindgen::{ prelude::*, }; +mod state_management; + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Tsify)] #[tsify(into_wasm_abi, from_wasm_abi)] pub struct TokenDeposit { @@ -41,7 +46,7 @@ impl_wasm_traits!(FieldValuePair); pub struct DotrainOrderGui { dotrain_order: DotrainOrder, deployment: GuiDeployment, - field_values: HashMap, + field_values: BTreeMap, deposits: Vec, } #[wasm_bindgen] @@ -65,7 +70,7 @@ impl DotrainOrderGui { Ok(Self { dotrain_order, deployment: gui_deployment.clone(), - field_values: HashMap::new(), + field_values: BTreeMap::new(), deposits: vec![], }) } @@ -172,11 +177,19 @@ pub enum GuiError { FieldBindingNotFound(String), #[error("Deposit token not found in gui config: {0}")] DepositTokenNotFound(String), + #[error("Deserialized config mismatch")] + DeserializedConfigMismatch, #[error(transparent)] DotrainOrderError(#[from] DotrainOrderError), #[error(transparent)] ParseGuiConfigSourceError(#[from] ParseGuiConfigSourceError), #[error(transparent)] + IoError(#[from] std::io::Error), + #[error(transparent)] + BincodeError(#[from] bincode::Error), + #[error(transparent)] + Base64Error(#[from] base64::DecodeError), + #[error(transparent)] SerdeWasmBindgenError(#[from] serde_wasm_bindgen::Error), } impl From for JsValue { diff --git a/crates/js_api/src/gui/state_management.rs b/crates/js_api/src/gui/state_management.rs new file mode 100644 index 000000000..5cd50e817 --- /dev/null +++ b/crates/js_api/src/gui/state_management.rs @@ -0,0 +1,62 @@ +use super::*; +use sha2::{Digest, Sha256}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +struct SerializedGuiState { + config_hash: String, + field_values: BTreeMap, + deposits: Vec, +} + +#[wasm_bindgen] +impl DotrainOrderGui { + fn compute_config_hash(&self) -> String { + let config = self.get_gui_config(); + let bytes = bincode::serialize(&config).expect("Failed to serialize config"); + let hash = Sha256::digest(&bytes); + format!("{:x}", hash) + } + + #[wasm_bindgen(js_name = "serializeState")] + pub fn serialize(&self) -> Result { + let config_hash = self.compute_config_hash(); + + let state = SerializedGuiState { + config_hash, + field_values: self.field_values.clone(), + deposits: self.deposits.clone(), + }; + let bytes = bincode::serialize(&state)?; + + let mut encoder = GzEncoder::new(Vec::new(), Compression::default()); + encoder.write_all(&bytes)?; + let compressed = encoder.finish()?; + + Ok(URL_SAFE.encode(compressed)) + } + + #[wasm_bindgen(js_name = "deserializeState")] + pub fn deserialize_state(&mut self, serialized: String) -> Result<(), GuiError> { + let compressed = URL_SAFE.decode(serialized)?; + + let mut decoder = GzDecoder::new(&compressed[..]); + let mut bytes = Vec::new(); + decoder.read_to_end(&mut bytes)?; + + let state: SerializedGuiState = bincode::deserialize(&bytes)?; + self.field_values = state.field_values; + self.deposits = state.deposits; + + if state.config_hash != self.compute_config_hash() { + return Err(GuiError::DeserializedConfigMismatch); + } + + Ok(()) + } + + #[wasm_bindgen(js_name = "clearState")] + pub fn clear_state(&mut self) { + self.field_values.clear(); + self.deposits.clear(); + } +} diff --git a/packages/orderbook/test/js_api/gui.test.ts b/packages/orderbook/test/js_api/gui.test.ts index b0f9aa422..c7515e128 100644 --- a/packages/orderbook/test/js_api/gui.test.ts +++ b/packages/orderbook/test/js_api/gui.test.ts @@ -40,6 +40,30 @@ gui: - value: "582.1" - value: "648.239" `; +const guiConfig2 = ` +gui: + name: Test test + description: Test test test + deployments: + - deployment: other-deployment + name: Test test + description: Test test test + deposits: + - token: token1 + min: 0 + presets: + - "0" + - token: token2 + min: 0 + presets: + - "0" + fields: + - binding: test-binding + name: Test binding + description: Test binding description + presets: + - value: "test-value" +`; const dotrain = ` networks: @@ -97,6 +121,9 @@ deployments: some-deployment: scenario: some-scenario order: some-order + other-deployment: + scenario: some-scenario + order: some-order --- #calculate-io _ _: 0 0; @@ -256,4 +283,68 @@ describe("Rain Orderbook JS API Package Bindgen Tests - Gui", async function () ); }); }); + + describe("state management tests", async () => { + let serializedString = + "H4sIAAAAAAAA_3WMuw3CQBBE-RgkMiQIXQGS0e79fM6I6eLu9hZZSCZxQAdIBCCKoQECGqAMmiBgIyQmmZkXvM3gG3IeCUxWDROSy16TjTpapSKj14lTysaAR4bIpG1tgvG1TxY4MDY0Es9MOrYdtd2uwpUAOKLSxrraNxBiosz__q9CjQUgwFDmVLo_7HOHhTwLa7eU_VhUk1d5256eoZz353dxv1w_3kRs8-0AAAA="; + let gui: DotrainOrderGui; + beforeAll(async () => { + gui = await DotrainOrderGui.init(dotrainWithGui, "some-deployment"); + + gui.saveFieldValue( + "binding-1", + "0x1234567890abcdef1234567890abcdef12345678" + ); + gui.saveFieldValue("binding-2", "100"); + gui.saveDeposit("token1", "50.6"); + }); + + it("should serialize gui state", async () => { + const serialized = gui.serializeState(); + assert.equal(serialized, serializedString); + }); + + it("should deserialize gui state", async () => { + gui.clearState(); + gui.deserializeState(serializedString); + const fieldValues = gui.getAllFieldValues(); + assert.equal(fieldValues.length, 2); + assert.equal(fieldValues[0].binding, "binding-1"); + assert.equal( + fieldValues[0].value, + "0x1234567890abcdef1234567890abcdef12345678" + ); + assert.equal(fieldValues[1].binding, "binding-2"); + assert.equal(fieldValues[1].value, "100"); + const deposits = gui.getDeposits(); + assert.equal(deposits.length, 1); + assert.equal(deposits[0].token, "token1"); + assert.equal(deposits[0].amount, "50.6"); + assert.equal( + deposits[0].address, + "0xc2132d05d31c914a87c6611c10748aeb04b58e8f" + ); + }); + + it("should throw error during deserialize if config is different", async () => { + let dotrain2 = ` +${guiConfig2} + +${dotrain} +`; + let gui2 = await DotrainOrderGui.init(dotrain2, "other-deployment"); + let serialized = gui2.serializeState(); + expect(() => gui.deserializeState(serialized)).toThrow( + "Deserialized config mismatch" + ); + }); + + it("should clear state", async () => { + gui.clearState(); + const fieldValues = gui.getAllFieldValues(); + assert.equal(fieldValues.length, 0); + const deposits = gui.getDeposits(); + assert.equal(deposits.length, 0); + }); + }); });