diff --git a/Cargo.lock b/Cargo.lock index c10100b..4da0e47 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -475,7 +475,7 @@ dependencies = [ [[package]] name = "bitkitcore" -version = "0.1.48" +version = "0.1.49" dependencies = [ "android_logger", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 3738dc5..62d926c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bitkitcore" -version = "0.1.48" +version = "0.1.49" edition = "2021" [lib] diff --git a/Package.swift b/Package.swift index 5a517ba..4edc843 100644 --- a/Package.swift +++ b/Package.swift @@ -3,8 +3,8 @@ import PackageDescription -let tag = "v0.1.48" -let checksum = "198887dcce722343abf54945765eff820b95871971cdf678c6db11f5b3e5a929" +let tag = "v0.1.49" +let checksum = "988388df24c4b06b8e542592787005def02d2e9a3c894545047e27a9f9d87da6" 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 32de70b..54b1b82 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.1.48 +version=0.1.49 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 f2a8c92..1b1cf2f 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 af5c024..11e89d8 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 a12bafb..3861f05 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 17adbcc..81cac3f 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 322161e..e06c91f 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 @@ -1398,6 +1398,10 @@ internal typealias UniffiVTableCallbackInterfaceTrezorUiCallbackUniffiByValue = + + + + @@ -1643,6 +1647,12 @@ internal object IntegrityCheckingUniffiLib : Library { if (uniffi_bitkitcore_checksum_func_onchain_get_address_info() != 4749.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } + if (uniffi_bitkitcore_checksum_func_onchain_get_transaction_detail() != 24151.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (uniffi_bitkitcore_checksum_func_onchain_get_transaction_history() != 4452.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } if (uniffi_bitkitcore_checksum_func_open_channel() != 21402.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } @@ -2058,6 +2068,12 @@ internal object IntegrityCheckingUniffiLib : Library { external fun uniffi_bitkitcore_checksum_func_onchain_get_address_info( ): Short @JvmStatic + external fun uniffi_bitkitcore_checksum_func_onchain_get_transaction_detail( + ): Short + @JvmStatic + external fun uniffi_bitkitcore_checksum_func_onchain_get_transaction_history( + ): Short + @JvmStatic external fun uniffi_bitkitcore_checksum_func_open_channel( ): Short @JvmStatic @@ -2761,6 +2777,21 @@ internal object UniffiLib : Library { `network`: RustBufferByValue, ): Long @JvmStatic + external fun uniffi_bitkitcore_fn_func_onchain_get_transaction_detail( + `extendedKey`: RustBufferByValue, + `electrumUrl`: RustBufferByValue, + `txid`: RustBufferByValue, + `network`: RustBufferByValue, + `scriptType`: RustBufferByValue, + ): Long + @JvmStatic + external fun uniffi_bitkitcore_fn_func_onchain_get_transaction_history( + `extendedKey`: RustBufferByValue, + `electrumUrl`: RustBufferByValue, + `network`: RustBufferByValue, + `scriptType`: RustBufferByValue, + ): Long + @JvmStatic external fun uniffi_bitkitcore_fn_func_open_channel( `orderId`: RustBufferByValue, `connectionString`: RustBufferByValue, @@ -4987,6 +5018,52 @@ public object FfiConverterTypeGetAddressesResponse: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): HistoryTransaction { + return HistoryTransaction( + FfiConverterString.read(buf), + FfiConverterULong.read(buf), + FfiConverterULong.read(buf), + FfiConverterLong.read(buf), + FfiConverterOptionalULong.read(buf), + FfiConverterULong.read(buf), + FfiConverterTypeTxDirection.read(buf), + FfiConverterOptionalUInt.read(buf), + FfiConverterOptionalULong.read(buf), + FfiConverterUInt.read(buf), + ) + } + + override fun allocationSize(value: HistoryTransaction): ULong = ( + FfiConverterString.allocationSize(value.`txid`) + + FfiConverterULong.allocationSize(value.`received`) + + FfiConverterULong.allocationSize(value.`sent`) + + FfiConverterLong.allocationSize(value.`net`) + + FfiConverterOptionalULong.allocationSize(value.`fee`) + + FfiConverterULong.allocationSize(value.`amount`) + + FfiConverterTypeTxDirection.allocationSize(value.`direction`) + + FfiConverterOptionalUInt.allocationSize(value.`blockHeight`) + + FfiConverterOptionalULong.allocationSize(value.`timestamp`) + + FfiConverterUInt.allocationSize(value.`confirmations`) + ) + + override fun write(value: HistoryTransaction, buf: ByteBuffer) { + FfiConverterString.write(value.`txid`, buf) + FfiConverterULong.write(value.`received`, buf) + FfiConverterULong.write(value.`sent`, buf) + FfiConverterLong.write(value.`net`, buf) + FfiConverterOptionalULong.write(value.`fee`, buf) + FfiConverterULong.write(value.`amount`, buf) + FfiConverterTypeTxDirection.write(value.`direction`, buf) + FfiConverterOptionalUInt.write(value.`blockHeight`, buf) + FfiConverterOptionalULong.write(value.`timestamp`, buf) + FfiConverterUInt.write(value.`confirmations`, buf) + } +} + + + + public object FfiConverterTypeIBt0ConfMinTxFeeWindow: FfiConverterRustBuffer { override fun read(buf: ByteBuffer): IBt0ConfMinTxFeeWindow { return IBt0ConfMinTxFeeWindow( @@ -6629,6 +6706,70 @@ public object FfiConverterTypeSweepableBalances: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): TransactionDetail { + return TransactionDetail( + FfiConverterString.read(buf), + FfiConverterULong.read(buf), + FfiConverterULong.read(buf), + FfiConverterLong.read(buf), + FfiConverterULong.read(buf), + FfiConverterOptionalULong.read(buf), + FfiConverterTypeTxDirection.read(buf), + FfiConverterOptionalUInt.read(buf), + FfiConverterOptionalULong.read(buf), + FfiConverterUInt.read(buf), + FfiConverterSequenceTypeTxDetailInput.read(buf), + FfiConverterSequenceTypeTxDetailOutput.read(buf), + FfiConverterUInt.read(buf), + FfiConverterUInt.read(buf), + FfiConverterUInt.read(buf), + FfiConverterOptionalDouble.read(buf), + ) + } + + override fun allocationSize(value: TransactionDetail): ULong = ( + FfiConverterString.allocationSize(value.`txid`) + + FfiConverterULong.allocationSize(value.`received`) + + FfiConverterULong.allocationSize(value.`sent`) + + FfiConverterLong.allocationSize(value.`net`) + + FfiConverterULong.allocationSize(value.`amount`) + + FfiConverterOptionalULong.allocationSize(value.`fee`) + + FfiConverterTypeTxDirection.allocationSize(value.`direction`) + + FfiConverterOptionalUInt.allocationSize(value.`blockHeight`) + + FfiConverterOptionalULong.allocationSize(value.`timestamp`) + + FfiConverterUInt.allocationSize(value.`confirmations`) + + FfiConverterSequenceTypeTxDetailInput.allocationSize(value.`inputs`) + + FfiConverterSequenceTypeTxDetailOutput.allocationSize(value.`outputs`) + + FfiConverterUInt.allocationSize(value.`size`) + + FfiConverterUInt.allocationSize(value.`vsize`) + + FfiConverterUInt.allocationSize(value.`weight`) + + FfiConverterOptionalDouble.allocationSize(value.`feeRate`) + ) + + override fun write(value: TransactionDetail, buf: ByteBuffer) { + FfiConverterString.write(value.`txid`, buf) + FfiConverterULong.write(value.`received`, buf) + FfiConverterULong.write(value.`sent`, buf) + FfiConverterLong.write(value.`net`, buf) + FfiConverterULong.write(value.`amount`, buf) + FfiConverterOptionalULong.write(value.`fee`, buf) + FfiConverterTypeTxDirection.write(value.`direction`, buf) + FfiConverterOptionalUInt.write(value.`blockHeight`, buf) + FfiConverterOptionalULong.write(value.`timestamp`, buf) + FfiConverterUInt.write(value.`confirmations`, buf) + FfiConverterSequenceTypeTxDetailInput.write(value.`inputs`, buf) + FfiConverterSequenceTypeTxDetailOutput.write(value.`outputs`, buf) + FfiConverterUInt.write(value.`size`, buf) + FfiConverterUInt.write(value.`vsize`, buf) + FfiConverterUInt.write(value.`weight`, buf) + FfiConverterOptionalDouble.write(value.`feeRate`, buf) + } +} + + + + public object FfiConverterTypeTransactionDetails: FfiConverterRustBuffer { override fun read(buf: ByteBuffer): TransactionDetails { return TransactionDetails( @@ -6657,6 +6798,37 @@ public object FfiConverterTypeTransactionDetails: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): TransactionHistoryResult { + return TransactionHistoryResult( + FfiConverterSequenceTypeHistoryTransaction.read(buf), + FfiConverterTypeWalletBalance.read(buf), + FfiConverterUInt.read(buf), + FfiConverterUInt.read(buf), + FfiConverterTypeAccountType.read(buf), + ) + } + + override fun allocationSize(value: TransactionHistoryResult): ULong = ( + FfiConverterSequenceTypeHistoryTransaction.allocationSize(value.`transactions`) + + FfiConverterTypeWalletBalance.allocationSize(value.`balance`) + + FfiConverterUInt.allocationSize(value.`txCount`) + + FfiConverterUInt.allocationSize(value.`blockHeight`) + + FfiConverterTypeAccountType.allocationSize(value.`accountType`) + ) + + override fun write(value: TransactionHistoryResult, buf: ByteBuffer) { + FfiConverterSequenceTypeHistoryTransaction.write(value.`transactions`, buf) + FfiConverterTypeWalletBalance.write(value.`balance`, buf) + FfiConverterUInt.write(value.`txCount`, buf) + FfiConverterUInt.write(value.`blockHeight`, buf) + FfiConverterTypeAccountType.write(value.`accountType`, buf) + } +} + + + + public object FfiConverterTypeTrezorAddressResponse: FfiConverterRustBuffer { override fun read(buf: ByteBuffer): TrezorAddressResponse { return TrezorAddressResponse( @@ -7222,6 +7394,65 @@ public object FfiConverterTypeTrezorVerifyMessageParams: FfiConverterRustBuffer< +public object FfiConverterTypeTxDetailInput: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): TxDetailInput { + return TxDetailInput( + FfiConverterString.read(buf), + FfiConverterUInt.read(buf), + FfiConverterUInt.read(buf), + FfiConverterString.read(buf), + FfiConverterSequenceString.read(buf), + ) + } + + override fun allocationSize(value: TxDetailInput): ULong = ( + FfiConverterString.allocationSize(value.`txid`) + + FfiConverterUInt.allocationSize(value.`vout`) + + FfiConverterUInt.allocationSize(value.`sequence`) + + FfiConverterString.allocationSize(value.`scriptSig`) + + FfiConverterSequenceString.allocationSize(value.`witness`) + ) + + override fun write(value: TxDetailInput, buf: ByteBuffer) { + FfiConverterString.write(value.`txid`, buf) + FfiConverterUInt.write(value.`vout`, buf) + FfiConverterUInt.write(value.`sequence`, buf) + FfiConverterString.write(value.`scriptSig`, buf) + FfiConverterSequenceString.write(value.`witness`, buf) + } +} + + + + +public object FfiConverterTypeTxDetailOutput: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): TxDetailOutput { + return TxDetailOutput( + FfiConverterULong.read(buf), + FfiConverterString.read(buf), + FfiConverterOptionalString.read(buf), + FfiConverterBoolean.read(buf), + ) + } + + override fun allocationSize(value: TxDetailOutput): ULong = ( + FfiConverterULong.allocationSize(value.`value`) + + FfiConverterString.allocationSize(value.`scriptPubkey`) + + FfiConverterOptionalString.allocationSize(value.`address`) + + FfiConverterBoolean.allocationSize(value.`isMine`) + ) + + override fun write(value: TxDetailOutput, buf: ByteBuffer) { + FfiConverterULong.write(value.`value`, buf) + FfiConverterString.write(value.`scriptPubkey`, buf) + FfiConverterOptionalString.write(value.`address`, buf) + FfiConverterBoolean.write(value.`isMine`, buf) + } +} + + + + public object FfiConverterTypeTxInput: FfiConverterRustBuffer { override fun read(buf: ByteBuffer): TxInput { return TxInput( @@ -7309,6 +7540,40 @@ public object FfiConverterTypeValidationResult: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): WalletBalance { + return WalletBalance( + FfiConverterULong.read(buf), + FfiConverterULong.read(buf), + FfiConverterULong.read(buf), + FfiConverterULong.read(buf), + FfiConverterULong.read(buf), + FfiConverterULong.read(buf), + ) + } + + override fun allocationSize(value: WalletBalance): ULong = ( + FfiConverterULong.allocationSize(value.`confirmed`) + + FfiConverterULong.allocationSize(value.`immature`) + + FfiConverterULong.allocationSize(value.`trustedPending`) + + FfiConverterULong.allocationSize(value.`untrustedPending`) + + FfiConverterULong.allocationSize(value.`spendable`) + + FfiConverterULong.allocationSize(value.`total`) + ) + + override fun write(value: WalletBalance, buf: ByteBuffer) { + FfiConverterULong.write(value.`confirmed`, buf) + FfiConverterULong.write(value.`immature`, buf) + FfiConverterULong.write(value.`trustedPending`, buf) + FfiConverterULong.write(value.`untrustedPending`, buf) + FfiConverterULong.write(value.`spendable`, buf) + FfiConverterULong.write(value.`total`, buf) + } +} + + + + public object FfiConverterTypeWalletParams: FfiConverterRustBuffer { override fun read(buf: ByteBuffer): WalletParams { return WalletParams( @@ -9411,6 +9676,24 @@ public object FfiConverterTypeTrezorTransportType: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): TxDirection = try { + TxDirection.entries[buf.getInt() - 1] + } catch (e: IndexOutOfBoundsException) { + throw RuntimeException("invalid enum value, something is very wrong!!", e) + } + + override fun allocationSize(value: TxDirection): ULong = 4UL + + override fun write(value: TxDirection, buf: ByteBuffer) { + buf.putInt(value.ordinal + 1) + } +} + + + + + public object FfiConverterTypeWordCount: FfiConverterRustBuffer { override fun read(buf: ByteBuffer): WordCount = try { WordCount.entries[buf.getInt() - 1] @@ -9515,6 +9798,35 @@ public object FfiConverterOptionalULong: FfiConverterRustBuffer { +public object FfiConverterOptionalDouble: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): kotlin.Double? { + if (buf.get().toInt() == 0) { + return null + } + return FfiConverterDouble.read(buf) + } + + override fun allocationSize(value: kotlin.Double?): ULong { + if (value == null) { + return 1UL + } else { + return 1UL + FfiConverterDouble.allocationSize(value) + } + } + + override fun write(value: kotlin.Double?, buf: ByteBuffer) { + if (value == null) { + buf.put(0) + } else { + buf.put(1) + FfiConverterDouble.write(value, buf) + } + } +} + + + + public object FfiConverterOptionalBoolean: FfiConverterRustBuffer { override fun read(buf: ByteBuffer): kotlin.Boolean? { if (buf.get().toInt() == 0) { @@ -10937,6 +11249,31 @@ public object FfiConverterSequenceTypeGetAddressResponse: FfiConverterRustBuffer +public object FfiConverterSequenceTypeHistoryTransaction: FfiConverterRustBuffer> { + override fun read(buf: ByteBuffer): List { + val len = buf.getInt() + return List(len) { + FfiConverterTypeHistoryTransaction.read(buf) + } + } + + override fun allocationSize(value: List): ULong { + val sizeForLength = 4UL + val sizeForItems = value.sumOf { FfiConverterTypeHistoryTransaction.allocationSize(it) } + return sizeForLength + sizeForItems + } + + override fun write(value: List, buf: ByteBuffer) { + buf.putInt(value.size) + value.iterator().forEach { + FfiConverterTypeHistoryTransaction.write(it, buf) + } + } +} + + + + public object FfiConverterSequenceTypeIBtOnchainTransaction: FfiConverterRustBuffer> { override fun read(buf: ByteBuffer): List { val len = buf.getInt() @@ -11362,6 +11699,56 @@ public object FfiConverterSequenceTypeTrezorTxOutput: FfiConverterRustBuffer> { + override fun read(buf: ByteBuffer): List { + val len = buf.getInt() + return List(len) { + FfiConverterTypeTxDetailInput.read(buf) + } + } + + override fun allocationSize(value: List): ULong { + val sizeForLength = 4UL + val sizeForItems = value.sumOf { FfiConverterTypeTxDetailInput.allocationSize(it) } + return sizeForLength + sizeForItems + } + + override fun write(value: List, buf: ByteBuffer) { + buf.putInt(value.size) + value.iterator().forEach { + FfiConverterTypeTxDetailInput.write(it, buf) + } + } +} + + + + +public object FfiConverterSequenceTypeTxDetailOutput: FfiConverterRustBuffer> { + override fun read(buf: ByteBuffer): List { + val len = buf.getInt() + return List(len) { + FfiConverterTypeTxDetailOutput.read(buf) + } + } + + override fun allocationSize(value: List): ULong { + val sizeForLength = 4UL + val sizeForItems = value.sumOf { FfiConverterTypeTxDetailOutput.allocationSize(it) } + return sizeForLength + sizeForItems + } + + override fun write(value: List, buf: ByteBuffer) { + buf.putInt(value.size) + value.iterator().forEach { + FfiConverterTypeTxDetailOutput.write(it, buf) + } + } +} + + + + public object FfiConverterSequenceTypeTxInput: FfiConverterRustBuffer> { override fun read(buf: ByteBuffer): List { val len = buf.getInt() @@ -12479,6 +12866,53 @@ public suspend fun `onchainGetAddressInfo`(`address`: kotlin.String, `electrumUr ) } +/** + * Get full details for a single transaction by txid. + */ +@Throws(AccountInfoException::class, kotlin.coroutines.cancellation.CancellationException::class) +public suspend fun `onchainGetTransactionDetail`(`extendedKey`: kotlin.String, `electrumUrl`: kotlin.String, `txid`: kotlin.String, `network`: Network?, `scriptType`: AccountType?): TransactionDetail { + return uniffiRustCallAsync( + UniffiLib.uniffi_bitkitcore_fn_func_onchain_get_transaction_detail( + FfiConverterString.lower(`extendedKey`), + FfiConverterString.lower(`electrumUrl`), + FfiConverterString.lower(`txid`), + FfiConverterOptionalTypeNetwork.lower(`network`), + FfiConverterOptionalTypeAccountType.lower(`scriptType`), + ), + { future, callback, continuation -> UniffiLib.ffi_bitkitcore_rust_future_poll_rust_buffer(future, callback, continuation) }, + { future, continuation -> UniffiLib.ffi_bitkitcore_rust_future_complete_rust_buffer(future, continuation) }, + { future -> UniffiLib.ffi_bitkitcore_rust_future_free_rust_buffer(future) }, + { future -> UniffiLib.ffi_bitkitcore_rust_future_cancel_rust_buffer(future) }, + // lift function + { FfiConverterTypeTransactionDetail.lift(it) }, + // Error FFI converter + AccountInfoExceptionErrorHandler, + ) +} + +/** + * Query transaction history and balance for an extended public key via Electrum. + */ +@Throws(AccountInfoException::class, kotlin.coroutines.cancellation.CancellationException::class) +public suspend fun `onchainGetTransactionHistory`(`extendedKey`: kotlin.String, `electrumUrl`: kotlin.String, `network`: Network?, `scriptType`: AccountType?): TransactionHistoryResult { + return uniffiRustCallAsync( + UniffiLib.uniffi_bitkitcore_fn_func_onchain_get_transaction_history( + FfiConverterString.lower(`extendedKey`), + FfiConverterString.lower(`electrumUrl`), + FfiConverterOptionalTypeNetwork.lower(`network`), + FfiConverterOptionalTypeAccountType.lower(`scriptType`), + ), + { future, callback, continuation -> UniffiLib.ffi_bitkitcore_rust_future_poll_rust_buffer(future, callback, continuation) }, + { future, continuation -> UniffiLib.ffi_bitkitcore_rust_future_complete_rust_buffer(future, continuation) }, + { future -> UniffiLib.ffi_bitkitcore_rust_future_free_rust_buffer(future) }, + { future -> UniffiLib.ffi_bitkitcore_rust_future_cancel_rust_buffer(future) }, + // lift function + { FfiConverterTypeTransactionHistoryResult.lift(it) }, + // Error FFI converter + AccountInfoExceptionErrorHandler, + ) +} + @Throws(BlocktankException::class, kotlin.coroutines.cancellation.CancellationException::class) public suspend fun `openChannel`(`orderId`: kotlin.String, `connectionString`: kotlin.String): IBtOrder { return uniffiRustCallAsync( 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 7379bbd..4305ed7 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 @@ -621,6 +621,60 @@ public data class GetAddressesResponse ( +/** + * A single transaction in the wallet's history. + */ +@kotlinx.serialization.Serializable +public data class HistoryTransaction ( + /** + * Transaction ID (hex) + */ + val `txid`: kotlin.String, + /** + * Amount received by the wallet (sats) + */ + val `received`: kotlin.ULong, + /** + * Amount sent by the wallet (sats) — includes change sent back to self + */ + val `sent`: kotlin.ULong, + /** + * Net value from wallet's perspective: received - sent (positive = inflow, negative = outflow) + */ + val `net`: kotlin.Long, + /** + * Transaction fee in sats (None if not available, e.g. for received-only txs) + */ + val `fee`: kotlin.ULong?, + /** + * Display amount in sats: + * - Received: the received value + * - Sent: amount that left the wallet (sent - received - fee) + * - SelfTransfer: the fee paid + */ + val `amount`: kotlin.ULong, + /** + * Transaction direction + */ + val `direction`: TxDirection, + /** + * Block height (None if unconfirmed/mempool) + */ + val `blockHeight`: kotlin.UInt?, + /** + * Block timestamp as unix epoch seconds (None if unconfirmed) + */ + val `timestamp`: kotlin.ULong?, + /** + * Number of confirmations (0 if unconfirmed) + */ + val `confirmations`: kotlin.UInt +) { + public companion object +} + + + @kotlinx.serialization.Serializable public data class IBt0ConfMinTxFeeWindow ( val `satPerVbyte`: kotlin.Double, @@ -1438,6 +1492,81 @@ public data class SweepableBalances ( +/** + * Full details for a single transaction, including raw inputs/outputs and size metrics. + */ +@kotlinx.serialization.Serializable +public data class TransactionDetail ( + /** + * Transaction ID (hex) + */ + val `txid`: kotlin.String, + /** + * Amount received by the wallet (sats) + */ + val `received`: kotlin.ULong, + /** + * Amount sent by the wallet (sats) — includes change sent back to self + */ + val `sent`: kotlin.ULong, + /** + * Net value from wallet's perspective: received - sent (positive = inflow, negative = outflow) + */ + val `net`: kotlin.Long, + /** + * Display amount in sats (same semantics as HistoryTransaction.amount) + */ + val `amount`: kotlin.ULong, + /** + * Transaction fee in sats (None if not available) + */ + val `fee`: kotlin.ULong?, + /** + * Transaction direction + */ + val `direction`: TxDirection, + /** + * Block height (None if unconfirmed/mempool) + */ + val `blockHeight`: kotlin.UInt?, + /** + * Block timestamp as unix epoch seconds (None if unconfirmed) + */ + val `timestamp`: kotlin.ULong?, + /** + * Number of confirmations (0 if unconfirmed) + */ + val `confirmations`: kotlin.UInt, + /** + * Transaction inputs + */ + val `inputs`: List, + /** + * Transaction outputs + */ + val `outputs`: List, + /** + * Serialized transaction size in bytes + */ + val `size`: kotlin.UInt, + /** + * Virtual size in vbytes (ceil(weight / 4)) + */ + val `vsize`: kotlin.UInt, + /** + * Transaction weight in weight units + */ + val `weight`: kotlin.UInt, + /** + * Fee rate in sat/vB (fee / vsize), None if fee or vsize unavailable + */ + val `feeRate`: kotlin.Double? +) { + public companion object +} + + + /** * Details about an onchain transaction. */ @@ -1470,6 +1599,37 @@ public data class TransactionDetails ( +/** + * Result from querying transaction history for an xpub. + */ +@kotlinx.serialization.Serializable +public data class TransactionHistoryResult ( + /** + * All transactions, sorted: unconfirmed first, then by timestamp descending + */ + val `transactions`: List, + /** + * Balance breakdown + */ + val `balance`: WalletBalance, + /** + * Total number of transactions + */ + val `txCount`: kotlin.UInt, + /** + * Current blockchain tip height + */ + val `blockHeight`: kotlin.UInt, + /** + * The detected or specified account type + */ + val `accountType`: AccountType +) { + public companion object +} + + + /** * Address response from device. */ @@ -2063,6 +2223,64 @@ public data class TrezorVerifyMessageParams ( +/** + * A transaction input with full details. + */ +@kotlinx.serialization.Serializable +public data class TxDetailInput ( + /** + * Previous output transaction ID (hex) + */ + val `txid`: kotlin.String, + /** + * Previous output index + */ + val `vout`: kotlin.UInt, + /** + * Sequence number + */ + val `sequence`: kotlin.UInt, + /** + * Script signature (hex-encoded) + */ + val `scriptSig`: kotlin.String, + /** + * Witness stack (each element hex-encoded) + */ + val `witness`: List +) { + public companion object +} + + + +/** + * A transaction output with full details. + */ +@kotlinx.serialization.Serializable +public data class TxDetailOutput ( + /** + * Output value in sats + */ + val `value`: kotlin.ULong, + /** + * Script public key (hex-encoded) + */ + val `scriptPubkey`: kotlin.String, + /** + * Decoded address (None if script is not decodable to an address) + */ + val `address`: kotlin.String?, + /** + * Whether this output belongs to the queried wallet + */ + val `isMine`: kotlin.Boolean +) { + public companion object +} + + + /** * Details about a transaction input. */ @@ -2136,6 +2354,41 @@ public data class ValidationResult ( +/** + * Balance breakdown from BDK. + */ +@kotlinx.serialization.Serializable +public data class WalletBalance ( + /** + * Confirmed and spendable balance (sats) + */ + val `confirmed`: kotlin.ULong, + /** + * Immature coinbase outputs (sats) + */ + val `immature`: kotlin.ULong, + /** + * Unconfirmed UTXOs from trusted sources (own change) (sats) + */ + val `trustedPending`: kotlin.ULong, + /** + * Unconfirmed UTXOs from external sources (sats) + */ + val `untrustedPending`: kotlin.ULong, + /** + * Total spendable: confirmed + trusted_pending (sats) + */ + val `spendable`: kotlin.ULong, + /** + * Grand total: all categories (sats) + */ + val `total`: kotlin.ULong +) { + public companion object +} + + + /** * Common parameters for creating and syncing a watch-only BDK wallet. */ @@ -3554,6 +3807,33 @@ public enum class TrezorTransportType { +/** + * Transaction direction from the wallet's perspective. + */ + +@kotlinx.serialization.Serializable +public enum class TxDirection { + + /** + * Wallet sent funds to an external address + */ + SENT, + /** + * Wallet received funds from an external source + */ + RECEIVED, + /** + * Wallet sent funds to itself (e.g. consolidation, change-only) + */ + SELF_TRANSFER; + public companion object +} + + + + + + @kotlinx.serialization.Serializable public enum class WordCount { @@ -3726,6 +4006,14 @@ public enum class WordCount { + + + + + + + + diff --git a/bindings/ios/BitkitCore.xcframework.zip b/bindings/ios/BitkitCore.xcframework.zip index 39e6b69..795118d 100644 Binary files a/bindings/ios/BitkitCore.xcframework.zip and b/bindings/ios/BitkitCore.xcframework.zip differ diff --git a/bindings/ios/BitkitCore.xcframework/ios-arm64-simulator/Headers/bitkitcoreFFI.h b/bindings/ios/BitkitCore.xcframework/ios-arm64-simulator/Headers/bitkitcoreFFI.h index be0b90c..9d5af34 100644 --- a/bindings/ios/BitkitCore.xcframework/ios-arm64-simulator/Headers/bitkitcoreFFI.h +++ b/bindings/ios/BitkitCore.xcframework/ios-arm64-simulator/Headers/bitkitcoreFFI.h @@ -810,6 +810,16 @@ uint64_t uniffi_bitkitcore_fn_func_onchain_get_account_info(RustBuffer extended_ uint64_t uniffi_bitkitcore_fn_func_onchain_get_address_info(RustBuffer address, RustBuffer electrum_url, RustBuffer network ); #endif +#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_ONCHAIN_GET_TRANSACTION_DETAIL +#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_ONCHAIN_GET_TRANSACTION_DETAIL +uint64_t uniffi_bitkitcore_fn_func_onchain_get_transaction_detail(RustBuffer extended_key, RustBuffer electrum_url, RustBuffer txid, RustBuffer network, RustBuffer script_type +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_ONCHAIN_GET_TRANSACTION_HISTORY +#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_ONCHAIN_GET_TRANSACTION_HISTORY +uint64_t uniffi_bitkitcore_fn_func_onchain_get_transaction_history(RustBuffer extended_key, RustBuffer electrum_url, RustBuffer network, RustBuffer script_type +); +#endif #ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_OPEN_CHANNEL #define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_OPEN_CHANNEL uint64_t uniffi_bitkitcore_fn_func_open_channel(RustBuffer order_id, RustBuffer connection_string @@ -1789,6 +1799,18 @@ uint16_t uniffi_bitkitcore_checksum_func_onchain_get_account_info(void #define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_ONCHAIN_GET_ADDRESS_INFO uint16_t uniffi_bitkitcore_checksum_func_onchain_get_address_info(void +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_ONCHAIN_GET_TRANSACTION_DETAIL +#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_ONCHAIN_GET_TRANSACTION_DETAIL +uint16_t uniffi_bitkitcore_checksum_func_onchain_get_transaction_detail(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_ONCHAIN_GET_TRANSACTION_HISTORY +#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_ONCHAIN_GET_TRANSACTION_HISTORY +uint16_t uniffi_bitkitcore_checksum_func_onchain_get_transaction_history(void + ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_OPEN_CHANNEL diff --git a/bindings/ios/BitkitCore.xcframework/ios-arm64-simulator/libbitkitcore.a b/bindings/ios/BitkitCore.xcframework/ios-arm64-simulator/libbitkitcore.a index 6ca7957..d3671ec 100644 Binary files a/bindings/ios/BitkitCore.xcframework/ios-arm64-simulator/libbitkitcore.a and b/bindings/ios/BitkitCore.xcframework/ios-arm64-simulator/libbitkitcore.a differ diff --git a/bindings/ios/BitkitCore.xcframework/ios-arm64/Headers/bitkitcoreFFI.h b/bindings/ios/BitkitCore.xcframework/ios-arm64/Headers/bitkitcoreFFI.h index be0b90c..9d5af34 100644 --- a/bindings/ios/BitkitCore.xcframework/ios-arm64/Headers/bitkitcoreFFI.h +++ b/bindings/ios/BitkitCore.xcframework/ios-arm64/Headers/bitkitcoreFFI.h @@ -810,6 +810,16 @@ uint64_t uniffi_bitkitcore_fn_func_onchain_get_account_info(RustBuffer extended_ uint64_t uniffi_bitkitcore_fn_func_onchain_get_address_info(RustBuffer address, RustBuffer electrum_url, RustBuffer network ); #endif +#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_ONCHAIN_GET_TRANSACTION_DETAIL +#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_ONCHAIN_GET_TRANSACTION_DETAIL +uint64_t uniffi_bitkitcore_fn_func_onchain_get_transaction_detail(RustBuffer extended_key, RustBuffer electrum_url, RustBuffer txid, RustBuffer network, RustBuffer script_type +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_ONCHAIN_GET_TRANSACTION_HISTORY +#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_ONCHAIN_GET_TRANSACTION_HISTORY +uint64_t uniffi_bitkitcore_fn_func_onchain_get_transaction_history(RustBuffer extended_key, RustBuffer electrum_url, RustBuffer network, RustBuffer script_type +); +#endif #ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_OPEN_CHANNEL #define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_OPEN_CHANNEL uint64_t uniffi_bitkitcore_fn_func_open_channel(RustBuffer order_id, RustBuffer connection_string @@ -1789,6 +1799,18 @@ uint16_t uniffi_bitkitcore_checksum_func_onchain_get_account_info(void #define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_ONCHAIN_GET_ADDRESS_INFO uint16_t uniffi_bitkitcore_checksum_func_onchain_get_address_info(void +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_ONCHAIN_GET_TRANSACTION_DETAIL +#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_ONCHAIN_GET_TRANSACTION_DETAIL +uint16_t uniffi_bitkitcore_checksum_func_onchain_get_transaction_detail(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_ONCHAIN_GET_TRANSACTION_HISTORY +#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_ONCHAIN_GET_TRANSACTION_HISTORY +uint16_t uniffi_bitkitcore_checksum_func_onchain_get_transaction_history(void + ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_OPEN_CHANNEL diff --git a/bindings/ios/BitkitCore.xcframework/ios-arm64/libbitkitcore.a b/bindings/ios/BitkitCore.xcframework/ios-arm64/libbitkitcore.a index 234efe0..aba67b5 100644 Binary files a/bindings/ios/BitkitCore.xcframework/ios-arm64/libbitkitcore.a and b/bindings/ios/BitkitCore.xcframework/ios-arm64/libbitkitcore.a differ diff --git a/bindings/ios/bitkitcore.swift b/bindings/ios/bitkitcore.swift index 2f92f67..ce82cbb 100644 --- a/bindings/ios/bitkitcore.swift +++ b/bindings/ios/bitkitcore.swift @@ -3430,6 +3430,211 @@ public func FfiConverterTypeGetAddressesResponse_lower(_ value: GetAddressesResp } +/** + * A single transaction in the wallet's history. + */ +public struct HistoryTransaction { + /** + * Transaction ID (hex) + */ + public var txid: String + /** + * Amount received by the wallet (sats) + */ + public var received: UInt64 + /** + * Amount sent by the wallet (sats) — includes change sent back to self + */ + public var sent: UInt64 + /** + * Net value from wallet's perspective: received - sent (positive = inflow, negative = outflow) + */ + public var net: Int64 + /** + * Transaction fee in sats (None if not available, e.g. for received-only txs) + */ + public var fee: UInt64? + /** + * Display amount in sats: + * - Received: the received value + * - Sent: amount that left the wallet (sent - received - fee) + * - SelfTransfer: the fee paid + */ + public var amount: UInt64 + /** + * Transaction direction + */ + public var direction: TxDirection + /** + * Block height (None if unconfirmed/mempool) + */ + public var blockHeight: UInt32? + /** + * Block timestamp as unix epoch seconds (None if unconfirmed) + */ + public var timestamp: UInt64? + /** + * Number of confirmations (0 if unconfirmed) + */ + public var confirmations: UInt32 + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init( + /** + * Transaction ID (hex) + */txid: String, + /** + * Amount received by the wallet (sats) + */received: UInt64, + /** + * Amount sent by the wallet (sats) — includes change sent back to self + */sent: UInt64, + /** + * Net value from wallet's perspective: received - sent (positive = inflow, negative = outflow) + */net: Int64, + /** + * Transaction fee in sats (None if not available, e.g. for received-only txs) + */fee: UInt64?, + /** + * Display amount in sats: + * - Received: the received value + * - Sent: amount that left the wallet (sent - received - fee) + * - SelfTransfer: the fee paid + */amount: UInt64, + /** + * Transaction direction + */direction: TxDirection, + /** + * Block height (None if unconfirmed/mempool) + */blockHeight: UInt32?, + /** + * Block timestamp as unix epoch seconds (None if unconfirmed) + */timestamp: UInt64?, + /** + * Number of confirmations (0 if unconfirmed) + */confirmations: UInt32) { + self.txid = txid + self.received = received + self.sent = sent + self.net = net + self.fee = fee + self.amount = amount + self.direction = direction + self.blockHeight = blockHeight + self.timestamp = timestamp + self.confirmations = confirmations + } +} + +#if compiler(>=6) +extension HistoryTransaction: Sendable {} +#endif + + +extension HistoryTransaction: Equatable, Hashable { + public static func ==(lhs: HistoryTransaction, rhs: HistoryTransaction) -> Bool { + if lhs.txid != rhs.txid { + return false + } + if lhs.received != rhs.received { + return false + } + if lhs.sent != rhs.sent { + return false + } + if lhs.net != rhs.net { + return false + } + if lhs.fee != rhs.fee { + return false + } + if lhs.amount != rhs.amount { + return false + } + if lhs.direction != rhs.direction { + return false + } + if lhs.blockHeight != rhs.blockHeight { + return false + } + if lhs.timestamp != rhs.timestamp { + return false + } + if lhs.confirmations != rhs.confirmations { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(txid) + hasher.combine(received) + hasher.combine(sent) + hasher.combine(net) + hasher.combine(fee) + hasher.combine(amount) + hasher.combine(direction) + hasher.combine(blockHeight) + hasher.combine(timestamp) + hasher.combine(confirmations) + } +} + +extension HistoryTransaction: Codable {} + + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeHistoryTransaction: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> HistoryTransaction { + return + try HistoryTransaction( + txid: FfiConverterString.read(from: &buf), + received: FfiConverterUInt64.read(from: &buf), + sent: FfiConverterUInt64.read(from: &buf), + net: FfiConverterInt64.read(from: &buf), + fee: FfiConverterOptionUInt64.read(from: &buf), + amount: FfiConverterUInt64.read(from: &buf), + direction: FfiConverterTypeTxDirection.read(from: &buf), + blockHeight: FfiConverterOptionUInt32.read(from: &buf), + timestamp: FfiConverterOptionUInt64.read(from: &buf), + confirmations: FfiConverterUInt32.read(from: &buf) + ) + } + + public static func write(_ value: HistoryTransaction, into buf: inout [UInt8]) { + FfiConverterString.write(value.txid, into: &buf) + FfiConverterUInt64.write(value.received, into: &buf) + FfiConverterUInt64.write(value.sent, into: &buf) + FfiConverterInt64.write(value.net, into: &buf) + FfiConverterOptionUInt64.write(value.fee, into: &buf) + FfiConverterUInt64.write(value.amount, into: &buf) + FfiConverterTypeTxDirection.write(value.direction, into: &buf) + FfiConverterOptionUInt32.write(value.blockHeight, into: &buf) + FfiConverterOptionUInt64.write(value.timestamp, into: &buf) + FfiConverterUInt32.write(value.confirmations, into: &buf) + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeHistoryTransaction_lift(_ buf: RustBuffer) throws -> HistoryTransaction { + return try FfiConverterTypeHistoryTransaction.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeHistoryTransaction_lower(_ value: HistoryTransaction) -> RustBuffer { + return FfiConverterTypeHistoryTransaction.lower(value) +} + + public struct IBt0ConfMinTxFeeWindow { public var satPerVbyte: Double public var validityEndsAt: String @@ -8563,69 +8768,179 @@ public func FfiConverterTypeSweepableBalances_lower(_ value: SweepableBalances) /** - * Details about an onchain transaction. + * Full details for a single transaction, including raw inputs/outputs and size metrics. */ -public struct TransactionDetails { +public struct TransactionDetail { /** - * The transaction ID. + * Transaction ID (hex) */ - public var txId: String + public var txid: String /** - * The net amount in this transaction (in satoshis). - * - * This is calculated as: (received - sent). For incoming payments, - * this will be positive. For outgoing payments, this will be negative. - * - * Note: This amount does NOT include transaction fees. + * Amount received by the wallet (sats) */ - public var amountSats: Int64 + public var received: UInt64 /** - * The transaction inputs with full details. + * Amount sent by the wallet (sats) — includes change sent back to self */ - public var inputs: [TxInput] + public var sent: UInt64 /** - * The transaction outputs with full details. + * Net value from wallet's perspective: received - sent (positive = inflow, negative = outflow) */ - public var outputs: [TxOutput] + public var net: Int64 + /** + * Display amount in sats (same semantics as HistoryTransaction.amount) + */ + public var amount: UInt64 + /** + * Transaction fee in sats (None if not available) + */ + public var fee: UInt64? + /** + * Transaction direction + */ + public var direction: TxDirection + /** + * Block height (None if unconfirmed/mempool) + */ + public var blockHeight: UInt32? + /** + * Block timestamp as unix epoch seconds (None if unconfirmed) + */ + public var timestamp: UInt64? + /** + * Number of confirmations (0 if unconfirmed) + */ + public var confirmations: UInt32 + /** + * Transaction inputs + */ + public var inputs: [TxDetailInput] + /** + * Transaction outputs + */ + public var outputs: [TxDetailOutput] + /** + * Serialized transaction size in bytes + */ + public var size: UInt32 + /** + * Virtual size in vbytes (ceil(weight / 4)) + */ + public var vsize: UInt32 + /** + * Transaction weight in weight units + */ + public var weight: UInt32 + /** + * Fee rate in sat/vB (fee / vsize), None if fee or vsize unavailable + */ + public var feeRate: Double? // Default memberwise initializers are never public by default, so we // declare one manually. public init( /** - * The transaction ID. - */txId: String, + * Transaction ID (hex) + */txid: String, /** - * The net amount in this transaction (in satoshis). - * - * This is calculated as: (received - sent). For incoming payments, - * this will be positive. For outgoing payments, this will be negative. - * - * Note: This amount does NOT include transaction fees. - */amountSats: Int64, + * Amount received by the wallet (sats) + */received: UInt64, /** - * The transaction inputs with full details. - */inputs: [TxInput], + * Amount sent by the wallet (sats) — includes change sent back to self + */sent: UInt64, /** - * The transaction outputs with full details. - */outputs: [TxOutput]) { - self.txId = txId - self.amountSats = amountSats + * Net value from wallet's perspective: received - sent (positive = inflow, negative = outflow) + */net: Int64, + /** + * Display amount in sats (same semantics as HistoryTransaction.amount) + */amount: UInt64, + /** + * Transaction fee in sats (None if not available) + */fee: UInt64?, + /** + * Transaction direction + */direction: TxDirection, + /** + * Block height (None if unconfirmed/mempool) + */blockHeight: UInt32?, + /** + * Block timestamp as unix epoch seconds (None if unconfirmed) + */timestamp: UInt64?, + /** + * Number of confirmations (0 if unconfirmed) + */confirmations: UInt32, + /** + * Transaction inputs + */inputs: [TxDetailInput], + /** + * Transaction outputs + */outputs: [TxDetailOutput], + /** + * Serialized transaction size in bytes + */size: UInt32, + /** + * Virtual size in vbytes (ceil(weight / 4)) + */vsize: UInt32, + /** + * Transaction weight in weight units + */weight: UInt32, + /** + * Fee rate in sat/vB (fee / vsize), None if fee or vsize unavailable + */feeRate: Double?) { + self.txid = txid + self.received = received + self.sent = sent + self.net = net + self.amount = amount + self.fee = fee + self.direction = direction + self.blockHeight = blockHeight + self.timestamp = timestamp + self.confirmations = confirmations self.inputs = inputs self.outputs = outputs + self.size = size + self.vsize = vsize + self.weight = weight + self.feeRate = feeRate } } #if compiler(>=6) -extension TransactionDetails: Sendable {} +extension TransactionDetail: Sendable {} #endif -extension TransactionDetails: Equatable, Hashable { - public static func ==(lhs: TransactionDetails, rhs: TransactionDetails) -> Bool { - if lhs.txId != rhs.txId { +extension TransactionDetail: Equatable, Hashable { + public static func ==(lhs: TransactionDetail, rhs: TransactionDetail) -> Bool { + if lhs.txid != rhs.txid { return false } - if lhs.amountSats != rhs.amountSats { + if lhs.received != rhs.received { + return false + } + if lhs.sent != rhs.sent { + return false + } + if lhs.net != rhs.net { + return false + } + if lhs.amount != rhs.amount { + return false + } + if lhs.fee != rhs.fee { + return false + } + if lhs.direction != rhs.direction { + return false + } + if lhs.blockHeight != rhs.blockHeight { + return false + } + if lhs.timestamp != rhs.timestamp { + return false + } + if lhs.confirmations != rhs.confirmations { return false } if lhs.inputs != rhs.inputs { @@ -8634,13 +8949,186 @@ extension TransactionDetails: Equatable, Hashable { if lhs.outputs != rhs.outputs { return false } - return true - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(txId) - hasher.combine(amountSats) - hasher.combine(inputs) + if lhs.size != rhs.size { + return false + } + if lhs.vsize != rhs.vsize { + return false + } + if lhs.weight != rhs.weight { + return false + } + if lhs.feeRate != rhs.feeRate { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(txid) + hasher.combine(received) + hasher.combine(sent) + hasher.combine(net) + hasher.combine(amount) + hasher.combine(fee) + hasher.combine(direction) + hasher.combine(blockHeight) + hasher.combine(timestamp) + hasher.combine(confirmations) + hasher.combine(inputs) + hasher.combine(outputs) + hasher.combine(size) + hasher.combine(vsize) + hasher.combine(weight) + hasher.combine(feeRate) + } +} + +extension TransactionDetail: Codable {} + + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeTransactionDetail: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> TransactionDetail { + return + try TransactionDetail( + txid: FfiConverterString.read(from: &buf), + received: FfiConverterUInt64.read(from: &buf), + sent: FfiConverterUInt64.read(from: &buf), + net: FfiConverterInt64.read(from: &buf), + amount: FfiConverterUInt64.read(from: &buf), + fee: FfiConverterOptionUInt64.read(from: &buf), + direction: FfiConverterTypeTxDirection.read(from: &buf), + blockHeight: FfiConverterOptionUInt32.read(from: &buf), + timestamp: FfiConverterOptionUInt64.read(from: &buf), + confirmations: FfiConverterUInt32.read(from: &buf), + inputs: FfiConverterSequenceTypeTxDetailInput.read(from: &buf), + outputs: FfiConverterSequenceTypeTxDetailOutput.read(from: &buf), + size: FfiConverterUInt32.read(from: &buf), + vsize: FfiConverterUInt32.read(from: &buf), + weight: FfiConverterUInt32.read(from: &buf), + feeRate: FfiConverterOptionDouble.read(from: &buf) + ) + } + + public static func write(_ value: TransactionDetail, into buf: inout [UInt8]) { + FfiConverterString.write(value.txid, into: &buf) + FfiConverterUInt64.write(value.received, into: &buf) + FfiConverterUInt64.write(value.sent, into: &buf) + FfiConverterInt64.write(value.net, into: &buf) + FfiConverterUInt64.write(value.amount, into: &buf) + FfiConverterOptionUInt64.write(value.fee, into: &buf) + FfiConverterTypeTxDirection.write(value.direction, into: &buf) + FfiConverterOptionUInt32.write(value.blockHeight, into: &buf) + FfiConverterOptionUInt64.write(value.timestamp, into: &buf) + FfiConverterUInt32.write(value.confirmations, into: &buf) + FfiConverterSequenceTypeTxDetailInput.write(value.inputs, into: &buf) + FfiConverterSequenceTypeTxDetailOutput.write(value.outputs, into: &buf) + FfiConverterUInt32.write(value.size, into: &buf) + FfiConverterUInt32.write(value.vsize, into: &buf) + FfiConverterUInt32.write(value.weight, into: &buf) + FfiConverterOptionDouble.write(value.feeRate, into: &buf) + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeTransactionDetail_lift(_ buf: RustBuffer) throws -> TransactionDetail { + return try FfiConverterTypeTransactionDetail.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeTransactionDetail_lower(_ value: TransactionDetail) -> RustBuffer { + return FfiConverterTypeTransactionDetail.lower(value) +} + + +/** + * Details about an onchain transaction. + */ +public struct TransactionDetails { + /** + * The transaction ID. + */ + public var txId: String + /** + * The net amount in this transaction (in satoshis). + * + * This is calculated as: (received - sent). For incoming payments, + * this will be positive. For outgoing payments, this will be negative. + * + * Note: This amount does NOT include transaction fees. + */ + public var amountSats: Int64 + /** + * The transaction inputs with full details. + */ + public var inputs: [TxInput] + /** + * The transaction outputs with full details. + */ + public var outputs: [TxOutput] + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init( + /** + * The transaction ID. + */txId: String, + /** + * The net amount in this transaction (in satoshis). + * + * This is calculated as: (received - sent). For incoming payments, + * this will be positive. For outgoing payments, this will be negative. + * + * Note: This amount does NOT include transaction fees. + */amountSats: Int64, + /** + * The transaction inputs with full details. + */inputs: [TxInput], + /** + * The transaction outputs with full details. + */outputs: [TxOutput]) { + self.txId = txId + self.amountSats = amountSats + self.inputs = inputs + self.outputs = outputs + } +} + +#if compiler(>=6) +extension TransactionDetails: Sendable {} +#endif + + +extension TransactionDetails: Equatable, Hashable { + public static func ==(lhs: TransactionDetails, rhs: TransactionDetails) -> Bool { + if lhs.txId != rhs.txId { + return false + } + if lhs.amountSats != rhs.amountSats { + return false + } + if lhs.inputs != rhs.inputs { + return false + } + if lhs.outputs != rhs.outputs { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(txId) + hasher.combine(amountSats) + hasher.combine(inputs) hasher.combine(outputs) } } @@ -8687,6 +9175,135 @@ public func FfiConverterTypeTransactionDetails_lower(_ value: TransactionDetails } +/** + * Result from querying transaction history for an xpub. + */ +public struct TransactionHistoryResult { + /** + * All transactions, sorted: unconfirmed first, then by timestamp descending + */ + public var transactions: [HistoryTransaction] + /** + * Balance breakdown + */ + public var balance: WalletBalance + /** + * Total number of transactions + */ + public var txCount: UInt32 + /** + * Current blockchain tip height + */ + public var blockHeight: UInt32 + /** + * The detected or specified account type + */ + public var accountType: AccountType + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init( + /** + * All transactions, sorted: unconfirmed first, then by timestamp descending + */transactions: [HistoryTransaction], + /** + * Balance breakdown + */balance: WalletBalance, + /** + * Total number of transactions + */txCount: UInt32, + /** + * Current blockchain tip height + */blockHeight: UInt32, + /** + * The detected or specified account type + */accountType: AccountType) { + self.transactions = transactions + self.balance = balance + self.txCount = txCount + self.blockHeight = blockHeight + self.accountType = accountType + } +} + +#if compiler(>=6) +extension TransactionHistoryResult: Sendable {} +#endif + + +extension TransactionHistoryResult: Equatable, Hashable { + public static func ==(lhs: TransactionHistoryResult, rhs: TransactionHistoryResult) -> Bool { + if lhs.transactions != rhs.transactions { + return false + } + if lhs.balance != rhs.balance { + return false + } + if lhs.txCount != rhs.txCount { + return false + } + if lhs.blockHeight != rhs.blockHeight { + return false + } + if lhs.accountType != rhs.accountType { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(transactions) + hasher.combine(balance) + hasher.combine(txCount) + hasher.combine(blockHeight) + hasher.combine(accountType) + } +} + +extension TransactionHistoryResult: Codable {} + + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeTransactionHistoryResult: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> TransactionHistoryResult { + return + try TransactionHistoryResult( + transactions: FfiConverterSequenceTypeHistoryTransaction.read(from: &buf), + balance: FfiConverterTypeWalletBalance.read(from: &buf), + txCount: FfiConverterUInt32.read(from: &buf), + blockHeight: FfiConverterUInt32.read(from: &buf), + accountType: FfiConverterTypeAccountType.read(from: &buf) + ) + } + + public static func write(_ value: TransactionHistoryResult, into buf: inout [UInt8]) { + FfiConverterSequenceTypeHistoryTransaction.write(value.transactions, into: &buf) + FfiConverterTypeWalletBalance.write(value.balance, into: &buf) + FfiConverterUInt32.write(value.txCount, into: &buf) + FfiConverterUInt32.write(value.blockHeight, into: &buf) + FfiConverterTypeAccountType.write(value.accountType, into: &buf) + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeTransactionHistoryResult_lift(_ buf: RustBuffer) throws -> TransactionHistoryResult { + return try FfiConverterTypeTransactionHistoryResult.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeTransactionHistoryResult_lower(_ value: TransactionHistoryResult) -> RustBuffer { + return FfiConverterTypeTransactionHistoryResult.lower(value) +} + + /** * Address response from device. */ @@ -11021,8 +11638,252 @@ public func FfiConverterTypeTrezorVerifyMessageParams_lift(_ buf: RustBuffer) th #if swift(>=5.8) @_documentation(visibility: private) #endif -public func FfiConverterTypeTrezorVerifyMessageParams_lower(_ value: TrezorVerifyMessageParams) -> RustBuffer { - return FfiConverterTypeTrezorVerifyMessageParams.lower(value) +public func FfiConverterTypeTrezorVerifyMessageParams_lower(_ value: TrezorVerifyMessageParams) -> RustBuffer { + return FfiConverterTypeTrezorVerifyMessageParams.lower(value) +} + + +/** + * A transaction input with full details. + */ +public struct TxDetailInput { + /** + * Previous output transaction ID (hex) + */ + public var txid: String + /** + * Previous output index + */ + public var vout: UInt32 + /** + * Sequence number + */ + public var sequence: UInt32 + /** + * Script signature (hex-encoded) + */ + public var scriptSig: String + /** + * Witness stack (each element hex-encoded) + */ + public var witness: [String] + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init( + /** + * Previous output transaction ID (hex) + */txid: String, + /** + * Previous output index + */vout: UInt32, + /** + * Sequence number + */sequence: UInt32, + /** + * Script signature (hex-encoded) + */scriptSig: String, + /** + * Witness stack (each element hex-encoded) + */witness: [String]) { + self.txid = txid + self.vout = vout + self.sequence = sequence + self.scriptSig = scriptSig + self.witness = witness + } +} + +#if compiler(>=6) +extension TxDetailInput: Sendable {} +#endif + + +extension TxDetailInput: Equatable, Hashable { + public static func ==(lhs: TxDetailInput, rhs: TxDetailInput) -> Bool { + if lhs.txid != rhs.txid { + return false + } + if lhs.vout != rhs.vout { + return false + } + if lhs.sequence != rhs.sequence { + return false + } + if lhs.scriptSig != rhs.scriptSig { + return false + } + if lhs.witness != rhs.witness { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(txid) + hasher.combine(vout) + hasher.combine(sequence) + hasher.combine(scriptSig) + hasher.combine(witness) + } +} + +extension TxDetailInput: Codable {} + + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeTxDetailInput: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> TxDetailInput { + return + try TxDetailInput( + txid: FfiConverterString.read(from: &buf), + vout: FfiConverterUInt32.read(from: &buf), + sequence: FfiConverterUInt32.read(from: &buf), + scriptSig: FfiConverterString.read(from: &buf), + witness: FfiConverterSequenceString.read(from: &buf) + ) + } + + public static func write(_ value: TxDetailInput, into buf: inout [UInt8]) { + FfiConverterString.write(value.txid, into: &buf) + FfiConverterUInt32.write(value.vout, into: &buf) + FfiConverterUInt32.write(value.sequence, into: &buf) + FfiConverterString.write(value.scriptSig, into: &buf) + FfiConverterSequenceString.write(value.witness, into: &buf) + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeTxDetailInput_lift(_ buf: RustBuffer) throws -> TxDetailInput { + return try FfiConverterTypeTxDetailInput.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeTxDetailInput_lower(_ value: TxDetailInput) -> RustBuffer { + return FfiConverterTypeTxDetailInput.lower(value) +} + + +/** + * A transaction output with full details. + */ +public struct TxDetailOutput { + /** + * Output value in sats + */ + public var value: UInt64 + /** + * Script public key (hex-encoded) + */ + public var scriptPubkey: String + /** + * Decoded address (None if script is not decodable to an address) + */ + public var address: String? + /** + * Whether this output belongs to the queried wallet + */ + public var isMine: Bool + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init( + /** + * Output value in sats + */value: UInt64, + /** + * Script public key (hex-encoded) + */scriptPubkey: String, + /** + * Decoded address (None if script is not decodable to an address) + */address: String?, + /** + * Whether this output belongs to the queried wallet + */isMine: Bool) { + self.value = value + self.scriptPubkey = scriptPubkey + self.address = address + self.isMine = isMine + } +} + +#if compiler(>=6) +extension TxDetailOutput: Sendable {} +#endif + + +extension TxDetailOutput: Equatable, Hashable { + public static func ==(lhs: TxDetailOutput, rhs: TxDetailOutput) -> Bool { + if lhs.value != rhs.value { + return false + } + if lhs.scriptPubkey != rhs.scriptPubkey { + return false + } + if lhs.address != rhs.address { + return false + } + if lhs.isMine != rhs.isMine { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(value) + hasher.combine(scriptPubkey) + hasher.combine(address) + hasher.combine(isMine) + } +} + +extension TxDetailOutput: Codable {} + + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeTxDetailOutput: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> TxDetailOutput { + return + try TxDetailOutput( + value: FfiConverterUInt64.read(from: &buf), + scriptPubkey: FfiConverterString.read(from: &buf), + address: FfiConverterOptionString.read(from: &buf), + isMine: FfiConverterBool.read(from: &buf) + ) + } + + public static func write(_ value: TxDetailOutput, into buf: inout [UInt8]) { + FfiConverterUInt64.write(value.value, into: &buf) + FfiConverterString.write(value.scriptPubkey, into: &buf) + FfiConverterOptionString.write(value.address, into: &buf) + FfiConverterBool.write(value.isMine, into: &buf) + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeTxDetailOutput_lift(_ buf: RustBuffer) throws -> TxDetailOutput { + return try FfiConverterTypeTxDetailOutput.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeTxDetailOutput_lower(_ value: TxDetailOutput) -> RustBuffer { + return FfiConverterTypeTxDetailOutput.lower(value) } @@ -11364,6 +12225,149 @@ public func FfiConverterTypeValidationResult_lower(_ value: ValidationResult) -> } +/** + * Balance breakdown from BDK. + */ +public struct WalletBalance { + /** + * Confirmed and spendable balance (sats) + */ + public var confirmed: UInt64 + /** + * Immature coinbase outputs (sats) + */ + public var immature: UInt64 + /** + * Unconfirmed UTXOs from trusted sources (own change) (sats) + */ + public var trustedPending: UInt64 + /** + * Unconfirmed UTXOs from external sources (sats) + */ + public var untrustedPending: UInt64 + /** + * Total spendable: confirmed + trusted_pending (sats) + */ + public var spendable: UInt64 + /** + * Grand total: all categories (sats) + */ + public var total: UInt64 + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init( + /** + * Confirmed and spendable balance (sats) + */confirmed: UInt64, + /** + * Immature coinbase outputs (sats) + */immature: UInt64, + /** + * Unconfirmed UTXOs from trusted sources (own change) (sats) + */trustedPending: UInt64, + /** + * Unconfirmed UTXOs from external sources (sats) + */untrustedPending: UInt64, + /** + * Total spendable: confirmed + trusted_pending (sats) + */spendable: UInt64, + /** + * Grand total: all categories (sats) + */total: UInt64) { + self.confirmed = confirmed + self.immature = immature + self.trustedPending = trustedPending + self.untrustedPending = untrustedPending + self.spendable = spendable + self.total = total + } +} + +#if compiler(>=6) +extension WalletBalance: Sendable {} +#endif + + +extension WalletBalance: Equatable, Hashable { + public static func ==(lhs: WalletBalance, rhs: WalletBalance) -> Bool { + if lhs.confirmed != rhs.confirmed { + return false + } + if lhs.immature != rhs.immature { + return false + } + if lhs.trustedPending != rhs.trustedPending { + return false + } + if lhs.untrustedPending != rhs.untrustedPending { + return false + } + if lhs.spendable != rhs.spendable { + return false + } + if lhs.total != rhs.total { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(confirmed) + hasher.combine(immature) + hasher.combine(trustedPending) + hasher.combine(untrustedPending) + hasher.combine(spendable) + hasher.combine(total) + } +} + +extension WalletBalance: Codable {} + + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeWalletBalance: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> WalletBalance { + return + try WalletBalance( + confirmed: FfiConverterUInt64.read(from: &buf), + immature: FfiConverterUInt64.read(from: &buf), + trustedPending: FfiConverterUInt64.read(from: &buf), + untrustedPending: FfiConverterUInt64.read(from: &buf), + spendable: FfiConverterUInt64.read(from: &buf), + total: FfiConverterUInt64.read(from: &buf) + ) + } + + public static func write(_ value: WalletBalance, into buf: inout [UInt8]) { + FfiConverterUInt64.write(value.confirmed, into: &buf) + FfiConverterUInt64.write(value.immature, into: &buf) + FfiConverterUInt64.write(value.trustedPending, into: &buf) + FfiConverterUInt64.write(value.untrustedPending, into: &buf) + FfiConverterUInt64.write(value.spendable, into: &buf) + FfiConverterUInt64.write(value.total, into: &buf) + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeWalletBalance_lift(_ buf: RustBuffer) throws -> WalletBalance { + return try FfiConverterTypeWalletBalance.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeWalletBalance_lower(_ value: WalletBalance) -> RustBuffer { + return FfiConverterTypeWalletBalance.lower(value) +} + + /** * Common parameters for creating and syncing a watch-only BDK wallet. */ @@ -15565,6 +16569,97 @@ extension TrezorTransportType: Codable {} +// Note that we don't yet support `indirect` for enums. +// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. +/** + * Transaction direction from the wallet's perspective. + */ + +public enum TxDirection { + + /** + * Wallet sent funds to an external address + */ + case sent + /** + * Wallet received funds from an external source + */ + case received + /** + * Wallet sent funds to itself (e.g. consolidation, change-only) + */ + case selfTransfer +} + + +#if compiler(>=6) +extension TxDirection: Sendable {} +#endif + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeTxDirection: FfiConverterRustBuffer { + typealias SwiftType = TxDirection + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> TxDirection { + let variant: Int32 = try readInt(&buf) + switch variant { + + case 1: return .sent + + case 2: return .received + + case 3: return .selfTransfer + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: TxDirection, into buf: inout [UInt8]) { + switch value { + + + case .sent: + writeInt(&buf, Int32(1)) + + + case .received: + writeInt(&buf, Int32(2)) + + + case .selfTransfer: + writeInt(&buf, Int32(3)) + + } + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeTxDirection_lift(_ buf: RustBuffer) throws -> TxDirection { + return try FfiConverterTypeTxDirection.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeTxDirection_lower(_ value: TxDirection) -> RustBuffer { + return FfiConverterTypeTxDirection.lower(value) +} + + +extension TxDirection: Equatable, Hashable {} + +extension TxDirection: Codable {} + + + + + + // Note that we don't yet support `indirect` for enums. // See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. @@ -15745,6 +16840,30 @@ fileprivate struct FfiConverterOptionUInt64: FfiConverterRustBuffer { } } +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterOptionDouble: FfiConverterRustBuffer { + typealias SwiftType = Double? + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + guard let value = value else { + writeInt(&buf, Int8(0)) + return + } + writeInt(&buf, Int8(1)) + FfiConverterDouble.write(value, into: &buf) + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + switch try readInt(&buf) as Int8 { + case 0: return nil + case 1: return try FfiConverterDouble.read(from: &buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} + #if swift(>=5.8) @_documentation(visibility: private) #endif @@ -16952,6 +18071,31 @@ fileprivate struct FfiConverterSequenceTypeGetAddressResponse: FfiConverterRustB } } +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterSequenceTypeHistoryTransaction: FfiConverterRustBuffer { + typealias SwiftType = [HistoryTransaction] + + public static func write(_ value: [HistoryTransaction], into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + for item in value { + FfiConverterTypeHistoryTransaction.write(item, into: &buf) + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [HistoryTransaction] { + let len: Int32 = try readInt(&buf) + var seq = [HistoryTransaction]() + seq.reserveCapacity(Int(len)) + for _ in 0 ..< len { + seq.append(try FfiConverterTypeHistoryTransaction.read(from: &buf)) + } + return seq + } +} + #if swift(>=5.8) @_documentation(visibility: private) #endif @@ -17377,6 +18521,56 @@ fileprivate struct FfiConverterSequenceTypeTrezorTxOutput: FfiConverterRustBuffe } } +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterSequenceTypeTxDetailInput: FfiConverterRustBuffer { + typealias SwiftType = [TxDetailInput] + + public static func write(_ value: [TxDetailInput], into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + for item in value { + FfiConverterTypeTxDetailInput.write(item, into: &buf) + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [TxDetailInput] { + let len: Int32 = try readInt(&buf) + var seq = [TxDetailInput]() + seq.reserveCapacity(Int(len)) + for _ in 0 ..< len { + seq.append(try FfiConverterTypeTxDetailInput.read(from: &buf)) + } + return seq + } +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterSequenceTypeTxDetailOutput: FfiConverterRustBuffer { + typealias SwiftType = [TxDetailOutput] + + public static func write(_ value: [TxDetailOutput], into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + for item in value { + FfiConverterTypeTxDetailOutput.write(item, into: &buf) + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [TxDetailOutput] { + let len: Int32 = try readInt(&buf) + var seq = [TxDetailOutput]() + seq.reserveCapacity(Int(len)) + for _ in 0 ..< len { + seq.append(try FfiConverterTypeTxDetailOutput.read(from: &buf)) + } + return seq + } +} + #if swift(>=5.8) @_documentation(visibility: private) #endif @@ -18286,6 +19480,40 @@ public func onchainGetAddressInfo(address: String, electrumUrl: String, network: errorHandler: FfiConverterTypeAccountInfoError_lift ) } +/** + * Get full details for a single transaction by txid. + */ +public func onchainGetTransactionDetail(extendedKey: String, electrumUrl: String, txid: String, network: Network?, scriptType: AccountType?)async throws -> TransactionDetail { + return + try await uniffiRustCallAsync( + rustFutureFunc: { + uniffi_bitkitcore_fn_func_onchain_get_transaction_detail(FfiConverterString.lower(extendedKey),FfiConverterString.lower(electrumUrl),FfiConverterString.lower(txid),FfiConverterOptionTypeNetwork.lower(network),FfiConverterOptionTypeAccountType.lower(scriptType) + ) + }, + pollFunc: ffi_bitkitcore_rust_future_poll_rust_buffer, + completeFunc: ffi_bitkitcore_rust_future_complete_rust_buffer, + freeFunc: ffi_bitkitcore_rust_future_free_rust_buffer, + liftFunc: FfiConverterTypeTransactionDetail_lift, + errorHandler: FfiConverterTypeAccountInfoError_lift + ) +} +/** + * Query transaction history and balance for an extended public key via Electrum. + */ +public func onchainGetTransactionHistory(extendedKey: String, electrumUrl: String, network: Network?, scriptType: AccountType?)async throws -> TransactionHistoryResult { + return + try await uniffiRustCallAsync( + rustFutureFunc: { + uniffi_bitkitcore_fn_func_onchain_get_transaction_history(FfiConverterString.lower(extendedKey),FfiConverterString.lower(electrumUrl),FfiConverterOptionTypeNetwork.lower(network),FfiConverterOptionTypeAccountType.lower(scriptType) + ) + }, + pollFunc: ffi_bitkitcore_rust_future_poll_rust_buffer, + completeFunc: ffi_bitkitcore_rust_future_complete_rust_buffer, + freeFunc: ffi_bitkitcore_rust_future_free_rust_buffer, + liftFunc: FfiConverterTypeTransactionHistoryResult_lift, + errorHandler: FfiConverterTypeAccountInfoError_lift + ) +} public func openChannel(orderId: String, connectionString: String)async throws -> IBtOrder { return try await uniffiRustCallAsync( @@ -19232,6 +20460,12 @@ private let initializationResult: InitializationResult = { if (uniffi_bitkitcore_checksum_func_onchain_get_address_info() != 4749) { return InitializationResult.apiChecksumMismatch } + if (uniffi_bitkitcore_checksum_func_onchain_get_transaction_detail() != 24151) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_bitkitcore_checksum_func_onchain_get_transaction_history() != 4452) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_bitkitcore_checksum_func_open_channel() != 21402) { return InitializationResult.apiChecksumMismatch } diff --git a/bindings/ios/bitkitcoreFFI.h b/bindings/ios/bitkitcoreFFI.h index be0b90c..9d5af34 100644 --- a/bindings/ios/bitkitcoreFFI.h +++ b/bindings/ios/bitkitcoreFFI.h @@ -810,6 +810,16 @@ uint64_t uniffi_bitkitcore_fn_func_onchain_get_account_info(RustBuffer extended_ uint64_t uniffi_bitkitcore_fn_func_onchain_get_address_info(RustBuffer address, RustBuffer electrum_url, RustBuffer network ); #endif +#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_ONCHAIN_GET_TRANSACTION_DETAIL +#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_ONCHAIN_GET_TRANSACTION_DETAIL +uint64_t uniffi_bitkitcore_fn_func_onchain_get_transaction_detail(RustBuffer extended_key, RustBuffer electrum_url, RustBuffer txid, RustBuffer network, RustBuffer script_type +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_ONCHAIN_GET_TRANSACTION_HISTORY +#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_ONCHAIN_GET_TRANSACTION_HISTORY +uint64_t uniffi_bitkitcore_fn_func_onchain_get_transaction_history(RustBuffer extended_key, RustBuffer electrum_url, RustBuffer network, RustBuffer script_type +); +#endif #ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_OPEN_CHANNEL #define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_OPEN_CHANNEL uint64_t uniffi_bitkitcore_fn_func_open_channel(RustBuffer order_id, RustBuffer connection_string @@ -1789,6 +1799,18 @@ uint16_t uniffi_bitkitcore_checksum_func_onchain_get_account_info(void #define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_ONCHAIN_GET_ADDRESS_INFO uint16_t uniffi_bitkitcore_checksum_func_onchain_get_address_info(void +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_ONCHAIN_GET_TRANSACTION_DETAIL +#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_ONCHAIN_GET_TRANSACTION_DETAIL +uint16_t uniffi_bitkitcore_checksum_func_onchain_get_transaction_detail(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_ONCHAIN_GET_TRANSACTION_HISTORY +#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_ONCHAIN_GET_TRANSACTION_HISTORY +uint16_t uniffi_bitkitcore_checksum_func_onchain_get_transaction_history(void + ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_OPEN_CHANNEL diff --git a/bindings/python/bitkitcore/bitkitcore.py b/bindings/python/bitkitcore/bitkitcore.py index 1eb6ec2..0a379c9 100644 --- a/bindings/python/bitkitcore/bitkitcore.py +++ b/bindings/python/bitkitcore/bitkitcore.py @@ -595,6 +595,10 @@ def _uniffi_check_api_checksums(lib): raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_bitkitcore_checksum_func_onchain_get_address_info() != 4749: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + if lib.uniffi_bitkitcore_checksum_func_onchain_get_transaction_detail() != 24151: + raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + if lib.uniffi_bitkitcore_checksum_func_onchain_get_transaction_history() != 4452: + raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_bitkitcore_checksum_func_open_channel() != 21402: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_bitkitcore_checksum_func_prepare_sweep_transaction() != 18273: @@ -1373,6 +1377,21 @@ class _UniffiVTableCallbackInterfaceTrezorUiCallback(ctypes.Structure): _UniffiRustBuffer, ) _UniffiLib.uniffi_bitkitcore_fn_func_onchain_get_address_info.restype = ctypes.c_uint64 +_UniffiLib.uniffi_bitkitcore_fn_func_onchain_get_transaction_detail.argtypes = ( + _UniffiRustBuffer, + _UniffiRustBuffer, + _UniffiRustBuffer, + _UniffiRustBuffer, + _UniffiRustBuffer, +) +_UniffiLib.uniffi_bitkitcore_fn_func_onchain_get_transaction_detail.restype = ctypes.c_uint64 +_UniffiLib.uniffi_bitkitcore_fn_func_onchain_get_transaction_history.argtypes = ( + _UniffiRustBuffer, + _UniffiRustBuffer, + _UniffiRustBuffer, + _UniffiRustBuffer, +) +_UniffiLib.uniffi_bitkitcore_fn_func_onchain_get_transaction_history.restype = ctypes.c_uint64 _UniffiLib.uniffi_bitkitcore_fn_func_open_channel.argtypes = ( _UniffiRustBuffer, _UniffiRustBuffer, @@ -2103,6 +2122,12 @@ class _UniffiVTableCallbackInterfaceTrezorUiCallback(ctypes.Structure): _UniffiLib.uniffi_bitkitcore_checksum_func_onchain_get_address_info.argtypes = ( ) _UniffiLib.uniffi_bitkitcore_checksum_func_onchain_get_address_info.restype = ctypes.c_uint16 +_UniffiLib.uniffi_bitkitcore_checksum_func_onchain_get_transaction_detail.argtypes = ( +) +_UniffiLib.uniffi_bitkitcore_checksum_func_onchain_get_transaction_detail.restype = ctypes.c_uint16 +_UniffiLib.uniffi_bitkitcore_checksum_func_onchain_get_transaction_history.argtypes = ( +) +_UniffiLib.uniffi_bitkitcore_checksum_func_onchain_get_transaction_history.restype = ctypes.c_uint16 _UniffiLib.uniffi_bitkitcore_checksum_func_open_channel.argtypes = ( ) _UniffiLib.uniffi_bitkitcore_checksum_func_open_channel.restype = ctypes.c_uint16 @@ -3617,6 +3642,145 @@ def write(value, buf): _UniffiConverterSequenceTypeGetAddressResponse.write(value.addresses, buf) +class HistoryTransaction: + """ + A single transaction in the wallet's history. + """ + + txid: "str" + """ + Transaction ID (hex) + """ + + received: "int" + """ + Amount received by the wallet (sats) + """ + + sent: "int" + """ + Amount sent by the wallet (sats) — includes change sent back to self + """ + + net: "int" + """ + Net value from wallet's perspective: received - sent (positive = inflow, negative = outflow) + """ + + fee: "typing.Optional[int]" + """ + Transaction fee in sats (None if not available, e.g. for received-only txs) + """ + + amount: "int" + """ + Display amount in sats: + - Received: the received value + - Sent: amount that left the wallet (sent - received - fee) + - SelfTransfer: the fee paid + """ + + direction: "TxDirection" + """ + Transaction direction + """ + + block_height: "typing.Optional[int]" + """ + Block height (None if unconfirmed/mempool) + """ + + timestamp: "typing.Optional[int]" + """ + Block timestamp as unix epoch seconds (None if unconfirmed) + """ + + confirmations: "int" + """ + Number of confirmations (0 if unconfirmed) + """ + + def __init__(self, *, txid: "str", received: "int", sent: "int", net: "int", fee: "typing.Optional[int]", amount: "int", direction: "TxDirection", block_height: "typing.Optional[int]", timestamp: "typing.Optional[int]", confirmations: "int"): + self.txid = txid + self.received = received + self.sent = sent + self.net = net + self.fee = fee + self.amount = amount + self.direction = direction + self.block_height = block_height + self.timestamp = timestamp + self.confirmations = confirmations + + def __str__(self): + return "HistoryTransaction(txid={}, received={}, sent={}, net={}, fee={}, amount={}, direction={}, block_height={}, timestamp={}, confirmations={})".format(self.txid, self.received, self.sent, self.net, self.fee, self.amount, self.direction, self.block_height, self.timestamp, self.confirmations) + + def __eq__(self, other): + if self.txid != other.txid: + return False + if self.received != other.received: + return False + if self.sent != other.sent: + return False + if self.net != other.net: + return False + if self.fee != other.fee: + return False + if self.amount != other.amount: + return False + if self.direction != other.direction: + return False + if self.block_height != other.block_height: + return False + if self.timestamp != other.timestamp: + return False + if self.confirmations != other.confirmations: + return False + return True + +class _UniffiConverterTypeHistoryTransaction(_UniffiConverterRustBuffer): + @staticmethod + def read(buf): + return HistoryTransaction( + txid=_UniffiConverterString.read(buf), + received=_UniffiConverterUInt64.read(buf), + sent=_UniffiConverterUInt64.read(buf), + net=_UniffiConverterInt64.read(buf), + fee=_UniffiConverterOptionalUInt64.read(buf), + amount=_UniffiConverterUInt64.read(buf), + direction=_UniffiConverterTypeTxDirection.read(buf), + block_height=_UniffiConverterOptionalUInt32.read(buf), + timestamp=_UniffiConverterOptionalUInt64.read(buf), + confirmations=_UniffiConverterUInt32.read(buf), + ) + + @staticmethod + def check_lower(value): + _UniffiConverterString.check_lower(value.txid) + _UniffiConverterUInt64.check_lower(value.received) + _UniffiConverterUInt64.check_lower(value.sent) + _UniffiConverterInt64.check_lower(value.net) + _UniffiConverterOptionalUInt64.check_lower(value.fee) + _UniffiConverterUInt64.check_lower(value.amount) + _UniffiConverterTypeTxDirection.check_lower(value.direction) + _UniffiConverterOptionalUInt32.check_lower(value.block_height) + _UniffiConverterOptionalUInt64.check_lower(value.timestamp) + _UniffiConverterUInt32.check_lower(value.confirmations) + + @staticmethod + def write(value, buf): + _UniffiConverterString.write(value.txid, buf) + _UniffiConverterUInt64.write(value.received, buf) + _UniffiConverterUInt64.write(value.sent, buf) + _UniffiConverterInt64.write(value.net, buf) + _UniffiConverterOptionalUInt64.write(value.fee, buf) + _UniffiConverterUInt64.write(value.amount, buf) + _UniffiConverterTypeTxDirection.write(value.direction, buf) + _UniffiConverterOptionalUInt32.write(value.block_height, buf) + _UniffiConverterOptionalUInt64.write(value.timestamp, buf) + _UniffiConverterUInt32.write(value.confirmations, buf) + + class IBt0ConfMinTxFeeWindow: sat_per_vbyte: "float" validity_ends_at: "str" @@ -6913,6 +7077,208 @@ def write(value, buf): _UniffiConverterUInt32.write(value.total_utxos_count, buf) +class TransactionDetail: + """ + Full details for a single transaction, including raw inputs/outputs and size metrics. + """ + + txid: "str" + """ + Transaction ID (hex) + """ + + received: "int" + """ + Amount received by the wallet (sats) + """ + + sent: "int" + """ + Amount sent by the wallet (sats) — includes change sent back to self + """ + + net: "int" + """ + Net value from wallet's perspective: received - sent (positive = inflow, negative = outflow) + """ + + amount: "int" + """ + Display amount in sats (same semantics as HistoryTransaction.amount) + """ + + fee: "typing.Optional[int]" + """ + Transaction fee in sats (None if not available) + """ + + direction: "TxDirection" + """ + Transaction direction + """ + + block_height: "typing.Optional[int]" + """ + Block height (None if unconfirmed/mempool) + """ + + timestamp: "typing.Optional[int]" + """ + Block timestamp as unix epoch seconds (None if unconfirmed) + """ + + confirmations: "int" + """ + Number of confirmations (0 if unconfirmed) + """ + + inputs: "typing.List[TxDetailInput]" + """ + Transaction inputs + """ + + outputs: "typing.List[TxDetailOutput]" + """ + Transaction outputs + """ + + size: "int" + """ + Serialized transaction size in bytes + """ + + vsize: "int" + """ + Virtual size in vbytes (ceil(weight / 4)) + """ + + weight: "int" + """ + Transaction weight in weight units + """ + + fee_rate: "typing.Optional[float]" + """ + Fee rate in sat/vB (fee / vsize), None if fee or vsize unavailable + """ + + def __init__(self, *, txid: "str", received: "int", sent: "int", net: "int", amount: "int", fee: "typing.Optional[int]", direction: "TxDirection", block_height: "typing.Optional[int]", timestamp: "typing.Optional[int]", confirmations: "int", inputs: "typing.List[TxDetailInput]", outputs: "typing.List[TxDetailOutput]", size: "int", vsize: "int", weight: "int", fee_rate: "typing.Optional[float]"): + self.txid = txid + self.received = received + self.sent = sent + self.net = net + self.amount = amount + self.fee = fee + self.direction = direction + self.block_height = block_height + self.timestamp = timestamp + self.confirmations = confirmations + self.inputs = inputs + self.outputs = outputs + self.size = size + self.vsize = vsize + self.weight = weight + self.fee_rate = fee_rate + + def __str__(self): + return "TransactionDetail(txid={}, received={}, sent={}, net={}, amount={}, fee={}, direction={}, block_height={}, timestamp={}, confirmations={}, inputs={}, outputs={}, size={}, vsize={}, weight={}, fee_rate={})".format(self.txid, self.received, self.sent, self.net, self.amount, self.fee, self.direction, self.block_height, self.timestamp, self.confirmations, self.inputs, self.outputs, self.size, self.vsize, self.weight, self.fee_rate) + + def __eq__(self, other): + if self.txid != other.txid: + return False + if self.received != other.received: + return False + if self.sent != other.sent: + return False + if self.net != other.net: + return False + if self.amount != other.amount: + return False + if self.fee != other.fee: + return False + if self.direction != other.direction: + return False + if self.block_height != other.block_height: + return False + if self.timestamp != other.timestamp: + return False + if self.confirmations != other.confirmations: + return False + if self.inputs != other.inputs: + return False + if self.outputs != other.outputs: + return False + if self.size != other.size: + return False + if self.vsize != other.vsize: + return False + if self.weight != other.weight: + return False + if self.fee_rate != other.fee_rate: + return False + return True + +class _UniffiConverterTypeTransactionDetail(_UniffiConverterRustBuffer): + @staticmethod + def read(buf): + return TransactionDetail( + txid=_UniffiConverterString.read(buf), + received=_UniffiConverterUInt64.read(buf), + sent=_UniffiConverterUInt64.read(buf), + net=_UniffiConverterInt64.read(buf), + amount=_UniffiConverterUInt64.read(buf), + fee=_UniffiConverterOptionalUInt64.read(buf), + direction=_UniffiConverterTypeTxDirection.read(buf), + block_height=_UniffiConverterOptionalUInt32.read(buf), + timestamp=_UniffiConverterOptionalUInt64.read(buf), + confirmations=_UniffiConverterUInt32.read(buf), + inputs=_UniffiConverterSequenceTypeTxDetailInput.read(buf), + outputs=_UniffiConverterSequenceTypeTxDetailOutput.read(buf), + size=_UniffiConverterUInt32.read(buf), + vsize=_UniffiConverterUInt32.read(buf), + weight=_UniffiConverterUInt32.read(buf), + fee_rate=_UniffiConverterOptionalDouble.read(buf), + ) + + @staticmethod + def check_lower(value): + _UniffiConverterString.check_lower(value.txid) + _UniffiConverterUInt64.check_lower(value.received) + _UniffiConverterUInt64.check_lower(value.sent) + _UniffiConverterInt64.check_lower(value.net) + _UniffiConverterUInt64.check_lower(value.amount) + _UniffiConverterOptionalUInt64.check_lower(value.fee) + _UniffiConverterTypeTxDirection.check_lower(value.direction) + _UniffiConverterOptionalUInt32.check_lower(value.block_height) + _UniffiConverterOptionalUInt64.check_lower(value.timestamp) + _UniffiConverterUInt32.check_lower(value.confirmations) + _UniffiConverterSequenceTypeTxDetailInput.check_lower(value.inputs) + _UniffiConverterSequenceTypeTxDetailOutput.check_lower(value.outputs) + _UniffiConverterUInt32.check_lower(value.size) + _UniffiConverterUInt32.check_lower(value.vsize) + _UniffiConverterUInt32.check_lower(value.weight) + _UniffiConverterOptionalDouble.check_lower(value.fee_rate) + + @staticmethod + def write(value, buf): + _UniffiConverterString.write(value.txid, buf) + _UniffiConverterUInt64.write(value.received, buf) + _UniffiConverterUInt64.write(value.sent, buf) + _UniffiConverterInt64.write(value.net, buf) + _UniffiConverterUInt64.write(value.amount, buf) + _UniffiConverterOptionalUInt64.write(value.fee, buf) + _UniffiConverterTypeTxDirection.write(value.direction, buf) + _UniffiConverterOptionalUInt32.write(value.block_height, buf) + _UniffiConverterOptionalUInt64.write(value.timestamp, buf) + _UniffiConverterUInt32.write(value.confirmations, buf) + _UniffiConverterSequenceTypeTxDetailInput.write(value.inputs, buf) + _UniffiConverterSequenceTypeTxDetailOutput.write(value.outputs, buf) + _UniffiConverterUInt32.write(value.size, buf) + _UniffiConverterUInt32.write(value.vsize, buf) + _UniffiConverterUInt32.write(value.weight, buf) + _UniffiConverterOptionalDouble.write(value.fee_rate, buf) + + class TransactionDetails: """ Details about an onchain transaction. @@ -6988,6 +7354,87 @@ def write(value, buf): _UniffiConverterSequenceTypeTxOutput.write(value.outputs, buf) +class TransactionHistoryResult: + """ + Result from querying transaction history for an xpub. + """ + + transactions: "typing.List[HistoryTransaction]" + """ + All transactions, sorted: unconfirmed first, then by timestamp descending + """ + + balance: "WalletBalance" + """ + Balance breakdown + """ + + tx_count: "int" + """ + Total number of transactions + """ + + block_height: "int" + """ + Current blockchain tip height + """ + + account_type: "AccountType" + """ + The detected or specified account type + """ + + def __init__(self, *, transactions: "typing.List[HistoryTransaction]", balance: "WalletBalance", tx_count: "int", block_height: "int", account_type: "AccountType"): + self.transactions = transactions + self.balance = balance + self.tx_count = tx_count + self.block_height = block_height + self.account_type = account_type + + def __str__(self): + return "TransactionHistoryResult(transactions={}, balance={}, tx_count={}, block_height={}, account_type={})".format(self.transactions, self.balance, self.tx_count, self.block_height, self.account_type) + + def __eq__(self, other): + if self.transactions != other.transactions: + return False + if self.balance != other.balance: + return False + if self.tx_count != other.tx_count: + return False + if self.block_height != other.block_height: + return False + if self.account_type != other.account_type: + return False + return True + +class _UniffiConverterTypeTransactionHistoryResult(_UniffiConverterRustBuffer): + @staticmethod + def read(buf): + return TransactionHistoryResult( + transactions=_UniffiConverterSequenceTypeHistoryTransaction.read(buf), + balance=_UniffiConverterTypeWalletBalance.read(buf), + tx_count=_UniffiConverterUInt32.read(buf), + block_height=_UniffiConverterUInt32.read(buf), + account_type=_UniffiConverterTypeAccountType.read(buf), + ) + + @staticmethod + def check_lower(value): + _UniffiConverterSequenceTypeHistoryTransaction.check_lower(value.transactions) + _UniffiConverterTypeWalletBalance.check_lower(value.balance) + _UniffiConverterUInt32.check_lower(value.tx_count) + _UniffiConverterUInt32.check_lower(value.block_height) + _UniffiConverterTypeAccountType.check_lower(value.account_type) + + @staticmethod + def write(value, buf): + _UniffiConverterSequenceTypeHistoryTransaction.write(value.transactions, buf) + _UniffiConverterTypeWalletBalance.write(value.balance, buf) + _UniffiConverterUInt32.write(value.tx_count, buf) + _UniffiConverterUInt32.write(value.block_height, buf) + _UniffiConverterTypeAccountType.write(value.account_type, buf) + + class TrezorAddressResponse: """ Address response from device. @@ -8410,33 +8857,184 @@ def __eq__(self, other): return False if self.message != other.message: return False - if self.coin != other.coin: + if self.coin != other.coin: + return False + return True + +class _UniffiConverterTypeTrezorVerifyMessageParams(_UniffiConverterRustBuffer): + @staticmethod + def read(buf): + return TrezorVerifyMessageParams( + address=_UniffiConverterString.read(buf), + signature=_UniffiConverterString.read(buf), + message=_UniffiConverterString.read(buf), + coin=_UniffiConverterOptionalTypeTrezorCoinType.read(buf), + ) + + @staticmethod + def check_lower(value): + _UniffiConverterString.check_lower(value.address) + _UniffiConverterString.check_lower(value.signature) + _UniffiConverterString.check_lower(value.message) + _UniffiConverterOptionalTypeTrezorCoinType.check_lower(value.coin) + + @staticmethod + def write(value, buf): + _UniffiConverterString.write(value.address, buf) + _UniffiConverterString.write(value.signature, buf) + _UniffiConverterString.write(value.message, buf) + _UniffiConverterOptionalTypeTrezorCoinType.write(value.coin, buf) + + +class TxDetailInput: + """ + A transaction input with full details. + """ + + txid: "str" + """ + Previous output transaction ID (hex) + """ + + vout: "int" + """ + Previous output index + """ + + sequence: "int" + """ + Sequence number + """ + + script_sig: "str" + """ + Script signature (hex-encoded) + """ + + witness: "typing.List[str]" + """ + Witness stack (each element hex-encoded) + """ + + def __init__(self, *, txid: "str", vout: "int", sequence: "int", script_sig: "str", witness: "typing.List[str]"): + self.txid = txid + self.vout = vout + self.sequence = sequence + self.script_sig = script_sig + self.witness = witness + + def __str__(self): + return "TxDetailInput(txid={}, vout={}, sequence={}, script_sig={}, witness={})".format(self.txid, self.vout, self.sequence, self.script_sig, self.witness) + + def __eq__(self, other): + if self.txid != other.txid: + return False + if self.vout != other.vout: + return False + if self.sequence != other.sequence: + return False + if self.script_sig != other.script_sig: + return False + if self.witness != other.witness: + return False + return True + +class _UniffiConverterTypeTxDetailInput(_UniffiConverterRustBuffer): + @staticmethod + def read(buf): + return TxDetailInput( + txid=_UniffiConverterString.read(buf), + vout=_UniffiConverterUInt32.read(buf), + sequence=_UniffiConverterUInt32.read(buf), + script_sig=_UniffiConverterString.read(buf), + witness=_UniffiConverterSequenceString.read(buf), + ) + + @staticmethod + def check_lower(value): + _UniffiConverterString.check_lower(value.txid) + _UniffiConverterUInt32.check_lower(value.vout) + _UniffiConverterUInt32.check_lower(value.sequence) + _UniffiConverterString.check_lower(value.script_sig) + _UniffiConverterSequenceString.check_lower(value.witness) + + @staticmethod + def write(value, buf): + _UniffiConverterString.write(value.txid, buf) + _UniffiConverterUInt32.write(value.vout, buf) + _UniffiConverterUInt32.write(value.sequence, buf) + _UniffiConverterString.write(value.script_sig, buf) + _UniffiConverterSequenceString.write(value.witness, buf) + + +class TxDetailOutput: + """ + A transaction output with full details. + """ + + value: "int" + """ + Output value in sats + """ + + script_pubkey: "str" + """ + Script public key (hex-encoded) + """ + + address: "typing.Optional[str]" + """ + Decoded address (None if script is not decodable to an address) + """ + + is_mine: "bool" + """ + Whether this output belongs to the queried wallet + """ + + def __init__(self, *, value: "int", script_pubkey: "str", address: "typing.Optional[str]", is_mine: "bool"): + self.value = value + self.script_pubkey = script_pubkey + self.address = address + self.is_mine = is_mine + + def __str__(self): + return "TxDetailOutput(value={}, script_pubkey={}, address={}, is_mine={})".format(self.value, self.script_pubkey, self.address, self.is_mine) + + def __eq__(self, other): + if self.value != other.value: + return False + if self.script_pubkey != other.script_pubkey: + return False + if self.address != other.address: + return False + if self.is_mine != other.is_mine: return False return True -class _UniffiConverterTypeTrezorVerifyMessageParams(_UniffiConverterRustBuffer): +class _UniffiConverterTypeTxDetailOutput(_UniffiConverterRustBuffer): @staticmethod def read(buf): - return TrezorVerifyMessageParams( - address=_UniffiConverterString.read(buf), - signature=_UniffiConverterString.read(buf), - message=_UniffiConverterString.read(buf), - coin=_UniffiConverterOptionalTypeTrezorCoinType.read(buf), + return TxDetailOutput( + value=_UniffiConverterUInt64.read(buf), + script_pubkey=_UniffiConverterString.read(buf), + address=_UniffiConverterOptionalString.read(buf), + is_mine=_UniffiConverterBool.read(buf), ) @staticmethod def check_lower(value): - _UniffiConverterString.check_lower(value.address) - _UniffiConverterString.check_lower(value.signature) - _UniffiConverterString.check_lower(value.message) - _UniffiConverterOptionalTypeTrezorCoinType.check_lower(value.coin) + _UniffiConverterUInt64.check_lower(value.value) + _UniffiConverterString.check_lower(value.script_pubkey) + _UniffiConverterOptionalString.check_lower(value.address) + _UniffiConverterBool.check_lower(value.is_mine) @staticmethod def write(value, buf): - _UniffiConverterString.write(value.address, buf) - _UniffiConverterString.write(value.signature, buf) - _UniffiConverterString.write(value.message, buf) - _UniffiConverterOptionalTypeTrezorCoinType.write(value.coin, buf) + _UniffiConverterUInt64.write(value.value, buf) + _UniffiConverterString.write(value.script_pubkey, buf) + _UniffiConverterOptionalString.write(value.address, buf) + _UniffiConverterBool.write(value.is_mine, buf) class TxInput: @@ -8644,6 +9242,98 @@ def write(value, buf): _UniffiConverterTypeAddressType.write(value.address_type, buf) +class WalletBalance: + """ + Balance breakdown from BDK. + """ + + confirmed: "int" + """ + Confirmed and spendable balance (sats) + """ + + immature: "int" + """ + Immature coinbase outputs (sats) + """ + + trusted_pending: "int" + """ + Unconfirmed UTXOs from trusted sources (own change) (sats) + """ + + untrusted_pending: "int" + """ + Unconfirmed UTXOs from external sources (sats) + """ + + spendable: "int" + """ + Total spendable: confirmed + trusted_pending (sats) + """ + + total: "int" + """ + Grand total: all categories (sats) + """ + + def __init__(self, *, confirmed: "int", immature: "int", trusted_pending: "int", untrusted_pending: "int", spendable: "int", total: "int"): + self.confirmed = confirmed + self.immature = immature + self.trusted_pending = trusted_pending + self.untrusted_pending = untrusted_pending + self.spendable = spendable + self.total = total + + def __str__(self): + return "WalletBalance(confirmed={}, immature={}, trusted_pending={}, untrusted_pending={}, spendable={}, total={})".format(self.confirmed, self.immature, self.trusted_pending, self.untrusted_pending, self.spendable, self.total) + + def __eq__(self, other): + if self.confirmed != other.confirmed: + return False + if self.immature != other.immature: + return False + if self.trusted_pending != other.trusted_pending: + return False + if self.untrusted_pending != other.untrusted_pending: + return False + if self.spendable != other.spendable: + return False + if self.total != other.total: + return False + return True + +class _UniffiConverterTypeWalletBalance(_UniffiConverterRustBuffer): + @staticmethod + def read(buf): + return WalletBalance( + confirmed=_UniffiConverterUInt64.read(buf), + immature=_UniffiConverterUInt64.read(buf), + trusted_pending=_UniffiConverterUInt64.read(buf), + untrusted_pending=_UniffiConverterUInt64.read(buf), + spendable=_UniffiConverterUInt64.read(buf), + total=_UniffiConverterUInt64.read(buf), + ) + + @staticmethod + def check_lower(value): + _UniffiConverterUInt64.check_lower(value.confirmed) + _UniffiConverterUInt64.check_lower(value.immature) + _UniffiConverterUInt64.check_lower(value.trusted_pending) + _UniffiConverterUInt64.check_lower(value.untrusted_pending) + _UniffiConverterUInt64.check_lower(value.spendable) + _UniffiConverterUInt64.check_lower(value.total) + + @staticmethod + def write(value, buf): + _UniffiConverterUInt64.write(value.confirmed, buf) + _UniffiConverterUInt64.write(value.immature, buf) + _UniffiConverterUInt64.write(value.trusted_pending, buf) + _UniffiConverterUInt64.write(value.untrusted_pending, buf) + _UniffiConverterUInt64.write(value.spendable, buf) + _UniffiConverterUInt64.write(value.total, buf) + + class WalletParams: """ Common parameters for creating and syncing a watch-only BDK wallet. @@ -12867,6 +13557,68 @@ def write(value, buf): +class TxDirection(enum.Enum): + """ + Transaction direction from the wallet's perspective. + """ + + SENT = 0 + """ + Wallet sent funds to an external address + """ + + + RECEIVED = 1 + """ + Wallet received funds from an external source + """ + + + SELF_TRANSFER = 2 + """ + Wallet sent funds to itself (e.g. consolidation, change-only) + """ + + + + +class _UniffiConverterTypeTxDirection(_UniffiConverterRustBuffer): + @staticmethod + def read(buf): + variant = buf.read_i32() + if variant == 1: + return TxDirection.SENT + if variant == 2: + return TxDirection.RECEIVED + if variant == 3: + return TxDirection.SELF_TRANSFER + raise InternalError("Raw enum value doesn't match any cases") + + @staticmethod + def check_lower(value): + if value == TxDirection.SENT: + return + if value == TxDirection.RECEIVED: + return + if value == TxDirection.SELF_TRANSFER: + return + raise ValueError(value) + + @staticmethod + def write(value, buf): + if value == TxDirection.SENT: + buf.write_i32(1) + if value == TxDirection.RECEIVED: + buf.write_i32(2) + if value == TxDirection.SELF_TRANSFER: + buf.write_i32(3) + + + + + + + class WordCount(enum.Enum): WORDS12 = 12 """ @@ -13028,6 +13780,33 @@ def read(cls, buf): +class _UniffiConverterOptionalDouble(_UniffiConverterRustBuffer): + @classmethod + def check_lower(cls, value): + if value is not None: + _UniffiConverterDouble.check_lower(value) + + @classmethod + def write(cls, value, buf): + if value is None: + buf.write_u8(0) + return + + buf.write_u8(1) + _UniffiConverterDouble.write(value, buf) + + @classmethod + def read(cls, buf): + flag = buf.read_u8() + if flag == 0: + return None + elif flag == 1: + return _UniffiConverterDouble.read(buf) + else: + raise InternalError("Unexpected flag byte for optional type") + + + class _UniffiConverterOptionalBool(_UniffiConverterRustBuffer): @classmethod def check_lower(cls, value): @@ -14364,6 +15143,31 @@ def read(cls, buf): +class _UniffiConverterSequenceTypeHistoryTransaction(_UniffiConverterRustBuffer): + @classmethod + def check_lower(cls, value): + for item in value: + _UniffiConverterTypeHistoryTransaction.check_lower(item) + + @classmethod + def write(cls, value, buf): + items = len(value) + buf.write_i32(items) + for item in value: + _UniffiConverterTypeHistoryTransaction.write(item, buf) + + @classmethod + def read(cls, buf): + count = buf.read_i32() + if count < 0: + raise InternalError("Unexpected negative sequence length") + + return [ + _UniffiConverterTypeHistoryTransaction.read(buf) for i in range(count) + ] + + + class _UniffiConverterSequenceTypeIBtOnchainTransaction(_UniffiConverterRustBuffer): @classmethod def check_lower(cls, value): @@ -14789,6 +15593,56 @@ def read(cls, buf): +class _UniffiConverterSequenceTypeTxDetailInput(_UniffiConverterRustBuffer): + @classmethod + def check_lower(cls, value): + for item in value: + _UniffiConverterTypeTxDetailInput.check_lower(item) + + @classmethod + def write(cls, value, buf): + items = len(value) + buf.write_i32(items) + for item in value: + _UniffiConverterTypeTxDetailInput.write(item, buf) + + @classmethod + def read(cls, buf): + count = buf.read_i32() + if count < 0: + raise InternalError("Unexpected negative sequence length") + + return [ + _UniffiConverterTypeTxDetailInput.read(buf) for i in range(count) + ] + + + +class _UniffiConverterSequenceTypeTxDetailOutput(_UniffiConverterRustBuffer): + @classmethod + def check_lower(cls, value): + for item in value: + _UniffiConverterTypeTxDetailOutput.check_lower(item) + + @classmethod + def write(cls, value, buf): + items = len(value) + buf.write_i32(items) + for item in value: + _UniffiConverterTypeTxDetailOutput.write(item, buf) + + @classmethod + def read(cls, buf): + count = buf.read_i32() + if count < 0: + raise InternalError("Unexpected negative sequence length") + + return [ + _UniffiConverterTypeTxDetailOutput.read(buf) for i in range(count) + ] + + + class _UniffiConverterSequenceTypeTxInput(_UniffiConverterRustBuffer): @classmethod def check_lower(cls, value): @@ -17062,6 +17916,69 @@ async def onchain_get_address_info(address: "str",electrum_url: "str",network: " # Error FFI converter _UniffiConverterTypeAccountInfoError, + ) +async def onchain_get_transaction_detail(extended_key: "str",electrum_url: "str",txid: "str",network: "typing.Optional[Network]",script_type: "typing.Optional[AccountType]") -> "TransactionDetail": + + """ + Get full details for a single transaction by txid. + """ + + _UniffiConverterString.check_lower(extended_key) + + _UniffiConverterString.check_lower(electrum_url) + + _UniffiConverterString.check_lower(txid) + + _UniffiConverterOptionalTypeNetwork.check_lower(network) + + _UniffiConverterOptionalTypeAccountType.check_lower(script_type) + + return await _uniffi_rust_call_async( + _UniffiLib.uniffi_bitkitcore_fn_func_onchain_get_transaction_detail( + _UniffiConverterString.lower(extended_key), + _UniffiConverterString.lower(electrum_url), + _UniffiConverterString.lower(txid), + _UniffiConverterOptionalTypeNetwork.lower(network), + _UniffiConverterOptionalTypeAccountType.lower(script_type)), + _UniffiLib.ffi_bitkitcore_rust_future_poll_rust_buffer, + _UniffiLib.ffi_bitkitcore_rust_future_complete_rust_buffer, + _UniffiLib.ffi_bitkitcore_rust_future_free_rust_buffer, + # lift function + _UniffiConverterTypeTransactionDetail.lift, + + # Error FFI converter +_UniffiConverterTypeAccountInfoError, + + ) +async def onchain_get_transaction_history(extended_key: "str",electrum_url: "str",network: "typing.Optional[Network]",script_type: "typing.Optional[AccountType]") -> "TransactionHistoryResult": + + """ + Query transaction history and balance for an extended public key via Electrum. + """ + + _UniffiConverterString.check_lower(extended_key) + + _UniffiConverterString.check_lower(electrum_url) + + _UniffiConverterOptionalTypeNetwork.check_lower(network) + + _UniffiConverterOptionalTypeAccountType.check_lower(script_type) + + return await _uniffi_rust_call_async( + _UniffiLib.uniffi_bitkitcore_fn_func_onchain_get_transaction_history( + _UniffiConverterString.lower(extended_key), + _UniffiConverterString.lower(electrum_url), + _UniffiConverterOptionalTypeNetwork.lower(network), + _UniffiConverterOptionalTypeAccountType.lower(script_type)), + _UniffiLib.ffi_bitkitcore_rust_future_poll_rust_buffer, + _UniffiLib.ffi_bitkitcore_rust_future_complete_rust_buffer, + _UniffiLib.ffi_bitkitcore_rust_future_free_rust_buffer, + # lift function + _UniffiConverterTypeTransactionHistoryResult.lift, + + # Error FFI converter +_UniffiConverterTypeAccountInfoError, + ) async def open_channel(order_id: "str",connection_string: "str") -> "IBtOrder": @@ -18014,6 +18931,7 @@ def wipe_all_transaction_details() -> None: "TrezorError", "TrezorScriptType", "TrezorTransportType", + "TxDirection", "WordCount", "AccountAddresses", "AccountInfoResult", @@ -18033,6 +18951,7 @@ def wipe_all_transaction_details() -> None: "FundingTx", "GetAddressResponse", "GetAddressesResponse", + "HistoryTransaction", "IBt0ConfMinTxFeeWindow", "IBtBolt11Invoice", "IBtChannel", @@ -18076,7 +18995,9 @@ def wipe_all_transaction_details() -> None: "SweepResult", "SweepTransactionPreview", "SweepableBalances", + "TransactionDetail", "TransactionDetails", + "TransactionHistoryResult", "TrezorAddressResponse", "TrezorCallMessageResult", "TrezorDeviceInfo", @@ -18096,9 +19017,12 @@ def wipe_all_transaction_details() -> None: "TrezorTxInput", "TrezorTxOutput", "TrezorVerifyMessageParams", + "TxDetailInput", + "TxDetailOutput", "TxInput", "TxOutput", "ValidationResult", + "WalletBalance", "WalletParams", "activity_wipe_all", "add_pre_activity_metadata", @@ -18167,6 +19091,8 @@ def wipe_all_transaction_details() -> None: "onchain_compose_transaction", "onchain_get_account_info", "onchain_get_address_info", + "onchain_get_transaction_detail", + "onchain_get_transaction_history", "open_channel", "prepare_sweep_transaction", "refresh_active_cjit_entries", diff --git a/bindings/python/bitkitcore/libbitkitcore.dylib b/bindings/python/bitkitcore/libbitkitcore.dylib index 31084da..561637c 100755 Binary files a/bindings/python/bitkitcore/libbitkitcore.dylib and b/bindings/python/bitkitcore/libbitkitcore.dylib differ diff --git a/src/lib.rs b/src/lib.rs index 5fb2724..f926ca8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -37,7 +37,7 @@ pub use modules::activity; use crate::modules::pubky::{PubkyError, PubkyProfile}; use crate::activity::{ActivityError, ActivityDB, OnchainActivity, LightningActivity, Activity, ActivityFilter, SortDirection, PaymentType, DbError, ClosedChannelDetails, ActivityTags, PreActivityMetadata, TransactionDetails}; use crate::modules::blocktank::{BlocktankDB, BlocktankError, IBtInfo, IBtOrder, CreateOrderOptions, BtOrderState2, IBt0ConfMinTxFeeWindow, IBtEstimateFeeResponse, IBtEstimateFeeResponse2, CreateCjitOptions, ICJitEntry, CJitStateEnum, IBtBolt11Invoice, IGift, ChannelLiquidityOptions, ChannelLiquidityParams, DefaultLspBalanceParams}; -use crate::onchain::{AddressError, BroadcastError, AccountInfoError, ValidationResult, GetAddressResponse, Network, GetAddressesResponse, SweepError, SweepResult, SweepTransactionPreview, SweepableBalances, broadcast_raw_tx, AccountInfoResult, SingleAddressInfoResult, AccountType, get_account_info, get_address_info}; +use crate::onchain::{AddressError, BroadcastError, AccountInfoError, ValidationResult, GetAddressResponse, Network, GetAddressesResponse, SweepError, SweepResult, SweepTransactionPreview, SweepableBalances, broadcast_raw_tx, AccountInfoResult, SingleAddressInfoResult, AccountType, get_account_info, get_address_info, get_transaction_history, get_transaction_detail, TransactionHistoryResult, TransactionDetail}; use crate::modules::trezor::{TrezorError, TrezorDeviceInfo, TrezorFeatures, TrezorGetAddressParams, TrezorAddressResponse, TrezorGetPublicKeyParams, TrezorPublicKeyResponse, TrezorScriptType, TrezorManager, TrezorSignMessageParams, TrezorSignedMessageResponse, TrezorVerifyMessageParams, TrezorSignTxParams, TrezorSignedTx, TrezorCoinType}; use crate::modules::trezor::account_type_to_script_type; use crate::onchain::{compose_transaction, ComposeParams, ComposeResult}; @@ -1766,6 +1766,39 @@ pub async fn onchain_get_account_info( })) } +/// Query transaction history and balance for an extended public key via Electrum. +#[uniffi::export] +pub async fn onchain_get_transaction_history( + extended_key: String, + electrum_url: String, + network: Option, + script_type: Option, +) -> Result { + let rt = ensure_runtime(); + rt.spawn(async move { + get_transaction_history(&extended_key, &electrum_url, network, script_type).await + }).await.unwrap_or_else(|e| Err(AccountInfoError::SyncError { + error_details: format!("Runtime error: {}", e), + })) +} + +/// Get full details for a single transaction by txid. +#[uniffi::export] +pub async fn onchain_get_transaction_detail( + extended_key: String, + electrum_url: String, + txid: String, + network: Option, + script_type: Option, +) -> Result { + let rt = ensure_runtime(); + rt.spawn(async move { + get_transaction_detail(&extended_key, &electrum_url, &txid, network, script_type).await + }).await.unwrap_or_else(|e| Err(AccountInfoError::SyncError { + error_details: format!("Runtime error: {}", e), + })) +} + /// Query balance and UTXOs for a single Bitcoin address via Electrum. #[uniffi::export] pub async fn onchain_get_address_info( diff --git a/src/modules/onchain/implementation.rs b/src/modules/onchain/implementation.rs index ec923d7..2eab785 100644 --- a/src/modules/onchain/implementation.rs +++ b/src/modules/onchain/implementation.rs @@ -7,8 +7,8 @@ use bdk::bitcoin::bip32::ExtendedPubKey; use bdk::bitcoin::consensus::deserialize; use bdk::bitcoin::psbt::PartiallySignedTransaction as Psbt; use bdk::bitcoin::{ - Address as BdkAddress, Network as BdkNetwork, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, - TxOut, Witness, + Address as BdkAddress, Network as BdkNetwork, OutPoint, ScriptBuf, Sequence, Transaction, Txid, + TxIn, TxOut, Witness, }; use bdk::blockchain::ElectrumBlockchain; use bdk::database::MemoryDatabase; @@ -25,8 +25,9 @@ use bitcoin_address_generator; use super::errors::AccountInfoError; use super::types::{ AccountAddresses, AccountInfoResult, AccountType, AccountUtxo, AddressInfo, - AddressType, ComposeAccount, Network as OnchainNetwork, SingleAddressInfoResult, - ValidationResult, + AddressType, ComposeAccount, HistoryTransaction, Network as OnchainNetwork, + SingleAddressInfoResult, TransactionDetail, TransactionHistoryResult, TxDetailInput, + TxDetailOutput, TxDirection, ValidationResult, WalletBalance, }; use crate::modules::scanner::NetworkType; use crate::onchain::types::{ @@ -1082,6 +1083,294 @@ pub async fn get_account_info( }) } +/// Query transaction history and balance for an extended public key via Electrum. +pub async fn get_transaction_history( + extended_key: &str, + electrum_url: &str, + network: Option, + script_type: Option, +) -> Result { + let setup = resolve_wallet_setup(extended_key, network, script_type, None)?; + let account_type = setup.account_type; + + let electrum_url = electrum_url.to_string(); + + let result = tokio::task::spawn_blocking(move || { + let client = connect_electrum(&electrum_url)?; + let header = client.block_headers_subscribe().map_err(|e| { + AccountInfoError::ElectrumError { + error_details: format!("Failed to get block height: {}", e), + } + })?; + let tip_height = u32::try_from(header.height).map_err(|_| { + AccountInfoError::ElectrumError { + error_details: format!("Invalid block height: {}", header.height), + } + })?; + + let wallet = create_and_sync_wallet(&setup, client)?; + + // Balance + let bdk_balance = wallet.get_balance().map_err(|e| AccountInfoError::WalletError { + error_details: format!("Failed to get balance: {}", e), + })?; + let balance = WalletBalance { + confirmed: bdk_balance.confirmed, + immature: bdk_balance.immature, + trusted_pending: bdk_balance.trusted_pending, + untrusted_pending: bdk_balance.untrusted_pending, + spendable: bdk_balance.confirmed + bdk_balance.trusted_pending, + total: bdk_balance.confirmed + + bdk_balance.immature + + bdk_balance.trusted_pending + + bdk_balance.untrusted_pending, + }; + + // Transaction history + let txs = wallet + .list_transactions(false) + .map_err(|e| AccountInfoError::WalletError { + error_details: format!("Failed to list transactions: {}", e), + })?; + + let mut history: Vec = txs + .iter() + .map(|tx| { + let sent = tx.sent; + let received = tx.received; + let fee = tx.fee; + let net = received as i64 - sent as i64; + + let direction = if sent > 0 && received > 0 { + // Self-transfer: the net outflow equals (or is less than) the fee + match fee { + Some(f) if sent.saturating_sub(received) <= f => TxDirection::SelfTransfer, + _ => TxDirection::Sent, + } + } else if sent > 0 { + TxDirection::Sent + } else { + TxDirection::Received + }; + + let amount = match direction { + TxDirection::Received => received, + TxDirection::Sent => { + sent.saturating_sub(received).saturating_sub(fee.unwrap_or(0)) + } + TxDirection::SelfTransfer => fee.unwrap_or(0), + }; + + let (block_height, timestamp, confirmations) = + match tx.confirmation_time.as_ref() { + Some(conf) => { + let confs = tip_height.saturating_sub(conf.height) + 1; + (Some(conf.height), Some(conf.timestamp), confs) + } + None => (None, None, 0), + }; + + HistoryTransaction { + txid: tx.txid.to_string(), + received, + sent, + net, + fee, + amount, + direction, + block_height, + timestamp, + confirmations, + } + }) + .collect(); + + // Sort: unconfirmed first, then by timestamp descending + history.sort_by(|a, b| { + match (a.timestamp, b.timestamp) { + (None, Some(_)) => std::cmp::Ordering::Less, + (Some(_), None) => std::cmp::Ordering::Greater, + (None, None) => std::cmp::Ordering::Equal, + (Some(a_ts), Some(b_ts)) => b_ts.cmp(&a_ts), + } + }); + + let tx_count = u32::try_from(history.len()).unwrap_or(u32::MAX); + + Ok((history, balance, tx_count, tip_height)) + }) + .await + .map_err(|e| AccountInfoError::SyncError { + error_details: format!("Task failed: {}", e), + })??; + + let (transactions, balance, tx_count, block_height) = result; + + Ok(TransactionHistoryResult { + transactions, + balance, + tx_count, + block_height, + account_type, + }) +} + +/// Get full details for a single transaction by txid. +/// +/// Requires the extended public key because BDK needs to create a wallet to determine +/// which outputs belong to the wallet and to compute sent/received amounts. +pub async fn get_transaction_detail( + extended_key: &str, + electrum_url: &str, + txid: &str, + network: Option, + script_type: Option, +) -> Result { + let target_txid = Txid::from_str(txid).map_err(|e| AccountInfoError::InvalidTxid { + error_details: format!("Invalid txid '{}': {}", txid, e), + })?; + + let setup = resolve_wallet_setup(extended_key, network, script_type, None)?; + let wallet_network = setup.network; + + let electrum_url = electrum_url.to_string(); + + let result = tokio::task::spawn_blocking(move || { + let client = connect_electrum(&electrum_url)?; + let header = client.block_headers_subscribe().map_err(|e| { + AccountInfoError::ElectrumError { + error_details: format!("Failed to get block height: {}", e), + } + })?; + let tip_height = u32::try_from(header.height).map_err(|_| { + AccountInfoError::ElectrumError { + error_details: format!("Invalid block height: {}", header.height), + } + })?; + + let wallet = create_and_sync_wallet(&setup, client)?; + + // Include raw transaction data + let txs = wallet + .list_transactions(true) + .map_err(|e| AccountInfoError::WalletError { + error_details: format!("Failed to list transactions: {}", e), + })?; + + let tx = txs + .iter() + .find(|t| t.txid == target_txid) + .ok_or_else(|| AccountInfoError::InvalidTxid { + error_details: format!("Transaction {} not found in wallet", target_txid), + })?; + + // Summary fields (same logic as get_transaction_history) + let sent = tx.sent; + let received = tx.received; + let fee = tx.fee; + let net = received as i64 - sent as i64; + + let direction = if sent > 0 && received > 0 { + match fee { + Some(f) if sent.saturating_sub(received) <= f => TxDirection::SelfTransfer, + _ => TxDirection::Sent, + } + } else if sent > 0 { + TxDirection::Sent + } else { + TxDirection::Received + }; + + let amount = match direction { + TxDirection::Received => received, + TxDirection::Sent => { + sent.saturating_sub(received).saturating_sub(fee.unwrap_or(0)) + } + TxDirection::SelfTransfer => fee.unwrap_or(0), + }; + + let (block_height, timestamp, confirmations) = match tx.confirmation_time.as_ref() { + Some(conf) => { + let confs = tip_height.saturating_sub(conf.height) + 1; + (Some(conf.height), Some(conf.timestamp), confs) + } + None => (None, None, 0), + }; + + // Raw transaction details + let raw_tx = tx.transaction.as_ref().ok_or_else(|| { + AccountInfoError::WalletError { + error_details: format!( + "Raw transaction data not available for {}", + target_txid + ), + } + })?; + + let inputs: Vec = raw_tx + .input + .iter() + .map(|inp| TxDetailInput { + txid: inp.previous_output.txid.to_string(), + vout: inp.previous_output.vout, + sequence: inp.sequence.0, + script_sig: hex::encode(inp.script_sig.as_bytes()), + witness: inp.witness.iter().map(|w| hex::encode(w)).collect(), + }) + .collect(); + + let outputs: Vec = raw_tx + .output + .iter() + .map(|out| { + let address = BdkAddress::from_script(&out.script_pubkey, wallet_network) + .ok() + .map(|a| a.to_string()); + let is_mine = wallet.is_mine(&out.script_pubkey).unwrap_or(false); + TxDetailOutput { + value: out.value, + script_pubkey: hex::encode(out.script_pubkey.as_bytes()), + address, + is_mine, + } + }) + .collect(); + + let size = raw_tx.size() as u32; + let vsize = raw_tx.vsize() as u32; + let weight = raw_tx.weight().to_wu() as u32; + let fee_rate = match fee { + Some(f) if vsize > 0 => Some(f as f64 / vsize as f64), + _ => None, + }; + + Ok(TransactionDetail { + txid: tx.txid.to_string(), + received, + sent, + net, + amount, + fee, + direction, + block_height, + timestamp, + confirmations, + inputs, + outputs, + size, + vsize, + weight, + fee_rate, + }) + }) + .await + .map_err(|e| AccountInfoError::SyncError { + error_details: format!("Task failed: {}", e), + })??; + + Ok(result) +} + /// Query balance and UTXOs for a single Bitcoin address via Electrum. /// /// When `network` is `None`, the address is accepted without network validation diff --git a/src/modules/onchain/mod.rs b/src/modules/onchain/mod.rs index 0f984d2..1b53bbd 100644 --- a/src/modules/onchain/mod.rs +++ b/src/modules/onchain/mod.rs @@ -6,15 +6,16 @@ mod compose; pub use errors::{AccountInfoError, AddressError, BroadcastError, SweepError}; pub use implementation::{ broadcast_raw_tx, build_descriptors, derive_base_path, detect_account_type, - detect_network_from_key, get_account_info, get_address_info, normalize_extended_key, - BitcoinAddressValidator, + detect_network_from_key, get_account_info, get_address_info, get_transaction_detail, + get_transaction_history, normalize_extended_key, BitcoinAddressValidator, }; pub use types::{ AccountAddresses, AccountInfoResult, AccountType, AccountUtxo, AddressInfo, AddressType, CoinSelection, ComposeAccount, ComposeOutput, ComposeParams, - ComposeResult, GetAddressResponse, GetAddressesResponse, Network, + ComposeResult, GetAddressResponse, GetAddressesResponse, HistoryTransaction, Network, SingleAddressInfoResult, SweepResult, SweepTransactionPreview, SweepableBalances, - ValidationResult, WalletParams, WordCount, + TransactionDetail, TransactionHistoryResult, TxDetailInput, TxDetailOutput, TxDirection, + ValidationResult, WalletBalance, WalletParams, WordCount, }; pub use compose::compose_transaction; diff --git a/src/modules/onchain/tests.rs b/src/modules/onchain/tests.rs index bbba215..c8dd5da 100644 --- a/src/modules/onchain/tests.rs +++ b/src/modules/onchain/tests.rs @@ -1109,6 +1109,305 @@ mod tests { assert!(result.is_err(), "Expected error for invalid electrum URL"); } + // ======================================================================== + // Transaction History Tests + // ======================================================================== + + #[test] + fn test_get_transaction_history_invalid_key() { + use crate::modules::onchain::get_transaction_history; + + let rt = tokio::runtime::Runtime::new().unwrap(); + let result = rt.block_on(get_transaction_history( + "not_a_valid_xpub", + ACCOUNT_INFO_ELECTRUM_URL, + None, + None, + )); + + assert!(result.is_err(), "Expected error for invalid key"); + } + + #[test] + fn test_get_transaction_history_network_mismatch() { + use crate::modules::onchain::get_transaction_history; + use crate::modules::onchain::{AccountInfoError, Network as OnchainNetwork}; + + let rt = tokio::runtime::Runtime::new().unwrap(); + // tpub is testnet, but we specify Bitcoin (mainnet) — should get NetworkMismatch + let result = rt.block_on(get_transaction_history( + TEST_TPUB, + ACCOUNT_INFO_ELECTRUM_URL, + Some(OnchainNetwork::Bitcoin), + None, + )); + + assert!(result.is_err(), "Expected NetworkMismatch error"); + match result.unwrap_err() { + AccountInfoError::NetworkMismatch { .. } => {} + other => panic!("Expected NetworkMismatch, got: {:?}", other), + } + } + + #[tokio::test] + #[ignore] + async fn test_get_transaction_history_vpub() { + use crate::modules::onchain::get_transaction_history; + use crate::modules::onchain::{AccountType, TxDirection}; + + let result = get_transaction_history( + TEST_VPUB, + ACCOUNT_INFO_ELECTRUM_URL, + None, + None, + ) + .await; + + let info = result.expect("get_transaction_history(vpub) should succeed"); + assert_eq!(info.account_type, AccountType::NativeSegwit); + assert!(info.block_height > 0, "Expected block_height > 0"); + assert!(info.tx_count > 0, "Expected at least 1 transaction"); + assert_eq!(info.tx_count, info.transactions.len() as u32); + + // Balance should be consistent + assert!(info.balance.total >= info.balance.confirmed); + assert_eq!( + info.balance.spendable, + info.balance.confirmed + info.balance.trusted_pending + ); + + // Verify transaction fields + for tx in &info.transactions { + assert!(!tx.txid.is_empty(), "Transaction should have non-empty txid"); + assert!(tx.received > 0 || tx.sent > 0, "Transaction should have some value"); + + match tx.direction { + TxDirection::Sent => assert!(tx.sent > 0), + TxDirection::Received => assert!(tx.received > 0), + TxDirection::SelfTransfer => { + assert!(tx.sent > 0); + assert!(tx.received > 0); + } + } + } + + // Verify sort order: unconfirmed first, then by timestamp descending + let mut prev_timestamp: Option = None; + let mut seen_confirmed = false; + for tx in &info.transactions { + if tx.timestamp.is_none() { + assert!(!seen_confirmed, "Unconfirmed txs should come before confirmed"); + } else { + seen_confirmed = true; + if let Some(prev) = prev_timestamp { + assert!(tx.timestamp.unwrap() <= prev, "Confirmed txs should be sorted newest first"); + } + prev_timestamp = tx.timestamp; + } + } + + println!( + "vpub tx history: tx_count={}, balance={}, block_height={}", + info.tx_count, info.balance.confirmed, info.block_height + ); + } + + // ======================================================================== + // Transaction Detail Tests + // ======================================================================== + + #[test] + fn test_get_transaction_detail_invalid_key() { + use crate::modules::onchain::get_transaction_detail; + + let rt = tokio::runtime::Runtime::new().unwrap(); + let result = rt.block_on(get_transaction_detail( + "not_a_valid_xpub", + ACCOUNT_INFO_ELECTRUM_URL, + "0000000000000000000000000000000000000000000000000000000000000000", + None, + None, + )); + + assert!(result.is_err(), "Expected error for invalid key"); + } + + #[test] + fn test_get_transaction_detail_invalid_txid() { + use crate::modules::onchain::get_transaction_detail; + use crate::modules::onchain::AccountInfoError; + + let rt = tokio::runtime::Runtime::new().unwrap(); + let result = rt.block_on(get_transaction_detail( + TEST_VPUB, + ACCOUNT_INFO_ELECTRUM_URL, + "not_a_valid_txid", + None, + None, + )); + + assert!(result.is_err(), "Expected error for invalid txid"); + match result.unwrap_err() { + AccountInfoError::InvalidTxid { .. } => {} + other => panic!("Expected InvalidTxid, got: {:?}", other), + } + } + + #[test] + fn test_get_transaction_detail_network_mismatch() { + use crate::modules::onchain::get_transaction_detail; + use crate::modules::onchain::{AccountInfoError, Network as OnchainNetwork}; + + let rt = tokio::runtime::Runtime::new().unwrap(); + let result = rt.block_on(get_transaction_detail( + TEST_TPUB, + ACCOUNT_INFO_ELECTRUM_URL, + "0000000000000000000000000000000000000000000000000000000000000000", + Some(OnchainNetwork::Bitcoin), + None, + )); + + assert!(result.is_err(), "Expected NetworkMismatch error"); + match result.unwrap_err() { + AccountInfoError::NetworkMismatch { .. } => {} + other => panic!("Expected NetworkMismatch, got: {:?}", other), + } + } + + #[tokio::test] + #[ignore] + async fn test_get_transaction_detail_vpub() { + use crate::modules::onchain::{get_transaction_detail, get_transaction_history, TxDirection}; + + // First get a known txid from the history + let history = get_transaction_history( + TEST_VPUB, + ACCOUNT_INFO_ELECTRUM_URL, + None, + None, + ) + .await + .expect("get_transaction_history should succeed"); + + assert!(!history.transactions.is_empty(), "Need at least 1 tx to test detail"); + + let target_tx = &history.transactions[0]; + let target_txid = &target_tx.txid; + + let detail = get_transaction_detail( + TEST_VPUB, + ACCOUNT_INFO_ELECTRUM_URL, + target_txid, + None, + None, + ) + .await + .expect("get_transaction_detail should succeed"); + + // Verify txid matches + assert_eq!(detail.txid, *target_txid); + + // Verify summary fields match the history entry + assert_eq!(detail.received, target_tx.received); + assert_eq!(detail.sent, target_tx.sent); + assert_eq!(detail.net, target_tx.net); + assert_eq!(detail.amount, target_tx.amount); + assert_eq!(detail.fee, target_tx.fee); + assert_eq!(detail.direction, target_tx.direction); + + // Verify detail fields + assert!(!detail.inputs.is_empty(), "Transaction should have inputs"); + assert!(!detail.outputs.is_empty(), "Transaction should have outputs"); + assert!(detail.size > 0, "Transaction size should be > 0"); + assert!(detail.vsize > 0, "Transaction vsize should be > 0"); + assert!(detail.weight > 0, "Transaction weight should be > 0"); + assert!(detail.vsize <= detail.size, "vsize should be <= size"); + + // Fee rate should be present when fee is known + if detail.fee.is_some() { + assert!(detail.fee_rate.is_some(), "fee_rate should be present when fee is known"); + assert!(detail.fee_rate.unwrap() > 0.0, "fee_rate should be positive"); + } + + // For received txs, at least one output should be ours + if detail.direction == TxDirection::Received { + assert!( + detail.outputs.iter().any(|o| o.is_mine), + "Received tx should have at least one is_mine output" + ); + } + + // Verify input fields + for inp in &detail.inputs { + assert!(!inp.txid.is_empty(), "Input should have non-empty txid"); + } + + // Verify output fields + for out in &detail.outputs { + assert!(!out.script_pubkey.is_empty(), "Output should have script_pubkey"); + } + + println!( + "vpub tx detail: txid={}, inputs={}, outputs={}, size={}, vsize={}, fee_rate={:?}", + detail.txid, + detail.inputs.len(), + detail.outputs.len(), + detail.size, + detail.vsize, + detail.fee_rate + ); + } + + #[tokio::test] + #[ignore] + async fn test_history_transaction_amount() { + use crate::modules::onchain::{get_transaction_history, TxDirection}; + + let history = get_transaction_history( + TEST_VPUB, + ACCOUNT_INFO_ELECTRUM_URL, + None, + None, + ) + .await + .expect("get_transaction_history should succeed"); + + assert!(!history.transactions.is_empty(), "Need at least 1 tx"); + + for tx in &history.transactions { + match tx.direction { + TxDirection::Received => { + assert_eq!( + tx.amount, tx.received, + "Received amount should equal received (txid={})", + tx.txid + ); + } + TxDirection::Sent => { + let expected = tx + .sent + .saturating_sub(tx.received) + .saturating_sub(tx.fee.unwrap_or(0)); + assert_eq!( + tx.amount, expected, + "Sent amount should equal sent - received - fee (txid={})", + tx.txid + ); + } + TxDirection::SelfTransfer => { + assert_eq!( + tx.amount, + tx.fee.unwrap_or(0), + "SelfTransfer amount should equal fee (txid={})", + tx.txid + ); + } + } + } + + println!("Verified amount field for {} transactions", history.transactions.len()); + } + // ======================================================================== // Compose Transaction Tests (BDK-based, signer-agnostic) // ======================================================================== diff --git a/src/modules/onchain/types.rs b/src/modules/onchain/types.rs index 22d21c9..5e0d33a 100644 --- a/src/modules/onchain/types.rs +++ b/src/modules/onchain/types.rs @@ -386,3 +386,147 @@ pub enum ComposeResult { error: String, }, } + +// ============================================================================ +// Transaction history types +// ============================================================================ + +/// Transaction direction from the wallet's perspective. +#[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Enum)] +pub enum TxDirection { + /// Wallet sent funds to an external address + Sent, + /// Wallet received funds from an external source + Received, + /// Wallet sent funds to itself (e.g. consolidation, change-only) + SelfTransfer, +} + +/// A single transaction in the wallet's history. +#[derive(Debug, Clone, uniffi::Record)] +pub struct HistoryTransaction { + /// Transaction ID (hex) + pub txid: String, + /// Amount received by the wallet (sats) + pub received: u64, + /// Amount sent by the wallet (sats) — includes change sent back to self + pub sent: u64, + /// Net value from wallet's perspective: received - sent (positive = inflow, negative = outflow) + pub net: i64, + /// Transaction fee in sats (None if not available, e.g. for received-only txs) + pub fee: Option, + /// Display amount in sats: + /// - Received: the received value + /// - Sent: amount that left the wallet (sent - received - fee) + /// - SelfTransfer: the fee paid + pub amount: u64, + /// Transaction direction + pub direction: TxDirection, + /// Block height (None if unconfirmed/mempool) + pub block_height: Option, + /// Block timestamp as unix epoch seconds (None if unconfirmed) + pub timestamp: Option, + /// Number of confirmations (0 if unconfirmed) + pub confirmations: u32, +} + +/// Balance breakdown from BDK. +#[derive(Debug, Clone, uniffi::Record)] +pub struct WalletBalance { + /// Confirmed and spendable balance (sats) + pub confirmed: u64, + /// Immature coinbase outputs (sats) + pub immature: u64, + /// Unconfirmed UTXOs from trusted sources (own change) (sats) + pub trusted_pending: u64, + /// Unconfirmed UTXOs from external sources (sats) + pub untrusted_pending: u64, + /// Total spendable: confirmed + trusted_pending (sats) + pub spendable: u64, + /// Grand total: all categories (sats) + pub total: u64, +} + +/// Result from querying transaction history for an xpub. +#[derive(Debug, Clone, uniffi::Record)] +pub struct TransactionHistoryResult { + /// All transactions, sorted: unconfirmed first, then by timestamp descending + pub transactions: Vec, + /// Balance breakdown + pub balance: WalletBalance, + /// Total number of transactions + pub tx_count: u32, + /// Current blockchain tip height + pub block_height: u32, + /// The detected or specified account type + pub account_type: AccountType, +} + +// ============================================================================ +// Transaction detail types (single-tx endpoint) +// ============================================================================ + +/// A transaction input with full details. +#[derive(Debug, Clone, uniffi::Record)] +pub struct TxDetailInput { + /// Previous output transaction ID (hex) + pub txid: String, + /// Previous output index + pub vout: u32, + /// Sequence number + pub sequence: u32, + /// Script signature (hex-encoded) + pub script_sig: String, + /// Witness stack (each element hex-encoded) + pub witness: Vec, +} + +/// A transaction output with full details. +#[derive(Debug, Clone, uniffi::Record)] +pub struct TxDetailOutput { + /// Output value in sats + pub value: u64, + /// Script public key (hex-encoded) + pub script_pubkey: String, + /// Decoded address (None if script is not decodable to an address) + pub address: Option, + /// Whether this output belongs to the queried wallet + pub is_mine: bool, +} + +/// Full details for a single transaction, including raw inputs/outputs and size metrics. +#[derive(Debug, Clone, uniffi::Record)] +pub struct TransactionDetail { + /// Transaction ID (hex) + pub txid: String, + /// Amount received by the wallet (sats) + pub received: u64, + /// Amount sent by the wallet (sats) — includes change sent back to self + pub sent: u64, + /// Net value from wallet's perspective: received - sent (positive = inflow, negative = outflow) + pub net: i64, + /// Display amount in sats (same semantics as HistoryTransaction.amount) + pub amount: u64, + /// Transaction fee in sats (None if not available) + pub fee: Option, + /// Transaction direction + pub direction: TxDirection, + /// Block height (None if unconfirmed/mempool) + pub block_height: Option, + /// Block timestamp as unix epoch seconds (None if unconfirmed) + pub timestamp: Option, + /// Number of confirmations (0 if unconfirmed) + pub confirmations: u32, + /// Transaction inputs + pub inputs: Vec, + /// Transaction outputs + pub outputs: Vec, + /// Serialized transaction size in bytes + pub size: u32, + /// Virtual size in vbytes (ceil(weight / 4)) + pub vsize: u32, + /// Transaction weight in weight units + pub weight: u32, + /// Fee rate in sat/vB (fee / vsize), None if fee or vsize unavailable + pub fee_rate: Option, +}