Skip to content

BDK Kyoto Dream API Discussion #63

@thunderbiscuit

Description

@thunderbiscuit

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).

// My understanding is that there is currently a utility function that builds both of those in one go but I imagine it's roughly the same
val node: KyotoNode = KyotoNode()
    .configure1()
    .configure2()
    .build() // build a custom CBF node optimized for user requirements

val client: KyotoClient = KyotoClient()
    .node(nodeToSpeakTo)
    .configureX()
    .configureY()
    .build()

// The node emits X types of events (say Log, StartSync, UpToDate, NewBlock)

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.

sealed class KyotoEvent {
    data class LogInfo(message: String) : KyotoEvent
    object NodeSyncedTip : KyotoEvent
    object NewBlock : KyotoEvent
    data class WalletUpdate(update: BdkUpdate) : KyotoEvent 
}

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 a while loop) I can probably refactor this loop into emitting these events so they can be collected as flows by the application.

1. Kotlin Flows

val eventFlow: Flow<KyotoEvent> = client.getEventFlow()

coroutineScope.launch {
    eventFlow.collect { event ->
	when (event) {
	    is LogInfo       -> logger.info(event.message)
	    is NodeSyncedTip -> showSnackbar("All synced up!")
            is NewBlock      -> showSnackbar("New block")
            is WalletUpdate  -> wallet.applyUpdate(event.update)
	}
    }
}

2. Standard while loop awaiting the updates (I think this is roughly the current approach?)

coroutineScope.launch(Dispatchers.IO) {
    launch {
	while (nodeIsLive) {
            val event: KyotoEvent = client.update() // Suspend and wait for events
	    event?.let {
	        wallet.applyUpdate(it)
	        wallet.persist()
	        triggerCommunicationWithUser()
            }
	}
    }
   otherInterleavingAsynchronousWork()
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions