Introduction to DlcDevKit
Creating DLC applications is hard, but it does not have to be. DlcDevKit allows application developers to focus on the user experience. Not the complexities of Bitcoin and DLCs.
tl;dr: dlcs are so back
When I was first exploring the DLC ecosystem I wanted an easy way to prototype my idea. There are excellent libraries for DLCs but the application tooling fell short to quickly prototype. I found this a hard stop for application developers. If someone new to bitcoin development were to start with DLCs they would be encumbered with Bitcoin implementation details before even starting their application. In part, this is why DLCs never got their chance to shine in the open market.
This lead me to create dlcdevkit
(or ddk
). An application development kit that manages contract creation, management, storage, message handling, and comes with a bdk
wallet out of the box. Shifting the focus for application developers to focus on their ideas, not bitcoin internal complexities.
ddk
is written in rust btw 🦀. It uses rust-dlc under the hood for contract management, execution, and creation of DLC transactions. rust-dlc
lacks a direct wallet interface so ddk
is packaged with bdk
in a nice bow for you to create your next billion dollar idea.
- Sports betting app?
ddk
is here for you - Insurance claims?
ddk
is here for you - Derivatives trading?
ddk
is here for you
Usage of ddk
Creating a fully features DLC application is straightforward. App developers just need to answer 3 questions.
- How will my users communicate with each other?.
- What database will I use to store contracts?
- What oracle will I use?
ddk
composes to three main traits that consumers will implements to handle those operations internally. If you are familiar with the project ldk-node then the ddk
API will feel very familiar.
First a full example
use ddk::builder::Builder;
use ddk::storage::SledStorage;
use ddk::transport::lightning::LightningTransport; // with "lightning" feature
use ddk::oracle::KormirOracleClient;
use bitcoin::Network;
use std::sync::Arc;
#[tokio::main]
fn main() {
let transport = Arc::new(LightningTransport::new([0u8;32], 9735, Network::Signet));
let storage = Arc::new(SledStorage::new("/tmp/ddk")?);
let oracle_client = Arc::new(KormirOracleClient::new("https://kormir.dlcdevkit.com").await?);
let ddk: ApplicationDdk = Builder::new()
.set_seed_bytes([0u8;32])
.set_network(Network::Regtest)
.set_esplora_path("http://mutinynet.com") // shouts out mutiny
.set_transport(transport.clone())
.set_storage(storage.clone())
.set_oracle(oracle_client.clone())
.finish()
.expect("skill issue");
ddk.start().expect("skill issue");
}
The builder takes in information like seed_bytes for the internal wallet, network to be on, esplora host, and then the consumer creates the necessary traits for transport, storage, and oracle. ddk
have three example crates out of the box to quickly get started.
- Lightning transport - BOLT 8 compliant messaging using the LDK peer manager
- Sled Storage - file based storage using sleddb
- Kormir Oracle Client - Server implementation of the kormir crate
Transport Trait
DLC participants need some way to communicate with each other. Passing messages such as Offers, Accept, and Sign. Typically app developers would have to build and maintain this logic by themselves but ddk
abstracts this with the Transport
trait.
#[async_trait]
/// Allows ddk to open a listening connection and send/receive dlc messages functionality.
pub trait Transport: Send + Sync + 'static {
/// Name for the transport service.
fn name(&self) -> String;
/// Open an incoming listener for DLC messages from peers.
async fn listen(&self);
/// Get messages that have not been processed yet.
async fn receive_messages<S: Storage, O: Oracle>(
&self,
manager: Arc<DlcDevKitDlcManager<S, O>>,
);
/// Send a message to a specific counterparty.
fn send_message(&self, counterparty: PublicKey, message: Message);
/// Connect to another peer
async fn connect_outbound(&self, pubkey: PublicKey, host: &str);
}
LightnigTransport
example that is available
Consumers implement a listener for which ddk
will listen on for incoming connections. Then a function for receiving messages, this is passed to the manager to handle the corresponding DLC message. And then of course functions for sending messages and connecting to counterparties.
This opens up an opportunity for consumers to implement a transport listener for whatever platform they want to build on. You can create a nostr
implementation for mobile applications that have dynamic IP addresses. You can build a client/server model for passing messages between counterparties. Or you could use the lightning gossip layer!
Storage Trait
The storage trait is used for the storage of contracts, wallet change sets, and counterparty peer information. It is a super trait of the rust-dlc/dlc-manager
trait. It is technically a super trait for bdk
but there is a wrapper struct that is created because of the bdk::PersistedWallet
trait bounds.
/// Storage for DLC contracts.
pub trait Storage: dlc_manager::Storage + Send + Sync + 'static {
///// Instantiate the storage for the BDK wallet.
fn initialize_bdk(&self) -> Result<ChangeSet, WalletError>;
/// Save changeset to the wallet storage.
fn persist_bdk(&self, changeset: &ChangeSet) -> Result<(), WalletError>;
/// Connected counterparties.
fn list_peers(&self) -> anyhow::Result<Vec<PeerInformation>>;
/// Persis counterparty.
fn save_peer(&self, peer: PeerInformation) -> anyhow::Result<()>;
// For another blog!
// #[cfg(feature = "marketplace")]
fn save_announcement(&self, announcement: OracleAnnouncement) -> anyhow::Result<()>;
// #[cfg(feature = "marketplace")]
fn get_marketplace_announcements(&self) -> anyhow::Result<Vec<OracleAnnouncement>>;
}
Oracle Trait
There is not much to the oracle client. Similar to the storage trait it is a super trait to rust-dlc/dlc-manager
storage trait. All that is required is get_announcement()
and get_attestation()
. If consumers are interested in creating their own oracle implementation, I recommend using kormir
.
/// Oracle client
pub trait Oracle: dlc_manager::Oracle + Send + Sync + 'static {
fn name(&self) -> String;
}
How it glues together
Now with all of the traits implemented and built with the ddk::builder::Builder
. Calling the start()
method starts the listeners, processes dlc messages, checks contracts, and syncs the on-chain wallet.
pub fn start(&self) -> anyhow::Result<()> {
let mut runtime_lock = self.runtime.write().unwrap();
if runtime_lock.is_some() {
return Err(anyhow!("DDK is still running."));
}
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?;
let manager_clone = self.manager.clone();
let receiver_clone = self.receiver.clone();
runtime.spawn(async move { Self::run_manager(manager_clone, receiver_clone).await });
let transport_clone = self.transport.clone();
runtime.spawn(async move {
transport_clone.listen().await;
});
let transport_clone = self.transport.clone();
let manager_clone = self.manager.clone();
runtime.spawn(async move {
transport_clone.receive_messages(manager_clone).await;
});
let wallet_clone = self.wallet.clone();
runtime.spawn(async move {
let mut timer = tokio::time::interval(Duration::from_secs(10));
loop {
timer.tick().await;
wallet_clone.sync().unwrap();
}
});
let processor = self.sender.clone();
runtime.spawn(async move {
let mut timer = tokio::time::interval(Duration::from_secs(5));
loop {
timer.tick().await;
processor
.send(DlcManagerMessage::PeriodicCheck)
.expect("couldn't send periodic check");
}
});
*runtime_lock = Some(runtime);
Ok(())
}
Flexibility
Like I mentioned before, ddk
is flexible for whatever platform you are building your app on. Want to use nostr
for transport? Create a nostr transport! Create a relational database with sql or go document based with mongo. You can create a gRPC server or a REST service.
ddk-node
Don't want to code and start using DLCs? Check out ddk-node
. A pre-built node and cli using the pre-build components maintained in dlcdevkkit
.
Installation
Start building with dlcdevkit
today!
$ cargo add ddk
Start executing DLCs today!
$ cargo install ddk-node
Currently, DDK requires an async runtime which is not merged yet with rust-dlc
so you may have to install or add with the GitHub link. But there is a passing PR
$ cargo add --git https://github.com/bennyhodl/dlcdevkit.git ddk
$ cargo install --git https://github.com/bennyhodl/dlcdevkit.git ddk-node
Contribute to ddk
I am looking for contributors and users of dlcdevkit
. I extend an invitation to participate.
Star the project on GitHub
Read the documentation
Follow me on twitter