-
Notifications
You must be signed in to change notification settings - Fork 7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
BDK Kyoto Dream API Discussion #63
Comments
The above is just throwing my notes here to start he conversation. But attempting to write down what I was thinking about the new bdk_kyoto client opened up a few questions I'll leave here:
|
Unfortunately I cannot speak very well for Kotlin developers, but I can walk through how my PR on the bindings works, which of course uses this crate. This crate, as well as the bindings, prioritizes the user that simply wants to use the
class MessageHandler: ObservableObject, NodeMessageHandler {
@Published var progress: Double = 20
@Published var height: UInt32? = nil
func blocksDisconnected(blocks: [UInt32]) {}
func connectionsMet() {}
func dialog(dialog: String) {
print(dialog)
}
func stateChanged(state: BitcoinDevKit.NodeState) {
DispatchQueue.main.async { [self] in
switch state {
case .behind:
progress = 20
case .headersSynced:
progress = 40
case .filterHeadersSynced:
progress = 60
case .filtersSynced:
progress = 80
case .transactionsSynced:
progress = 100
}
}
}
func synced(tip: UInt32) {
print("Synced to \(tip)")
}
func txFailed(txid: BitcoinDevKit.Txid) {}
func txSent(txid: BitcoinDevKit.Txid) {}
func warning(warning: BitcoinDevKit.Warning) {
switch warning {
case .notEnoughConnections:
print("Searching for connections")
case .peerTimedOut:
print("A peer timed out")
case .unsolicitedMessage:
print("A peer sent an unsolicited message")
case .couldNotConnect:
print("The node reached out to a peer and could not connect")
case .corruptedHeaders:
print("The loaded headers do not link together")
case .transactionRejected:
print("A transaction was rejected")
case .failedPersistance(warning: let warning):
print(warning)
case .evaluatingFork:
print("Evaluating a potential fork")
case .emptyPeerDatabase:
print("The peer database is empty")
case .unexpectedSyncError(warning: let warning):
print(warning)
case .noCompactFilters:
print("A connected peer does not serve compact block filters")
case .potentialStaleTip:
print("The node has not seen a new block for a long duration")
case .unlinkableAnchor:
print("The configured recovery does not link to block headers stored in the database")
}
}
}
Task {
while true {
try await bdkClient.sync(logger)
self.walletSyncState = .synced
self.getBalance()
self.getTransactions()
}
} With this design, I think we are preserving the event-based style of API, where the user must define the So in summary:
As far as missing events, the channel receiver is popping events off in a loop, so the only way that would happen is to have more than 32 events yet to be processed. Since most of these are printing to the console or calling a simple method, its an extremely fringe and unlikely scenario to miss anything |
Thanks for the detailed response. I will implement Kyoto in the Android example wallet this week and will be able to provide more extensive feedback! Some of those questions might pertain more to the way the API is wrapped in the bindings, not sure. Will likely post/comment on your bitcoindevkit/bdk-ffi#591 PR. For example, at first glance the Task {
while true {
try await bdkClient.sync(logger)
} instead of configured once on the client (but maybe there is a reason to change the callbacks we want applied over time? In which case it might be better to receive the event as an event and apply our logic there instead of the callback). Anyway I'm about halfway done on the Android side, will keep digging! |
Yeah, good observation. We had this designed as you described before, where the callback trait that was configured once. I found it very annoying when integrating with the Swift wallet because the UI component that is responding to minor changes in state must be passed all the way down to the app initialization code. I decided it may be easier for the app developer to simply pass a UI component at the callsite. I conceptualize Also, on Wednesday and throughout the rest of the week I will be on a continental US timezone if you wanted my live feedback as you work on the Android wallet |
I implemented the client in this branch of the example Android wallet. It works! The node + UI pick up the new blocks as I mine them on regtest super fast. It's snappy and super cool in comparison to the electrum workflow. 🚀 A few quick thoughts (will come back to this tomorrow).
Overall, super work! I love to see it in action. |
|
Quick question. I don't understand what |
Agree. The |
What's the problem with serializing a block? If the problem is too much data then I guess kyoto could widdle down the block data to what's relevant I suppose. I'm guessing iit has a list of spks (etc) it's interested in. This seems like a neater idea than forcing things into Another question: |
Indeed, you can use
With how UniFFI is made it's either needless serialization and deserialization operations or heap allocations. |
I've been writing down some notes while looking at the library and implementing it in the example Android wallet.
Here are some thoughts, meant to start discussions mostly to sharpen my understanding of Kyoto and what's maybe possible vs maybe not. I could have opened this issue in the Kyoto repo too, I just figured it was mostly related to how I would use Kyoto through BDK as one of the blockchain clients.
My Dream Kyoto API?
I'm trying to clean up my mental model for kyoto, bdk_kyoto, and the bindings for both. This is very much in brainstorming mode but I've been meaning to write it down so I can iterate. Here is my dream API for Kyoto from the Kotlin perspective (I'm not familiar enough with tokio to write it in Rust).
In this workflow, there are no full scans nor syncs, no sync button to press.
The Node emits events, and the users use those as triggers for different kinds of wallet and UI operations.
Question for consideration: is it better to pass in a lambda to be triggered by the Node whenever an event happens on is it better to listen for events and trigger our own operations? In general I think passing callbacks is less flexible and increases reading complexity; on the other hand on simple tasks (say events that are simply logs) that's a good approach, because you just let the node know what to do with this event upon construction and don't need to think about it afterwards. For anything that needs more complex domain logic it's not great though. Anyway just a thought.
Top API: the Node emits a flow of events which I react to through wallet updates, persistence, UI messages, etc. Unfortunately I don't think we can expose
Flow
directly because it's a Kotlin coroutines' construct that doesn't have a direct equivalent in Swift and Python. But I do think that as is (see my option 2, standard suspension in awhile
loop) I can probably refactor this loop into emitting these events so they can be collected as flows by the application.1. Kotlin Flows
2. Standard
while
loop awaiting the updates (I think this is roughly the current approach?)The text was updated successfully, but these errors were encountered: