Skip to content

Commit 413963d

Browse files
committed
feat: add coin selection with BDK algorithms and manual UTXO control
Add coin selection functionality allowing users to control which UTXOs are spent in on-chain transactions. This includes: - New methods to list spendable outputs (listSpendableOutputs) and select UTXOs with BDK's built-in algorithms (selectUtxosWithAlgorithm) - Four coin selection algorithms: BranchAndBound, LargestFirst, OldestFirst, and SingleRandomDraw - Manual UTXO selection support in send_to_address() - Automatic filtering of channel funding UTXOs for safety - New SpendableUtxo type and CoinSelectionFailed error These features enable advanced wallet functionality including coin control for privacy, fee optimization, and UTXO management while ensuring channel funding outputs are protected from accidental spending.
1 parent c5dcd19 commit 413963d

File tree

12 files changed

+819
-135
lines changed

12 files changed

+819
-135
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "ldk-node"
3-
version = "0.6.0-rc.2"
3+
version = "0.6.0-rc.3"
44
authors = ["Elias Rohrer <[email protected]>"]
55
homepage = "https://lightningdevkit.org/"
66
license = "MIT OR Apache-2.0"

Package.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33

44
import PackageDescription
55

6-
let tag = "v0.6.0-rc.2"
7-
let checksum = "5fbb59a02cb596707b25d56d255ba0706873852962d1108826b57778347c859e"
6+
let tag = "v0.6.0-rc.3"
7+
let checksum = "18cf48845b55560b2cdf85852879252e31d2680360af53b8de8ffaa898e1b081"
88
let url = "https://github.com/synonymdev/ldk-node/releases/download/\(tag)/LDKNodeFFI.xcframework.zip"
99

1010
let package = Package(

bindings/kotlin/ldk-node-android/gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ org.gradle.jvmargs=-Xmx1536m
22
android.useAndroidX=true
33
android.enableJetifier=true
44
kotlin.code.style=official
5-
libraryVersion=v0.6.0-rc.2
5+
libraryVersion=v0.6.0-rc.3

bindings/ldk_node.udl

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -216,18 +216,34 @@ interface SpontaneousPayment {
216216
};
217217

218218
interface OnchainPayment {
219-
[Throws=NodeError]
220-
Address new_address();
221-
[Throws=NodeError]
222-
Txid send_to_address([ByRef]Address address, u64 amount_sats, FeeRate? fee_rate);
223-
[Throws=NodeError]
224-
Txid send_all_to_address([ByRef]Address address, boolean retain_reserve, FeeRate? fee_rate);
225-
[Throws=NodeError]
219+
[Throws=NodeError]
220+
Address new_address();
221+
[Throws=NodeError]
222+
sequence<SpendableUtxo> list_spendable_outputs();
223+
[Throws=NodeError]
224+
sequence<SpendableUtxo> select_utxos_with_algorithm(u64 target_amount_sats, FeeRate? fee_rate, CoinSelectionAlgorithm algorithm, sequence<SpendableUtxo>? utxos);
225+
[Throws=NodeError]
226+
Txid send_to_address([ByRef]Address address, u64 amount_sats, FeeRate? fee_rate, sequence<SpendableUtxo>? utxos_to_spend);
227+
[Throws=NodeError]
228+
Txid send_all_to_address([ByRef]Address address, boolean retain_reserve, FeeRate? fee_rate);
229+
[Throws=NodeError]
226230
Txid bump_fee_by_rbf([ByRef]Txid txid, FeeRate fee_rate);
227231
[Throws=NodeError]
228232
Txid accelerate_by_cpfp([ByRef]Txid txid, FeeRate? fee_rate, Address? destination_address);
229233
};
230234

235+
enum CoinSelectionAlgorithm {
236+
"BranchAndBound",
237+
"LargestFirst",
238+
"OldestFirst",
239+
"SingleRandomDraw",
240+
};
241+
242+
dictionary SpendableUtxo {
243+
OutPoint outpoint;
244+
u64 value_sats;
245+
};
246+
231247
interface FeeRate {
232248
[Name=from_sat_per_kwu]
233249
constructor(u64 sat_kwu);
@@ -310,6 +326,7 @@ enum NodeError {
310326
"TransactionNotFound",
311327
"TransactionAlreadyConfirmed",
312328
"NoSpendableOutputs",
329+
"CoinSelectionFailed",
313330
};
314331

315332
dictionary NodeStatus {

bindings/swift/Sources/LDKNode/LDKNode.swift

Lines changed: 210 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2475,11 +2475,15 @@ public protocol OnchainPaymentProtocol : AnyObject {
24752475

24762476
func bumpFeeByRbf(txid: Txid, feeRate: FeeRate) throws -> Txid
24772477

2478+
func listSpendableOutputs() throws -> [SpendableUtxo]
2479+
24782480
func newAddress() throws -> Address
24792481

2482+
func selectUtxosWithAlgorithm(targetAmountSats: UInt64, feeRate: FeeRate?, algorithm: CoinSelectionAlgorithm, utxos: [SpendableUtxo]?) throws -> [SpendableUtxo]
2483+
24802484
func sendAllToAddress(address: Address, retainReserve: Bool, feeRate: FeeRate?) throws -> Txid
24812485

2482-
func sendToAddress(address: Address, amountSats: UInt64, feeRate: FeeRate?) throws -> Txid
2486+
func sendToAddress(address: Address, amountSats: UInt64, feeRate: FeeRate?, utxosToSpend: [SpendableUtxo]?) throws -> Txid
24832487

24842488
}
24852489

@@ -2543,13 +2547,31 @@ open func bumpFeeByRbf(txid: Txid, feeRate: FeeRate)throws -> Txid {
25432547
})
25442548
}
25452549

2550+
open func listSpendableOutputs()throws -> [SpendableUtxo] {
2551+
return try FfiConverterSequenceTypeSpendableUtxo.lift(try rustCallWithError(FfiConverterTypeNodeError.lift) {
2552+
uniffi_ldk_node_fn_method_onchainpayment_list_spendable_outputs(self.uniffiClonePointer(),$0
2553+
)
2554+
})
2555+
}
2556+
25462557
open func newAddress()throws -> Address {
25472558
return try FfiConverterTypeAddress.lift(try rustCallWithError(FfiConverterTypeNodeError.lift) {
25482559
uniffi_ldk_node_fn_method_onchainpayment_new_address(self.uniffiClonePointer(),$0
25492560
)
25502561
})
25512562
}
25522563

2564+
open func selectUtxosWithAlgorithm(targetAmountSats: UInt64, feeRate: FeeRate?, algorithm: CoinSelectionAlgorithm, utxos: [SpendableUtxo]?)throws -> [SpendableUtxo] {
2565+
return try FfiConverterSequenceTypeSpendableUtxo.lift(try rustCallWithError(FfiConverterTypeNodeError.lift) {
2566+
uniffi_ldk_node_fn_method_onchainpayment_select_utxos_with_algorithm(self.uniffiClonePointer(),
2567+
FfiConverterUInt64.lower(targetAmountSats),
2568+
FfiConverterOptionTypeFeeRate.lower(feeRate),
2569+
FfiConverterTypeCoinSelectionAlgorithm.lower(algorithm),
2570+
FfiConverterOptionSequenceTypeSpendableUtxo.lower(utxos),$0
2571+
)
2572+
})
2573+
}
2574+
25532575
open func sendAllToAddress(address: Address, retainReserve: Bool, feeRate: FeeRate?)throws -> Txid {
25542576
return try FfiConverterTypeTxid.lift(try rustCallWithError(FfiConverterTypeNodeError.lift) {
25552577
uniffi_ldk_node_fn_method_onchainpayment_send_all_to_address(self.uniffiClonePointer(),
@@ -2560,12 +2582,13 @@ open func sendAllToAddress(address: Address, retainReserve: Bool, feeRate: FeeRa
25602582
})
25612583
}
25622584

2563-
open func sendToAddress(address: Address, amountSats: UInt64, feeRate: FeeRate?)throws -> Txid {
2585+
open func sendToAddress(address: Address, amountSats: UInt64, feeRate: FeeRate?, utxosToSpend: [SpendableUtxo]?)throws -> Txid {
25642586
return try FfiConverterTypeTxid.lift(try rustCallWithError(FfiConverterTypeNodeError.lift) {
25652587
uniffi_ldk_node_fn_method_onchainpayment_send_to_address(self.uniffiClonePointer(),
25662588
FfiConverterTypeAddress.lower(address),
25672589
FfiConverterUInt64.lower(amountSats),
2568-
FfiConverterOptionTypeFeeRate.lower(feeRate),$0
2590+
FfiConverterOptionTypeFeeRate.lower(feeRate),
2591+
FfiConverterOptionSequenceTypeSpendableUtxo.lower(utxosToSpend),$0
25692592
)
25702593
})
25712594
}
@@ -5340,6 +5363,63 @@ public func FfiConverterTypeSendingParameters_lower(_ value: SendingParameters)
53405363
return FfiConverterTypeSendingParameters.lower(value)
53415364
}
53425365

5366+
5367+
public struct SpendableUtxo {
5368+
public var outpoint: OutPoint
5369+
public var valueSats: UInt64
5370+
5371+
// Default memberwise initializers are never public by default, so we
5372+
// declare one manually.
5373+
public init(outpoint: OutPoint, valueSats: UInt64) {
5374+
self.outpoint = outpoint
5375+
self.valueSats = valueSats
5376+
}
5377+
}
5378+
5379+
5380+
5381+
extension SpendableUtxo: Equatable, Hashable {
5382+
public static func ==(lhs: SpendableUtxo, rhs: SpendableUtxo) -> Bool {
5383+
if lhs.outpoint != rhs.outpoint {
5384+
return false
5385+
}
5386+
if lhs.valueSats != rhs.valueSats {
5387+
return false
5388+
}
5389+
return true
5390+
}
5391+
5392+
public func hash(into hasher: inout Hasher) {
5393+
hasher.combine(outpoint)
5394+
hasher.combine(valueSats)
5395+
}
5396+
}
5397+
5398+
5399+
public struct FfiConverterTypeSpendableUtxo: FfiConverterRustBuffer {
5400+
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SpendableUtxo {
5401+
return
5402+
try SpendableUtxo(
5403+
outpoint: FfiConverterTypeOutPoint.read(from: &buf),
5404+
valueSats: FfiConverterUInt64.read(from: &buf)
5405+
)
5406+
}
5407+
5408+
public static func write(_ value: SpendableUtxo, into buf: inout [UInt8]) {
5409+
FfiConverterTypeOutPoint.write(value.outpoint, into: &buf)
5410+
FfiConverterUInt64.write(value.valueSats, into: &buf)
5411+
}
5412+
}
5413+
5414+
5415+
public func FfiConverterTypeSpendableUtxo_lift(_ buf: RustBuffer) throws -> SpendableUtxo {
5416+
return try FfiConverterTypeSpendableUtxo.lift(buf)
5417+
}
5418+
5419+
public func FfiConverterTypeSpendableUtxo_lower(_ value: SpendableUtxo) -> RustBuffer {
5420+
return FfiConverterTypeSpendableUtxo.lower(value)
5421+
}
5422+
53435423
// Note that we don't yet support `indirect` for enums.
53445424
// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion.
53455425

@@ -5774,6 +5854,75 @@ extension ClosureReason: Equatable, Hashable {}
57745854

57755855

57765856

5857+
// Note that we don't yet support `indirect` for enums.
5858+
// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion.
5859+
5860+
public enum CoinSelectionAlgorithm {
5861+
5862+
case branchAndBound
5863+
case largestFirst
5864+
case oldestFirst
5865+
case singleRandomDraw
5866+
}
5867+
5868+
5869+
public struct FfiConverterTypeCoinSelectionAlgorithm: FfiConverterRustBuffer {
5870+
typealias SwiftType = CoinSelectionAlgorithm
5871+
5872+
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> CoinSelectionAlgorithm {
5873+
let variant: Int32 = try readInt(&buf)
5874+
switch variant {
5875+
5876+
case 1: return .branchAndBound
5877+
5878+
case 2: return .largestFirst
5879+
5880+
case 3: return .oldestFirst
5881+
5882+
case 4: return .singleRandomDraw
5883+
5884+
default: throw UniffiInternalError.unexpectedEnumCase
5885+
}
5886+
}
5887+
5888+
public static func write(_ value: CoinSelectionAlgorithm, into buf: inout [UInt8]) {
5889+
switch value {
5890+
5891+
5892+
case .branchAndBound:
5893+
writeInt(&buf, Int32(1))
5894+
5895+
5896+
case .largestFirst:
5897+
writeInt(&buf, Int32(2))
5898+
5899+
5900+
case .oldestFirst:
5901+
writeInt(&buf, Int32(3))
5902+
5903+
5904+
case .singleRandomDraw:
5905+
writeInt(&buf, Int32(4))
5906+
5907+
}
5908+
}
5909+
}
5910+
5911+
5912+
public func FfiConverterTypeCoinSelectionAlgorithm_lift(_ buf: RustBuffer) throws -> CoinSelectionAlgorithm {
5913+
return try FfiConverterTypeCoinSelectionAlgorithm.lift(buf)
5914+
}
5915+
5916+
public func FfiConverterTypeCoinSelectionAlgorithm_lower(_ value: CoinSelectionAlgorithm) -> RustBuffer {
5917+
return FfiConverterTypeCoinSelectionAlgorithm.lower(value)
5918+
}
5919+
5920+
5921+
5922+
extension CoinSelectionAlgorithm: Equatable, Hashable {}
5923+
5924+
5925+
57775926
// Note that we don't yet support `indirect` for enums.
57785927
// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion.
57795928

@@ -6577,6 +6726,8 @@ public enum NodeError {
65776726

65786727
case NoSpendableOutputs(message: String)
65796728

6729+
case CoinSelectionFailed(message: String)
6730+
65806731
}
65816732

65826733

@@ -6814,6 +6965,10 @@ public struct FfiConverterTypeNodeError: FfiConverterRustBuffer {
68146965
message: try FfiConverterString.read(from: &buf)
68156966
)
68166967

6968+
case 57: return .CoinSelectionFailed(
6969+
message: try FfiConverterString.read(from: &buf)
6970+
)
6971+
68176972

68186973
default: throw UniffiInternalError.unexpectedEnumCase
68196974
}
@@ -6937,6 +7092,8 @@ public struct FfiConverterTypeNodeError: FfiConverterRustBuffer {
69377092
writeInt(&buf, Int32(55))
69387093
case .NoSpendableOutputs(_ /* message is ignored*/):
69397094
writeInt(&buf, Int32(56))
7095+
case .CoinSelectionFailed(_ /* message is ignored*/):
7096+
writeInt(&buf, Int32(57))
69407097

69417098

69427099
}
@@ -8145,6 +8302,27 @@ fileprivate struct FfiConverterOptionTypePaymentFailureReason: FfiConverterRustB
81458302
}
81468303
}
81478304

8305+
fileprivate struct FfiConverterOptionSequenceTypeSpendableUtxo: FfiConverterRustBuffer {
8306+
typealias SwiftType = [SpendableUtxo]?
8307+
8308+
public static func write(_ value: SwiftType, into buf: inout [UInt8]) {
8309+
guard let value = value else {
8310+
writeInt(&buf, Int8(0))
8311+
return
8312+
}
8313+
writeInt(&buf, Int8(1))
8314+
FfiConverterSequenceTypeSpendableUtxo.write(value, into: &buf)
8315+
}
8316+
8317+
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType {
8318+
switch try readInt(&buf) as Int8 {
8319+
case 0: return nil
8320+
case 1: return try FfiConverterSequenceTypeSpendableUtxo.read(from: &buf)
8321+
default: throw UniffiInternalError.unexpectedOptionalTag
8322+
}
8323+
}
8324+
}
8325+
81488326
fileprivate struct FfiConverterOptionSequenceTypeSocketAddress: FfiConverterRustBuffer {
81498327
typealias SwiftType = [SocketAddress]?
81508328

@@ -8530,6 +8708,28 @@ fileprivate struct FfiConverterSequenceTypeRouteHintHop: FfiConverterRustBuffer
85308708
}
85318709
}
85328710

8711+
fileprivate struct FfiConverterSequenceTypeSpendableUtxo: FfiConverterRustBuffer {
8712+
typealias SwiftType = [SpendableUtxo]
8713+
8714+
public static func write(_ value: [SpendableUtxo], into buf: inout [UInt8]) {
8715+
let len = Int32(value.count)
8716+
writeInt(&buf, len)
8717+
for item in value {
8718+
FfiConverterTypeSpendableUtxo.write(item, into: &buf)
8719+
}
8720+
}
8721+
8722+
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [SpendableUtxo] {
8723+
let len: Int32 = try readInt(&buf)
8724+
var seq = [SpendableUtxo]()
8725+
seq.reserveCapacity(Int(len))
8726+
for _ in 0 ..< len {
8727+
seq.append(try FfiConverterTypeSpendableUtxo.read(from: &buf))
8728+
}
8729+
return seq
8730+
}
8731+
}
8732+
85338733
fileprivate struct FfiConverterSequenceTypeLightningBalance: FfiConverterRustBuffer {
85348734
typealias SwiftType = [LightningBalance]
85358735

@@ -9815,13 +10015,19 @@ private var initializationResult: InitializationResult {
981510015
if (uniffi_ldk_node_checksum_method_onchainpayment_bump_fee_by_rbf() != 53877) {
981610016
return InitializationResult.apiChecksumMismatch
981710017
}
10018+
if (uniffi_ldk_node_checksum_method_onchainpayment_list_spendable_outputs() != 19144) {
10019+
return InitializationResult.apiChecksumMismatch
10020+
}
981810021
if (uniffi_ldk_node_checksum_method_onchainpayment_new_address() != 37251) {
981910022
return InitializationResult.apiChecksumMismatch
982010023
}
10024+
if (uniffi_ldk_node_checksum_method_onchainpayment_select_utxos_with_algorithm() != 14084) {
10025+
return InitializationResult.apiChecksumMismatch
10026+
}
982110027
if (uniffi_ldk_node_checksum_method_onchainpayment_send_all_to_address() != 37748) {
982210028
return InitializationResult.apiChecksumMismatch
982310029
}
9824-
if (uniffi_ldk_node_checksum_method_onchainpayment_send_to_address() != 55646) {
10030+
if (uniffi_ldk_node_checksum_method_onchainpayment_send_to_address() != 28826) {
982510031
return InitializationResult.apiChecksumMismatch
982610032
}
982710033
if (uniffi_ldk_node_checksum_method_spontaneouspayment_send() != 48210) {

0 commit comments

Comments
 (0)