Skip to main content

breez_sdk_spark/sdk/
unilateral_exit_backup.rs

1use 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/// The exported payload. It embeds `LeafPedigree`'s own serde representation,
18/// so `version` is what keeps an export readable once those internals change.
19#[derive(Debug, Serialize, Deserialize)]
20struct ExitStateEnvelope {
21    version: u32,
22    network: Network,
23    identity_public_key: String,
24    pedigrees: Vec<LeafPedigree>,
25}
26
27/// Rejects a payload this build cannot read at all: a version whose layout is
28/// unknown, or another network's state.
29fn 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    /// Serializes everything needed to unilaterally exit this wallet's funds
52    /// while the Spark operators are unreachable, so it can be kept somewhere
53    /// the wallet's own storage cannot take with it.
54    ///
55    /// The state goes stale as the wallet is used: export again whenever a
56    /// `UnilateralExitStateChanged` event arrives.
57    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    /// Merges a previously exported exit state back into the wallet, without
80    /// contacting the Spark operators. A leaf the exit state does not record
81    /// this wallet as the owner of is skipped.
82    ///
83    /// A leaf the wallet can already exit keeps the data it has: an exit state
84    /// carries no mark of when it was taken, so the imported copy is used only
85    /// where the wallet has nothing that works. Importing an out of date state
86    /// therefore never costs the wallet the ability to exit a leaf.
87    ///
88    /// The exit state must come from the same network the SDK is configured
89    /// for.
90    ///
91    /// An out of date exit state can restore funds that have since been spent,
92    /// so the balance may read high until the next sync reconciles it with the
93    /// Spark operators.
94    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            // Not a rejection: the wallet filters leaf by leaf and reports what
103            // it dropped.
104            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}