[export] Implement the wallet import/export format from FullyNoded

This commit closes #31
This commit is contained in:
Alekos Filini 2020-08-07 10:19:06 +02:00
parent 82c7e11bd5
commit 5f80950971
No known key found for this signature in database
GPG Key ID: 5E8AFC3034FDFA4F
2 changed files with 237 additions and 0 deletions

236
src/wallet/export.rs Normal file
View File

@ -0,0 +1,236 @@
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use miniscript::{Descriptor, ScriptContext, Terminal};
use crate::blockchain::Blockchain;
use crate::database::BatchDatabase;
use crate::wallet::Wallet;
#[derive(Debug, Serialize, Deserialize)]
pub struct WalletExport {
descriptor: String,
pub blockheight: u32,
pub label: String,
}
impl WalletExport {
pub fn export_wallet<B: Blockchain, D: BatchDatabase>(
wallet: &Wallet<B, D>,
label: &str,
include_blockheight: bool,
) -> Result<Self, &'static str> {
let descriptor = wallet.descriptor.as_ref().to_string();
Self::is_compatible_with_core(&descriptor)?;
let blockheight = match wallet.database.borrow().iter_txs(false) {
_ if !include_blockheight => 0,
Err(_) => 0,
Ok(txs) => {
let mut heights = txs
.into_iter()
.map(|tx| tx.height.unwrap_or(0))
.collect::<Vec<_>>();
heights.sort();
*heights.last().unwrap_or(&0)
}
};
let export = WalletExport {
descriptor,
label: label.into(),
blockheight,
};
if export.change_descriptor()
!= wallet
.change_descriptor
.as_ref()
.map(|d| d.as_ref().to_string())
{
return Err("Incompatible change descriptor");
}
Ok(export)
}
fn is_compatible_with_core(descriptor: &str) -> Result<(), &'static str> {
fn check_ms<Ctx: ScriptContext>(
terminal: Terminal<String, Ctx>,
) -> Result<(), &'static str> {
if let Terminal::Multi(_, _) = terminal {
Ok(())
} else {
Err("The descriptor contains operators not supported by Bitcoin Core")
}
}
match Descriptor::<String>::from_str(descriptor).map_err(|_| "Invalid descriptor")? {
Descriptor::Pk(_)
| Descriptor::Pkh(_)
| Descriptor::Wpkh(_)
| Descriptor::ShWpkh(_) => Ok(()),
Descriptor::Sh(ms) => check_ms(ms.node),
Descriptor::Wsh(ms) | Descriptor::ShWsh(ms) => check_ms(ms.node),
_ => Err("The descriptor is not compatible with Bitcoin Core"),
}
}
pub fn descriptor(&self) -> String {
self.descriptor.clone()
}
pub fn change_descriptor(&self) -> Option<String> {
let replaced = self.descriptor.replace("/0/*", "/1/*");
if replaced != self.descriptor {
Some(replaced)
} else {
None
}
}
}
#[cfg(test)]
mod test {
use std::str::FromStr;
use bitcoin::{Network, Txid};
use super::*;
use crate::database::{memory::MemoryDatabase, BatchOperations};
use crate::types::TransactionDetails;
use crate::wallet::{OfflineWallet, Wallet};
fn get_test_db() -> MemoryDatabase {
let mut db = MemoryDatabase::new();
db.set_tx(&TransactionDetails {
transaction: None,
txid: Txid::from_str(
"4ddff1fa33af17f377f62b72357b43107c19110a8009b36fb832af505efed98a",
)
.unwrap(),
timestamp: 12345678,
received: 100_000,
sent: 0,
height: Some(5000),
})
.unwrap();
db
}
#[test]
fn test_export_bip44() {
let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/*)";
let wallet: OfflineWallet<_> = Wallet::new_offline(
descriptor,
Some(change_descriptor),
Network::Testnet,
get_test_db(),
)
.unwrap();
let export = WalletExport::export_wallet(&wallet, "Test Label", true).unwrap();
assert_eq!(export.descriptor(), descriptor);
assert_eq!(export.change_descriptor(), Some(change_descriptor.into()));
assert_eq!(export.blockheight, 5000);
assert_eq!(export.label, "Test Label");
}
#[test]
#[should_panic(expected = "Incompatible change descriptor")]
fn test_export_no_change() {
// This wallet explicitly doesn't have a change descriptor. It should be impossible to
// export, because exporting this kind of external descriptor normally implies the
// existence of an internal descriptor
let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
let wallet: OfflineWallet<_> =
Wallet::new_offline(descriptor, None, Network::Testnet, get_test_db()).unwrap();
WalletExport::export_wallet(&wallet, "Test Label", true).unwrap();
}
#[test]
#[should_panic(expected = "Incompatible change descriptor")]
fn test_export_incompatible_change() {
// This wallet has a change descriptor, but the derivation path is not in the "standard"
// bip44/49/etc format
let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/50'/0'/1/*)";
let wallet: OfflineWallet<_> = Wallet::new_offline(
descriptor,
Some(change_descriptor),
Network::Testnet,
get_test_db(),
)
.unwrap();
WalletExport::export_wallet(&wallet, "Test Label", true).unwrap();
}
#[test]
fn test_export_multi() {
let descriptor = "wsh(multi(2,\
[73756c7f/48h/0h/0h/2h]tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3/0/*,\
[f9f62194/48h/0h/0h/2h]tpubDDp3ZSH1yCwusRppH7zgSxq2t1VEUyXSeEp8E5aFS8m43MknUjiF1bSLo3CGWAxbDyhF1XowA5ukPzyJZjznYk3kYi6oe7QxtX2euvKWsk4/0/*,\
[c98b1535/48h/0h/0h/2h]tpubDCDi5W4sP6zSnzJeowy8rQDVhBdRARaPhK1axABi8V1661wEPeanpEXj4ZLAUEoikVtoWcyK26TKKJSecSfeKxwHCcRrge9k1ybuiL71z4a/0/*\
))";
let change_descriptor = "wsh(multi(2,\
[73756c7f/48h/0h/0h/2h]tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3/1/*,\
[f9f62194/48h/0h/0h/2h]tpubDDp3ZSH1yCwusRppH7zgSxq2t1VEUyXSeEp8E5aFS8m43MknUjiF1bSLo3CGWAxbDyhF1XowA5ukPzyJZjznYk3kYi6oe7QxtX2euvKWsk4/1/*,\
[c98b1535/48h/0h/0h/2h]tpubDCDi5W4sP6zSnzJeowy8rQDVhBdRARaPhK1axABi8V1661wEPeanpEXj4ZLAUEoikVtoWcyK26TKKJSecSfeKxwHCcRrge9k1ybuiL71z4a/1/*\
))";
let wallet: OfflineWallet<_> = Wallet::new_offline(
descriptor,
Some(change_descriptor),
Network::Testnet,
get_test_db(),
)
.unwrap();
let export = WalletExport::export_wallet(&wallet, "Test Label", true).unwrap();
assert_eq!(export.descriptor(), descriptor);
assert_eq!(export.change_descriptor(), Some(change_descriptor.into()));
assert_eq!(export.blockheight, 5000);
assert_eq!(export.label, "Test Label");
}
#[test]
fn test_export_to_json() {
let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/*)";
let wallet: OfflineWallet<_> = Wallet::new_offline(
descriptor,
Some(change_descriptor),
Network::Testnet,
get_test_db(),
)
.unwrap();
let export = WalletExport::export_wallet(&wallet, "Test Label", true).unwrap();
assert_eq!(serde_json::to_string(&export).unwrap(), "{\"descriptor\":\"wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44\'/0\'/0\'/0/*)\",\"blockheight\":5000,\"label\":\"Test Label\"}");
}
#[test]
fn test_export_from_json() {
let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/*)";
let import_str = "{\"descriptor\":\"wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44\'/0\'/0\'/0/*)\",\"blockheight\":5000,\"label\":\"Test Label\"}";
let export: WalletExport = serde_json::from_str(import_str).unwrap();
assert_eq!(export.descriptor(), descriptor);
assert_eq!(export.change_descriptor(), Some(change_descriptor.into()));
assert_eq!(export.blockheight, 5000);
assert_eq!(export.label, "Test Label");
}
}

View File

@ -17,6 +17,7 @@ use miniscript::BitcoinSig;
use log::{debug, error, info, trace}; use log::{debug, error, info, trace};
pub mod coin_selection; pub mod coin_selection;
pub mod export;
pub mod time; pub mod time;
pub mod tx_builder; pub mod tx_builder;
pub mod utils; pub mod utils;