feat: inspect spks on request types

This commit is contained in:
Matthew 2024-05-17 15:10:49 -05:00
parent 1f7c782d49
commit 89f803a753
No known key found for this signature in database
GPG Key ID: 8D4FCD82DD54DDD2
6 changed files with 172 additions and 4 deletions

View File

@ -176,6 +176,11 @@ interface PsbtParseError {
Base64Encoding(string error_message); Base64Encoding(string error_message);
}; };
[Error]
interface InspectError {
RequestAlreadyConsumed();
};
[Error] [Error]
interface SignerError { interface SignerError {
MissingKey(); MissingKey();
@ -274,9 +279,23 @@ dictionary CanonicalTx {
ChainPosition chain_position; ChainPosition chain_position;
}; };
interface FullScanRequest {}; interface FullScanRequest {
[Throws=InspectError]
FullScanRequest inspect_spks_for_all_keychains(FullScanScriptInspector inspector);
};
interface SyncRequest {}; interface SyncRequest {
[Throws=InspectError]
SyncRequest inspect_spks(SyncScriptInspector inspector);
};
callback interface SyncScriptInspector {
void inspect(Script script, u64 total);
};
callback interface FullScanScriptInspector {
void inspect(KeychainKind keychain, u32 index, Script script);
};
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// bdk crate - wallet module // bdk crate - wallet module

View File

@ -375,6 +375,12 @@ pub enum FeeRateError {
ArithmeticOverflow, ArithmeticOverflow,
} }
#[derive(Debug, thiserror::Error)]
pub enum InspectError {
#[error("the request has already been consumed")]
RequestAlreadyConsumed,
}
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum ParseAmountError { pub enum ParseAmountError {
#[error("amount is negative")] #[error("amount is negative")]
@ -1080,8 +1086,8 @@ mod test {
use crate::error::{ use crate::error::{
AddressError, Bip32Error, Bip39Error, CannotConnectError, CreateTxError, DescriptorError, AddressError, Bip32Error, Bip39Error, CannotConnectError, CreateTxError, DescriptorError,
DescriptorKeyError, ElectrumError, EsploraError, ExtractTxError, FeeRateError, DescriptorKeyError, ElectrumError, EsploraError, ExtractTxError, FeeRateError,
ParseAmountError, PersistenceError, PsbtParseError, TransactionError, TxidParseError, InspectError, ParseAmountError, PersistenceError, PsbtParseError, TransactionError,
WalletCreationError, TxidParseError, WalletCreationError,
}; };
use crate::CalculateFeeError; use crate::CalculateFeeError;
use crate::OutPoint; use crate::OutPoint;
@ -1671,6 +1677,18 @@ mod test {
} }
} }
#[test]
fn test_error_inspect() {
let cases = vec![(
InspectError::RequestAlreadyConsumed,
"the request has already been consumed",
)];
for (error, expected_message) in cases {
assert_eq!(error.to_string(), expected_message);
}
}
#[test] #[test]
fn test_error_parse_amount() { fn test_error_parse_amount() {
let cases = vec![ let cases = vec![

View File

@ -30,6 +30,7 @@ use crate::error::ElectrumError;
use crate::error::EsploraError; use crate::error::EsploraError;
use crate::error::ExtractTxError; use crate::error::ExtractTxError;
use crate::error::FeeRateError; use crate::error::FeeRateError;
use crate::error::InspectError;
use crate::error::ParseAmountError; use crate::error::ParseAmountError;
use crate::error::PersistenceError; use crate::error::PersistenceError;
use crate::error::PsbtParseError; use crate::error::PsbtParseError;
@ -47,9 +48,11 @@ use crate::types::Balance;
use crate::types::CanonicalTx; use crate::types::CanonicalTx;
use crate::types::ChainPosition; use crate::types::ChainPosition;
use crate::types::FullScanRequest; use crate::types::FullScanRequest;
use crate::types::FullScanScriptInspector;
use crate::types::LocalOutput; use crate::types::LocalOutput;
use crate::types::ScriptAmount; use crate::types::ScriptAmount;
use crate::types::SyncRequest; use crate::types::SyncRequest;
use crate::types::SyncScriptInspector;
use crate::wallet::BumpFeeTxBuilder; use crate::wallet::BumpFeeTxBuilder;
use crate::wallet::SentAndReceivedValues; use crate::wallet::SentAndReceivedValues;
use crate::wallet::TxBuilder; use crate::wallet::TxBuilder;

View File

@ -1,5 +1,6 @@
use crate::bitcoin::{Address, OutPoint, Script, Transaction, TxOut}; use crate::bitcoin::{Address, OutPoint, Script, Transaction, TxOut};
use bdk::bitcoin::ScriptBuf as BdkScriptBuf;
use bdk::chain::spk_client::FullScanRequest as BdkFullScanRequest; use bdk::chain::spk_client::FullScanRequest as BdkFullScanRequest;
use bdk::chain::spk_client::SyncRequest as BdkSyncRequest; use bdk::chain::spk_client::SyncRequest as BdkSyncRequest;
use bdk::chain::tx_graph::CanonicalTx as BdkCanonicalTx; use bdk::chain::tx_graph::CanonicalTx as BdkCanonicalTx;
@ -12,6 +13,7 @@ use bdk::LocalOutput as BdkLocalOutput;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use crate::bitcoin::Amount; use crate::bitcoin::Amount;
use crate::error::InspectError;
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum ChainPosition { pub enum ChainPosition {
@ -112,6 +114,55 @@ impl From<BdkLocalOutput> for LocalOutput {
} }
} }
// Callback for the FullScanRequest
pub trait FullScanScriptInspector: Sync + Send {
fn inspect(&self, keychain: KeychainKind, index: u32, script: Arc<Script>);
}
// Callback for the SyncRequest
pub trait SyncScriptInspector: Sync + Send {
fn inspect(&self, script: Arc<Script>, total: u64);
}
pub struct FullScanRequest(pub(crate) Mutex<Option<BdkFullScanRequest<KeychainKind>>>); pub struct FullScanRequest(pub(crate) Mutex<Option<BdkFullScanRequest<KeychainKind>>>);
pub struct SyncRequest(pub(crate) Mutex<Option<BdkSyncRequest>>); pub struct SyncRequest(pub(crate) Mutex<Option<BdkSyncRequest>>);
impl SyncRequest {
pub fn inspect_spks(
&self,
inspector: Box<dyn SyncScriptInspector>,
) -> Result<Arc<Self>, InspectError> {
let mut guard = self.0.lock().unwrap();
if let Some(sync_request) = guard.take() {
let total = sync_request.spks.len() as u64;
let sync_request = sync_request.inspect_spks(move |spk| {
inspector.inspect(Arc::new(BdkScriptBuf::from(spk).into()), total)
});
Ok(Arc::new(SyncRequest(Mutex::new(Some(sync_request)))))
} else {
Err(InspectError::RequestAlreadyConsumed)
}
}
}
impl FullScanRequest {
pub fn inspect_spks_for_all_keychains(
&self,
inspector: Box<dyn FullScanScriptInspector>,
) -> Result<Arc<Self>, InspectError> {
let mut guard = self.0.lock().unwrap();
if let Some(full_scan_request) = guard.take() {
let inspector = Arc::new(inspector);
let full_scan_request =
full_scan_request.inspect_spks_for_all_keychains(move |k, spk_i, script| {
inspector.inspect(k, spk_i, Arc::new(BdkScriptBuf::from(script).into()))
});
Ok(Arc::new(FullScanRequest(Mutex::new(Some(
full_scan_request,
)))))
} else {
Err(InspectError::RequestAlreadyConsumed)
}
}
}

View File

@ -6,6 +6,7 @@ private const val SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net"
private const val TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud" private const val TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud"
class LiveMemoryWalletTest { class LiveMemoryWalletTest {
@Test @Test
fun testSyncedBalance() { fun testSyncedBalance() {
val descriptor: Descriptor = Descriptor( val descriptor: Descriptor = Descriptor(
@ -33,4 +34,38 @@ class LiveMemoryWalletTest {
println("Received ${sentAndReceived.received.toSat()}") println("Received ${sentAndReceived.received.toSat()}")
} }
} }
@Test
fun testScriptInspector() {
val descriptor: Descriptor = Descriptor(
"wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
Network.SIGNET
)
val changeDescriptor: Descriptor = Descriptor(
"wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/1/*)",
Network.SIGNET
)
val wallet: Wallet = Wallet.newNoPersist(descriptor, changeDescriptor, Network.SIGNET)
val esploraClient: EsploraClient = EsploraClient(SIGNET_ESPLORA_URL)
val scriptInspector: FullScriptInspector = FullScriptInspector()
val fullScanRequest: FullScanRequest = wallet.startFullScan().inspectSpksForAllKeychains(scriptInspector)
val update = esploraClient.fullScan(fullScanRequest, 21uL, 1uL)
wallet.applyUpdate(update)
wallet.commit()
println("Balance: ${wallet.getBalance().total.toSat()}")
assert(wallet.getBalance().total.toSat() > 0uL) {
"Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address.asString()} and try again."
}
}
}
class FullScriptInspector: FullScanScriptInspector {
override fun inspect(keychain: KeychainKind, index: UInt, script: Script){
println("Inspecting index $index for keychain $keychain")
}
} }

View File

@ -5,6 +5,7 @@ private let SIGNET_ESPLORA_URL = "http://signet.bitcoindevkit.net"
private let TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud" private let TESTNET_ESPLORA_URL = "https://esplora.testnet.kuutamo.cloud"
final class LiveMemoryWalletTests: XCTestCase { final class LiveMemoryWalletTests: XCTestCase {
func testSyncedBalance() throws { func testSyncedBalance() throws {
let descriptor = try Descriptor( let descriptor = try Descriptor(
descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)", descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
@ -41,4 +42,45 @@ final class LiveMemoryWalletTests: XCTestCase {
print("Received \(sentAndReceived.received.toSat())") print("Received \(sentAndReceived.received.toSat())")
} }
} }
func testScriptInspector() throws {
let descriptor = try Descriptor(
descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/0/*)",
network: Network.signet
)
let changeDescriptor = try Descriptor(
descriptor: "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/0h/1/*)",
network: Network.signet
)
let wallet = try Wallet.newNoPersist(
descriptor: descriptor,
changeDescriptor: changeDescriptor,
network: .signet
)
let esploraClient = EsploraClient(url: SIGNET_ESPLORA_URL)
let scriptInspector = FullScriptInspector()
let fullScanRequest = try wallet.startFullScan().inspectSpksForAllKeychains(inspector: scriptInspector)
let update = try esploraClient.fullScan(
fullScanRequest: fullScanRequest,
stopGap: 21,
parallelRequests: 1
)
try wallet.applyUpdate(update: update)
let _ = try wallet.commit()
let address = try wallet.revealNextAddress(keychain: KeychainKind.external).address.asString()
XCTAssertGreaterThan(
wallet.getBalance().total.toSat(),
UInt64(0),
"Wallet must have positive balance, please send funds to \(address)"
)
}
}
class FullScriptInspector: FullScanScriptInspector {
func inspect(keychain: KeychainKind, index: UInt32, script: Script) {
print("Inspecting index \(index) for keychain \(keychain)")
}
} }