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!(
105 "Importing an exit state exported by another wallet: {}",
106 envelope.identity_public_key
107 );
108 }
109
110 let imported = self
111 .spark_wallet
112 .import_exit_state(envelope.pedigrees)
113 .await?;
114 let imported_leaves = u32::try_from(imported.imported_leaves)?;
115 let skipped_foreign_leaves = u32::try_from(imported.skipped_foreign_leaves)?;
116 let skipped_conflicting_leaves = u32::try_from(imported.skipped_conflicting_leaves)?;
117 let skipped_chains = u32::try_from(imported.skipped_chains)?;
118 debug!(
119 imported_leaves,
120 skipped_foreign_leaves,
121 skipped_conflicting_leaves,
122 skipped_chains,
123 "import_unilateral_exit_state: exit state merged"
124 );
125
126 Ok(ImportUnilateralExitStateResponse {
127 imported_leaves,
128 skipped_foreign_leaves,
129 skipped_conflicting_leaves,
130 skipped_chains,
131 })
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use spark_wallet::{TreeNodeStatus, tree_store_tests::create_test_node_with_parent};
138
139 use super::*;
140
141 #[cfg(feature = "browser-tests")]
142 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
143
144 fn regtest_envelope() -> ExitStateEnvelope {
145 let root = create_test_node_with_parent("root", None, TreeNodeStatus::Splitted);
146 let leaf = create_test_node_with_parent("leaf", Some("root"), TreeNodeStatus::Available);
147 let identity_public_key = leaf.owner_identity_public_key.unwrap().to_string();
148 let pedigrees = vec![LeafPedigree {
149 leaf,
150 ancestors: vec![root],
151 }];
152 ExitStateEnvelope {
153 version: EXIT_STATE_VERSION,
154 network: Network::Regtest,
155 identity_public_key,
156 pedigrees,
157 }
158 }
159
160 #[test]
161 fn envelope_round_trips_through_json() {
162 let envelope = regtest_envelope();
163 let json = serde_json::to_string(&envelope).unwrap();
164 let decoded: ExitStateEnvelope = serde_json::from_str(&json).unwrap();
165
166 assert_eq!(decoded.version, envelope.version);
167 assert_eq!(decoded.network, envelope.network);
168 assert_eq!(decoded.identity_public_key, envelope.identity_public_key);
169 assert_eq!(decoded.pedigrees.len(), 1);
170
171 let pedigree = &decoded.pedigrees[0];
172 let original = &envelope.pedigrees[0];
173 assert_eq!(pedigree.leaf.id, original.leaf.id);
174 assert_eq!(pedigree.leaf.parent_node_id, original.leaf.parent_node_id);
175 assert_eq!(pedigree.leaf.node_tx, original.leaf.node_tx);
176 assert_eq!(pedigree.leaf.value, original.leaf.value);
177 let ancestor_ids: Vec<String> = pedigree
178 .ancestors
179 .iter()
180 .map(|a| a.id.to_string())
181 .collect();
182 assert_eq!(ancestor_ids, vec!["root".to_string()]);
183
184 check_envelope_readable(&decoded, Network::Regtest).unwrap();
185 }
186
187 #[test]
188 fn envelope_with_unknown_version_is_rejected() {
189 let mut envelope = regtest_envelope();
190 envelope.version = EXIT_STATE_VERSION + 1;
191
192 match check_envelope_readable(&envelope, Network::Regtest) {
193 Err(SdkError::InvalidInput(message)) => assert!(
194 message.contains("version"),
195 "expected a version complaint, got {message}"
196 ),
197 other => panic!("expected InvalidInput, got {other:?}"),
198 }
199 }
200
201 #[test]
202 fn envelope_from_another_network_is_rejected() {
203 let envelope = regtest_envelope();
204
205 match check_envelope_readable(&envelope, Network::Mainnet) {
206 Err(SdkError::InvalidInput(message)) => assert!(
207 message.contains("network"),
208 "expected a network complaint, got {message}"
209 ),
210 other => panic!("expected InvalidInput, got {other:?}"),
211 }
212 }
213}