diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c87184..0859a1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +- Xpub watcher transaction events now include the synchronized wallet's next unused external receive address and full derivation path, allowing clients to render receive state without starting a second account scan. - The Android AAR now ships targeted R8 consumer keep rules for the UniFFI/JNA FFI surface, so consuming apps can enable R8 full mode without extra keep rules for this library. - Add a generic hardware-wallet catalog with Foundation Passport support, multipart UR QR encoding and decoding, Passport single-signature account export parsing, and signed PSBT finalization across the UniFFI bindings. - Swap status updates now reconcile against Boltz's REST status whenever a swap is (re)subscribed, both on `boltz_start_swap_updates` and when `boltz_create_reverse_swap` adds a swap to a running stream. A confirmed reverse-swap lockup is therefore caught up and auto-claimed even when its live WebSocket event was missed (for example because the updates stream was down while the lockup confirmed), instead of the swap silently stalling until a manual claim. No FFI signature change. diff --git a/Cargo.lock b/Cargo.lock index 90b8f93..9f26a20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -589,7 +589,7 @@ dependencies = [ [[package]] name = "bitkitcore" -version = "0.5.9" +version = "0.5.10" dependencies = [ "android_logger", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 5e665f8..dc01ce5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bitkitcore" -version = "0.5.9" +version = "0.5.10" edition = "2021" [lib] diff --git a/Package.swift b/Package.swift index c1bf2fa..c1bf8d3 100644 --- a/Package.swift +++ b/Package.swift @@ -3,8 +3,8 @@ import PackageDescription -let tag = "v0.5.9" -let checksum = "31a298c85a783224d4b4050746c283b57e321585c979368f20bb3d5757f8ea04" +let tag = "v0.5.10" +let checksum = "8760267026f7f94bf256ee68341054cae5ca60d0ef0e1421fb6b582af5658cf1" let url = "https://github.com/synonymdev/bitkit-core/releases/download/\(tag)/BitkitCore.xcframework.zip" let package = Package( diff --git a/bindings/android/gradle.properties b/bindings/android/gradle.properties index d923351..d5328d7 100644 --- a/bindings/android/gradle.properties +++ b/bindings/android/gradle.properties @@ -3,4 +3,4 @@ android.useAndroidX=true android.enableJetifier=true kotlin.code.style=official group=com.synonym -version=0.5.9 +version=0.5.10 diff --git a/bindings/android/lib/src/main/jniLibs/arm64-v8a/libbitkitcore.so b/bindings/android/lib/src/main/jniLibs/arm64-v8a/libbitkitcore.so index 0fd3fcf..393815b 100755 Binary files a/bindings/android/lib/src/main/jniLibs/arm64-v8a/libbitkitcore.so and b/bindings/android/lib/src/main/jniLibs/arm64-v8a/libbitkitcore.so differ diff --git a/bindings/android/lib/src/main/jniLibs/armeabi-v7a/libbitkitcore.so b/bindings/android/lib/src/main/jniLibs/armeabi-v7a/libbitkitcore.so index 9291d38..fcaf504 100755 Binary files a/bindings/android/lib/src/main/jniLibs/armeabi-v7a/libbitkitcore.so and b/bindings/android/lib/src/main/jniLibs/armeabi-v7a/libbitkitcore.so differ diff --git a/bindings/android/lib/src/main/jniLibs/x86/libbitkitcore.so b/bindings/android/lib/src/main/jniLibs/x86/libbitkitcore.so index cfcfb48..1efdf7f 100755 Binary files a/bindings/android/lib/src/main/jniLibs/x86/libbitkitcore.so and b/bindings/android/lib/src/main/jniLibs/x86/libbitkitcore.so differ diff --git a/bindings/android/lib/src/main/jniLibs/x86_64/libbitkitcore.so b/bindings/android/lib/src/main/jniLibs/x86_64/libbitkitcore.so index e60893a..e5e6959 100755 Binary files a/bindings/android/lib/src/main/jniLibs/x86_64/libbitkitcore.so and b/bindings/android/lib/src/main/jniLibs/x86_64/libbitkitcore.so differ diff --git a/bindings/android/lib/src/main/kotlin/com/synonym/bitkitcore/bitkitcore.android.kt b/bindings/android/lib/src/main/kotlin/com/synonym/bitkitcore/bitkitcore.android.kt index b2df344..3253fe0 100644 --- a/bindings/android/lib/src/main/kotlin/com/synonym/bitkitcore/bitkitcore.android.kt +++ b/bindings/android/lib/src/main/kotlin/com/synonym/bitkitcore/bitkitcore.android.kt @@ -12678,6 +12678,7 @@ public object FfiConverterTypeWatcherEvent : FfiConverterRustBuffer WatcherEvent.Error( FfiConverterString.read(buf), @@ -12701,6 +12702,7 @@ public object FfiConverterTypeWatcherEvent : FfiConverterRustBuffer { @@ -12735,6 +12737,7 @@ public object FfiConverterTypeWatcherEvent : FfiConverterRustBuffer { diff --git a/bindings/android/lib/src/main/kotlin/com/synonym/bitkitcore/bitkitcore.common.kt b/bindings/android/lib/src/main/kotlin/com/synonym/bitkitcore/bitkitcore.common.kt index ac23f33..14bc3b4 100644 --- a/bindings/android/lib/src/main/kotlin/com/synonym/bitkitcore/bitkitcore.common.kt +++ b/bindings/android/lib/src/main/kotlin/com/synonym/bitkitcore/bitkitcore.common.kt @@ -5008,6 +5008,8 @@ public sealed class WatcherEvent { * wallet's perspective, and DB-valid timestamps, so the app can store them * directly through the normal Core activity APIs (e.g. `upsert_activity` / * `upsert_transaction_details`). The two vecs are parallel by `tx_id`. + * `next_unused_external_address` is the synchronized wallet's current + * external receive address. */@kotlinx.serialization.Serializable public data class TransactionsChanged( val `activities`: List, @@ -5016,6 +5018,7 @@ public sealed class WatcherEvent { val `txCount`: kotlin.UInt, val `blockHeight`: kotlin.UInt, val `accountType`: AccountType, + val `nextUnusedExternalAddress`: AddressInfo, ) : WatcherEvent() { } diff --git a/bindings/ios/BitkitCore.xcframework.zip b/bindings/ios/BitkitCore.xcframework.zip index e04bbad..9dda3b1 100644 Binary files a/bindings/ios/BitkitCore.xcframework.zip and b/bindings/ios/BitkitCore.xcframework.zip differ diff --git a/bindings/ios/BitkitCore.xcframework/Info.plist b/bindings/ios/BitkitCore.xcframework/Info.plist index 478a88f..b7357e0 100644 --- a/bindings/ios/BitkitCore.xcframework/Info.plist +++ b/bindings/ios/BitkitCore.xcframework/Info.plist @@ -10,7 +10,7 @@ HeadersPath Headers LibraryIdentifier - ios-arm64 + ios-arm64-simulator LibraryPath libbitkitcore.a SupportedArchitectures @@ -19,6 +19,8 @@ SupportedPlatform ios + SupportedPlatformVariant + simulator BinaryPath @@ -26,7 +28,7 @@ HeadersPath Headers LibraryIdentifier - ios-arm64-simulator + ios-arm64 LibraryPath libbitkitcore.a SupportedArchitectures @@ -35,8 +37,6 @@ SupportedPlatform ios - SupportedPlatformVariant - simulator CFBundlePackageType diff --git a/bindings/ios/bitkitcore.swift b/bindings/ios/bitkitcore.swift index f6cae7c..dfae58f 100644 --- a/bindings/ios/bitkitcore.swift +++ b/bindings/ios/bitkitcore.swift @@ -20704,8 +20704,10 @@ public enum WatcherEvent { * wallet's perspective, and DB-valid timestamps, so the app can store them * directly through the normal Core activity APIs (e.g. `upsert_activity` / * `upsert_transaction_details`). The two vecs are parallel by `tx_id`. + * `next_unused_external_address` is the synchronized wallet's current + * external receive address. */ - case transactionsChanged(activities: [Activity], transactionDetails: [TransactionDetails], balance: WalletBalance, txCount: UInt32, blockHeight: UInt32, accountType: AccountType + case transactionsChanged(activities: [Activity], transactionDetails: [TransactionDetails], balance: WalletBalance, txCount: UInt32, blockHeight: UInt32, accountType: AccountType, nextUnusedExternalAddress: AddressInfo ) /** * An error occurred in the watcher loop. @@ -20738,7 +20740,7 @@ public struct FfiConverterTypeWatcherEvent: FfiConverterRustBuffer { let variant: Int32 = try readInt(&buf) switch variant { - case 1: return .transactionsChanged(activities: try FfiConverterSequenceTypeActivity.read(from: &buf), transactionDetails: try FfiConverterSequenceTypeTransactionDetails.read(from: &buf), balance: try FfiConverterTypeWalletBalance.read(from: &buf), txCount: try FfiConverterUInt32.read(from: &buf), blockHeight: try FfiConverterUInt32.read(from: &buf), accountType: try FfiConverterTypeAccountType.read(from: &buf) + case 1: return .transactionsChanged(activities: try FfiConverterSequenceTypeActivity.read(from: &buf), transactionDetails: try FfiConverterSequenceTypeTransactionDetails.read(from: &buf), balance: try FfiConverterTypeWalletBalance.read(from: &buf), txCount: try FfiConverterUInt32.read(from: &buf), blockHeight: try FfiConverterUInt32.read(from: &buf), accountType: try FfiConverterTypeAccountType.read(from: &buf), nextUnusedExternalAddress: try FfiConverterTypeAddressInfo.read(from: &buf) ) case 2: return .error(message: try FfiConverterString.read(from: &buf) @@ -20757,7 +20759,7 @@ public struct FfiConverterTypeWatcherEvent: FfiConverterRustBuffer { switch value { - case let .transactionsChanged(activities,transactionDetails,balance,txCount,blockHeight,accountType): + case let .transactionsChanged(activities,transactionDetails,balance,txCount,blockHeight,accountType,nextUnusedExternalAddress): writeInt(&buf, Int32(1)) FfiConverterSequenceTypeActivity.write(activities, into: &buf) FfiConverterSequenceTypeTransactionDetails.write(transactionDetails, into: &buf) @@ -20765,6 +20767,7 @@ public struct FfiConverterTypeWatcherEvent: FfiConverterRustBuffer { FfiConverterUInt32.write(txCount, into: &buf) FfiConverterUInt32.write(blockHeight, into: &buf) FfiConverterTypeAccountType.write(accountType, into: &buf) + FfiConverterTypeAddressInfo.write(nextUnusedExternalAddress, into: &buf) case let .error(message): diff --git a/bindings/python/bitkitcore/bitkitcore.py b/bindings/python/bitkitcore/bitkitcore.py index debae06..4a33ef2 100644 --- a/bindings/python/bitkitcore/bitkitcore.py +++ b/bindings/python/bitkitcore/bitkitcore.py @@ -17629,6 +17629,8 @@ class TRANSACTIONS_CHANGED: wallet's perspective, and DB-valid timestamps, so the app can store them directly through the normal Core activity APIs (e.g. `upsert_activity` / `upsert_transaction_details`). The two vecs are parallel by `tx_id`. + `next_unused_external_address` is the synchronized wallet's current + external receive address. """ activities: "typing.List[Activity]" @@ -17637,17 +17639,19 @@ class TRANSACTIONS_CHANGED: tx_count: "int" block_height: "int" account_type: "AccountType" + next_unused_external_address: "AddressInfo" - def __init__(self,activities: "typing.List[Activity]", transaction_details: "typing.List[TransactionDetails]", balance: "WalletBalance", tx_count: "int", block_height: "int", account_type: "AccountType"): + def __init__(self,activities: "typing.List[Activity]", transaction_details: "typing.List[TransactionDetails]", balance: "WalletBalance", tx_count: "int", block_height: "int", account_type: "AccountType", next_unused_external_address: "AddressInfo"): self.activities = activities self.transaction_details = transaction_details self.balance = balance self.tx_count = tx_count self.block_height = block_height self.account_type = account_type + self.next_unused_external_address = next_unused_external_address def __str__(self): - return "WatcherEvent.TRANSACTIONS_CHANGED(activities={}, transaction_details={}, balance={}, tx_count={}, block_height={}, account_type={})".format(self.activities, self.transaction_details, self.balance, self.tx_count, self.block_height, self.account_type) + return "WatcherEvent.TRANSACTIONS_CHANGED(activities={}, transaction_details={}, balance={}, tx_count={}, block_height={}, account_type={}, next_unused_external_address={})".format(self.activities, self.transaction_details, self.balance, self.tx_count, self.block_height, self.account_type, self.next_unused_external_address) def __eq__(self, other): if not other.is_TRANSACTIONS_CHANGED(): @@ -17664,6 +17668,8 @@ def __eq__(self, other): return False if self.account_type != other.account_type: return False + if self.next_unused_external_address != other.next_unused_external_address: + return False return True class ERROR: @@ -17768,6 +17774,7 @@ def read(buf): _UniffiConverterUInt32.read(buf), _UniffiConverterUInt32.read(buf), _UniffiConverterTypeAccountType.read(buf), + _UniffiConverterTypeAddressInfo.read(buf), ) if variant == 2: return WatcherEvent.ERROR( @@ -17791,6 +17798,7 @@ def check_lower(value): _UniffiConverterUInt32.check_lower(value.tx_count) _UniffiConverterUInt32.check_lower(value.block_height) _UniffiConverterTypeAccountType.check_lower(value.account_type) + _UniffiConverterTypeAddressInfo.check_lower(value.next_unused_external_address) return if value.is_ERROR(): _UniffiConverterString.check_lower(value.message) @@ -17812,6 +17820,7 @@ def write(value, buf): _UniffiConverterUInt32.write(value.tx_count, buf) _UniffiConverterUInt32.write(value.block_height, buf) _UniffiConverterTypeAccountType.write(value.account_type, buf) + _UniffiConverterTypeAddressInfo.write(value.next_unused_external_address, buf) if value.is_ERROR(): buf.write_i32(2) _UniffiConverterString.write(value.message, buf) diff --git a/bindings/python/bitkitcore/libbitkitcore.dylib b/bindings/python/bitkitcore/libbitkitcore.dylib index eeb5251..cbed382 100755 Binary files a/bindings/python/bitkitcore/libbitkitcore.dylib and b/bindings/python/bitkitcore/libbitkitcore.dylib differ diff --git a/bindings/python/setup.py b/bindings/python/setup.py index ef2600e..9612372 100644 --- a/bindings/python/setup.py +++ b/bindings/python/setup.py @@ -2,7 +2,7 @@ setup( name="bitkitcore", - version="0.5.9", + version="0.5.10", packages=find_packages(), package_data={ "bitkitcore": ["*.so", "*.dylib", "*.dll"], diff --git a/src/modules/onchain/listener.rs b/src/modules/onchain/listener.rs index 79a484c..0e7e81c 100644 --- a/src/modules/onchain/listener.rs +++ b/src/modules/onchain/listener.rs @@ -18,7 +18,8 @@ use super::implementation::{ watch_only_activity_from_detail, }; use super::types::{ - AccountType, Network as OnchainNetwork, WalletBalance, WatcherEvent, DEFAULT_GAP_LIMIT, + AccountType, AddressInfo, Network as OnchainNetwork, WalletBalance, WatcherEvent, + DEFAULT_GAP_LIMIT, }; use bdk::bitcoin::Network as BdkNetwork; @@ -343,6 +344,7 @@ fn build_tx_changed_event( account_type: AccountType, wallet_id: &str, wallet_network: BdkNetwork, + base_path: &str, ) -> WatcherEvent { let bdk_balance = match wallet.get_balance() { Ok(b) => b, @@ -382,6 +384,14 @@ fn build_tx_changed_event( } let tx_count = u32::try_from(activities.len()).unwrap_or(u32::MAX); + let next_unused_external_address = match next_unused_external_address(wallet, base_path) { + Ok(address) => address, + Err(error) => { + return WatcherEvent::Error { + message: error.to_string(), + }; + } + }; WatcherEvent::TransactionsChanged { activities, @@ -390,9 +400,27 @@ fn build_tx_changed_event( tx_count, block_height: tip_height, account_type, + next_unused_external_address, } } +fn next_unused_external_address( + wallet: &Wallet, + base_path: &str, +) -> Result { + let address = wallet + .get_address(BdkAddressIndex::LastUnused) + .map_err(|error| AccountInfoError::WalletError { + error_details: format!("Failed to get last unused external address: {}", error), + })?; + + Ok(AddressInfo { + address: address.address.to_string(), + path: format!("{}/0/{}", base_path, address.index), + transfers: 0, + }) +} + /// Subscribe to all scripts in a single batched RPC. /// /// The failure is surfaced rather than ignored: `electrum-client` registers the @@ -579,6 +607,7 @@ fn watcher_init_and_loop( let sync_stop_gap = (gap as usize).max(DEFAULT_GAP_LIMIT as usize); let account_type = setup.account_type; let wallet_network = setup.network; + let base_path = setup.base_path.clone(); let wallet_id = params.wallet_id.clone(); let watcher_id = params.watcher_id.clone(); @@ -677,6 +706,7 @@ fn watcher_init_and_loop( account_type, &wallet_id, wallet_network, + &base_path, ), ); @@ -873,6 +903,7 @@ fn watcher_init_and_loop( account_type, &wallet_id, wallet_network, + &base_path, ), ); } @@ -890,3 +921,6 @@ fn watcher_init_and_loop( } } } + +#[cfg(test)] +mod tests; diff --git a/src/modules/onchain/listener/tests.rs b/src/modules/onchain/listener/tests.rs new file mode 100644 index 0000000..b3a9929 --- /dev/null +++ b/src/modules/onchain/listener/tests.rs @@ -0,0 +1,203 @@ +use std::str::FromStr; + +use bdk::bitcoin::absolute::LockTime; +use bdk::bitcoin::bip32::{ChildNumber, ExtendedPubKey}; +use bdk::bitcoin::{Transaction, TxIn, TxOut}; +use bdk::database::BatchOperations; +use bdk::{BlockTime, KeychainKind, TransactionDetails}; + +use super::*; +use crate::modules::activity::Activity; +use crate::modules::onchain::implementation::WalletSetup; + +const TEST_TPUB: &str = "tpubDC7jGaaSE66VDB6VhEDFYQSCAyugXmfnMnrMVyHNzW9wryyTxvha7TmfAHd7GRXrr2TaAn2HXn9T8ep4gyNX1bzGiieqcTUNcu2poyntrET"; +const BARE_PUBKEY_DESCRIPTOR: &str = + "pk(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)"; + +fn test_setup(account_index: u32) -> WalletSetup { + let mut xpub = ExtendedPubKey::from_str(TEST_TPUB).unwrap(); + xpub.child_number = ChildNumber::from_hardened_idx(account_index).unwrap(); + resolve_wallet_setup( + &xpub.to_string(), + Some(OnchainNetwork::Regtest), + Some(AccountType::NativeSegwit), + None, + ) + .unwrap() +} + +fn wallet_with_received_output( + setup: &WalletSetup, + keychain: KeychainKind, +) -> Wallet { + let derivation_wallet = create_wallet(setup).unwrap(); + let address = match keychain { + KeychainKind::External => derivation_wallet.get_address(BdkAddressIndex::Peek(0)), + KeychainKind::Internal => derivation_wallet.get_internal_address(BdkAddressIndex::Peek(0)), + } + .unwrap(); + let transaction = Transaction { + version: 2, + lock_time: LockTime::ZERO, + input: vec![TxIn::default()], + output: vec![TxOut { + value: 25_000, + script_pubkey: address.address.script_pubkey(), + }], + }; + let txid = transaction.txid(); + let mut database = MemoryDatabase::new(); + database.set_last_index(keychain, 0).unwrap(); + database + .set_tx(&TransactionDetails { + transaction: Some(transaction), + txid, + received: 25_000, + sent: 0, + fee: Some(500), + confirmation_time: Some(BlockTime { + height: 100, + timestamp: 1_700_000_000, + }), + }) + .unwrap(); + + let wallet = Wallet::new( + &setup.external_desc, + Some(&setup.internal_desc), + setup.network, + database, + ) + .unwrap(); + wallet.ensure_addresses_cached(1).unwrap(); + wallet +} + +#[test] +fn transactions_changed_event_updates_receive_address_after_external_use() { + let setup = test_setup(7); + let initial_wallet = create_wallet(&setup).unwrap(); + let expected_initial_address = initial_wallet + .get_address(BdkAddressIndex::Peek(0)) + .unwrap() + .address + .to_string(); + + let initial_event = build_tx_changed_event( + &initial_wallet, + 144, + setup.account_type, + "hardware-wallet", + setup.network, + &setup.base_path, + ); + let WatcherEvent::TransactionsChanged { + activities, + transaction_details, + balance, + tx_count, + block_height, + account_type, + next_unused_external_address, + } = initial_event + else { + panic!("expected initial transaction event"); + }; + assert!(activities.is_empty()); + assert!(transaction_details.is_empty()); + assert_eq!(balance.total, 0); + assert_eq!(tx_count, 0); + assert_eq!(block_height, 144); + assert_eq!(account_type, AccountType::NativeSegwit); + assert_eq!( + next_unused_external_address.address, + expected_initial_address + ); + assert_eq!(next_unused_external_address.path, "m/84'/1'/7'/0/0"); + assert_eq!(next_unused_external_address.transfers, 0); + + let updated_wallet = wallet_with_received_output(&setup, KeychainKind::External); + let expected_updated_address = updated_wallet + .get_address(BdkAddressIndex::Peek(1)) + .unwrap() + .address + .to_string(); + let updated_event = build_tx_changed_event( + &updated_wallet, + 145, + setup.account_type, + "hardware-wallet", + setup.network, + &setup.base_path, + ); + let WatcherEvent::TransactionsChanged { + activities, + transaction_details, + balance, + tx_count, + block_height, + account_type, + next_unused_external_address, + } = updated_event + else { + panic!("expected updated transaction event"); + }; + assert_eq!(activities.len(), 1); + assert_eq!(transaction_details.len(), 1); + assert_eq!(balance.total, 0); + assert_eq!(tx_count, 1); + assert_eq!(block_height, 145); + assert_eq!(account_type, AccountType::NativeSegwit); + assert_eq!( + next_unused_external_address.address, + expected_updated_address + ); + assert_eq!(next_unused_external_address.path, "m/84'/1'/7'/0/1"); + assert_eq!(next_unused_external_address.transfers, 0); + let Activity::Onchain(activity) = &activities[0] else { + panic!("expected onchain activity"); + }; + assert_eq!(activity.wallet_id, "hardware-wallet"); + assert_eq!(activity.value, 25_000); + assert_eq!(transaction_details[0].wallet_id, "hardware-wallet"); + assert_eq!(transaction_details[0].amount_sats, 25_000); +} + +#[test] +fn change_use_does_not_advance_external_receive_address() { + let setup = test_setup(0); + let wallet = wallet_with_received_output(&setup, KeychainKind::Internal); + + let address = next_unused_external_address(&wallet, &setup.base_path).unwrap(); + + assert_eq!(address.path, "m/84'/1'/0'/0/0"); + assert_eq!(address.transfers, 0); +} + +#[test] +fn transactions_changed_event_reports_address_derivation_error() { + let wallet = Wallet::new( + BARE_PUBKEY_DESCRIPTOR, + None, + BdkNetwork::Regtest, + MemoryDatabase::new(), + ) + .unwrap(); + + let event = build_tx_changed_event( + &wallet, + 144, + AccountType::NativeSegwit, + "hardware-wallet", + BdkNetwork::Regtest, + "m/84'/1'/0'", + ); + + let WatcherEvent::Error { message } = event else { + panic!("expected watcher error event"); + }; + assert_eq!( + message, + "Wallet error: Failed to get last unused external address: Script doesn't have address form" + ); +} diff --git a/src/modules/onchain/types.rs b/src/modules/onchain/types.rs index 1d5a80f..a6f6d85 100644 --- a/src/modules/onchain/types.rs +++ b/src/modules/onchain/types.rs @@ -554,6 +554,8 @@ pub enum WatcherEvent { /// wallet's perspective, and DB-valid timestamps, so the app can store them /// directly through the normal Core activity APIs (e.g. `upsert_activity` / /// `upsert_transaction_details`). The two vecs are parallel by `tx_id`. + /// `next_unused_external_address` is the synchronized wallet's current + /// external receive address. TransactionsChanged { activities: Vec, transaction_details: Vec, @@ -561,6 +563,7 @@ pub enum WatcherEvent { tx_count: u32, block_height: u32, account_type: AccountType, + next_unused_external_address: AddressInfo, }, /// An error occurred in the watcher loop. Error { message: String },