From f46f743141758f346364be516293d9ed1aaa4350 Mon Sep 17 00:00:00 2001 From: rustaceanrob Date: Sat, 8 Mar 2025 10:58:16 -0800 Subject: [PATCH] refactor(kyoto): switch to proc macros --- bdk-ffi/src/bdk.udl | 214 ------------------------------------------- bdk-ffi/src/error.rs | 4 +- bdk-ffi/src/kyoto.rs | 111 +++++++++++++++++++--- bdk-ffi/src/lib.rs | 11 --- 4 files changed, 101 insertions(+), 239 deletions(-) diff --git a/bdk-ffi/src/bdk.udl b/bdk-ffi/src/bdk.udl index a95d9edf..48e7aea6 100644 --- a/bdk-ffi/src/bdk.udl +++ b/bdk-ffi/src/bdk.udl @@ -330,20 +330,6 @@ interface TxidParseError { InvalidTxid(string txid); }; -/// Errors that may occur building a light client. -[Error] -interface LightClientBuilderError { - /// A database could not be opened or created. - DatabaseError(string reason); -}; - -/// Errors that may occur sending messages to a node. -[Error] -enum LightClientError { - /// The node is not currently running. - "NodeStopped", -}; - // ------------------------------------------------------------------------ // bdk_wallet crate - types module // ------------------------------------------------------------------------ @@ -1322,206 +1308,6 @@ interface AddressData { // bdk-kyoto crate // ------------------------------------------------------------------------ -/// Build a BIP 157/158 light client to fetch transactions for a `Wallet`. -/// -/// Options: -/// * List of `Peer`: Bitcoin full-nodes for the light client to connect to. May be empty. -/// * `connections`: The number of connections for the light client to maintain. -/// * `scan_type`: Sync, recover, or start a new wallet. For more information see [`ScanType`]. -/// * `data_dir`: Optional directory to store block headers and peers. -/// -/// A note on recovering wallets. Developers should allow users to provide an -/// approximate recovery height and an estimated number of transactions for the -/// wallet. When determining how many scripts to check filters for, the `Wallet` -/// `lookahead` value will be used. To ensure all transactions are recovered, the -/// `lookahead` should be roughly the number of transactions in the wallet history. -interface LightClientBuilder { - /// Start a new [`LightClientBuilder`] - constructor(); - - /// The number of connections for the light client to maintain. Default is two. - LightClientBuilder connections(u8 connections); - - /// Directory to store block headers and peers. If none is provided, the current - /// working directory will be used. - LightClientBuilder data_dir(string data_dir); - - /// Select between syncing, recovering, or scanning for new wallets. - LightClientBuilder scan_type(ScanType scan_type); - - /// Bitcoin full-nodes to attempt a connection with. - LightClientBuilder peers(sequence peers); - - /// Construct a [`LightClient`] for a [`Wallet`]. - [Throws=LightClientBuilderError] - LightClient build([ByRef] Wallet wallet); -}; - -/// A [`Client`] handles wallet updates from a [`LightNode`]. -interface Client { - /// Return the next available log message from a node. If none is returned, the node has stopped. - [Async, Throws=LightClientError] - Log next_log(); - - /// Return the next available warning message from a node. If none is returned, the node has stopped. - [Async, Throws=LightClientError] - Warning next_warning(); - - /// Return an [`Update`]. This is method returns once the node syncs to the rest of - /// the network or a new block has been gossiped. - [Async] - Update? update(); - - /// Add scripts for the node to watch for as they are revealed. Typically used after creating - /// a transaction or revealing a receive address. - /// - /// Note that only future blocks will be checked for these scripts, not past blocks. - [Async, Throws=LightClientError] - void add_revealed_scripts([ByRef] Wallet wallet); - - /// The minimum fee rate required to broadcast a transcation to all connected peers. - [Async, Throws=LightClientError] - FeeRate min_broadcast_feerate(); - - /// Broadcast a transaction to the network, erroring if the node has stopped running. - [Async, Throws=LightClientError] - void broadcast([ByRef] Transaction transaction); - - /// Check if the node is still running in the background. - [Async] - boolean is_running(); - - /// Stop the [`LightNode`]. Errors if the node is already stopped. - [Async, Throws=LightClientError] - void shutdown(); -}; - -/// A [`LightNode`] gathers transactions for a [`Wallet`]. -/// To receive [`Update`] for [`Wallet`], refer to the -/// [`Client`]. The [`LightNode`] will run until instructed -/// to stop. -interface LightNode { - /// Start the node on a detached OS thread and immediately return. - void run(); -}; - -/// Receive a [`Client`] and [`LightNode`]. -dictionary LightClient { - /// Publish events to the node, like broadcasting transactions or adding scripts. - Client client; - - /// The node to run and fetch transactions for a [`Wallet`]. - LightNode node; -}; - -/// Sync a wallet from the last known block hash, recover a wallet from a specified height, -/// or perform an expedited block header download for a new wallet. -[Enum] -interface ScanType { - /// Perform an expedited header and filter download for a new wallet. - /// If this option is not set, and the wallet has no history, the - /// entire chain will be scanned for script inclusions. - New(); - - /// Sync an existing wallet from the last stored chain checkpoint. - Sync(); - - /// Recover an existing wallet by scanning from the specified height. - Recovery(u32 from_height); -}; - -/// A peer to connect to over the Bitcoin peer-to-peer network. -dictionary Peer { - /// The IP address to reach the node. - IpAddress address; - - /// The port to reach the node. If none is provided, the default - /// port for the selected network will be used. - u16? port; - - /// Does the remote node offer encrypted peer-to-peer connection. - boolean v2_transport; -}; - -/// An IP address to connect to over TCP. -interface IpAddress { - /// Build an IPv4 address. - [Name=from_ipv4] - constructor(u8 q1, u8 q2, u8 q3, u8 q4); - - /// Build an IPv6 address. - [Name=from_ipv6] - constructor(u16 a, u16 b, u16 c, u16 d, u16 e, u16 f, u16 g, u16 h); -}; - -/// A log message from the node. -[Enum] -interface Log { - /// A human-readable debug message. - Debug(string log); - - /// All the required connections have been met. This is subject to change. - ConnectionsMet(); - - /// A percentage value of filters that have been scanned. - Progress(f32 progress); - - /// A state in the node syncing process. - StateUpdate(NodeState node_state); - - /// A transaction was broadcast over the wire. - /// The transaction may or may not be rejected by recipient nodes. - TxSent(string txid); -}; - -/// Warnings a node may issue while running. -[Enum] -interface Warning { - /// The node is looking for connections to peers. - NeedConnections(); - - /// A connection to a peer timed out. - PeerTimedOut(); - - /// The node was unable to connect to a peer in the database. - CouldNotConnect(); - - /// A connection was maintained, but the peer does not signal for compact block filers. - NoCompactFilters(); - - /// The node has been waiting for new inv and will find new peers to avoid block withholding. - PotentialStaleTip(); - - /// A peer sent us a peer-to-peer message the node did not request. - UnsolicitedMessage(); - - /// The provided starting height is deeper than the database history. - /// This should not occur under normal use. - InvalidStartHeight(); - - /// The headers in the database do not link together. - /// Recoverable by deleting the database. - CorruptedHeaders(); - - /// A transaction got rejected, likely for being an insufficient fee or non-standard transaction. - TransactionRejected(string txid, string? reason); - - /// A database failed to persist some data and may retry again. - FailedPersistence(string warning); - - /// The peer sent us a potential fork. - EvaluatingFork(); - - /// The peer database has no values. - EmptyPeerDatabase(); - - /// An unexpected error occured processing a peer-to-peer message. - UnexpectedSyncError(string warning); - - /// The node failed to respond to a message sent from the client. - RequestFailed(); -}; - /// The state of the node with respect to connected peers. [Remote] enum NodeState { diff --git a/bdk-ffi/src/error.rs b/bdk-ffi/src/error.rs index 4456afe2..a3c08cbd 100644 --- a/bdk-ffi/src/error.rs +++ b/bdk-ffi/src/error.rs @@ -782,13 +782,13 @@ pub enum TxidParseError { InvalidTxid { txid: String }, } -#[derive(Debug, thiserror::Error)] +#[derive(Debug, thiserror::Error, uniffi::Error)] pub enum LightClientBuilderError { #[error("the database could not be opened or created: {reason}")] DatabaseError { reason: String }, } -#[derive(Debug, thiserror::Error)] +#[derive(Debug, thiserror::Error, uniffi::Error)] pub enum LightClientError { #[error("the node is no longer running")] NodeStopped, diff --git a/bdk-ffi/src/kyoto.rs b/bdk-ffi/src/kyoto.rs index b37f5d80..4c9c7205 100644 --- a/bdk-ffi/src/kyoto.rs +++ b/bdk-ffi/src/kyoto.rs @@ -33,11 +33,17 @@ const TIMEOUT: u64 = 10; const DEFAULT_CONNECTIONS: u8 = 2; const CWD_PATH: &str = "."; +/// Receive a [`Client`] and [`LightNode`]. +#[derive(Debug, uniffi::Record)] pub struct LightClient { + /// Publish events to the node, like broadcasting transactions or adding scripts. pub client: Arc, + /// The node to run and fetch transactions for a [`Wallet`]. pub node: Arc, } +/// A [`Client`] handles wallet updates from a [`LightNode`]. +#[derive(Debug, uniffi::Object)] pub struct Client { sender: Arc, log_rx: Mutex>, @@ -45,11 +51,18 @@ pub struct Client { update_rx: Mutex, } +/// A [`LightNode`] gathers transactions for a [`Wallet`]. +/// To receive [`Update`] for [`Wallet`], refer to the +/// [`Client`]. The [`LightNode`] will run until instructed +/// to stop. +#[derive(Debug, uniffi::Object)] pub struct LightNode { node: NodeDefault, } +#[uniffi::export] impl LightNode { + /// Start the node on a detached OS thread and immediately return. pub fn run(self: Arc) { std::thread::spawn(|| { tokio::runtime::Builder::new_multi_thread() @@ -63,7 +76,20 @@ impl LightNode { } } -#[derive(Clone)] +/// Build a BIP 157/158 light client to fetch transactions for a `Wallet`. +/// +/// Options: +/// * List of `Peer`: Bitcoin full-nodes for the light client to connect to. May be empty. +/// * `connections`: The number of connections for the light client to maintain. +/// * `scan_type`: Sync, recover, or start a new wallet. For more information see [`ScanType`]. +/// * `data_dir`: Optional directory to store block headers and peers. +/// +/// A note on recovering wallets. Developers should allow users to provide an +/// approximate recovery height and an estimated number of transactions for the +/// wallet. When determining how many scripts to check filters for, the `Wallet` +/// `lookahead` value will be used. To ensure all transactions are recovered, the +/// `lookahead` should be roughly the number of transactions in the wallet history. +#[derive(Clone, uniffi::Object)] pub struct LightClientBuilder { connections: u8, data_dir: Option, @@ -71,7 +97,10 @@ pub struct LightClientBuilder { peers: Vec, } +#[uniffi::export] impl LightClientBuilder { + /// Start a new [`LightClientBuilder`] + #[uniffi::constructor] pub fn new() -> Self { LightClientBuilder { connections: DEFAULT_CONNECTIONS, @@ -81,6 +110,7 @@ impl LightClientBuilder { } } + /// The number of connections for the light client to maintain. Default is two. pub fn connections(&self, connections: u8) -> Arc { Arc::new(LightClientBuilder { connections, @@ -88,6 +118,8 @@ impl LightClientBuilder { }) } + /// Directory to store block headers and peers. If none is provided, the current + /// working directory will be used. pub fn data_dir(&self, data_dir: String) -> Arc { Arc::new(LightClientBuilder { data_dir: Some(data_dir), @@ -95,6 +127,7 @@ impl LightClientBuilder { }) } + /// Select between syncing, recovering, or scanning for new wallets. pub fn scan_type(&self, scan_type: ScanType) -> Arc { Arc::new(LightClientBuilder { scan_type, @@ -102,6 +135,7 @@ impl LightClientBuilder { }) } + /// Bitcoin full-nodes to attempt a connection with. pub fn peers(&self, peers: Vec) -> Arc { Arc::new(LightClientBuilder { peers, @@ -109,6 +143,7 @@ impl LightClientBuilder { }) } + /// Construct a [`LightClient`] for a [`Wallet`]. pub fn build(&self, wallet: &Wallet) -> Result { let wallet = wallet.get_wallet(); @@ -153,7 +188,9 @@ impl LightClientBuilder { } } +#[uniffi::export] impl Client { + /// Return the next available log message from a node. If none is returned, the node has stopped. pub async fn next_log(&self) -> Result { let mut log_rx = self.log_rx.lock().await; log_rx @@ -163,6 +200,7 @@ impl Client { .ok_or(LightClientError::NodeStopped) } + /// Return the next available warning message from a node. If none is returned, the node has stopped. pub async fn next_warning(&self) -> Result { let mut warn_rx = self.warning_rx.lock().await; warn_rx @@ -172,11 +210,17 @@ impl Client { .ok_or(LightClientError::NodeStopped) } + /// Return an [`Update`]. This is method returns once the node syncs to the rest of + /// the network or a new block has been gossiped. pub async fn update(&self) -> Option> { let update = self.update_rx.lock().await.update().await; update.map(|update| Arc::new(Update(update))) } + /// Add scripts for the node to watch for as they are revealed. Typically used after creating + /// a transaction or revealing a receive address. + /// + /// Note that only future blocks will be checked for these scripts, not past blocks. pub async fn add_revealed_scripts(&self, wallet: &Wallet) -> Result<(), LightClientError> { let script_iter: Vec = { let wallet_lock = wallet.get_wallet(); @@ -191,11 +235,13 @@ impl Client { Ok(()) } + /// Broadcast a transaction to the network, erroring if the node has stopped running. pub async fn broadcast(&self, transaction: &Transaction) -> Result<(), LightClientError> { let tx = transaction.into(); self.sender.broadcast_random(tx).await.map_err(From::from) } + /// The minimum fee rate required to broadcast a transcation to all connected peers. pub async fn min_broadcast_feerate(&self) -> Result, LightClientError> { self.sender .broadcast_min_feerate() @@ -204,20 +250,30 @@ impl Client { .map(|fee| Arc::new(FeeRate(fee))) } + /// Check if the node is still running in the background. pub async fn is_running(&self) -> bool { self.sender.is_running().await } + /// Stop the [`LightNode`]. Errors if the node is already stopped. pub async fn shutdown(&self) -> Result<(), LightClientError> { self.sender.shutdown().await.map_err(From::from) } } +/// A log message from the node. +#[derive(Debug, uniffi::Enum)] pub enum Log { + /// A human-readable debug message. Debug { log: String }, + /// All the required connections have been met. This is subject to change. ConnectionsMet, + /// A percentage value of filters that have been scanned. Progress { progress: f32 }, + /// A state in the node syncing process. StateUpdate { node_state: NodeState }, + /// A transaction was broadcast over the wire. + /// The transaction may or may not be rejected by recipient nodes. TxSent { txid: String }, } @@ -237,27 +293,41 @@ impl From for Log { } } +/// Warnings a node may issue while running. +#[derive(Debug, uniffi::Enum)] pub enum Warning { + /// The node is looking for connections to peers. NeedConnections, + /// A connection to a peer timed out. PeerTimedOut, + /// The node was unable to connect to a peer in the database. CouldNotConnect, + /// A connection was maintained, but the peer does not signal for compact block filers. NoCompactFilters, + /// The node has been waiting for new inv and will find new peers to avoid block withholding. PotentialStaleTip, + /// A peer sent us a peer-to-peer message the node did not request. UnsolicitedMessage, + /// The provided starting height is deeper than the database history. + /// This should not occur under normal use. InvalidStartHeight, + /// The headers in the database do not link together. + /// Recoverable by deleting the database. CorruptedHeaders, + /// A transaction got rejected, likely for being an insufficient fee or non-standard transaction. TransactionRejected { txid: String, reason: Option, }, - FailedPersistence { - warning: String, - }, + /// A database failed to persist some data and may retry again + FailedPersistence { warning: String }, + /// The peer sent us a potential fork. EvaluatingFork, + /// The peer database has no values. EmptyPeerDatabase, - UnexpectedSyncError { - warning: String, - }, + /// An unexpected error occured processing a peer-to-peer message. + UnexpectedSyncError { warning: String }, + /// The node failed to respond to a message sent from the client. RequestFailed, } @@ -291,14 +361,19 @@ impl From for Warning { } } -#[derive(Debug, Clone, Copy, Default)] +/// Sync a wallet from the last known block hash, recover a wallet from a specified height, +/// or perform an expedited block header download for a new wallet. +#[derive(Debug, Clone, Copy, Default, uniffi::Enum)] pub enum ScanType { + /// Perform an expedited header and filter download for a new wallet. + /// If this option is not set, and the wallet has no history, the + /// entire chain will be scanned for script inclusions. New, + /// Sync an existing wallet from the last stored chain checkpoint. #[default] Sync, - Recovery { - from_height: u32, - }, + /// Recover an existing wallet by scanning from the specified height. + Recovery { from_height: u32 }, } impl From for WalletScanType { @@ -311,25 +386,37 @@ impl From for WalletScanType { } } -#[derive(Clone)] +/// A peer to connect to over the Bitcoin peer-to-peer network. +#[derive(Clone, uniffi::Record)] pub struct Peer { + /// The IP address to reach the node. pub address: Arc, + /// The port to reach the node. If none is provided, the default + /// port for the selected network will be used. pub port: Option, + /// Does the remote node offer encrypted peer-to-peer connection. pub v2_transport: bool, } +/// An IP address to connect to over TCP. +#[derive(Debug, uniffi::Object)] pub struct IpAddress { inner: IpAddr, } +#[uniffi::export] impl IpAddress { + /// Build an IPv4 address. + #[uniffi::constructor] pub fn from_ipv4(q1: u8, q2: u8, q3: u8, q4: u8) -> Self { Self { inner: IpAddr::V4(Ipv4Addr::new(q1, q2, q3, q4)), } } + /// Build an IPv6 address. #[allow(clippy::too_many_arguments)] + #[uniffi::constructor] pub fn from_ipv6(a: u16, b: u16, c: u16, d: u16, e: u16, f: u16, g: u16, h: u16) -> Self { Self { inner: IpAddr::V6(Ipv6Addr::new(a, b, c, d, e, f, g, h)), diff --git a/bdk-ffi/src/lib.rs b/bdk-ffi/src/lib.rs index 705f9851..6cf28d3a 100644 --- a/bdk-ffi/src/lib.rs +++ b/bdk-ffi/src/lib.rs @@ -42,8 +42,6 @@ use crate::error::EsploraError; use crate::error::ExtractTxError; use crate::error::FeeRateError; use crate::error::FromScriptError; -use crate::error::LightClientBuilderError; -use crate::error::LightClientError; use crate::error::LoadWithPersistError; use crate::error::MiniscriptError; use crate::error::ParseAmountError; @@ -99,14 +97,5 @@ use bdk_wallet::ChangeSet; use bdk_wallet::KeychainKind; use bdk_kyoto::NodeState; -use kyoto::Client; -use kyoto::IpAddress; -use kyoto::LightClient; -use kyoto::LightClientBuilder; -use kyoto::LightNode; -use kyoto::Log; -use kyoto::Peer; -use kyoto::ScanType; -use kyoto::Warning; uniffi::include_scaffolding!("bdk");