breez_sdk_spark/sdk/
unilateral_exit_backup.rs1use serde::{Deserialize, Serialize};
2use spark_wallet::{LeafPedigree, Network};
3use tracing::{debug, warn};
4
5use crate::{
6 error::SdkError,
7 models::{
8 ExportUnilateralExitStateResponse, ImportUnilateralExitStateRequest,
9 ImportUnilateralExitStateResponse,
10 },
11};
12
13use super::BreezSdk;
14
15const EXIT_STATE_VERSION: u32 = 1;
16
17#[derive(Debug, Serialize, Deserialize)]
20struct ExitStateEnvelope {
21 version: u32,
22 network: Network,
23 identity_public_key: String,
24 pedigrees: Vec<LeafPedigree>,
25}
26
27fn check_envelope_readable(
30 envelope: &ExitStateEnvelope,
31 wallet_network: Network,
32) -> Result<(), SdkError> {
33 if envelope.version != EXIT_STATE_VERSION {
34 return Err(SdkError::InvalidInput(format!(
35 "Unsupported exit state version {}, expected {EXIT_STATE_VERSION}",
36 envelope.version
37 )));
38 }
39 if envelope.network != wallet_network {
40 return Err(SdkError::InvalidInput(format!(
41 "Exit state is for network {}, this wallet is on {wallet_network}",
42 envelope.network
43 )));
44 }
45 Ok(())
46}
47
48#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
49#[allow(clippy::needless_pass_by_value)]
50impl BreezSdk {
51 pub async fn export_unilateral_exit_state(
58 &self,
59 ) -> Result<ExportUnilateralExitStateResponse, SdkError> {
60 let export = self.spark_wallet.export_exit_state().await?;
61 let leaves = export.pedigrees.len();
62 let envelope = ExitStateEnvelope {
63 version: EXIT_STATE_VERSION,
64 network: self.config.network.into(),
65 identity_public_key: self.spark_wallet.get_identity_public_key().to_string(),
66 pedigrees: export.pedigrees,
67 };
68
69 let exit_state = serde_json::to_string(&envelope)
70 .map_err(|e| SdkError::Generic(format!("Failed to serialize exit state: {e}")))?;
71 debug!(
72 leaves,
73 "export_unilateral_exit_state: exit state serialized"
74 );
75
76 Ok(ExportUnilateralExitStateResponse { exit_state })
77 }
78
79 pub async fn import_unilateral_exit_state(
95 &self,
96 request: ImportUnilateralExitStateRequest,
97 ) -> Result<ImportUnilateralExitStateResponse, SdkError> {
98 let envelope: ExitStateEnvelope = serde_json::from_str(&request.exit_state)
99 .map_err(|e| SdkError::InvalidInput(format!("Invalid exit state: {e}")))?;
100 check_envelope_readable(&envelope, self.config.network.into())?;
101 if envelope.identity_public_key != self.spark_wallet.get_identity_public_key().to_string() {
102 warn!("Importing an exit state exported by another wallet");
105 debug!("Exporting wallet: {}", envelope.identity_public_key);
106 }
107
108 let imported = self
109 .spark_wallet
110 .import_exit_state(envelope.pedigrees)
111 .await?;
112 let imported_leaves = u32::try_from(imported.imported_leaves)?;
113 let skipped_foreign_leaves = u32::try_from(imported.skipped_foreign_leaves)?;
114 let skipped_conflicting_leaves = u32::try_from(imported.skipped_conflicting_leaves)?;
115 let skipped_chains = u32::try_from(imported.skipped_chains)?;
116 debug!(
117 imported_leaves,
118 skipped_foreign_leaves,
119 skipped_conflicting_leaves,
120 skipped_chains,
121 "import_unilateral_exit_state: exit state merged"
122 );
123
124 Ok(ImportUnilateralExitStateResponse {
125 imported_leaves,
126 skipped_foreign_leaves,
127 skipped_conflicting_leaves,
128 skipped_chains,
129 })
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use spark_wallet::{TreeNodeStatus, tree_store_tests::create_test_node_with_parent};
136
137 use super::*;
138
139 #[cfg(feature = "browser-tests")]
140 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
141
142 fn regtest_envelope() -> ExitStateEnvelope {
143 let root = create_test_node_with_parent("root", None, TreeNodeStatus::Splitted);
144 let leaf = create_test_node_with_parent("leaf", Some("root"), TreeNodeStatus::Available);
145 let identity_public_key = leaf.owner_identity_public_key.unwrap().to_string();
146 let pedigrees = vec![LeafPedigree {
147 leaf,
148 ancestors: vec![root],
149 }];
150 ExitStateEnvelope {
151 version: EXIT_STATE_VERSION,
152 network: Network::Regtest,
153 identity_public_key,
154 pedigrees,
155 }
156 }
157
158 #[test]
159 fn envelope_round_trips_through_json() {
160 let envelope = regtest_envelope();
161 let json = serde_json::to_string(&envelope).unwrap();
162 let decoded: ExitStateEnvelope = serde_json::from_str(&json).unwrap();
163
164 assert_eq!(decoded.version, envelope.version);
165 assert_eq!(decoded.network, envelope.network);
166 assert_eq!(decoded.identity_public_key, envelope.identity_public_key);
167 assert_eq!(decoded.pedigrees.len(), 1);
168
169 let pedigree = &decoded.pedigrees[0];
170 let original = &envelope.pedigrees[0];
171 assert_eq!(pedigree.leaf.id, original.leaf.id);
172 assert_eq!(pedigree.leaf.parent_node_id, original.leaf.parent_node_id);
173 assert_eq!(pedigree.leaf.node_tx, original.leaf.node_tx);
174 assert_eq!(pedigree.leaf.value, original.leaf.value);
175 let ancestor_ids: Vec<String> = pedigree
176 .ancestors
177 .iter()
178 .map(|a| a.id.to_string())
179 .collect();
180 assert_eq!(ancestor_ids, vec!["root".to_string()]);
181
182 check_envelope_readable(&decoded, Network::Regtest).unwrap();
183 }
184
185 #[test]
186 fn envelope_with_unknown_version_is_rejected() {
187 let mut envelope = regtest_envelope();
188 envelope.version = EXIT_STATE_VERSION + 1;
189
190 match check_envelope_readable(&envelope, Network::Regtest) {
191 Err(SdkError::InvalidInput(message)) => assert!(
192 message.contains("version"),
193 "expected a version complaint, got {message}"
194 ),
195 other => panic!("expected InvalidInput, got {other:?}"),
196 }
197 }
198
199 #[test]
200 fn envelope_from_another_network_is_rejected() {
201 let envelope = regtest_envelope();
202
203 match check_envelope_readable(&envelope, Network::Mainnet) {
204 Err(SdkError::InvalidInput(message)) => assert!(
205 message.contains("network"),
206 "expected a network complaint, got {message}"
207 ),
208 other => panic!("expected InvalidInput, got {other:?}"),
209 }
210 }
211}