1use bitcoin::{
2 Transaction,
3 consensus::serde::{Hex, With},
4 secp256k1::PublicKey,
5};
6use serde::{Deserialize, Serialize};
7use spark_wallet::{LeafPedigree, Network, SigningKeyshare, TreeNode, TreeNodeId, TreeNodeStatus};
8use tracing::{debug, warn};
9
10use crate::{
11 error::SdkError,
12 models::{
13 ExportUnilateralExitStateResponse, ImportUnilateralExitStateRequest,
14 ImportUnilateralExitStateResponse,
15 },
16};
17
18use super::BreezSdk;
19
20const EXIT_STATE_VERSION: u32 = 2;
22const STRUCT_TX_EXIT_STATE_VERSION: u32 = 1;
25
26#[derive(Debug, Serialize, Deserialize)]
29struct ExitStateEnvelope<P> {
30 version: u32,
31 network: Network,
32 identity_public_key: String,
33 pedigrees: Vec<P>,
34}
35
36#[derive(Deserialize)]
38struct ExitStateVersion {
39 version: u32,
40}
41
42#[derive(Serialize, Deserialize)]
45#[serde(remote = "TreeNode")]
46struct HexTreeNode {
47 id: TreeNodeId,
48 tree_id: String,
49 value: u64,
50 parent_node_id: Option<TreeNodeId>,
51 #[serde(with = "With::<Hex>")]
52 node_tx: Transaction,
53 #[serde(with = "optional_tx_hex")]
54 refund_tx: Option<Transaction>,
55 #[serde(with = "optional_tx_hex")]
56 direct_tx: Option<Transaction>,
57 #[serde(with = "optional_tx_hex")]
58 direct_refund_tx: Option<Transaction>,
59 #[serde(with = "optional_tx_hex")]
60 direct_from_cpfp_refund_tx: Option<Transaction>,
61 vout: u32,
62 verifying_public_key: PublicKey,
63 owner_identity_public_key: Option<PublicKey>,
64 signing_keyshare: SigningKeyshare,
65 status: TreeNodeStatus,
66}
67
68#[derive(Serialize, Deserialize)]
71struct HexNode(#[serde(with = "HexTreeNode")] TreeNode);
72
73#[derive(Serialize, Deserialize)]
75struct HexLeafPedigree {
76 leaf: HexNode,
77 ancestors: Vec<HexNode>,
78}
79
80impl From<LeafPedigree> for HexLeafPedigree {
81 fn from(LeafPedigree { leaf, ancestors }: LeafPedigree) -> Self {
82 Self {
83 leaf: HexNode(leaf),
84 ancestors: ancestors.into_iter().map(HexNode).collect(),
85 }
86 }
87}
88
89impl From<HexLeafPedigree> for LeafPedigree {
90 fn from(HexLeafPedigree { leaf, ancestors }: HexLeafPedigree) -> Self {
91 Self {
92 leaf: leaf.0,
93 ancestors: ancestors.into_iter().map(|node| node.0).collect(),
94 }
95 }
96}
97
98mod optional_tx_hex {
99 use bitcoin::{Transaction, consensus::encode};
100 use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error};
101
102 #[allow(clippy::ref_option)]
103 pub(super) fn serialize<S: Serializer>(
104 tx: &Option<Transaction>,
105 serializer: S,
106 ) -> Result<S::Ok, S::Error> {
107 tx.as_ref().map(encode::serialize_hex).serialize(serializer)
108 }
109
110 pub(super) fn deserialize<'de, D: Deserializer<'de>>(
111 deserializer: D,
112 ) -> Result<Option<Transaction>, D::Error> {
113 Option::<String>::deserialize(deserializer)?
114 .map(|hex| encode::deserialize_hex(&hex).map_err(D::Error::custom))
115 .transpose()
116 }
117}
118
119fn encode_exit_state(
120 network: Network,
121 identity_public_key: String,
122 pedigrees: Vec<LeafPedigree>,
123) -> Result<String, SdkError> {
124 let envelope = ExitStateEnvelope {
125 version: EXIT_STATE_VERSION,
126 network,
127 identity_public_key,
128 pedigrees: pedigrees.into_iter().map(HexLeafPedigree::from).collect(),
129 };
130 serde_json::to_string(&envelope)
131 .map_err(|e| SdkError::Generic(format!("Failed to serialize exit state: {e}")))
132}
133
134fn decode_exit_state(
137 exit_state: &str,
138 wallet_network: Network,
139) -> Result<ExitStateEnvelope<LeafPedigree>, SdkError> {
140 let ExitStateVersion { version } = parse_exit_state(exit_state)?;
141 let envelope = match version {
142 EXIT_STATE_VERSION => {
143 let envelope: ExitStateEnvelope<HexLeafPedigree> = parse_exit_state(exit_state)?;
144 ExitStateEnvelope {
145 version: envelope.version,
146 network: envelope.network,
147 identity_public_key: envelope.identity_public_key,
148 pedigrees: envelope
149 .pedigrees
150 .into_iter()
151 .map(LeafPedigree::from)
152 .collect(),
153 }
154 }
155 STRUCT_TX_EXIT_STATE_VERSION => parse_exit_state(exit_state)?,
156 _ => {
157 return Err(SdkError::InvalidInput(format!(
158 "Unsupported exit state version {version}"
159 )));
160 }
161 };
162 if envelope.network != wallet_network {
163 return Err(SdkError::InvalidInput(format!(
164 "Exit state is for network {}, this wallet is on {wallet_network}",
165 envelope.network
166 )));
167 }
168 Ok(envelope)
169}
170
171fn parse_exit_state<'a, T: Deserialize<'a>>(exit_state: &'a str) -> Result<T, SdkError> {
172 serde_json::from_str(exit_state)
173 .map_err(|e| SdkError::InvalidInput(format!("Invalid exit state: {e}")))
174}
175
176#[macros::instance_span]
177#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
178#[allow(clippy::needless_pass_by_value)]
179impl BreezSdk {
180 pub async fn export_unilateral_exit_state(
187 &self,
188 ) -> Result<ExportUnilateralExitStateResponse, SdkError> {
189 let export = self.spark_wallet.export_exit_state().await?;
190 let leaves = export.pedigrees.len();
191 let exit_state = encode_exit_state(
192 self.config.network.into(),
193 self.spark_wallet.get_identity_public_key().to_string(),
194 export.pedigrees,
195 )?;
196 debug!(
197 leaves,
198 "export_unilateral_exit_state: exit state serialized"
199 );
200
201 Ok(ExportUnilateralExitStateResponse { exit_state })
202 }
203
204 pub async fn import_unilateral_exit_state(
220 &self,
221 request: ImportUnilateralExitStateRequest,
222 ) -> Result<ImportUnilateralExitStateResponse, SdkError> {
223 let envelope = decode_exit_state(&request.exit_state, self.config.network.into())?;
224 if envelope.identity_public_key != self.spark_wallet.get_identity_public_key().to_string() {
225 warn!("Importing an exit state exported by another wallet");
228 debug!("Exporting wallet: {}", envelope.identity_public_key);
229 }
230
231 let imported = self
232 .spark_wallet
233 .import_exit_state(envelope.pedigrees)
234 .await?;
235 let imported_leaves = u32::try_from(imported.imported_leaves)?;
236 let skipped_foreign_leaves = u32::try_from(imported.skipped_foreign_leaves)?;
237 let skipped_conflicting_leaves = u32::try_from(imported.skipped_conflicting_leaves)?;
238 let skipped_chains = u32::try_from(imported.skipped_chains)?;
239 debug!(
240 imported_leaves,
241 skipped_foreign_leaves,
242 skipped_conflicting_leaves,
243 skipped_chains,
244 "import_unilateral_exit_state: exit state merged"
245 );
246
247 Ok(ImportUnilateralExitStateResponse {
248 imported_leaves,
249 skipped_foreign_leaves,
250 skipped_conflicting_leaves,
251 skipped_chains,
252 })
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use bitcoin::{
259 Amount, OutPoint, ScriptBuf, Sequence, TxIn, TxOut, Txid, Witness, absolute::LockTime,
260 consensus::encode::serialize_hex, hashes::Hash, transaction::Version,
261 };
262 use serde_json::Value;
263 use spark_wallet::{TreeNodeStatus, tree_store_tests::create_test_node_with_parent};
264
265 use super::*;
266
267 #[cfg(feature = "browser-tests")]
268 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
269
270 fn signed_tx(sequence: u32) -> Transaction {
273 Transaction {
274 version: Version::non_standard(3),
275 lock_time: LockTime::ZERO,
276 input: vec![TxIn {
277 previous_output: OutPoint::new(Txid::from_byte_array([1; 32]), 0),
278 script_sig: ScriptBuf::new(),
279 sequence: Sequence(sequence),
280 witness: Witness::from_slice(&[[2u8; 64]]),
281 }],
282 output: vec![TxOut {
283 value: Amount::from_sat(1_000),
284 script_pubkey: ScriptBuf::new(),
285 }],
286 }
287 }
288
289 fn regtest_pedigree() -> LeafPedigree {
290 let root = create_test_node_with_parent("root", None, TreeNodeStatus::Splitted);
291 let mut leaf =
292 create_test_node_with_parent("leaf", Some("root"), TreeNodeStatus::Available);
293 leaf.node_tx = signed_tx(0);
294 leaf.refund_tx = Some(signed_tx(1_900));
295 LeafPedigree {
296 leaf,
297 ancestors: vec![root],
298 }
299 }
300
301 fn identity_of(pedigree: &LeafPedigree) -> String {
302 pedigree.leaf.owner_identity_public_key.unwrap().to_string()
303 }
304
305 fn encode_regtest(pedigree: &LeafPedigree) -> String {
306 encode_exit_state(
307 Network::Regtest,
308 identity_of(pedigree),
309 vec![pedigree.clone()],
310 )
311 .unwrap()
312 }
313
314 fn assert_same_pedigree(decoded: &LeafPedigree, original: &LeafPedigree) {
315 assert_eq!(decoded.leaf, original.leaf);
316 assert_eq!(decoded.ancestors, original.ancestors);
317 }
318
319 #[test]
320 fn envelope_round_trips_through_json() {
321 let pedigree = regtest_pedigree();
322 let decoded = decode_exit_state(&encode_regtest(&pedigree), Network::Regtest).unwrap();
323
324 assert_eq!(decoded.version, EXIT_STATE_VERSION);
325 assert_eq!(decoded.network, Network::Regtest);
326 assert_eq!(decoded.identity_public_key, identity_of(&pedigree));
327 assert_eq!(decoded.pedigrees.len(), 1);
328 assert_same_pedigree(&decoded.pedigrees[0], &pedigree);
329 }
330
331 #[test]
332 fn envelope_writes_transactions_as_consensus_hex() {
333 let pedigree = regtest_pedigree();
334 let json: Value = serde_json::from_str(&encode_regtest(&pedigree)).unwrap();
335 let leaf = &json["pedigrees"][0]["leaf"];
336 let root = &json["pedigrees"][0]["ancestors"][0];
337
338 assert_eq!(leaf["node_tx"], serialize_hex(&pedigree.leaf.node_tx));
339 assert_eq!(
340 leaf["refund_tx"],
341 serialize_hex(pedigree.leaf.refund_tx.as_ref().unwrap())
342 );
343 assert_eq!(leaf["direct_tx"], Value::Null);
344 assert_eq!(
345 root["node_tx"],
346 serialize_hex(&pedigree.ancestors[0].node_tx)
347 );
348 }
349
350 #[test]
351 fn envelope_with_struct_form_transactions_is_still_read() {
352 let pedigree = regtest_pedigree();
353 let exit_state = serde_json::to_string(&ExitStateEnvelope {
354 version: STRUCT_TX_EXIT_STATE_VERSION,
355 network: Network::Regtest,
356 identity_public_key: identity_of(&pedigree),
357 pedigrees: vec![pedigree.clone()],
358 })
359 .unwrap();
360 let json: Value = serde_json::from_str(&exit_state).unwrap();
361 assert!(json["pedigrees"][0]["leaf"]["node_tx"].is_object());
362
363 let decoded = decode_exit_state(&exit_state, Network::Regtest).unwrap();
364
365 assert_eq!(decoded.pedigrees.len(), 1);
366 assert_same_pedigree(&decoded.pedigrees[0], &pedigree);
367 }
368
369 #[test]
370 fn envelope_with_unknown_version_is_rejected() {
371 let mut json: Value = serde_json::from_str(&encode_regtest(®test_pedigree())).unwrap();
372 json["version"] = (EXIT_STATE_VERSION + 1).into();
373
374 match decode_exit_state(&json.to_string(), Network::Regtest) {
375 Err(SdkError::InvalidInput(message)) => assert!(
376 message.contains("version"),
377 "expected a version complaint, got {message}"
378 ),
379 other => panic!("expected InvalidInput, got {other:?}"),
380 }
381 }
382
383 #[test]
384 fn envelope_from_another_network_is_rejected() {
385 match decode_exit_state(&encode_regtest(®test_pedigree()), Network::Mainnet) {
386 Err(SdkError::InvalidInput(message)) => assert!(
387 message.contains("network"),
388 "expected a network complaint, got {message}"
389 ),
390 other => panic!("expected InvalidInput, got {other:?}"),
391 }
392 }
393}