diff --git a/Cargo.toml b/Cargo.toml index 5ce10d6ad..39677fdcf 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ldk-node" -version = "0.6.0+git" +version = "0.6.0-rc.1" authors = ["Elias Rohrer "] homepage = "https://lightningdevkit.org/" license = "MIT OR Apache-2.0" diff --git a/Package.swift b/Package.swift index 61da2d5e7..d17130548 100644 --- a/Package.swift +++ b/Package.swift @@ -3,8 +3,8 @@ import PackageDescription -let tag = "v0.5.0" -let checksum = "fd9eb84a478402af8f790519a463b6e1bf6ab3987f5951cd8375afb9d39e7a4b" +let tag = "v0.6.0-rc.1" +let checksum = "6b14ee557400b0c1c12767ed8cc9971e6c818fae5c5e0d51bb6740ab39dcb5eb" let url = "https://github.com/lightningdevkit/ldk-node/releases/download/\(tag)/LDKNodeFFI.xcframework.zip" let package = Package( diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index c2f0166c8..cb39d3469 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -222,6 +222,10 @@ interface OnchainPayment { Txid send_to_address([ByRef]Address address, u64 amount_sats, FeeRate? fee_rate); [Throws=NodeError] Txid send_all_to_address([ByRef]Address address, boolean retain_reserve, FeeRate? fee_rate); + [Throws=NodeError] + Txid bump_fee_by_rbf([ByRef]Txid txid, FeeRate fee_rate); + [Throws=NodeError] + Txid accelerate_by_cpfp([ByRef]Txid txid, FeeRate? fee_rate, Address? destination_address); }; interface FeeRate { @@ -302,6 +306,10 @@ enum NodeError { "InsufficientFunds", "LiquiditySourceUnavailable", "LiquidityFeeTooHigh", + "CannotRbfFundingTransaction", + "TransactionNotFound", + "TransactionAlreadyConfirmed", + "NoSpendableOutputs", }; dictionary NodeStatus { diff --git a/bindings/swift/Sources/LDKNode/LDKNode.swift b/bindings/swift/Sources/LDKNode/LDKNode.swift index de5df5e00..879d43890 100644 --- a/bindings/swift/Sources/LDKNode/LDKNode.swift +++ b/bindings/swift/Sources/LDKNode/LDKNode.swift @@ -511,6 +511,252 @@ fileprivate struct FfiConverterData: FfiConverterRustBuffer { +public protocol Bolt11InvoiceProtocol : AnyObject { + + func amountMilliSatoshis() -> UInt64? + + func currency() -> Currency + + func description() -> Bolt11InvoiceDescription + + func expiryTimeSeconds() -> UInt64 + + func fallbackAddresses() -> [Address] + + func isExpired() -> Bool + + func minFinalCltvExpiryDelta() -> UInt64 + + func network() -> Network + + func paymentHash() -> PaymentHash + + func paymentSecret() -> PaymentSecret + + func recoverPayeePubKey() -> PublicKey + + func routeHints() -> [[RouteHintHop]] + + func secondsSinceEpoch() -> UInt64 + + func secondsUntilExpiry() -> UInt64 + + func signableHash() -> [UInt8] + + func wouldExpire(atTimeSeconds: UInt64) -> Bool + +} + +open class Bolt11Invoice: + Bolt11InvoiceProtocol { + fileprivate let pointer: UnsafeMutableRawPointer! + + /// Used to instantiate a [FFIObject] without an actual pointer, for fakes in tests, mostly. + public struct NoPointer { + public init() {} + } + + // TODO: We'd like this to be `private` but for Swifty reasons, + // we can't implement `FfiConverter` without making this `required` and we can't + // make it `required` without making it `public`. + required public init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { + self.pointer = pointer + } + + /// This constructor can be used to instantiate a fake object. + /// - Parameter noPointer: Placeholder value so we can have a constructor separate from the default empty one that may be implemented for classes extending [FFIObject]. + /// + /// - Warning: + /// Any object instantiated with this constructor cannot be passed to an actual Rust-backed object. Since there isn't a backing [Pointer] the FFI lower functions will crash. + public init(noPointer: NoPointer) { + self.pointer = nil + } + + public func uniffiClonePointer() -> UnsafeMutableRawPointer { + return try! rustCall { uniffi_ldk_node_fn_clone_bolt11invoice(self.pointer, $0) } + } + // No primary constructor declared for this class. + + deinit { + guard let pointer = pointer else { + return + } + + try! rustCall { uniffi_ldk_node_fn_free_bolt11invoice(pointer, $0) } + } + + +public static func fromStr(invoiceStr: String)throws -> Bolt11Invoice { + return try FfiConverterTypeBolt11Invoice.lift(try rustCallWithError(FfiConverterTypeNodeError.lift) { + uniffi_ldk_node_fn_constructor_bolt11invoice_from_str( + FfiConverterString.lower(invoiceStr),$0 + ) +}) +} + + + +open func amountMilliSatoshis() -> UInt64? { + return try! FfiConverterOptionUInt64.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_amount_milli_satoshis(self.uniffiClonePointer(),$0 + ) +}) +} + +open func currency() -> Currency { + return try! FfiConverterTypeCurrency.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_currency(self.uniffiClonePointer(),$0 + ) +}) +} + +open func description() -> Bolt11InvoiceDescription { + return try! FfiConverterTypeBolt11InvoiceDescription.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_description(self.uniffiClonePointer(),$0 + ) +}) +} + +open func expiryTimeSeconds() -> UInt64 { + return try! FfiConverterUInt64.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_expiry_time_seconds(self.uniffiClonePointer(),$0 + ) +}) +} + +open func fallbackAddresses() -> [Address] { + return try! FfiConverterSequenceTypeAddress.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_fallback_addresses(self.uniffiClonePointer(),$0 + ) +}) +} + +open func isExpired() -> Bool { + return try! FfiConverterBool.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_is_expired(self.uniffiClonePointer(),$0 + ) +}) +} + +open func minFinalCltvExpiryDelta() -> UInt64 { + return try! FfiConverterUInt64.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_min_final_cltv_expiry_delta(self.uniffiClonePointer(),$0 + ) +}) +} + +open func network() -> Network { + return try! FfiConverterTypeNetwork.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_network(self.uniffiClonePointer(),$0 + ) +}) +} + +open func paymentHash() -> PaymentHash { + return try! FfiConverterTypePaymentHash.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_payment_hash(self.uniffiClonePointer(),$0 + ) +}) +} + +open func paymentSecret() -> PaymentSecret { + return try! FfiConverterTypePaymentSecret.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_payment_secret(self.uniffiClonePointer(),$0 + ) +}) +} + +open func recoverPayeePubKey() -> PublicKey { + return try! FfiConverterTypePublicKey.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_recover_payee_pub_key(self.uniffiClonePointer(),$0 + ) +}) +} + +open func routeHints() -> [[RouteHintHop]] { + return try! FfiConverterSequenceSequenceTypeRouteHintHop.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_route_hints(self.uniffiClonePointer(),$0 + ) +}) +} + +open func secondsSinceEpoch() -> UInt64 { + return try! FfiConverterUInt64.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_seconds_since_epoch(self.uniffiClonePointer(),$0 + ) +}) +} + +open func secondsUntilExpiry() -> UInt64 { + return try! FfiConverterUInt64.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_seconds_until_expiry(self.uniffiClonePointer(),$0 + ) +}) +} + +open func signableHash() -> [UInt8] { + return try! FfiConverterSequenceUInt8.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_signable_hash(self.uniffiClonePointer(),$0 + ) +}) +} + +open func wouldExpire(atTimeSeconds: UInt64) -> Bool { + return try! FfiConverterBool.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_would_expire(self.uniffiClonePointer(), + FfiConverterUInt64.lower(atTimeSeconds),$0 + ) +}) +} + + +} + +public struct FfiConverterTypeBolt11Invoice: FfiConverter { + + typealias FfiType = UnsafeMutableRawPointer + typealias SwiftType = Bolt11Invoice + + public static func lift(_ pointer: UnsafeMutableRawPointer) throws -> Bolt11Invoice { + return Bolt11Invoice(unsafeFromRawPointer: pointer) + } + + public static func lower(_ value: Bolt11Invoice) -> UnsafeMutableRawPointer { + return value.uniffiClonePointer() + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Bolt11Invoice { + let v: UInt64 = try readInt(&buf) + // The Rust code won't compile if a pointer won't fit in a UInt64. + // We have to go via `UInt` because that's the thing that's the size of a pointer. + let ptr = UnsafeMutableRawPointer(bitPattern: UInt(truncatingIfNeeded: v)) + if (ptr == nil) { + throw UniffiInternalError.unexpectedNullPointer + } + return try lift(ptr!) + } + + public static func write(_ value: Bolt11Invoice, into buf: inout [UInt8]) { + // This fiddling is because `Int` is the thing that's the same size as a pointer. + // The Rust code won't compile if a pointer won't fit in a `UInt64`. + writeInt(&buf, UInt64(bitPattern: Int64(Int(bitPattern: lower(value))))) + } +} + + + + +public func FfiConverterTypeBolt11Invoice_lift(_ pointer: UnsafeMutableRawPointer) throws -> Bolt11Invoice { + return try FfiConverterTypeBolt11Invoice.lift(pointer) +} + +public func FfiConverterTypeBolt11Invoice_lower(_ value: Bolt11Invoice) -> UnsafeMutableRawPointer { + return FfiConverterTypeBolt11Invoice.lower(value) +} + + + + public protocol Bolt11PaymentProtocol : AnyObject { func claimForHash(paymentHash: PaymentHash, claimableAmountMsat: UInt64, preimage: PaymentPreimage) throws @@ -2225,6 +2471,10 @@ public func FfiConverterTypeNode_lower(_ value: Node) -> UnsafeMutableRawPointer public protocol OnchainPaymentProtocol : AnyObject { + func accelerateByCpfp(txid: Txid, feeRate: FeeRate?, destinationAddress: Address?) throws -> Txid + + func bumpFeeByRbf(txid: Txid, feeRate: FeeRate) throws -> Txid + func newAddress() throws -> Address func sendAllToAddress(address: Address, retainReserve: Bool, feeRate: FeeRate?) throws -> Txid @@ -2274,6 +2524,25 @@ open class OnchainPayment: +open func accelerateByCpfp(txid: Txid, feeRate: FeeRate?, destinationAddress: Address?)throws -> Txid { + return try FfiConverterTypeTxid.lift(try rustCallWithError(FfiConverterTypeNodeError.lift) { + uniffi_ldk_node_fn_method_onchainpayment_accelerate_by_cpfp(self.uniffiClonePointer(), + FfiConverterTypeTxid.lower(txid), + FfiConverterOptionTypeFeeRate.lower(feeRate), + FfiConverterOptionTypeAddress.lower(destinationAddress),$0 + ) +}) +} + +open func bumpFeeByRbf(txid: Txid, feeRate: FeeRate)throws -> Txid { + return try FfiConverterTypeTxid.lift(try rustCallWithError(FfiConverterTypeNodeError.lift) { + uniffi_ldk_node_fn_method_onchainpayment_bump_fee_by_rbf(self.uniffiClonePointer(), + FfiConverterTypeTxid.lower(txid), + FfiConverterTypeFeeRate.lower(feeRate),$0 + ) +}) +} + open func newAddress()throws -> Address { return try FfiConverterTypeAddress.lift(try rustCallWithError(FfiConverterTypeNodeError.lift) { uniffi_ldk_node_fn_method_onchainpayment_new_address(self.uniffiClonePointer(),$0 @@ -2990,36 +3259,6 @@ public struct Bolt11PaymentInfo { -extension Bolt11PaymentInfo: Equatable, Hashable { - public static func ==(lhs: Bolt11PaymentInfo, rhs: Bolt11PaymentInfo) -> Bool { - if lhs.state != rhs.state { - return false - } - if lhs.expiresAt != rhs.expiresAt { - return false - } - if lhs.feeTotalSat != rhs.feeTotalSat { - return false - } - if lhs.orderTotalSat != rhs.orderTotalSat { - return false - } - if lhs.invoice != rhs.invoice { - return false - } - return true - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(state) - hasher.combine(expiresAt) - hasher.combine(feeTotalSat) - hasher.combine(orderTotalSat) - hasher.combine(invoice) - } -} - - public struct FfiConverterTypeBolt11PaymentInfo: FfiConverterRustBuffer { public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Bolt11PaymentInfo { return @@ -4883,6 +5122,95 @@ public func FfiConverterTypePeerDetails_lower(_ value: PeerDetails) -> RustBuffe } +public struct RouteHintHop { + public var srcNodeId: PublicKey + public var shortChannelId: UInt64 + public var cltvExpiryDelta: UInt16 + public var htlcMinimumMsat: UInt64? + public var htlcMaximumMsat: UInt64? + public var fees: RoutingFees + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(srcNodeId: PublicKey, shortChannelId: UInt64, cltvExpiryDelta: UInt16, htlcMinimumMsat: UInt64?, htlcMaximumMsat: UInt64?, fees: RoutingFees) { + self.srcNodeId = srcNodeId + self.shortChannelId = shortChannelId + self.cltvExpiryDelta = cltvExpiryDelta + self.htlcMinimumMsat = htlcMinimumMsat + self.htlcMaximumMsat = htlcMaximumMsat + self.fees = fees + } +} + + + +extension RouteHintHop: Equatable, Hashable { + public static func ==(lhs: RouteHintHop, rhs: RouteHintHop) -> Bool { + if lhs.srcNodeId != rhs.srcNodeId { + return false + } + if lhs.shortChannelId != rhs.shortChannelId { + return false + } + if lhs.cltvExpiryDelta != rhs.cltvExpiryDelta { + return false + } + if lhs.htlcMinimumMsat != rhs.htlcMinimumMsat { + return false + } + if lhs.htlcMaximumMsat != rhs.htlcMaximumMsat { + return false + } + if lhs.fees != rhs.fees { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(srcNodeId) + hasher.combine(shortChannelId) + hasher.combine(cltvExpiryDelta) + hasher.combine(htlcMinimumMsat) + hasher.combine(htlcMaximumMsat) + hasher.combine(fees) + } +} + + +public struct FfiConverterTypeRouteHintHop: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> RouteHintHop { + return + try RouteHintHop( + srcNodeId: FfiConverterTypePublicKey.read(from: &buf), + shortChannelId: FfiConverterUInt64.read(from: &buf), + cltvExpiryDelta: FfiConverterUInt16.read(from: &buf), + htlcMinimumMsat: FfiConverterOptionUInt64.read(from: &buf), + htlcMaximumMsat: FfiConverterOptionUInt64.read(from: &buf), + fees: FfiConverterTypeRoutingFees.read(from: &buf) + ) + } + + public static func write(_ value: RouteHintHop, into buf: inout [UInt8]) { + FfiConverterTypePublicKey.write(value.srcNodeId, into: &buf) + FfiConverterUInt64.write(value.shortChannelId, into: &buf) + FfiConverterUInt16.write(value.cltvExpiryDelta, into: &buf) + FfiConverterOptionUInt64.write(value.htlcMinimumMsat, into: &buf) + FfiConverterOptionUInt64.write(value.htlcMaximumMsat, into: &buf) + FfiConverterTypeRoutingFees.write(value.fees, into: &buf) + } +} + + +public func FfiConverterTypeRouteHintHop_lift(_ buf: RustBuffer) throws -> RouteHintHop { + return try FfiConverterTypeRouteHintHop.lift(buf) +} + +public func FfiConverterTypeRouteHintHop_lower(_ value: RouteHintHop) -> RustBuffer { + return FfiConverterTypeRouteHintHop.lower(value) +} + + public struct RoutingFees { public var baseMsat: UInt32 public var proportionalMillionths: UInt32 @@ -5506,6 +5834,82 @@ extension ConfirmationStatus: Equatable, Hashable {} +// Note that we don't yet support `indirect` for enums. +// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. + +public enum Currency { + + case bitcoin + case bitcoinTestnet + case regtest + case simnet + case signet +} + + +public struct FfiConverterTypeCurrency: FfiConverterRustBuffer { + typealias SwiftType = Currency + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Currency { + let variant: Int32 = try readInt(&buf) + switch variant { + + case 1: return .bitcoin + + case 2: return .bitcoinTestnet + + case 3: return .regtest + + case 4: return .simnet + + case 5: return .signet + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: Currency, into buf: inout [UInt8]) { + switch value { + + + case .bitcoin: + writeInt(&buf, Int32(1)) + + + case .bitcoinTestnet: + writeInt(&buf, Int32(2)) + + + case .regtest: + writeInt(&buf, Int32(3)) + + + case .simnet: + writeInt(&buf, Int32(4)) + + + case .signet: + writeInt(&buf, Int32(5)) + + } + } +} + + +public func FfiConverterTypeCurrency_lift(_ buf: RustBuffer) throws -> Currency { + return try FfiConverterTypeCurrency.lift(buf) +} + +public func FfiConverterTypeCurrency_lower(_ value: Currency) -> RustBuffer { + return FfiConverterTypeCurrency.lower(value) +} + + + +extension Currency: Equatable, Hashable {} + + + // Note that we don't yet support `indirect` for enums. // See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. @@ -6165,6 +6569,14 @@ public enum NodeError { case LiquidityFeeTooHigh(message: String) + case CannotRbfFundingTransaction(message: String) + + case TransactionNotFound(message: String) + + case TransactionAlreadyConfirmed(message: String) + + case NoSpendableOutputs(message: String) + } @@ -6386,6 +6798,22 @@ public struct FfiConverterTypeNodeError: FfiConverterRustBuffer { message: try FfiConverterString.read(from: &buf) ) + case 53: return .CannotRbfFundingTransaction( + message: try FfiConverterString.read(from: &buf) + ) + + case 54: return .TransactionNotFound( + message: try FfiConverterString.read(from: &buf) + ) + + case 55: return .TransactionAlreadyConfirmed( + message: try FfiConverterString.read(from: &buf) + ) + + case 56: return .NoSpendableOutputs( + message: try FfiConverterString.read(from: &buf) + ) + default: throw UniffiInternalError.unexpectedEnumCase } @@ -6501,6 +6929,14 @@ public struct FfiConverterTypeNodeError: FfiConverterRustBuffer { writeInt(&buf, Int32(51)) case .LiquidityFeeTooHigh(_ /* message is ignored*/): writeInt(&buf, Int32(52)) + case .CannotRbfFundingTransaction(_ /* message is ignored*/): + writeInt(&buf, Int32(53)) + case .TransactionNotFound(_ /* message is ignored*/): + writeInt(&buf, Int32(54)) + case .TransactionAlreadyConfirmed(_ /* message is ignored*/): + writeInt(&buf, Int32(55)) + case .NoSpendableOutputs(_ /* message is ignored*/): + writeInt(&buf, Int32(56)) } @@ -8072,6 +8508,28 @@ fileprivate struct FfiConverterSequenceTypePeerDetails: FfiConverterRustBuffer { } } +fileprivate struct FfiConverterSequenceTypeRouteHintHop: FfiConverterRustBuffer { + typealias SwiftType = [RouteHintHop] + + public static func write(_ value: [RouteHintHop], into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + for item in value { + FfiConverterTypeRouteHintHop.write(item, into: &buf) + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [RouteHintHop] { + let len: Int32 = try readInt(&buf) + var seq = [RouteHintHop]() + seq.reserveCapacity(Int(len)) + for _ in 0 ..< len { + seq.append(try FfiConverterTypeRouteHintHop.read(from: &buf)) + } + return seq + } +} + fileprivate struct FfiConverterSequenceTypeLightningBalance: FfiConverterRustBuffer { typealias SwiftType = [LightningBalance] @@ -8116,6 +8574,50 @@ fileprivate struct FfiConverterSequenceTypePendingSweepBalance: FfiConverterRust } } +fileprivate struct FfiConverterSequenceSequenceTypeRouteHintHop: FfiConverterRustBuffer { + typealias SwiftType = [[RouteHintHop]] + + public static func write(_ value: [[RouteHintHop]], into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + for item in value { + FfiConverterSequenceTypeRouteHintHop.write(item, into: &buf) + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [[RouteHintHop]] { + let len: Int32 = try readInt(&buf) + var seq = [[RouteHintHop]]() + seq.reserveCapacity(Int(len)) + for _ in 0 ..< len { + seq.append(try FfiConverterSequenceTypeRouteHintHop.read(from: &buf)) + } + return seq + } +} + +fileprivate struct FfiConverterSequenceTypeAddress: FfiConverterRustBuffer { + typealias SwiftType = [Address] + + public static func write(_ value: [Address], into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + for item in value { + FfiConverterTypeAddress.write(item, into: &buf) + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [Address] { + let len: Int32 = try readInt(&buf) + var seq = [Address]() + seq.reserveCapacity(Int(len)) + for _ in 0 ..< len { + seq.append(try FfiConverterTypeAddress.read(from: &buf)) + } + return seq + } +} + fileprivate struct FfiConverterSequenceTypeNodeId: FfiConverterRustBuffer { typealias SwiftType = [NodeId] @@ -8274,40 +8776,6 @@ public func FfiConverterTypeBlockHash_lower(_ value: BlockHash) -> RustBuffer { -/** - * Typealias from the type name used in the UDL file to the builtin type. This - * is needed because the UDL type name is used in function/method signatures. - */ -public typealias Bolt11Invoice = String -public struct FfiConverterTypeBolt11Invoice: FfiConverter { - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Bolt11Invoice { - return try FfiConverterString.read(from: &buf) - } - - public static func write(_ value: Bolt11Invoice, into buf: inout [UInt8]) { - return FfiConverterString.write(value, into: &buf) - } - - public static func lift(_ value: RustBuffer) throws -> Bolt11Invoice { - return try FfiConverterString.lift(value) - } - - public static func lower(_ value: Bolt11Invoice) -> RustBuffer { - return FfiConverterString.lower(value) - } -} - - -public func FfiConverterTypeBolt11Invoice_lift(_ value: RustBuffer) throws -> Bolt11Invoice { - return try FfiConverterTypeBolt11Invoice.lift(value) -} - -public func FfiConverterTypeBolt11Invoice_lower(_ value: Bolt11Invoice) -> RustBuffer { - return FfiConverterTypeBolt11Invoice.lower(value) -} - - - /** * Typealias from the type name used in the UDL file to the builtin type. This * is needed because the UDL type name is used in function/method signatures. @@ -9032,40 +9500,88 @@ private var initializationResult: InitializationResult { if (uniffi_ldk_node_checksum_func_generate_entropy_mnemonic() != 59926) { return InitializationResult.apiChecksumMismatch } + if (uniffi_ldk_node_checksum_method_bolt11invoice_amount_milli_satoshis() != 50823) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_bolt11invoice_currency() != 32179) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_bolt11invoice_description() != 9887) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_bolt11invoice_expiry_time_seconds() != 23625) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_bolt11invoice_fallback_addresses() != 55276) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_bolt11invoice_is_expired() != 15932) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_bolt11invoice_min_final_cltv_expiry_delta() != 8855) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_bolt11invoice_network() != 10420) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_bolt11invoice_payment_hash() != 42571) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_bolt11invoice_payment_secret() != 26081) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_bolt11invoice_recover_payee_pub_key() != 18874) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_bolt11invoice_route_hints() != 63051) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_bolt11invoice_seconds_since_epoch() != 53979) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_bolt11invoice_seconds_until_expiry() != 64193) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_bolt11invoice_signable_hash() != 30910) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_bolt11invoice_would_expire() != 30331) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_ldk_node_checksum_method_bolt11payment_claim_for_hash() != 52848) { return InitializationResult.apiChecksumMismatch } if (uniffi_ldk_node_checksum_method_bolt11payment_fail_for_hash() != 24516) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ldk_node_checksum_method_bolt11payment_receive() != 47624) { + if (uniffi_ldk_node_checksum_method_bolt11payment_receive() != 6073) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ldk_node_checksum_method_bolt11payment_receive_for_hash() != 36395) { + if (uniffi_ldk_node_checksum_method_bolt11payment_receive_for_hash() != 27050) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ldk_node_checksum_method_bolt11payment_receive_variable_amount() != 38916) { + if (uniffi_ldk_node_checksum_method_bolt11payment_receive_variable_amount() != 4893) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ldk_node_checksum_method_bolt11payment_receive_variable_amount_for_hash() != 9075) { + if (uniffi_ldk_node_checksum_method_bolt11payment_receive_variable_amount_for_hash() != 1402) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ldk_node_checksum_method_bolt11payment_receive_variable_amount_via_jit_channel() != 58805) { + if (uniffi_ldk_node_checksum_method_bolt11payment_receive_variable_amount_via_jit_channel() != 24506) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ldk_node_checksum_method_bolt11payment_receive_via_jit_channel() != 30211) { + if (uniffi_ldk_node_checksum_method_bolt11payment_receive_via_jit_channel() != 16532) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ldk_node_checksum_method_bolt11payment_send() != 39133) { + if (uniffi_ldk_node_checksum_method_bolt11payment_send() != 63952) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ldk_node_checksum_method_bolt11payment_send_probes() != 39625) { + if (uniffi_ldk_node_checksum_method_bolt11payment_send_probes() != 969) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ldk_node_checksum_method_bolt11payment_send_probes_using_amount() != 25010) { + if (uniffi_ldk_node_checksum_method_bolt11payment_send_probes_using_amount() != 50136) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ldk_node_checksum_method_bolt11payment_send_using_amount() != 19557) { + if (uniffi_ldk_node_checksum_method_bolt11payment_send_using_amount() != 36530) { return InitializationResult.apiChecksumMismatch } if (uniffi_ldk_node_checksum_method_bolt12payment_initiate_refund() != 38039) { @@ -9293,6 +9809,12 @@ private var initializationResult: InitializationResult { if (uniffi_ldk_node_checksum_method_node_wait_next_event() != 55101) { return InitializationResult.apiChecksumMismatch } + if (uniffi_ldk_node_checksum_method_onchainpayment_accelerate_by_cpfp() != 31954) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_onchainpayment_bump_fee_by_rbf() != 53877) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_ldk_node_checksum_method_onchainpayment_new_address() != 37251) { return InitializationResult.apiChecksumMismatch } @@ -9320,6 +9842,9 @@ private var initializationResult: InitializationResult { if (uniffi_ldk_node_checksum_method_vssheaderprovider_get_headers() != 7788) { return InitializationResult.apiChecksumMismatch } + if (uniffi_ldk_node_checksum_constructor_bolt11invoice_from_str() != 349) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_ldk_node_checksum_constructor_builder_from_config() != 994) { return InitializationResult.apiChecksumMismatch } diff --git a/src/error.rs b/src/error.rs index 2cb71186d..e6faba084 100644 --- a/src/error.rs +++ b/src/error.rs @@ -120,6 +120,14 @@ pub enum Error { LiquiditySourceUnavailable, /// The given operation failed due to the LSP's required opening fee being too high. LiquidityFeeTooHigh, + /// Cannot RBF a channel funding transaction. + CannotRbfFundingTransaction, + /// The transaction was not found in the wallet. + TransactionNotFound, + /// The transaction is already confirmed and cannot be modified. + TransactionAlreadyConfirmed, + /// The transaction has no spendable outputs. + NoSpendableOutputs } impl fmt::Display for Error { @@ -193,6 +201,10 @@ impl fmt::Display for Error { Self::LiquidityFeeTooHigh => { write!(f, "The given operation failed due to the LSP's required opening fee being too high.") }, + Self::CannotRbfFundingTransaction => write!(f, "Cannot RBF a channel funding transaction."), + Self::TransactionNotFound => write!(f, "The transaction was not found in the wallet."), + Self::TransactionAlreadyConfirmed => write!(f, "The transaction is already confirmed and cannot be modified."), + Self::NoSpendableOutputs => write!(f, "The transaction has no spendable outputs.") } } } diff --git a/src/payment/onchain.rs b/src/payment/onchain.rs index 046d66c69..a37806824 100644 --- a/src/payment/onchain.rs +++ b/src/payment/onchain.rs @@ -122,4 +122,94 @@ impl OnchainPayment { let fee_rate_opt = maybe_map_fee_rate_opt!(fee_rate); self.wallet.send_to_address(address, send_amount, fee_rate_opt) } -} + + /// Bumps the fee of an existing transaction using Replace-By-Fee (RBF). + /// + /// This allows a previously sent transaction to be replaced with a new version + /// that pays a higher fee. The original transaction must have been created with + /// RBF enabled (which is the default for transactions created by LDK). + /// + /// **Note:** This cannot be used on funding transactions as doing so would invalidate the channel. + /// + /// # Arguments + /// + /// * `txid` - The transaction ID of the transaction to be replaced + /// * `fee_rate` - The new fee rate to use (must be higher than the original fee rate) + /// + /// # Returns + /// + /// The transaction ID of the new transaction if successful. + /// + /// # Errors + /// + /// * [`Error::NotRunning`] - If the node is not running + /// * [`Error::TransactionNotFound`] - If the transaction can't be found in the wallet + /// * [`Error::TransactionAlreadyConfirmed`] - If the transaction is already confirmed + /// * [`Error::CannotRbfFundingTransaction`] - If the transaction is a channel funding transaction + /// * [`Error::InvalidFeeRate`] - If the new fee rate is not higher than the original + /// * [`Error::OnchainTxCreationFailed`] - If the new transaction couldn't be created + pub fn bump_fee_by_rbf(&self, txid: &Txid, fee_rate: FeeRate) -> Result { + let rt_lock = self.runtime.read().unwrap(); + if rt_lock.is_none() { + return Err(Error::NotRunning); + } + + // Pass through to the wallet implementation + #[cfg(not(feature = "uniffi"))] + let fee_rate_param = fee_rate; + #[cfg(feature = "uniffi")] + let fee_rate_param = *fee_rate; + + self.wallet.bump_fee_by_rbf(txid, fee_rate_param, &self.channel_manager) + } + + /// Accelerates confirmation of a transaction using Child-Pays-For-Parent (CPFP). + /// + /// This creates a new transaction (child) that spends an output from the + /// transaction to be accelerated (parent), with a high enough fee to pay for both. + /// + /// # Arguments + /// + /// * `txid` - The transaction ID of the transaction to be accelerated + /// * `fee_rate` - The fee rate to use for the child transaction (or None to calculate automatically) + /// * `destination_address` - Optional address to send the funds to (if None, funds are sent to an internal address) + /// + /// # Returns + /// + /// The transaction ID of the child transaction if successful. + /// + /// # Errors + /// + /// * [`Error::NotRunning`] - If the node is not running + /// * [`Error::TransactionNotFound`] - If the transaction can't be found + /// * [`Error::TransactionAlreadyConfirmed`] - If the transaction is already confirmed + /// * [`Error::NoSpendableOutputs`] - If the transaction has no spendable outputs + /// * [`Error::OnchainTxCreationFailed`] - If the child transaction couldn't be created + pub fn accelerate_by_cpfp( + &self, + txid: &Txid, + fee_rate: Option, + destination_address: Option
, + ) -> Result { + let rt_lock = self.runtime.read().unwrap(); + if rt_lock.is_none() { + return Err(Error::NotRunning); + } + + // Calculate fee rate if not provided + #[cfg(not(feature = "uniffi"))] + let fee_rate_param = match fee_rate { + Some(rate) => rate, + None => self.wallet.calculate_cpfp_fee_rate(txid, true)?, + }; + + #[cfg(feature = "uniffi")] + let fee_rate_param = match fee_rate { + Some(rate) => *rate, + None => self.wallet.calculate_cpfp_fee_rate(txid, true)?, + }; + + // Pass through to the wallet implementation + self.wallet.accelerate_by_cpfp(txid, fee_rate_param, destination_address) + } +} \ No newline at end of file diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 65fa2e24d..8fed96503 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -13,7 +13,7 @@ use crate::logger::{log_debug, log_error, log_info, log_trace, LdkLogger}; use crate::fee_estimator::{ConfirmationTarget, FeeEstimator}; use crate::payment::store::ConfirmationStatus; use crate::payment::{PaymentDetails, PaymentDirection, PaymentStatus}; -use crate::types::PaymentStore; +use crate::types::{ChannelManager, PaymentStore}; use crate::Error; use lightning::chain::chaininterface::BroadcasterInterface; @@ -95,6 +95,28 @@ where Self { inner, persister, broadcaster, fee_estimator, payment_store, config, logger } } + pub(crate) fn is_funding_transaction( + &self, + txid: &Txid, + channel_manager: &ChannelManager + ) -> bool { + // Check all channels (pending and confirmed) for matching funding txid + for channel in channel_manager.list_channels() { + if let Some(funding_txo) = channel.funding_txo { + if funding_txo.txid == *txid { + log_debug!( + self.logger, + "Transaction {} is a funding transaction for channel {}", + txid, + channel.channel_id + ); + return true; + } + } + } + false + } + pub(crate) fn get_full_scan_request(&self) -> FullScanRequest { self.inner.lock().unwrap().start_full_scan().build() } @@ -151,6 +173,451 @@ where Ok(()) } + /// Bumps the fee of an existing transaction using Replace-By-Fee (RBF). + /// + /// This allows a previously sent transaction to be replaced with a new version + /// that pays a higher fee. The original transaction must have been created with + /// RBF enabled (which is the default for transactions created by LDK). + /// + /// Returns the txid of the new transaction if successful. + pub(crate) fn bump_fee_by_rbf( + &self, + txid: &Txid, + fee_rate: FeeRate, + channel_manager: &ChannelManager, + ) -> Result { + // Check if this is a funding transaction + if self.is_funding_transaction(txid, channel_manager) { + log_error!( + self.logger, + "Cannot RBF transaction {}: it is a channel funding transaction", + txid + ); + return Err(Error::CannotRbfFundingTransaction); + } + let mut locked_wallet = self.inner.lock().unwrap(); + + // Find the transaction in the wallet + let tx_node = locked_wallet + .get_tx(*txid) + .ok_or_else(|| { + log_error!(self.logger, "Transaction not found in wallet: {}", txid); + Error::TransactionNotFound + })?; + + // Check if transaction is confirmed - can't replace confirmed transactions + if tx_node.chain_position.is_confirmed() { + log_error!(self.logger, "Cannot replace confirmed transaction: {}", txid); + return Err(Error::TransactionAlreadyConfirmed); + } + + // Calculate original transaction fee and fee rate + let original_tx = &tx_node.tx_node.tx; + let original_fee = locked_wallet.calculate_fee(original_tx).map_err(|e| { + log_error!(self.logger, "Failed to calculate original fee: {}", e); + Error::WalletOperationFailed + })?; + + // Use Bitcoin crate's built-in fee rate calculation for accuracy + let original_fee_rate = original_fee / original_tx.weight(); + + // Log detailed information for debugging + log_info!(self.logger, "RBF Analysis for transaction {}", txid); + log_info!(self.logger, " Original fee: {} sats", original_fee.to_sat()); + log_info!(self.logger, " Original weight: {} WU ({} vB)", + original_tx.weight().to_wu(), original_tx.weight().to_vbytes_ceil()); + log_info!(self.logger, " Original fee rate: {} sat/kwu ({} sat/vB)", + original_fee_rate.to_sat_per_kwu(), original_fee_rate.to_sat_per_vb_ceil()); + log_info!(self.logger, " Requested fee rate: {} sat/kwu ({} sat/vB)", + fee_rate.to_sat_per_kwu(), fee_rate.to_sat_per_vb_ceil()); + + // Essential validation: new fee rate must be higher than original + // This prevents definite rejections by the Bitcoin network + if fee_rate <= original_fee_rate { + log_error!( + self.logger, + "RBF rejected: New fee rate ({} sat/vB) must be higher than original fee rate ({} sat/vB)", + fee_rate.to_sat_per_vb_ceil(), + original_fee_rate.to_sat_per_vb_ceil() + ); + return Err(Error::InvalidFeeRate); + } + + log_info!( + self.logger, + "RBF approved: Fee rate increase from {} to {} sat/vB", + original_fee_rate.to_sat_per_vb_ceil(), + fee_rate.to_sat_per_vb_ceil() + ); + + // Build a new transaction with higher fee using BDK's fee bump functionality + let mut tx_builder = locked_wallet.build_fee_bump(*txid).map_err(|e| { + log_error!(self.logger, "Failed to create fee bump builder: {}", e); + Error::OnchainTxCreationFailed + })?; + + // Set the new fee rate + tx_builder.fee_rate(fee_rate); + + // Finalize the transaction + let mut psbt = match tx_builder.finish() { + Ok(psbt) => { + log_trace!(self.logger, "Created RBF PSBT: {:?}", psbt); + psbt + }, + Err(err) => { + log_error!(self.logger, "Failed to create RBF transaction: {}", err); + return Err(Error::OnchainTxCreationFailed); + }, + }; + + // Sign the transaction + match locked_wallet.sign(&mut psbt, SignOptions::default()) { + Ok(finalized) => { + if !finalized { + log_error!(self.logger, "Failed to finalize RBF transaction"); + return Err(Error::OnchainTxSigningFailed); + } + }, + Err(err) => { + log_error!(self.logger, "Failed to sign RBF transaction: {}", err); + return Err(Error::OnchainTxSigningFailed); + }, + } + + // Persist wallet changes + let mut locked_persister = self.persister.lock().unwrap(); + locked_wallet.persist(&mut locked_persister).map_err(|e| { + log_error!(self.logger, "Failed to persist wallet: {}", e); + Error::PersistenceFailed + })?; + + // Extract and broadcast the transaction + let tx = psbt.extract_tx().map_err(|e| { + log_error!(self.logger, "Failed to extract transaction: {}", e); + Error::OnchainTxCreationFailed + })?; + + self.broadcaster.broadcast_transactions(&[&tx]); + + let new_txid = tx.compute_txid(); + + // Calculate and log the actual fee increase achieved + let new_fee = locked_wallet.calculate_fee(&tx).unwrap_or(Amount::ZERO); + let actual_fee_rate = new_fee / tx.weight(); + + log_info!( + self.logger, + "RBF transaction created successfully!" + ); + log_info!( + self.logger, + " Original: {} ({} sat/vB, {} sats fee)", + txid, original_fee_rate.to_sat_per_vb_ceil(), original_fee.to_sat() + ); + log_info!( + self.logger, + " Replacement: {} ({} sat/vB, {} sats fee)", + new_txid, actual_fee_rate.to_sat_per_vb_ceil(), new_fee.to_sat() + ); + log_info!( + self.logger, + " Additional fee paid: {} sats", + new_fee.to_sat().saturating_sub(original_fee.to_sat()) + ); + + Ok(new_txid) + } + + /// Accelerates confirmation of a transaction using Child-Pays-For-Parent (CPFP). + /// + /// This creates a new transaction (child) that spends an output from the + /// transaction to be accelerated (parent), with a high enough fee to pay for both. + /// + /// Returns the txid of the child transaction if successful. + pub(crate) fn accelerate_by_cpfp( + &self, + txid: &Txid, + fee_rate: FeeRate, + destination_address: Option
, + ) -> Result { + let mut locked_wallet = self.inner.lock().unwrap(); + + // Find the transaction in the wallet + let parent_tx_node = locked_wallet + .get_tx(*txid) + .ok_or_else(|| { + log_error!(self.logger, "Transaction not found in wallet: {}", txid); + Error::TransactionNotFound + })?; + + // Check if transaction is confirmed - can't accelerate confirmed transactions + if parent_tx_node.chain_position.is_confirmed() { + log_error!(self.logger, "Cannot accelerate confirmed transaction: {}", txid); + return Err(Error::TransactionAlreadyConfirmed); + } + + // Calculate parent transaction fee and fee rate for validation + let parent_tx = &parent_tx_node.tx_node.tx; + let parent_fee = locked_wallet.calculate_fee(parent_tx).map_err(|e| { + log_error!(self.logger, "Failed to calculate parent fee: {}", e); + Error::WalletOperationFailed + })?; + + // Use Bitcoin crate's built-in fee rate calculation for accuracy + let parent_fee_rate = parent_fee / parent_tx.weight(); + + // Log detailed information for debugging + log_info!(self.logger, "CPFP Analysis for transaction {}", txid); + log_info!(self.logger, " Parent fee: {} sats", parent_fee.to_sat()); + log_info!(self.logger, " Parent weight: {} WU ({} vB)", + parent_tx.weight().to_wu(), parent_tx.weight().to_vbytes_ceil()); + log_info!(self.logger, " Parent fee rate: {} sat/kwu ({} sat/vB)", + parent_fee_rate.to_sat_per_kwu(), parent_fee_rate.to_sat_per_vb_ceil()); + log_info!(self.logger, " Child fee rate: {} sat/kwu ({} sat/vB)", + fee_rate.to_sat_per_kwu(), fee_rate.to_sat_per_vb_ceil()); + + // Validate that child fee rate is higher than parent (for effective acceleration) + if fee_rate <= parent_fee_rate { + log_info!( + self.logger, + "CPFP warning: Child fee rate ({} sat/vB) is not higher than parent fee rate ({} sat/vB). This may not effectively accelerate confirmation.", + fee_rate.to_sat_per_vb_ceil(), + parent_fee_rate.to_sat_per_vb_ceil() + ); + // Note: We warn but don't reject - CPFP can still work in some cases + } else { + let acceleration_ratio = fee_rate.to_sat_per_kwu() as f64 / parent_fee_rate.to_sat_per_kwu() as f64; + log_info!( + self.logger, + "CPFP acceleration: Child fee rate is {:.1}x higher than parent ({} vs {} sat/vB)", + acceleration_ratio, + fee_rate.to_sat_per_vb_ceil(), + parent_fee_rate.to_sat_per_vb_ceil() + ); + } + + // Find spendable outputs from this transaction + let utxos = locked_wallet + .list_unspent() + .filter(|utxo| utxo.outpoint.txid == *txid) + .collect::>(); + + if utxos.is_empty() { + log_error!( + self.logger, + "No spendable outputs found for transaction: {}", + txid + ); + return Err(Error::NoSpendableOutputs); + } + + log_info!(self.logger, "Found {} spendable output(s) from parent transaction", utxos.len()); + let total_input_value: u64 = utxos.iter().map(|utxo| utxo.txout.value.to_sat()).sum(); + log_info!(self.logger, " Total input value: {} sats", total_input_value); + + // Determine where to send the funds + let script_pubkey = match destination_address { + Some(addr) => { + log_info!(self.logger, " Destination: {} (user-specified)", addr); + // Validate the address + self.parse_and_validate_address(self.config.network, &addr)?; + addr.script_pubkey() + }, + None => { + // Create a new address to send the funds to + let address_info = locked_wallet.next_unused_address(KeychainKind::Internal); + log_info!(self.logger, " Destination: {} (wallet internal address)", address_info.address); + address_info.address.script_pubkey() + } + }; + + // Build a transaction that spends these UTXOs + let mut tx_builder = locked_wallet.build_tx(); + + // Add the UTXOs explicitly + for utxo in &utxos { + match tx_builder.add_utxo(utxo.outpoint) { + Ok(_) => {}, + Err(e) => { + log_error!(self.logger, "Failed to add UTXO: {:?} - {}", utxo.outpoint, e); + return Err(Error::OnchainTxCreationFailed); + } + } + } + + // Set the fee rate for the child transaction + tx_builder.fee_rate(fee_rate); + + // Drain all inputs to the destination + tx_builder.drain_to(script_pubkey); + + // Finalize the transaction + let mut psbt = match tx_builder.finish() { + Ok(psbt) => { + log_trace!(self.logger, "Created CPFP PSBT: {:?}", psbt); + psbt + }, + Err(err) => { + log_error!(self.logger, "Failed to create CPFP transaction: {}", err); + return Err(Error::OnchainTxCreationFailed); + }, + }; + + // Sign the transaction + match locked_wallet.sign(&mut psbt, SignOptions::default()) { + Ok(finalized) => { + if !finalized { + log_error!(self.logger, "Failed to finalize CPFP transaction"); + return Err(Error::OnchainTxSigningFailed); + } + }, + Err(err) => { + log_error!(self.logger, "Failed to sign CPFP transaction: {}", err); + return Err(Error::OnchainTxSigningFailed); + }, + } + + // Persist wallet changes + let mut locked_persister = self.persister.lock().unwrap(); + locked_wallet.persist(&mut locked_persister).map_err(|e| { + log_error!(self.logger, "Failed to persist wallet: {}", e); + Error::PersistenceFailed + })?; + + // Extract and broadcast the transaction + let tx = psbt.extract_tx().map_err(|e| { + log_error!(self.logger, "Failed to extract transaction: {}", e); + Error::OnchainTxCreationFailed + })?; + + self.broadcaster.broadcast_transactions(&[&tx]); + + let child_txid = tx.compute_txid(); + + // Calculate and log the actual results + let child_fee = locked_wallet.calculate_fee(&tx).unwrap_or(Amount::ZERO); + let actual_child_fee_rate = child_fee / tx.weight(); + + log_info!( + self.logger, + "CPFP transaction created successfully!" + ); + log_info!( + self.logger, + " Parent: {} ({} sat/vB, {} sats fee)", + txid, parent_fee_rate.to_sat_per_vb_ceil(), parent_fee.to_sat() + ); + log_info!( + self.logger, + " Child: {} ({} sat/vB, {} sats fee)", + child_txid, actual_child_fee_rate.to_sat_per_vb_ceil(), child_fee.to_sat() + ); + log_info!( + self.logger, + " Combined package fee rate: approximately {:.1} sat/vB", + ((parent_fee.to_sat() + child_fee.to_sat()) as f64) / ((parent_tx.weight().to_vbytes_ceil() + tx.weight().to_vbytes_ceil()) as f64) + ); + + Ok(child_txid) + } + + /// Calculates an appropriate fee rate for a CPFP transaction to ensure + /// the parent transaction gets confirmed within the target number of blocks. + /// + /// Returns the fee rate that should be used for the child transaction. + pub(crate) fn calculate_cpfp_fee_rate( + &self, + parent_txid: &Txid, + urgent: bool, + ) -> Result { + let locked_wallet = self.inner.lock().unwrap(); + + // Get the parent transaction + let parent_tx_node = locked_wallet + .get_tx(*parent_txid) + .ok_or_else(|| { + log_error!(self.logger, "Transaction not found in wallet: {}", parent_txid); + Error::TransactionNotFound + })?; + + // Make sure it's not confirmed + if parent_tx_node.chain_position.is_confirmed() { + log_error!(self.logger, "Transaction is already confirmed: {}", parent_txid); + return Err(Error::TransactionAlreadyConfirmed); + } + + let parent_tx = &parent_tx_node.tx_node.tx; + + // Calculate parent fee and fee rate using accurate method + let parent_fee = locked_wallet.calculate_fee(parent_tx).map_err(|e| { + log_error!(self.logger, "Failed to calculate parent fee: {}", e); + Error::WalletOperationFailed + })?; + + // Use Bitcoin crate's built-in fee rate calculation for accuracy + let parent_fee_rate = parent_fee / parent_tx.weight(); + + // Get current mempool fee rates from fee estimator based on urgency + let target = if urgent { + ConfirmationTarget::Lightning(lightning::chain::chaininterface::ConfirmationTarget::UrgentOnChainSweep) + } else { + ConfirmationTarget::OnchainPayment + }; + + let target_fee_rate = self.fee_estimator.estimate_fee_rate(target); + + log_info!(self.logger, "CPFP Fee Rate Calculation for transaction {}", parent_txid); + log_info!(self.logger, " Parent fee: {} sats", parent_fee.to_sat()); + log_info!(self.logger, " Parent weight: {} WU ({} vB)", + parent_tx.weight().to_wu(), parent_tx.weight().to_vbytes_ceil()); + log_info!(self.logger, " Parent fee rate: {} sat/kwu ({} sat/vB)", + parent_fee_rate.to_sat_per_kwu(), parent_fee_rate.to_sat_per_vb_ceil()); + log_info!(self.logger, " Target fee rate: {} sat/kwu ({} sat/vB)", + target_fee_rate.to_sat_per_kwu(), target_fee_rate.to_sat_per_vb_ceil()); + log_info!(self.logger, " Urgency level: {}", if urgent { "HIGH" } else { "NORMAL" }); + + // If parent fee rate is already sufficient, return a slightly higher one + if parent_fee_rate >= target_fee_rate { + let recommended_rate = FeeRate::from_sat_per_kwu(parent_fee_rate.to_sat_per_kwu() + 250); // +1 sat/vB + log_info!( + self.logger, + "Parent fee rate is already sufficient. Recommending slightly higher rate: {} sat/vB", + recommended_rate.to_sat_per_vb_ceil() + ); + return Ok(recommended_rate); + } + + // Estimate child transaction size (weight units) + // Conservative estimate for a typical 1-input, 1-output transaction + let estimated_child_weight_units = 480; // ~120 vbytes * 4 = 480 wu + let estimated_child_vbytes = estimated_child_weight_units / 4; + + // Calculate the fee deficit for the parent (in sats) + //let parent_weight_units = parent_tx.weight().to_wu(); + let parent_vbytes = parent_tx.weight().to_vbytes_ceil(); + let parent_fee_deficit = (target_fee_rate.to_sat_per_vb_ceil() - parent_fee_rate.to_sat_per_vb_ceil()) * parent_vbytes; + + // Calculate what the child needs to pay to cover both transactions + let base_child_fee = target_fee_rate.to_sat_per_vb_ceil() * estimated_child_vbytes; + let total_child_fee = base_child_fee + parent_fee_deficit; + + // Calculate the effective fee rate for the child + let child_fee_rate_sat_vb = total_child_fee / estimated_child_vbytes; + let child_fee_rate = FeeRate::from_sat_per_vb(child_fee_rate_sat_vb) + .unwrap_or(FeeRate::from_sat_per_kwu(child_fee_rate_sat_vb * 250)); + + log_info!(self.logger, "CPFP Calculation Results:"); + log_info!(self.logger, " Parent fee deficit: {} sats", parent_fee_deficit); + log_info!(self.logger, " Base child fee needed: {} sats", base_child_fee); + log_info!(self.logger, " Total child fee needed: {} sats", total_child_fee); + log_info!(self.logger, " Recommended child fee rate: {} sat/vB", child_fee_rate.to_sat_per_vb_ceil()); + log_info!(self.logger, " Combined package rate: ~{} sat/vB", + ((parent_fee.to_sat() + total_child_fee) / (parent_vbytes + estimated_child_vbytes))); + + Ok(child_fee_rate) + } + fn update_payment_store<'a>( &self, locked_wallet: &'a mut PersistedWallet, ) -> Result<(), Error> { @@ -942,4 +1409,4 @@ where })?; Ok(address.script_pubkey()) } -} +} \ No newline at end of file