pub mod address;
mod command;
use crate::contract::ContractKeyProvider;
use crate::error::{wallet_err_to_manager_err, WalletError};
use crate::logger::Logger;
use crate::logger::{log_error, log_info, WriteLog};
use crate::wallet::address::AddressGenerator;
use crate::{chain::EsploraClient, Storage};
use bdk_chain::Balance;
use bdk_wallet::coin_selection::{
BranchAndBoundCoinSelection, CoinSelectionAlgorithm, SingleRandomDraw,
};
use bdk_wallet::descriptor::IntoWalletDescriptor;
use bdk_wallet::AsyncWalletPersister;
pub use bdk_wallet::LocalOutput;
use bdk_wallet::{
bitcoin::{
bip32::Xpriv,
secp256k1::{All, PublicKey, Secp256k1},
Address, Network, Txid,
},
template::Bip84,
AddressInfo, KeychainKind, SignOptions, Wallet,
};
use bdk_wallet::{Utxo, WeightedUtxo};
use bitcoin::bip32::Fingerprint;
use bitcoin::key::rand::thread_rng;
use bitcoin::Psbt;
use bitcoin::{secp256k1::SecretKey, Amount, FeeRate, ScriptBuf, Transaction};
use ddk_manager::{error::Error as ManagerError, SimpleSigner};
use lightning::chain::chaininterface::{ConfirmationTarget, FeeEstimator};
use std::collections::HashMap;
use std::fmt::Debug;
use std::future::Future;
use std::pin::Pin;
use std::sync::atomic::AtomicU32;
use std::sync::{atomic::Ordering, Arc};
use tokio::sync::{
mpsc::{channel, Sender},
oneshot,
};
type FutureResult<'a, T, E> = Pin<Box<dyn Future<Output = std::result::Result<T, E>> + Send + 'a>>;
type Result<T> = std::result::Result<T, WalletError>;
const MIN_CHANGE_SIZE: u64 = 25_000;
#[derive(Clone, Debug)]
pub struct WalletStorage(Arc<dyn Storage>);
impl AsyncWalletPersister for WalletStorage {
type Error = WalletError;
fn initialize<'a>(
persister: &'a mut Self,
) -> FutureResult<'a, bdk_wallet::ChangeSet, Self::Error>
where
Self: 'a,
{
Box::pin(persister.0.initialize_bdk())
}
fn persist<'a>(
persister: &'a mut Self,
changeset: &'a bdk_wallet::ChangeSet,
) -> FutureResult<'a, (), Self::Error>
where
Self: 'a,
{
Box::pin(persister.0.persist_bdk(changeset))
}
}
#[derive(Debug)]
pub enum WalletCommand {
Sync(oneshot::Sender<Result<()>>),
Balance(oneshot::Sender<Balance>),
NewExternalAddress(oneshot::Sender<Result<AddressInfo>>),
NewChangeAddress(oneshot::Sender<Result<AddressInfo>>),
SendToAddress(Address, Amount, FeeRate, oneshot::Sender<Result<Txid>>),
SendAll(Address, FeeRate, oneshot::Sender<Result<Txid>>),
GetTransactions(oneshot::Sender<Result<Vec<Arc<Transaction>>>>),
ListUtxos(oneshot::Sender<Result<Vec<LocalOutput>>>),
NextDerivationIndex(oneshot::Sender<Result<u32>>),
SignPsbtInput(
bitcoin::psbt::Psbt,
usize,
oneshot::Sender<std::result::Result<Psbt, ManagerError>>,
),
}
pub struct DlcDevKitWallet {
sender: Sender<WalletCommand>,
network: Network,
xprv: Xpriv,
secp: Secp256k1<All>,
fingerprint: Fingerprint,
contract_keys: ContractKeyProvider,
address_generator: Option<Arc<dyn AddressGenerator + Send + Sync>>,
logger: Arc<Logger>,
}
const MIN_FEERATE: u32 = 253;
impl DlcDevKitWallet {
#[tracing::instrument(name = "wallet", skip_all)]
pub async fn new(
seed_bytes: &[u8; 64],
blockchain: Arc<EsploraClient>,
network: Network,
storage: Arc<dyn Storage>,
address_generator: Option<Arc<dyn AddressGenerator + Send + Sync>>,
logger: Arc<Logger>,
) -> Result<DlcDevKitWallet> {
let secp = Secp256k1::new();
let xprv = Xpriv::new_master(network, seed_bytes)?;
let fingerprint = xprv.fingerprint(&secp);
let external_descriptor =
Bip84(xprv, KeychainKind::External).into_wallet_descriptor(&secp, network.into())?;
let internal_descriptor =
Bip84(xprv, KeychainKind::Internal).into_wallet_descriptor(&secp, network.into())?;
let mut storage = WalletStorage(storage);
let load_wallet = Wallet::load()
.descriptor(KeychainKind::External, Some(external_descriptor.clone()))
.descriptor(KeychainKind::Internal, Some(internal_descriptor.clone()))
.extract_keys()
.check_network(network)
.load_wallet_async(&mut storage)
.await
.map_err(|e| WalletError::WalletPersistanceError(e.to_string()))?;
let mut wallet = match load_wallet {
Some(w) => w,
None => Wallet::create(external_descriptor, internal_descriptor)
.network(network)
.create_wallet_async(&mut storage)
.await
.map_err(|e| WalletError::WalletPersistanceError(e.to_string()))?,
};
let contract_keys = ContractKeyProvider::from_xprv(xprv);
let (sender, mut receiver) = channel(100);
let logger_clone = logger.clone();
tokio::spawn(async move {
while let Some(command) = receiver.recv().await {
match command {
WalletCommand::Sync(sender) => {
let sync = command::sync(
&mut wallet,
&blockchain,
&mut storage,
logger_clone.clone(),
)
.await;
let _ = sender.send(sync).map_err(|e| {
log_error!(logger_clone, "Error sending sync command. error={:?}", e);
});
}
WalletCommand::Balance(sender) => {
let balance = wallet.balance();
let _ = sender.send(balance).map_err(|e| {
log_error!(
logger_clone,
"Error sending balance command. error={:?}",
e
);
});
}
WalletCommand::NewExternalAddress(sender) => {
let address = wallet.next_unused_address(KeychainKind::External);
let _ = wallet.persist_async(&mut storage).await;
let _ = sender.send(Ok(address)).map_err(|e| {
log_error!(
logger_clone,
"Error sending new external address command. error={:?}",
e
);
});
}
WalletCommand::NewChangeAddress(sender) => {
let address = wallet.next_unused_address(KeychainKind::Internal);
let _ = wallet.persist_async(&mut storage).await;
let _ = sender.send(Ok(address)).map_err(|e| {
log_error!(
logger_clone,
"Error sending new change address command. error={:?}",
e
);
});
}
WalletCommand::SendToAddress(address, amount, fee_rate, sender) => {
let mut txn_builder = wallet.build_tx();
txn_builder
.add_recipient(address.script_pubkey(), amount)
.version(2)
.fee_rate(fee_rate);
let mut psbt = match txn_builder.finish() {
Ok(psbt) => psbt,
Err(e) => {
let _ = sender.send(Err(WalletError::TxnBuilder(e))).map_err(|e| {
log_error!(
logger_clone,
"Error sending send to address command. error={:?}",
e
);
});
continue;
}
};
if let Err(e) = wallet.sign(&mut psbt, SignOptions::default()) {
let _ = sender.send(Err(WalletError::Signing(e))).map_err(|e| {
log_error!(
logger_clone,
"Error sending send to address command. error={:?}",
e
);
});
continue;
}
let tx = match psbt.extract_tx() {
Ok(tx) => tx,
Err(_) => {
let _ = sender.send(Err(WalletError::ExtractTx)).map_err(|e| {
log_error!(
logger_clone,
"Error sending send to address command. error={:?}",
e
);
});
continue;
}
};
let txid = tx.compute_txid();
if let Err(e) = blockchain.async_client.broadcast(&tx).await {
let _ = sender
.send(Err(WalletError::Esplora(e.to_string())))
.map_err(|e| {
log_error!(
logger_clone,
"Error sending send to address command. error={:?}",
e
);
});
continue;
}
let _ = sender.send(Ok(txid)).map_err(|e| {
log_error!(
logger_clone,
"Error sending send to address command. error={:?}",
e
);
});
}
WalletCommand::SendAll(address, fee_rate, sender) => {
let mut tx_builder = wallet.build_tx();
tx_builder.fee_rate(fee_rate);
tx_builder.drain_wallet();
tx_builder.drain_to(address.script_pubkey());
let mut psbt = match tx_builder.finish() {
Ok(psbt) => psbt,
Err(e) => {
let _ = sender.send(Err(WalletError::TxnBuilder(e))).map_err(|e| {
log_error!(
logger_clone,
"Error sending send all command. error={:?}",
e
);
});
continue;
}
};
if let Err(e) = wallet.sign(&mut psbt, SignOptions::default()) {
let _ = sender.send(Err(WalletError::Signing(e))).map_err(|e| {
log_error!(
logger_clone,
"Error sending send all command. error={:?}",
e
);
});
continue;
}
let tx = match psbt.extract_tx() {
Ok(tx) => tx,
Err(_) => {
let _ = sender.send(Err(WalletError::ExtractTx)).map_err(|e| {
log_error!(
logger_clone,
"Error sending send all command. error={:?}",
e
);
});
continue;
}
};
let txid = tx.compute_txid();
if let Err(e) = blockchain.async_client.broadcast(&tx).await {
let _ = sender
.send(Err(WalletError::Esplora(e.to_string())))
.map_err(|e| {
log_error!(
logger_clone,
"Error sending send all command. error={:?}",
e
);
});
continue;
}
let _ = sender.send(Ok(txid)).map_err(|e| {
log_error!(
logger_clone,
"Error sending send all command. error={:?}",
e
);
});
}
WalletCommand::GetTransactions(sender) => {
let txs = wallet
.transactions()
.map(|t| t.tx_node.tx)
.collect::<Vec<Arc<Transaction>>>();
let _ = sender.send(Ok(txs)).map_err(|e| {
log_error!(
logger_clone,
"Error sending get transactions command. error={:?}",
e
);
});
}
WalletCommand::ListUtxos(sender) => {
let utxos = wallet.list_unspent().map(|utxo| utxo.to_owned()).collect();
let _ = sender.send(Ok(utxos)).map_err(|e| {
log_error!(
logger_clone,
"Error sending list utxos command. error={:?}",
e
);
});
}
WalletCommand::NextDerivationIndex(sender) => {
let index = wallet.next_derivation_index(KeychainKind::External);
let _ = sender.send(Ok(index)).map_err(|e| {
log_error!(
logger_clone,
"Error sending next derivation index command. error={:?}",
e
);
});
}
WalletCommand::SignPsbtInput(mut psbt, input_index, sender) => {
let sign_opts = SignOptions {
trust_witness_utxo: true,
..Default::default()
};
let mut signed_psbt = psbt.clone();
if let Err(e) = wallet.sign(&mut signed_psbt, sign_opts) {
log_error!(logger_clone, "Could not sign PSBT. error={:?}", e);
let _ = sender
.send(Err(ManagerError::WalletError(
WalletError::Signing(e).into(),
)))
.map_err(|e| {
log_error!(
logger_clone,
"Error sending sign psbt input command. error={:?}",
e
);
});
} else {
psbt.inputs[input_index] = signed_psbt.inputs[input_index].clone();
let _ = sender.send(Ok(psbt)).map_err(|e| {
log_error!(
logger_clone,
"Error sending sign psbt input command. error={:?}",
e
);
});
}
}
}
}
});
Ok(DlcDevKitWallet {
sender,
network,
xprv,
secp,
fingerprint,
contract_keys,
address_generator,
logger,
})
}
#[tracing::instrument(skip(self))]
pub async fn sync(&self) -> Result<()> {
let (tx, rx) = oneshot::channel();
self.sender.send(WalletCommand::Sync(tx)).await?;
rx.await.map_err(WalletError::Receiver)?
}
#[tracing::instrument(skip(self))]
pub fn get_pubkey(&self) -> PublicKey {
PublicKey::from_secret_key(&self.secp, &self.xprv.private_key)
}
#[tracing::instrument(skip(self))]
pub async fn get_balance(&self) -> Result<Balance> {
let (tx, rx) = oneshot::channel();
self.sender.send(WalletCommand::Balance(tx)).await?;
rx.await.map_err(WalletError::Receiver)
}
#[tracing::instrument(skip(self))]
pub async fn new_external_address(&self) -> Result<AddressInfo> {
let (tx, rx) = oneshot::channel();
self.sender
.send(WalletCommand::NewExternalAddress(tx))
.await?;
rx.await.map_err(WalletError::Receiver)?
}
#[tracing::instrument(skip(self))]
pub async fn new_change_address(&self) -> Result<AddressInfo> {
let (tx, rx) = oneshot::channel();
self.sender
.send(WalletCommand::NewChangeAddress(tx))
.await?;
rx.await.map_err(WalletError::Receiver)?
}
#[tracing::instrument(skip(self))]
pub async fn send_to_address(
&self,
address: Address,
amount: Amount,
fee_rate: FeeRate,
) -> Result<Txid> {
let (tx, rx) = oneshot::channel();
self.sender
.send(WalletCommand::SendToAddress(address, amount, fee_rate, tx))
.await?;
rx.await.map_err(WalletError::Receiver)?
}
#[tracing::instrument(skip(self))]
pub async fn send_all(&self, address: Address, fee_rate: FeeRate) -> Result<Txid> {
let (tx, rx) = oneshot::channel();
self.sender
.send(WalletCommand::SendAll(address, fee_rate, tx))
.await?;
rx.await.map_err(WalletError::Receiver)?
}
#[tracing::instrument(skip(self))]
pub async fn get_transactions(&self) -> Result<Vec<Arc<Transaction>>> {
let (tx, rx) = oneshot::channel();
self.sender.send(WalletCommand::GetTransactions(tx)).await?;
rx.await.map_err(WalletError::Receiver)?
}
#[tracing::instrument(skip(self))]
pub async fn list_utxos(&self) -> Result<Vec<LocalOutput>> {
let (tx, rx) = oneshot::channel();
self.sender.send(WalletCommand::ListUtxos(tx)).await?;
rx.await.map_err(WalletError::Receiver)?
}
#[tracing::instrument(skip(self))]
async fn sign_psbt_input(
&self,
psbt: &mut bitcoin::psbt::Psbt,
input_index: usize,
) -> std::result::Result<(), ManagerError> {
let (tx, rx) = oneshot::channel();
self.sender
.send(WalletCommand::SignPsbtInput(psbt.clone(), input_index, tx))
.await
.map_err(|e| ManagerError::WalletError(Box::new(WalletError::Sender(e))))?;
let signed_psbt_received = rx
.await
.map_err(|e| ManagerError::WalletError(Box::new(WalletError::Receiver(e))))?;
*psbt = signed_psbt_received?;
Ok(())
}
}
impl FeeEstimator for DlcDevKitWallet {
#[tracing::instrument(skip(self))]
fn get_est_sat_per_1000_weight(&self, confirmation_target: ConfirmationTarget) -> u32 {
let fees = fee_estimator();
fees.get(&confirmation_target)
.unwrap()
.load(Ordering::Acquire)
}
}
impl ddk_manager::ContractSignerProvider for DlcDevKitWallet {
type Signer = SimpleSigner;
#[tracing::instrument(skip(self))]
fn derive_signer_key_id(&self, is_offer_party: bool, temp_id: [u8; 32]) -> [u8; 32] {
self.contract_keys
.derive_signer_key_id(is_offer_party, temp_id)
}
#[tracing::instrument(skip(self, key_id))]
fn derive_contract_signer(
&self,
key_id: [u8; 32],
) -> std::result::Result<Self::Signer, ManagerError> {
self.contract_keys.derive_contract_signer(key_id)
}
fn get_secret_key_for_pubkey(
&self,
_pubkey: &PublicKey,
) -> std::result::Result<SecretKey, ManagerError> {
unreachable!("get_secret_key_for_pubkey is only used in channels.")
}
fn get_new_secret_key(&self) -> std::result::Result<SecretKey, ManagerError> {
unreachable!("get_new_secret_key is only used for channels")
}
}
#[async_trait::async_trait]
impl ddk_manager::Wallet for DlcDevKitWallet {
async fn get_new_address(&self) -> std::result::Result<bitcoin::Address, ManagerError> {
if let Some(address_generator) = &self.address_generator {
let address = address_generator
.custom_external_address()
.await
.map_err(wallet_err_to_manager_err)?;
return Ok(address);
}
let address = self
.new_external_address()
.await
.map_err(wallet_err_to_manager_err)?;
log_info!(
self.logger.clone(),
"Revealed new address for contract. address={}",
address.address.to_string()
);
Ok(address.address)
}
async fn get_new_change_address(&self) -> std::result::Result<bitcoin::Address, ManagerError> {
if let Some(address_generator) = &self.address_generator {
let address = address_generator
.custom_change_address()
.await
.map_err(wallet_err_to_manager_err)?;
return Ok(address);
}
let address = self
.new_change_address()
.await
.map_err(wallet_err_to_manager_err)?;
log_info!(
self.logger.clone(),
"Revealed new change address for contract. address={}",
address.address.to_string()
);
Ok(address.address)
}
async fn sign_psbt_input(
&self,
psbt: &mut bitcoin::psbt::Psbt,
input_index: usize,
) -> std::result::Result<(), ManagerError> {
self.sign_psbt_input(psbt, input_index).await
}
fn unreserve_utxos(
&self,
_outpoints: &[bitcoin::OutPoint],
) -> std::result::Result<(), ManagerError> {
Ok(())
}
fn import_address(&self, _address: &bitcoin::Address) -> std::result::Result<(), ManagerError> {
Ok(())
}
#[tracing::instrument(skip(self))]
async fn get_utxos_for_amount(
&self,
amount: Amount,
fee_rate: u64,
_lock_utxos: bool,
) -> std::result::Result<Vec<ddk_manager::Utxo>, ManagerError> {
let local_utxos = self.list_utxos().await.map_err(wallet_err_to_manager_err)?;
let utxos = local_utxos
.iter()
.map(|utxo| WeightedUtxo {
satisfaction_weight: utxo.txout.weight(),
utxo: Utxo::Local(utxo.clone()),
})
.collect::<Vec<WeightedUtxo>>();
let selected_utxos = BranchAndBoundCoinSelection::new(MIN_CHANGE_SIZE, SingleRandomDraw)
.coin_select(
vec![],
utxos,
FeeRate::from_sat_per_vb(fee_rate).ok_or_else(|| {
ManagerError::WalletError(Box::new(WalletError::Esplora(format!(
"Invalid fee rate: {fee_rate}"
))))
})?,
amount,
ScriptBuf::new().as_script(),
&mut thread_rng(),
)
.map_err(|e| ManagerError::WalletError(Box::new(e)))?;
let dlc_utxos = selected_utxos
.selected
.iter()
.map(|utxo| {
let address =
Address::from_script(&utxo.txout().script_pubkey, self.network).unwrap();
ddk_manager::Utxo {
tx_out: utxo.txout().clone(),
outpoint: utxo.outpoint(),
address,
redeem_script: ScriptBuf::new(),
reserved: false,
}
})
.collect();
Ok(dlc_utxos)
}
}
fn fee_estimator() -> HashMap<ConfirmationTarget, AtomicU32> {
let mut fees: HashMap<ConfirmationTarget, AtomicU32> = HashMap::new();
fees.insert(ConfirmationTarget::UrgentOnChainSweep, AtomicU32::new(5000));
fees.insert(
ConfirmationTarget::MinAllowedAnchorChannelRemoteFee,
AtomicU32::new(25 * 250),
);
fees.insert(
ConfirmationTarget::MinAllowedAnchorChannelRemoteFee,
AtomicU32::new(MIN_FEERATE),
);
fees.insert(
ConfirmationTarget::MinAllowedNonAnchorChannelRemoteFee,
AtomicU32::new(MIN_FEERATE),
);
fees.insert(
ConfirmationTarget::AnchorChannelFee,
AtomicU32::new(MIN_FEERATE),
);
fees.insert(
ConfirmationTarget::NonAnchorChannelFee,
AtomicU32::new(2000),
);
fees.insert(
ConfirmationTarget::ChannelCloseMinimum,
AtomicU32::new(MIN_FEERATE),
);
fees
}
impl Debug for DlcDevKitWallet {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "DlcDevKitWallet")?;
write!(f, " fingerprint: {:?}", self.fingerprint)?;
write!(f, " network: {:?}", self.network)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::{collections::HashSet, str::FromStr, sync::Arc};
use crate::chain::EsploraClient;
use crate::logger::{LogLevel, Logger};
use crate::storage::memory::MemoryStorage;
use bitcoin::{
address::NetworkChecked, key::rand::Fill, Address, AddressType, Amount, FeeRate, Network,
};
use ddk_manager::{ContractSigner, ContractSignerProvider};
use super::DlcDevKitWallet;
async fn create_wallet() -> DlcDevKitWallet {
let esplora = ddk_testenv::env().esplora_host().to_string();
let storage = Arc::new(MemoryStorage::new());
let logger = Arc::new(Logger::console(
"console_logger".to_string(),
LogLevel::Info,
));
let esplora =
Arc::new(EsploraClient::new(&esplora, Network::Regtest, logger.clone()).unwrap());
let mut entropy = [0u8; 64];
entropy
.try_fill(&mut bitcoin::key::rand::thread_rng())
.unwrap();
DlcDevKitWallet::new(
&entropy,
esplora,
Network::Regtest,
storage.clone(),
None,
logger.clone(),
)
.await
.unwrap()
}
fn generate_blocks(num: u64) {
ddk_testenv::env().generate_blocks(num);
}
fn fund_address(address: &Address<NetworkChecked>) {
ddk_testenv::env().fund_address(address, Amount::from_btc(1.0).unwrap());
generate_blocks(4)
}
#[tokio::test]
async fn address_is_p2wpkh() {
let test = create_wallet().await;
let address = test.new_external_address().await.unwrap();
assert_eq!(address.address.address_type().unwrap(), AddressType::P2wpkh)
}
#[tokio::test]
async fn derive_contract_signer() {
let test = create_wallet().await;
let mut temp_key_id = [0u8; 32];
temp_key_id
.try_fill(&mut bitcoin::key::rand::thread_rng())
.unwrap();
let gen_key_id = test.derive_signer_key_id(true, temp_key_id);
let key_info = test.derive_contract_signer(gen_key_id);
assert!(key_info.is_ok())
}
#[tokio::test]
async fn send_all() {
let wallet = create_wallet().await;
let address = match wallet.network {
Network::Regtest => "bcrt1qt0yrvs7qx8guvpqsx8u9mypz6t4zr3pxthsjkm",
Network::Signet => "bcrt1q7h9uzwvyw29vrpujp69l7kce7e5w98mpn8kwsp",
_ => "bcrt1qt0yrvs7qx8guvpqsx8u9mypz6t4zr3pxthsjkm",
};
let addr_one = wallet.new_external_address().await.unwrap().address;
let addr_two = wallet.new_external_address().await.unwrap().address;
fund_address(&addr_one);
fund_address(&addr_two);
wallet.sync().await.unwrap();
let balance = wallet.get_balance().await.unwrap();
assert!(balance.confirmed > Amount::ZERO);
wallet
.send_all(
Address::from_str(address).unwrap().assume_checked(),
FeeRate::from_sat_per_vb(1).unwrap(),
)
.await
.unwrap();
generate_blocks(5);
wallet.sync().await.unwrap();
let balance = wallet.get_balance().await.unwrap();
assert!(balance.confirmed == Amount::ZERO)
}
#[tokio::test]
async fn derive_signer_key_id_deterministic() {
let wallet = create_wallet().await;
let temp_id = [0x55; 32];
let key_id1 = wallet.derive_signer_key_id(true, temp_id);
let key_id2 = wallet.derive_signer_key_id(false, temp_id);
let key_id3 = wallet.derive_signer_key_id(true, temp_id);
assert_eq!(key_id1, key_id2); assert_eq!(key_id1, key_id3); }
#[tokio::test]
async fn derive_signer_key_id_different_temps_produce_different_keys() {
let wallet = create_wallet().await;
let temp_id1 = [0x11; 32];
let temp_id2 = [0x22; 32];
let key_id1 = wallet.derive_signer_key_id(true, temp_id1);
let key_id2 = wallet.derive_signer_key_id(true, temp_id2);
assert_ne!(key_id1, key_id2);
}
#[tokio::test]
async fn derive_signer_key_id_includes_fingerprint() {
let wallet1 = create_wallet().await;
let wallet2 = create_wallet().await;
let temp_id = [0x99; 32];
let key_id1 = wallet1.derive_signer_key_id(true, temp_id);
let key_id2 = wallet2.derive_signer_key_id(true, temp_id);
assert_ne!(
key_id1, key_id2,
"Different wallets should produce different key_ids for same temp_id"
);
}
#[tokio::test]
async fn derive_contract_signer_creates_valid_signer() {
let wallet = create_wallet().await;
let temp_id = [0x77; 32];
let key_id = wallet.derive_signer_key_id(true, temp_id);
let signer = wallet
.derive_contract_signer(key_id)
.expect("Should create valid signer");
let public_key = signer.get_public_key(&wallet.secp).unwrap();
assert!(public_key
.verify(
&wallet.secp,
&bitcoin::secp256k1::Message::from_digest([0u8; 32]),
&wallet.secp.sign_ecdsa(
&bitcoin::secp256k1::Message::from_digest([0u8; 32]),
&signer.get_secret_key().unwrap()
)
)
.is_ok());
}
#[tokio::test]
async fn full_workflow_deterministic() {
let wallet = create_wallet().await;
let temp_id = [0xAB; 32];
let key_id = wallet.derive_signer_key_id(true, temp_id);
let signer1 = wallet.derive_contract_signer(key_id).unwrap();
let key_id2 = wallet.derive_signer_key_id(true, temp_id);
let signer2 = wallet.derive_contract_signer(key_id2).unwrap();
assert_eq!(key_id, key_id2);
assert_eq!(
signer1.get_public_key(&wallet.secp).unwrap(),
signer2.get_public_key(&wallet.secp).unwrap()
);
}
#[tokio::test]
async fn different_temp_ids_produce_different_signers() {
let wallet = create_wallet().await;
let temp_id1 = [0x01; 32];
let temp_id2 = [0x02; 32];
let key_id1 = wallet.derive_signer_key_id(true, temp_id1);
let key_id2 = wallet.derive_signer_key_id(true, temp_id2);
let signer1 = wallet.derive_contract_signer(key_id1).unwrap();
let signer2 = wallet.derive_contract_signer(key_id2).unwrap();
assert_ne!(key_id1, key_id2);
assert_ne!(
signer1.get_public_key(&wallet.secp).unwrap(),
signer2.get_public_key(&wallet.secp).unwrap()
);
}
#[tokio::test]
async fn collision_resistance_sample() {
let wallet = create_wallet().await;
let mut key_ids = HashSet::new();
let mut public_keys = HashSet::new();
for i in 0..1000u32 {
let mut temp_id = [0u8; 32];
temp_id[0..4].copy_from_slice(&i.to_be_bytes());
let key_id = wallet.derive_signer_key_id(true, temp_id);
let signer = wallet.derive_contract_signer(key_id).unwrap();
let public_key = signer.get_public_key(&wallet.secp).unwrap();
assert!(
key_ids.insert(key_id),
"Key ID collision detected at iteration {}",
i
);
assert!(
public_keys.insert(public_key),
"Public key collision detected at iteration {}",
i
);
}
assert_eq!(key_ids.len(), 1000);
assert_eq!(public_keys.len(), 1000);
}
#[tokio::test]
async fn recovery_scenario_simulation() {
let wallet = create_wallet().await;
let temp_id = [0xDE, 0xAD, 0xBE, 0xEF].repeat(8).try_into().unwrap();
let key_id = wallet.derive_signer_key_id(true, temp_id);
let original_signer = wallet.derive_contract_signer(key_id).unwrap();
let target_public_key = original_signer.get_public_key(&wallet.secp).unwrap();
let recovered_signer = wallet.derive_contract_signer(key_id).unwrap();
assert_eq!(
original_signer.get_public_key(&wallet.secp).unwrap(),
recovered_signer.get_public_key(&wallet.secp).unwrap()
);
let recovered_key_id = wallet.derive_signer_key_id(true, temp_id);
let temp_id_recovered_signer = wallet.derive_contract_signer(recovered_key_id).unwrap();
assert_eq!(key_id, recovered_key_id);
assert_eq!(
target_public_key,
temp_id_recovered_signer
.get_public_key(&wallet.secp)
.unwrap()
);
}
struct DummyAddressGenerator;
#[async_trait::async_trait]
impl super::address::AddressGenerator for DummyAddressGenerator {
async fn custom_external_address(&self) -> Result<Address, crate::error::WalletError> {
Ok(
Address::from_str("bcrt1qgnflehdvm85l5qmhf887lklda43ynh6tlx4ly0")
.unwrap()
.assume_checked(),
)
}
async fn custom_change_address(&self) -> Result<Address, crate::error::WalletError> {
Ok(
Address::from_str("bcrt1qqhxq8mgmlx3njn3kcx3zmxzuyarcrh5huhm55t")
.unwrap()
.assume_checked(),
)
}
}
#[tokio::test]
async fn custom_address_generator() {
use ddk_manager::Wallet;
let address = Address::from_str("bcrt1qgnflehdvm85l5qmhf887lklda43ynh6tlx4ly0")
.unwrap()
.assume_checked();
let change_address = Address::from_str("bcrt1qqhxq8mgmlx3njn3kcx3zmxzuyarcrh5huhm55t")
.unwrap()
.assume_checked();
let logger = Arc::new(Logger::console(
"console_logger".to_string(),
LogLevel::Info,
));
let esplora_host = ddk_testenv::env().esplora_host();
let esplora =
Arc::new(EsploraClient::new(esplora_host, Network::Regtest, logger.clone()).unwrap());
let mut seed = [0u8; 64];
seed.try_fill(&mut bitcoin::key::rand::thread_rng())
.unwrap();
let memory_storage = Arc::new(MemoryStorage::new());
let wallet = DlcDevKitWallet::new(
&seed,
esplora,
Network::Regtest,
memory_storage.clone(),
Some(Arc::new(DummyAddressGenerator)),
logger.clone(),
)
.await
.unwrap();
let generate_address = wallet.get_new_address().await.unwrap();
assert_eq!(generate_address, address);
let generate_change_address = wallet.get_new_change_address().await.unwrap();
assert_eq!(generate_change_address, change_address);
let internal_wallet_address = wallet.new_external_address().await.unwrap();
assert_ne!(internal_wallet_address.address, address);
let internal_wallet_change_address = wallet.new_change_address().await.unwrap();
assert_ne!(internal_wallet_change_address.address, change_address);
let check_again = wallet.get_new_address().await.unwrap();
assert_eq!(check_again, address);
let check_again_change = wallet.get_new_change_address().await.unwrap();
assert_eq!(check_again_change, change_address);
}
}