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#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
177#[allow(clippy::needless_pass_by_value)]
178impl BreezSdk {
179 pub async fn export_unilateral_exit_state(
186 &self,
187 ) -> Result<ExportUnilateralExitStateResponse, SdkError> {
188 let export = self.spark_wallet.export_exit_state().await?;
189 let leaves = export.pedigrees.len();
190 let exit_state = encode_exit_state(
191 self.config.network.into(),
192 self.spark_wallet.get_identity_public_key().to_string(),
193 export.pedigrees,
194 )?;
195 debug!(
196 leaves,
197 "export_unilateral_exit_state: exit state serialized"
198 );
199
200 Ok(ExportUnilateralExitStateResponse { exit_state })
201 }
202
203 pub async fn import_unilateral_exit_state(
219 &self,
220 request: ImportUnilateralExitStateRequest,
221 ) -> Result<ImportUnilateralExitStateResponse, SdkError> {
222 let envelope = decode_exit_state(&request.exit_state, self.config.network.into())?;
223 if envelope.identity_public_key != self.spark_wallet.get_identity_public_key().to_string() {
224 warn!("Importing an exit state exported by another wallet");
227 debug!("Exporting wallet: {}", envelope.identity_public_key);
228 }
229
230 let imported = self
231 .spark_wallet
232 .import_exit_state(envelope.pedigrees)
233 .await?;
234 let imported_leaves = u32::try_from(imported.imported_leaves)?;
235 let skipped_foreign_leaves = u32::try_from(imported.skipped_foreign_leaves)?;
236 let skipped_conflicting_leaves = u32::try_from(imported.skipped_conflicting_leaves)?;
237 let skipped_chains = u32::try_from(imported.skipped_chains)?;
238 debug!(
239 imported_leaves,
240 skipped_foreign_leaves,
241 skipped_conflicting_leaves,
242 skipped_chains,
243 "import_unilateral_exit_state: exit state merged"
244 );
245
246 Ok(ImportUnilateralExitStateResponse {
247 imported_leaves,
248 skipped_foreign_leaves,
249 skipped_conflicting_leaves,
250 skipped_chains,
251 })
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use bitcoin::{
258 Amount, OutPoint, ScriptBuf, Sequence, TxIn, TxOut, Txid, Witness, absolute::LockTime,
259 consensus::encode::serialize_hex, hashes::Hash, transaction::Version,
260 };
261 use serde_json::Value;
262 use spark_wallet::{TreeNodeStatus, tree_store_tests::create_test_node_with_parent};
263
264 use super::*;
265
266 #[cfg(feature = "browser-tests")]
267 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
268
269 fn signed_tx(sequence: u32) -> Transaction {
272 Transaction {
273 version: Version::non_standard(3),
274 lock_time: LockTime::ZERO,
275 input: vec![TxIn {
276 previous_output: OutPoint::new(Txid::from_byte_array([1; 32]), 0),
277 script_sig: ScriptBuf::new(),
278 sequence: Sequence(sequence),
279 witness: Witness::from_slice(&[[2u8; 64]]),
280 }],
281 output: vec![TxOut {
282 value: Amount::from_sat(1_000),
283 script_pubkey: ScriptBuf::new(),
284 }],
285 }
286 }
287
288 fn regtest_pedigree() -> LeafPedigree {
289 let root = create_test_node_with_parent("root", None, TreeNodeStatus::Splitted);
290 let mut leaf =
291 create_test_node_with_parent("leaf", Some("root"), TreeNodeStatus::Available);
292 leaf.node_tx = signed_tx(0);
293 leaf.refund_tx = Some(signed_tx(1_900));
294 LeafPedigree {
295 leaf,
296 ancestors: vec![root],
297 }
298 }
299
300 fn identity_of(pedigree: &LeafPedigree) -> String {
301 pedigree.leaf.owner_identity_public_key.unwrap().to_string()
302 }
303
304 fn encode_regtest(pedigree: &LeafPedigree) -> String {
305 encode_exit_state(
306 Network::Regtest,
307 identity_of(pedigree),
308 vec![pedigree.clone()],
309 )
310 .unwrap()
311 }
312
313 fn assert_same_pedigree(decoded: &LeafPedigree, original: &LeafPedigree) {
314 assert_eq!(decoded.leaf, original.leaf);
315 assert_eq!(decoded.ancestors, original.ancestors);
316 }
317
318 #[test]
319 fn envelope_round_trips_through_json() {
320 let pedigree = regtest_pedigree();
321 let decoded = decode_exit_state(&encode_regtest(&pedigree), Network::Regtest).unwrap();
322
323 assert_eq!(decoded.version, EXIT_STATE_VERSION);
324 assert_eq!(decoded.network, Network::Regtest);
325 assert_eq!(decoded.identity_public_key, identity_of(&pedigree));
326 assert_eq!(decoded.pedigrees.len(), 1);
327 assert_same_pedigree(&decoded.pedigrees[0], &pedigree);
328 }
329
330 #[test]
331 fn envelope_writes_transactions_as_consensus_hex() {
332 let pedigree = regtest_pedigree();
333 let json: Value = serde_json::from_str(&encode_regtest(&pedigree)).unwrap();
334 let leaf = &json["pedigrees"][0]["leaf"];
335 let root = &json["pedigrees"][0]["ancestors"][0];
336
337 assert_eq!(leaf["node_tx"], serialize_hex(&pedigree.leaf.node_tx));
338 assert_eq!(
339 leaf["refund_tx"],
340 serialize_hex(pedigree.leaf.refund_tx.as_ref().unwrap())
341 );
342 assert_eq!(leaf["direct_tx"], Value::Null);
343 assert_eq!(
344 root["node_tx"],
345 serialize_hex(&pedigree.ancestors[0].node_tx)
346 );
347 }
348
349 #[test]
350 fn envelope_with_struct_form_transactions_is_still_read() {
351 let pedigree = regtest_pedigree();
352 let exit_state = serde_json::to_string(&ExitStateEnvelope {
353 version: STRUCT_TX_EXIT_STATE_VERSION,
354 network: Network::Regtest,
355 identity_public_key: identity_of(&pedigree),
356 pedigrees: vec![pedigree.clone()],
357 })
358 .unwrap();
359 let json: Value = serde_json::from_str(&exit_state).unwrap();
360 assert!(json["pedigrees"][0]["leaf"]["node_tx"].is_object());
361
362 let decoded = decode_exit_state(&exit_state, Network::Regtest).unwrap();
363
364 assert_eq!(decoded.pedigrees.len(), 1);
365 assert_same_pedigree(&decoded.pedigrees[0], &pedigree);
366 }
367
368 #[test]
369 fn envelope_with_unknown_version_is_rejected() {
370 let mut json: Value = serde_json::from_str(&encode_regtest(®test_pedigree())).unwrap();
371 json["version"] = (EXIT_STATE_VERSION + 1).into();
372
373 match decode_exit_state(&json.to_string(), Network::Regtest) {
374 Err(SdkError::InvalidInput(message)) => assert!(
375 message.contains("version"),
376 "expected a version complaint, got {message}"
377 ),
378 other => panic!("expected InvalidInput, got {other:?}"),
379 }
380 }
381
382 #[test]
383 fn envelope_from_another_network_is_rejected() {
384 match decode_exit_state(&encode_regtest(®test_pedigree()), Network::Mainnet) {
385 Err(SdkError::InvalidInput(message)) => assert!(
386 message.contains("network"),
387 "expected a network complaint, got {message}"
388 ),
389 other => panic!("expected InvalidInput, got {other:?}"),
390 }
391 }
392}