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!("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}