diff --git a/crates/common/src/raindex_order_builder/order_operations.rs b/crates/common/src/raindex_order_builder/order_operations.rs index d5c5270938..2723d8e9be 100644 --- a/crates/common/src/raindex_order_builder/order_operations.rs +++ b/crates/common/src/raindex_order_builder/order_operations.rs @@ -58,6 +58,9 @@ pub struct DepositAndAddOrderCalldataResult(pub Bytes); #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct IOVaultIds(pub HashMap>>); +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct IOVaultless(pub HashMap>); + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct WithdrawCalldataResult(pub Vec); @@ -477,6 +480,58 @@ impl RaindexOrderBuilder { Ok(IOVaultIds(map)) } + pub fn set_vaultless( + &mut self, + r#type: VaultType, + token: String, + vaultless: bool, + ) -> Result<(), RaindexOrderBuilderError> { + let deployment = self.get_current_deployment()?; + self.dotrain_order + .dotrain_yaml() + .get_order(&deployment.deployment.order.key)? + .update_vaultless(r#type, token, vaultless)?; + + Ok(()) + } + + pub fn get_vaultless(&self) -> Result { + let deployment = self.get_current_deployment()?; + + let input_map = deployment + .deployment + .order + .inputs + .iter() + .map(|input| { + input + .token + .as_ref() + .map(|token| (token.key.clone(), input.vaultless)) + .ok_or(RaindexOrderBuilderError::SelectTokensNotSet) + }) + .collect::, _>>()?; + + let output_map = deployment + .deployment + .order + .outputs + .iter() + .map(|output| { + output + .token + .as_ref() + .map(|token| (token.key.clone(), output.vaultless)) + .ok_or(RaindexOrderBuilderError::SelectTokensNotSet) + }) + .collect::, _>>()?; + + Ok(IOVaultless(HashMap::from([ + ("input".to_string(), input_map), + ("output".to_string(), output_map), + ]))) + } + pub fn has_any_vault_id(&self) -> Result { let map = self.get_vault_ids()?; Ok(map @@ -811,6 +866,59 @@ mod tests { assert_eq!(res.0["output"]["token1"], Some(U256::from(888))); } + #[tokio::test] + async fn test_set_and_get_vaultless() { + let mut builder = initialize_builder(Some("other-deployment".to_string())).await; + + let res = builder.get_vaultless().unwrap(); + assert_eq!(res.0.len(), 2); + assert!(!res.0["input"]["token1"]); + assert!(!res.0["output"]["token1"]); + + builder + .set_vaultless(VaultType::Input, "token1".to_string(), true) + .unwrap(); + + let res = builder.get_vaultless().unwrap(); + assert!(res.0["input"]["token1"]); + assert!(!res.0["output"]["token1"]); + assert_eq!(builder.get_vault_ids().unwrap().0["input"]["token1"], None); + assert!(builder + .generate_dotrain_text() + .unwrap() + .contains("vaultless")); + + builder + .set_vault_id( + VaultType::Input, + "token1".to_string(), + Some("999".to_string()), + ) + .unwrap(); + + let res = builder.get_vaultless().unwrap(); + assert!(!res.0["input"]["token1"]); + assert_eq!( + builder.get_vault_ids().unwrap().0["input"]["token1"], + Some(U256::from(999)) + ); + assert!(!builder + .generate_dotrain_text() + .unwrap() + .contains("vaultless")); + + builder + .set_vaultless(VaultType::Input, "token1".to_string(), true) + .unwrap(); + builder + .set_vaultless(VaultType::Input, "token1".to_string(), false) + .unwrap(); + + let res = builder.get_vaultless().unwrap(); + assert!(!res.0["input"]["token1"]); + assert_eq!(builder.get_vault_ids().unwrap().0["input"]["token1"], None); + } + #[tokio::test] async fn test_has_any_vault_id() { let mut builder = initialize_builder(Some("other-deployment".to_string())).await; diff --git a/crates/common/src/raindex_order_builder/state_management.rs b/crates/common/src/raindex_order_builder/state_management.rs index ca1ebf04db..025c3f2cb9 100644 --- a/crates/common/src/raindex_order_builder/state_management.rs +++ b/crates/common/src/raindex_order_builder/state_management.rs @@ -33,6 +33,32 @@ struct SerializedBuilderState { vault_ids: BTreeMap<(VaultType, String), Option>, dotrain_hash: String, selected_deployment: String, + vaultless: BTreeMap<(VaultType, String), bool>, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(rename_all = "camelCase")] +struct LegacySerializedBuilderState { + field_values: BTreeMap, + deposits: BTreeMap, + select_tokens: BTreeMap, + vault_ids: BTreeMap<(VaultType, String), Option>, + dotrain_hash: String, + selected_deployment: String, +} + +impl From for SerializedBuilderState { + fn from(state: LegacySerializedBuilderState) -> Self { + Self { + field_values: state.field_values, + deposits: state.deposits, + select_tokens: state.select_tokens, + vault_ids: state.vault_ids, + dotrain_hash: state.dotrain_hash, + selected_deployment: state.selected_deployment, + vaultless: BTreeMap::new(), + } + } } impl RaindexOrderBuilder { @@ -86,6 +112,22 @@ impl RaindexOrderBuilder { Ok(vault_ids) } + fn parse_vaultless_for_order( + documents: Vec>>, + order_key: &str, + is_input: bool, + ) -> Result, RaindexOrderBuilderError> { + let r#type = if is_input { + VaultType::Input + } else { + VaultType::Output + }; + Ok(OrderCfg::parse_vaultless(documents, order_key, r#type)? + .into_iter() + .map(|(token, value)| ((r#type, token), value)) + .collect()) + } + pub fn generate_dotrain_builder_state_instance_v1( &self, ) -> Result { @@ -248,11 +290,24 @@ impl RaindexOrderBuilder { false, )?); + let mut vaultless = BTreeMap::new(); + vaultless.extend(Self::parse_vaultless_for_order( + self.dotrain_order.dotrain_yaml().documents.clone(), + &order_key, + true, + )?); + vaultless.extend(Self::parse_vaultless_for_order( + self.dotrain_order.dotrain_yaml().documents.clone(), + &order_key, + false, + )?); + let state = SerializedBuilderState { field_values: field_values.clone(), deposits: deposits.clone(), select_tokens: select_tokens.clone(), vault_ids: vault_ids.clone(), + vaultless: vaultless.clone(), dotrain_hash: self.dotrain_hash.clone(), selected_deployment: self.selected_deployment.clone(), }; @@ -275,7 +330,9 @@ impl RaindexOrderBuilder { let mut decoder = GzDecoder::new(&compressed[..]); let mut bytes = Vec::new(); decoder.read_to_end(&mut bytes)?; - let state: SerializedBuilderState = bincode::deserialize(&bytes)?; + let state: SerializedBuilderState = bincode::deserialize(&bytes).or_else(|_| { + bincode::deserialize::(&bytes).map(Into::into) + })?; let dotrain_order = DotrainOrder::create_with_profile( dotrain.clone(), @@ -341,6 +398,13 @@ impl RaindexOrderBuilder { builder.dotrain_order.dotrain_yaml().documents, &state.selected_deployment, )?; + for ((r#type, token), vaultless) in state.vaultless { + builder + .dotrain_order + .dotrain_yaml() + .get_order_for_builder_deployment(&order_key, &state.selected_deployment) + .and_then(|mut order| order.update_vaultless(r#type, token, vaultless))?; + } for ((is_input, index), vault_id) in state.vault_ids { builder .dotrain_order @@ -399,7 +463,8 @@ mod tests { use raindex_app_settings::{network::NetworkCfg, order::VaultType, yaml::YamlParsableHash}; use std::str::FromStr; - const SERIALIZED_STATE: &str = "H4sIAAAAAAAA_21QXWvCMBRt3NgY7EkGexrsByw0qROssIchgiIIStG-ahu0JE1Km1o__oQ_Was3FYv34Z5zc07uvUnDusYb4DKSYSRXmFomngApIXWTg-CAWBUz5AVQK85k61G3x8776h2qTMUMS6YLlXJz7wtwrXXStW2hgoVYq0x3O6TTttMkwHkqDqUDlRmZ0X1v8AG0-TvbHmsJNdEryF65w3cLPZt6ND6_pGHd4m5bWo2grovqqlOpjuv-APUp99NEFsNJO4udvD_ozcM8-s95T03Gnj_0pzHf0D1huPj7NH_BBAs0vjTFIUuE2sVM6hODT8eRygEAAA=="; + const SERIALIZED_STATE: &str = "H4sIAAAAAAAA_3VQXWvCMBRN3NgY7EkGexrsByw0qROssIchgjIQHGXr69aGWZImpU1XP_6EP1mrNxWr3od7zs09ufckLbSPO8DfWEWx-iMM2bgCZJQ2RS6GA4pqZskNoNGCq865aeeVx9U9VLlOOFHclDoT9t4T4MyYtO84Uoc_cqZz0-_RXtfJ0pAUmVxVClxlbFcP_dED0Pbr13zdSLiNb6HtVx6eO_ja1h-T7Uta6BBHblm9gnkebnbduut63gvQgIkgS1U5nnbzxC2Go8F3VMTvhRjo6cQPxsFnIv7ZknJSvj3av-CSh4bshpKIp1IvEq7MZVvo1AvaAGkw2IH4AQAA"; + const LEGACY_SERIALIZED_STATE_WITHOUT_VAULTLESS: &str = "H4sIAAAAAAAA_21QXWvCMBRt3NgY7EkGexrsByw0qROssIchgiIIStG-ahu0JE1Km1o__oQ_Was3FYv34Z5zc07uvUnDusYb4DKSYSRXmFomngApIXWTg-CAWBUz5AVQK85k61G3x8776h2qTMUMS6YLlXJz7wtwrXXStW2hgoVYq0x3O6TTttMkwHkqDqUDlRmZ0X1v8AG0-TvbHmsJNdEryF65w3cLPZt6ND6_pGHd4m5bWo2grovqqlOpjuv-APUp99NEFsNJO4udvD_ozcM8-s95T03Gnj_0pzHf0D1huPj7NH_BBAs0vjTFIUuE2sVM6hODT8eRygEAAA=="; fn encode_state(state: &SerializedBuilderState) -> String { let bytes = bincode::serialize(state).unwrap(); @@ -546,6 +611,45 @@ mod tests { ); } + #[tokio::test] + async fn test_new_from_legacy_state_without_vaultless() { + let builder = RaindexOrderBuilder::new_from_state( + get_yaml(), + None, + LEGACY_SERIALIZED_STATE_WITHOUT_VAULTLESS.to_string(), + ) + .await + .unwrap(); + + let vaultless = builder.get_vaultless().unwrap().0; + assert!(!vaultless.get("input").unwrap()["token1"]); + assert!(!vaultless.get("output").unwrap()["token2"]); + } + + #[tokio::test] + async fn test_serialize_state_round_trips_vaultless() { + let mut builder = initialize_builder_with_select_tokens().await; + builder + .set_vaultless(VaultType::Output, "token2".to_string(), true) + .unwrap(); + + let state = builder.serialize_state().unwrap(); + let restored = RaindexOrderBuilder::new_from_state(get_yaml(), None, state) + .await + .unwrap(); + + let vaultless = restored.get_vaultless().unwrap().0; + assert!(vaultless.get("output").unwrap()["token2"]); + assert_eq!( + restored.get_vault_ids().unwrap().0.get("output").unwrap()["token2"], + None + ); + assert!(restored + .generate_dotrain_text() + .unwrap() + .contains("vaultless")); + } + #[tokio::test] async fn test_new_from_state_invalid_dotrain() { let dotrain = r#" @@ -631,6 +735,7 @@ mod tests { deposits: BTreeMap::new(), select_tokens: BTreeMap::from([("token1".to_string(), token)]), vault_ids: BTreeMap::new(), + vaultless: BTreeMap::new(), dotrain_hash: RaindexOrderBuilder::compute_state_hash(&dotrain_order).unwrap(), selected_deployment: "select-token-deployment".to_string(), }); @@ -689,6 +794,7 @@ mod tests { deposits: BTreeMap::new(), select_tokens: BTreeMap::from([("token3".to_string(), replacement_token.clone())]), vault_ids: BTreeMap::new(), + vaultless: BTreeMap::new(), dotrain_hash: RaindexOrderBuilder::compute_state_hash(&dotrain_order).unwrap(), selected_deployment: "select-token-deployment".to_string(), }); diff --git a/crates/js_api/src/raindex_order_builder/order_operations.rs b/crates/js_api/src/raindex_order_builder/order_operations.rs index fa6e9a5f49..45e6026a08 100644 --- a/crates/js_api/src/raindex_order_builder/order_operations.rs +++ b/crates/js_api/src/raindex_order_builder/order_operations.rs @@ -55,6 +55,12 @@ pub struct IOVaultIds( ); impl_wasm_traits!(IOVaultIds); +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Tsify)] +pub struct IOVaultless( + #[tsify(type = "Map>")] pub HashMap>, +); +impl_wasm_traits!(IOVaultless); + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Tsify)] pub struct WithdrawCalldataResult(#[tsify(type = "string[]")] pub Vec); impl_wasm_traits!(WithdrawCalldataResult); @@ -177,6 +183,27 @@ impl RaindexOrderBuilder { Ok(self.inner.get_vault_ids()?) } + #[wasm_export(js_name = "setVaultless", unchecked_return_type = "void")] + pub fn set_vaultless( + &mut self, + #[wasm_export(param_description = "Vault type (input or output)")] r#type: VaultType, + #[wasm_export(param_description = "Token key")] token: String, + #[wasm_export(param_description = "Whether this IO is vaultless")] vaultless: bool, + ) -> Result<(), RaindexOrderBuilderWasmError> { + self.inner.set_vaultless(r#type, token, vaultless)?; + self.execute_state_update_callback()?; + Ok(()) + } + + #[wasm_export( + js_name = "getVaultless", + unchecked_return_type = "IOVaultless", + return_description = "Map of input/output vaultless flags by token" + )] + pub fn get_vaultless(&self) -> Result { + Ok(self.inner.get_vaultless()?) + } + #[wasm_export( js_name = "hasAnyVaultId", unchecked_return_type = "boolean", diff --git a/crates/settings/src/order.rs b/crates/settings/src/order.rs index 7efcbf5533..6cbaad5d99 100644 --- a/crates/settings/src/order.rs +++ b/crates/settings/src/order.rs @@ -79,7 +79,7 @@ impl OrderCfg { }) } - fn parse_vaultless(value: &StrictYaml, location: &str) -> Result { + fn parse_vaultless_flag(value: &StrictYaml, location: &str) -> Result { let value = &value["vaultless"]; if value.is_badvalue() { return Ok(false); @@ -101,7 +101,7 @@ impl OrderCfg { value: &StrictYaml, location: &str, ) -> Result<(bool, Option), YamlError> { - let vaultless = Self::parse_vaultless(value, location)?; + let vaultless = Self::parse_vaultless_flag(value, location)?; let vault_id_value = &value["vault-id"]; let vault_id = if vault_id_value.is_badvalue() { None @@ -693,6 +693,67 @@ impl OrderCfg { Ok(vault_ids) } + pub fn parse_vaultless( + documents: Vec>>, + order_key: &str, + r#type: VaultType, + ) -> Result, YamlError> { + let mut vaultless = HashMap::new(); + + for document in documents { + let document_read = document.read().map_err(|_| YamlError::ReadLockError)?; + + if let Ok(orders_hash) = require_hash(&document_read, Some("orders"), None) { + if let Some(order_yaml) = + orders_hash.get(&StrictYaml::String(order_key.to_string())) + { + let location = format!("order '{}'", order_key); + + let items = match r#type { + VaultType::Input => { + require_vec(order_yaml, "inputs", Some(location.clone()))? + } + VaultType::Output => { + require_vec(order_yaml, "outputs", Some(location.clone()))? + } + }; + + for (idx, item) in items.iter().enumerate() { + let token = require_string( + item, + Some("token"), + Some(format!( + "{} index '{}' in order '{}'", + if r#type == VaultType::Input { + "input" + } else { + "output" + }, + idx, + order_key + )), + )?; + let (is_vaultless, _) = + Self::parse_io_vault_fields(item, &format!("order '{}'", order_key))?; + vaultless.insert(token, is_vaultless); + } + } + } + } + + if vaultless.is_empty() { + return Err(YamlError::Field { + kind: FieldErrorKind::InvalidType { + field: "orders".to_string(), + expected: "a map".to_string(), + }, + location: "root".to_string(), + }); + } + + Ok(vaultless) + } + pub fn parse_io_token_keys( documents: Vec>>, order_key: &str, diff --git a/packages/raindex/test/js_api/builder.test.ts b/packages/raindex/test/js_api/builder.test.ts index 40588ea1c5..8a34f7e5c3 100644 --- a/packages/raindex/test/js_api/builder.test.ts +++ b/packages/raindex/test/js_api/builder.test.ts @@ -1145,7 +1145,7 @@ describe("Rain Raindex JS API Package Bindgen Tests - Builder", async function ( describe("state management tests", async () => { let serializedState = - "H4sIAAAAAAAA_21QsU7DMBCNAwIJMSDEioTEionrKEmpylBQVYZKIJFKdAyp20R17GC7BMRHMLLyAxVfwMrG9yA2iLBDo_YGv_O9d757BtZfbGtURCp4m7JRyiZA15C1tcjeR3RGbF3ZMAyfEtawTKxr9NCxX5PgSrKmsYEQWPUYrt_MgpJnBDKiCi6mpm9fY6JU3nIcyuOIJlyqVhM1PUfkMZwJ-lQqQHkCM7obXuzp9Ln9NT_8bM_fX7y37xsbn3y8xmAXbGo6LHc4wMDYDn992NZ_1L-hGuD7PljyVbGu6x7p9KzI6aB7FWZQBQMpewGO-kHnYdznBU2HdxMu8HnvujO-FMPTHd3DVUIEHJGc8seMMPUDQuHlHcsBAAA="; + "H4sIAAAAAAAA_3VQsU7DMBC1AwIJMSDEioTESojrKEmpylBQVYZKIJFKdAyp20R17GC7BMRHMLLyAxVfwMrG9yA2iLADUekNfud7d773DMFPbGpURCr7OmWjlE2griGw8Ze9jeiMWLqyZhg-JawBTKxq9NChX2vBVcuKxgZC8L_HcP1mBEqeEZsRVXAxNXO7GhOl8pbjUB5HNOFStZqo6Tkij-2ZoA9lByxPaFZ3w7MdnT62P-b77-3565P38nll4aO35xhuw3VNh6WGPQyN7fDbhwV-o_4N1QLf9-GCr4p1XfdApydFTgfdizCzVTCQshfgqB907sZ9XtB0eDPhAp_2LjvjczE83tIzXCVE2COSU36fEaaW6wGLIsAXbQ5sUfkBAAA="; let dotrain3: string; let builder: RaindexOrderBuilder; beforeAll(async () => { @@ -2067,6 +2067,115 @@ ${dotrainWithoutVaultIds}`; ); }); + it("should set and restore vaultless flags", async () => { + let stateUpdateCallback = vi.fn(); + mockServer + .forPost("/rpc-url") + .withBodyIncluding("0x82ad56cb") + .thenSendJsonRpcResult( + "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001a0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000007546f6b656e203100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000025431000000000000000000000000000000000000000000000000000000000000", + ); + + let testDotrain = ` + ${builderConfig2} + + ${dotrainWithoutVaultIds} + `; + let builderResult = await RaindexOrderBuilder.newWithDeployment( + testDotrain, + undefined, + "other-deployment", + stateUpdateCallback, + ); + const builder = extractWasmEncodedData(builderResult); + + assert.equal( + extractWasmEncodedData>>( + builder.getVaultless(), + ) + .get("output") + ?.get("token2"), + false, + ); + + builder.setVaultless("output", "token2", true); + + assert.equal( + extractWasmEncodedData>>( + builder.getVaultless(), + ) + .get("output") + ?.get("token2"), + true, + ); + assert.equal( + extractWasmEncodedData>>( + builder.getVaultIds(), + ) + .get("output") + ?.get("token2"), + undefined, + ); + assert.equal( + extractWasmEncodedData( + builder.getCurrentDeployment(), + ).deployment.order.outputs[0].vaultless, + true, + ); + + const serialized = extractWasmEncodedData( + builder.serializeState(), + ); + builderResult = await RaindexOrderBuilder.newFromState( + testDotrain, + undefined, + serialized, + ); + const restored = extractWasmEncodedData(builderResult); + + assert.equal( + extractWasmEncodedData>>( + restored.getVaultless(), + ) + .get("output") + ?.get("token2"), + true, + ); + + restored.setVaultId("output", "token2", "0x234"); + assert.equal( + extractWasmEncodedData>>( + restored.getVaultless(), + ) + .get("output") + ?.get("token2"), + false, + ); + assert.equal( + extractWasmEncodedData>>( + restored.getVaultIds(), + ) + .get("output") + ?.get("token2"), + "0x234", + ); + + const result = restored.setVaultId("output", "token2", "0"); + if (!result.error) expect.fail("Expected error"); + expect(result.error.msg).toContain("vault-id must be nonzero"); + + restored.setVaultless("output", "token2", false); + assert.equal( + extractWasmEncodedData>>( + restored.getVaultless(), + ) + .get("output") + ?.get("token2"), + false, + ); + assert.equal(stateUpdateCallback.mock.calls.length, 1); + }); + it("should skip deposits with zero amount for deposit calldata", async () => { // token1 info mockServer