From a3cd31b4dbc4ee0ee2812628f4929fc1bb1c0d5f Mon Sep 17 00:00:00 2001 From: bennyhodl Date: Mon, 25 Aug 2025 21:19:11 -0400 Subject: [PATCH 1/4] feat: seed builder for bip39 --- Cargo.lock | 1 + ddk-manager/tests/manager_tests.rs | 2 +- ddk-manager/tests/test_utils.rs | 2 +- ddk-node/src/lib.rs | 17 ++++-------- ddk-node/src/seed.rs | 14 ++++------ ddk/Cargo.toml | 1 + ddk/examples/lightning.rs | 10 ++----- ddk/examples/nostr.rs | 6 ++-- ddk/examples/postgres.rs | 6 ++-- ddk/src/builder.rs | 35 ++++++++++++++++++++---- ddk/src/error.rs | 2 ++ ddk/src/transport/nostr/relay_handler.rs | 2 +- ddk/src/wallet/mod.rs | 19 ++++--------- ddk/tests/nostr.rs | 5 ++-- ddk/tests/test_util.rs | 11 +++++--- 15 files changed, 71 insertions(+), 62 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 33f6bdc8..11e92224 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -946,6 +946,7 @@ dependencies = [ "bdk_chain", "bdk_esplora", "bdk_wallet", + "bip39", "bitcoin", "bitcoincore-rpc", "chrono", diff --git a/ddk-manager/tests/manager_tests.rs b/ddk-manager/tests/manager_tests.rs index c7d8e245..ebd184a2 100644 --- a/ddk-manager/tests/manager_tests.rs +++ b/ddk-manager/tests/manager_tests.rs @@ -28,7 +28,7 @@ async fn get_manager() -> TestManager { let blockchain = Arc::new(EsploraClient::new("http://localhost:30000", Network::Regtest).unwrap()); let store = Arc::new(MemoryStorage::new()); - let mut seed = [0u8; 32]; + let mut seed = [0u8; 64]; seed.try_fill(&mut bitcoin::key::rand::thread_rng()) .unwrap(); let wallet = Arc::new( diff --git a/ddk-manager/tests/test_utils.rs b/ddk-manager/tests/test_utils.rs index 8d64d4b0..ce8a1446 100644 --- a/ddk-manager/tests/test_utils.rs +++ b/ddk-manager/tests/test_utils.rs @@ -840,7 +840,7 @@ pub async fn create_and_fund_wallet() -> (DlcDevKitWallet, Arc) { .get_new_address(None, None) .unwrap() .assume_checked(); - let mut seed = [0u8; 32]; + let mut seed = [0u8; 64]; seed.try_fill(&mut bitcoin::key::rand::thread_rng()) .unwrap(); let memory_storage = Arc::new(MemoryStorage::new()); diff --git a/ddk-node/src/lib.rs b/ddk-node/src/lib.rs index 3ce1163c..2f0531dd 100644 --- a/ddk-node/src/lib.rs +++ b/ddk-node/src/lib.rs @@ -6,7 +6,7 @@ mod seed; use bitcoin::secp256k1::PublicKey; use bitcoin::{Address, Amount, FeeRate, Network}; -use ddk::builder::Builder; +use ddk::builder::{Builder, SeedConfig}; use ddk::oracle::kormir::KormirOracleClient; use ddk::storage::postgres::PostgresStore; use ddk::transport::nostr::NostrDlc; @@ -63,18 +63,11 @@ impl DdkNode { let network = Network::from_str(&opts.network)?; std::fs::create_dir_all(storage_path.clone())?; - let seed_bytes = crate::seed::xprv_from_path(storage_path.clone(), network)?; + let seed_bytes = crate::seed::xprv_from_path(storage_path.clone())?; tracing::info!("Starting DDK node."); - - let transport = Arc::new( - NostrDlc::new( - &seed_bytes.private_key.secret_bytes(), - "wss://nostr.dlcdevkit.com", - network, - ) - .await?, - ); + let transport = + Arc::new(NostrDlc::new(&seed_bytes, "wss://nostr.dlcdevkit.com", network).await?); let storage = Arc::new(PostgresStore::new(&opts.postgres_url, true, opts.name).await?); @@ -82,7 +75,7 @@ impl DdkNode { let oracle = Arc::new(KormirOracleClient::new(&opts.oracle_host, None).await?); let mut builder = Builder::new(); - builder.set_seed_bytes(seed_bytes.private_key.secret_bytes()); + builder.set_seed_bytes(SeedConfig::Bytes(seed_bytes)); builder.set_esplora_host(opts.esplora_host); builder.set_network(network); builder.set_transport(transport.clone()); diff --git a/ddk-node/src/seed.rs b/ddk-node/src/seed.rs index 6e1a0584..72d41143 100644 --- a/ddk-node/src/seed.rs +++ b/ddk-node/src/seed.rs @@ -1,6 +1,4 @@ -use bitcoin::bip32::Xpriv; use bitcoin::key::rand; -use bitcoin::Network; use rand::Fill; use std::{ fs::File, @@ -10,21 +8,19 @@ use std::{ /// Helper function that reads `[bitcoin::bip32::Xpriv]` bytes from a file. /// If the file does not exist then it will create a file `seed.ddk` in the specified path. -pub fn xprv_from_path(path: PathBuf, network: Network) -> anyhow::Result { +pub fn xprv_from_path(path: PathBuf) -> anyhow::Result<[u8; 64]> { let seed_path = path.join("seed.ddk"); let seed = if Path::new(&seed_path).exists() { let seed = std::fs::read(&seed_path)?; - let mut key = [0; 32]; + let mut key = [0; 64]; key.copy_from_slice(&seed); - Xpriv::new_master(network, &seed)? + key } else { let mut file = File::create(&seed_path)?; - let mut entropy = [0u8; 32]; + let mut entropy = [0u8; 64]; entropy.try_fill(&mut rand::thread_rng())?; - // let _mnemonic = Mnemonic::from_entropy(&entropy)?; - let xprv = Xpriv::new_master(network, &entropy)?; file.write_all(&entropy)?; - xprv + entropy }; Ok(seed) diff --git a/ddk/Cargo.toml b/ddk/Cargo.toml index 174e7a41..6bab66b0 100644 --- a/ddk/Cargo.toml +++ b/ddk/Cargo.toml @@ -62,6 +62,7 @@ kormir = { version = "0.5.0", path = "../kormir" } hmac = "0.12.1" sha2 = "0.10" nostr-database = { version = "0.40.0", optional = true } +bip39 = "2.2.0" [dev-dependencies] test-log = { version = "0.2.16", features = ["trace"] } diff --git a/ddk/examples/lightning.rs b/ddk/examples/lightning.rs index 3127bef4..c676cb8b 100644 --- a/ddk/examples/lightning.rs +++ b/ddk/examples/lightning.rs @@ -1,5 +1,4 @@ -use bitcoin::key::rand::Fill; -use ddk::builder::Builder; +use ddk::builder::{Builder, SeedConfig}; use ddk::oracle::kormir::KormirOracleClient; use ddk::storage::sled::SledStorage; use ddk::transport::lightning::LightningTransport; @@ -14,13 +13,8 @@ async fn main() -> Result<(), ddk::error::Error> { let storage = Arc::new(SledStorage::new(current_dir().unwrap().to_str().unwrap()).unwrap()); let oracle_client = Arc::new(KormirOracleClient::new("host", None).await?); - let mut seed_bytes = [0u8; 32]; - seed_bytes - .try_fill(&mut bitcoin::key::rand::thread_rng()) - .unwrap(); - let mut builder = Builder::new(); - builder.set_seed_bytes(seed_bytes); + builder.set_seed_bytes(SeedConfig::Random); builder.set_transport(transport.clone()); builder.set_storage(storage.clone()); builder.set_oracle(oracle_client.clone()); diff --git a/ddk/examples/nostr.rs b/ddk/examples/nostr.rs index c99f07fa..f361fc6c 100644 --- a/ddk/examples/nostr.rs +++ b/ddk/examples/nostr.rs @@ -1,6 +1,6 @@ use bitcoin::key::rand::Fill; use bitcoin::Network; -use ddk::builder::Builder; +use ddk::builder::{Builder, SeedConfig}; use ddk::oracle::memory::MemoryOracle; use ddk::storage::memory::MemoryStorage; use ddk::transport::nostr::NostrDlc; @@ -10,7 +10,7 @@ type NostrDdk = ddk::DlcDevKit; #[tokio::main] async fn main() -> Result<(), ddk::error::Error> { - let mut seed_bytes = [0u8; 32]; + let mut seed_bytes = [0u8; 64]; seed_bytes .try_fill(&mut bitcoin::key::rand::thread_rng()) .unwrap(); @@ -21,7 +21,7 @@ async fn main() -> Result<(), ddk::error::Error> { let oracle_client = Arc::new(MemoryOracle::default()); let mut builder = Builder::new(); - builder.set_seed_bytes(seed_bytes); + builder.set_seed_bytes(SeedConfig::Bytes(seed_bytes)); builder.set_transport(transport.clone()); builder.set_storage(storage.clone()); builder.set_oracle(oracle_client.clone()); diff --git a/ddk/examples/postgres.rs b/ddk/examples/postgres.rs index cc299575..06fc21bc 100644 --- a/ddk/examples/postgres.rs +++ b/ddk/examples/postgres.rs @@ -1,5 +1,5 @@ use bitcoin::key::rand::Fill; -use ddk::builder::Builder; +use ddk::builder::{Builder, SeedConfig}; use ddk::oracle::kormir::KormirOracleClient; use ddk::storage::postgres::PostgresStore; use ddk::transport::lightning::LightningTransport; @@ -21,13 +21,13 @@ async fn main() -> Result<(), ddk::error::Error> { let oracle_client = Arc::new(KormirOracleClient::new("https://kormir.dlcdevkit.com", None).await?); - let mut seed_bytes = [0u8; 32]; + let mut seed_bytes = [0u8; 64]; seed_bytes .try_fill(&mut bitcoin::key::rand::thread_rng()) .unwrap(); let mut builder = Builder::new(); - builder.set_seed_bytes(seed_bytes); + builder.set_seed_bytes(SeedConfig::Bytes(seed_bytes)); builder.set_transport(transport.clone()); builder.set_storage(storage.clone()); builder.set_oracle(oracle_client.clone()); diff --git a/ddk/src/builder.rs b/ddk/src/builder.rs index 9eb825de..83f83960 100644 --- a/ddk/src/builder.rs +++ b/ddk/src/builder.rs @@ -1,3 +1,5 @@ +use bip39::{Language, Mnemonic}; +use bitcoin::key::rand::Fill; use bitcoin::Network; use ddk_manager::manager::Manager; use ddk_manager::SystemTimeProvider; @@ -14,6 +16,17 @@ use crate::{Oracle, Storage, Transport}; const DEFAULT_ESPLORA_HOST: &str = "https://mutinynet.com/api"; const DEFAULT_NETWORK: Network = Network::Signet; +/// Configuration for the seed bytes for the wallet. +#[derive(Debug, Clone)] +pub enum SeedConfig { + /// Generates a random seed everytime ddk is run. + Random, + /// The first string is the mnemonic, the second is the passphrase. + Mnemonic(String, String), + /// The bytes to use for the seed. + Bytes([u8; 64]), +} + /// Builder pattern for creating a [`crate::ddk::DlcDevKit`] process. #[derive(Clone)] pub struct Builder { @@ -24,7 +37,7 @@ pub struct Builder { contract_address_generator: Option>, esplora_host: String, network: Network, - seed_bytes: [u8; 32], + seed_bytes: [u8; 64], } /// Defaults when creating a DDK application @@ -42,7 +55,7 @@ impl Default for Builder { contract_address_generator: None, esplora_host: DEFAULT_ESPLORA_HOST.to_string(), network: DEFAULT_NETWORK, - seed_bytes: [0u8; 32], + seed_bytes: [0u8; 64], } } } @@ -107,9 +120,21 @@ impl Builder { } /// Set the seed bytes for the wallet. - pub fn set_seed_bytes(&mut self, bytes: [u8; 32]) -> &mut Self { - self.seed_bytes = bytes; - self + pub fn set_seed_bytes(&mut self, seed_config: SeedConfig) -> Result<&mut Self, BuilderError> { + match seed_config { + SeedConfig::Random => { + let mut seed = [0u8; 64]; + seed.try_fill(&mut bitcoin::key::rand::thread_rng()) + .map_err(|_| BuilderError::SeedGenerationFailed)?; + self.seed_bytes = seed + } + SeedConfig::Mnemonic(mnemonic, passphrase) => { + let mnemonic = Mnemonic::parse_in_normalized(Language::English, &mnemonic).unwrap(); + self.seed_bytes = mnemonic.to_seed(passphrase) + } + SeedConfig::Bytes(bytes) => self.seed_bytes = bytes, + } + Ok(self) } /// Builds the `DlcDevKit` instance. Fails if any components are missing. diff --git a/ddk/src/error.rs b/ddk/src/error.rs index c5e8916b..04248d97 100644 --- a/ddk/src/error.rs +++ b/ddk/src/error.rs @@ -137,6 +137,8 @@ pub enum BuilderError { NoStorage, #[error("An oracle client was not provided.")] NoOracle, + #[error("Failed to generate random seed.")] + SeedGenerationFailed, } /// Errors related to Bitcoin wallet operations. diff --git a/ddk/src/transport/nostr/relay_handler.rs b/ddk/src/transport/nostr/relay_handler.rs index e093d162..95d57a61 100644 --- a/ddk/src/transport/nostr/relay_handler.rs +++ b/ddk/src/transport/nostr/relay_handler.rs @@ -20,7 +20,7 @@ pub struct NostrDlc { impl NostrDlc { pub async fn new( - seed_bytes: &[u8; 32], + seed_bytes: &[u8; 64], relay_host: &str, network: Network, ) -> Result { diff --git a/ddk/src/wallet/mod.rs b/ddk/src/wallet/mod.rs index 1c712382..5da1174a 100644 --- a/ddk/src/wallet/mod.rs +++ b/ddk/src/wallet/mod.rs @@ -229,7 +229,7 @@ impl DlcDevKitWallet { /// - Handles all BDK operations /// - Manages blockchain synchronization pub async fn new( - seed_bytes: &[u8; 32], + seed_bytes: &[u8; 64], esplora_url: &str, network: Network, storage: Arc, @@ -935,7 +935,7 @@ mod tests { use crate::storage::memory::MemoryStorage; use bitcoin::{ address::NetworkChecked, - bip32::{ChildNumber, Xpriv}, + bip32::ChildNumber, key::rand::Fill, secp256k1::{PublicKey, SecretKey}, Address, AddressType, Amount, FeeRate, Network, @@ -952,16 +952,9 @@ mod tests { entropy .try_fill(&mut bitcoin::key::rand::thread_rng()) .unwrap(); - let xpriv = Xpriv::new_master(Network::Regtest, &entropy).unwrap(); - DlcDevKitWallet::new( - &xpriv.private_key.secret_bytes(), - &esplora, - Network::Regtest, - storage.clone(), - None, - ) - .await - .unwrap() + DlcDevKitWallet::new(&entropy, &esplora, Network::Regtest, storage.clone(), None) + .await + .unwrap() } fn generate_blocks(num: u64) { @@ -1494,7 +1487,7 @@ mod tests { .unwrap() .assume_checked(); - let mut seed = [0u8; 32]; + let mut seed = [0u8; 64]; seed.try_fill(&mut bitcoin::key::rand::thread_rng()) .unwrap(); diff --git a/ddk/tests/nostr.rs b/ddk/tests/nostr.rs index ef601942..1c06c5f2 100644 --- a/ddk/tests/nostr.rs +++ b/ddk/tests/nostr.rs @@ -6,6 +6,7 @@ mod nostr_test { use bitcoin::Amount; use bitcoin::{key::rand::Fill, Network}; use chrono::{Local, TimeDelta}; + use ddk::builder::SeedConfig; use ddk::oracle::memory::MemoryOracle; use ddk::storage::memory::MemoryStorage; use ddk::transport::nostr::NostrDlc; @@ -17,7 +18,7 @@ mod nostr_test { type NostrDlcDevKit = DlcDevKit; async fn nostr_ddk(name: &str, oracle: Arc) -> NostrDlcDevKit { - let mut seed = [0u8; 32]; + let mut seed = [0u8; 64]; seed.try_fill(&mut bitcoin::key::rand::thread_rng()) .unwrap(); let esplora_host = "http://127.0.0.1:30000".to_string(); @@ -31,7 +32,7 @@ mod nostr_test { let ddk: NostrDlcDevKit = Builder::new() .set_network(Network::Regtest) - .set_seed_bytes(seed) + .set_seed_bytes(SeedConfig::Bytes(seed)) .set_esplora_host(esplora_host) .set_name(name) .set_oracle(oracle) diff --git a/ddk/tests/test_util.rs b/ddk/tests/test_util.rs index 411f9e32..d0916e7c 100644 --- a/ddk/tests/test_util.rs +++ b/ddk/tests/test_util.rs @@ -18,8 +18,11 @@ use std::{ use bitcoincore_rpc::RpcApi; use ddk::{ - builder::Builder, oracle::memory::MemoryOracle, storage::memory::MemoryStorage, - transport::memory::MemoryTransport, DlcDevKit, + builder::{Builder, SeedConfig}, + oracle::memory::MemoryOracle, + storage::memory::MemoryStorage, + transport::memory::MemoryTransport, + DlcDevKit, }; type TestDlcDevKit = DlcDevKit; @@ -108,7 +111,7 @@ pub struct TestSuite { impl TestSuite { pub async fn new(secp: &Secp256k1, name: &str, oracle: Arc) -> TestSuite { - let mut seed = [0u8; 32]; + let mut seed = [0u8; 64]; seed.try_fill(&mut bitcoin::key::rand::thread_rng()) .unwrap(); let esplora_host = "http://127.0.0.1:30000".to_string(); @@ -118,7 +121,7 @@ impl TestSuite { let ddk: TestDlcDevKit = Builder::new() .set_network(Network::Regtest) - .set_seed_bytes(seed) + .set_seed_bytes(SeedConfig::Bytes(seed)) .set_esplora_host(esplora_host) .set_name(name) .set_oracle(oracle) From 2a118b091d8ca0089e8b59c90bd1c4a4192abbab Mon Sep 17 00:00:00 2001 From: bennyhodl Date: Mon, 25 Aug 2025 22:06:31 -0400 Subject: [PATCH 2/4] fix: include types serde methods --- dlc-messages/src/channel.rs | 29 ++++--- dlc-messages/src/lib.rs | 46 ++-------- dlc-messages/src/ser_macros.rs | 28 ++++++ dlc-messages/src/types.rs | 154 +++++++++++++++++++++++++++++++++ 4 files changed, 203 insertions(+), 54 deletions(-) create mode 100644 dlc-messages/src/types.rs diff --git a/dlc-messages/src/channel.rs b/dlc-messages/src/channel.rs index c79aa963..c859ee30 100644 --- a/dlc-messages/src/channel.rs +++ b/dlc-messages/src/channel.rs @@ -12,6 +12,7 @@ use crate::FundingSignatures; use crate::{ contract_msgs::ContractInfo, ser_impls::{read_ecdsa_adaptor_signature, write_ecdsa_adaptor_signature}, + types::*, CetAdaptorSignatures, FundingInput, NegotiationFields, }; @@ -82,7 +83,7 @@ pub struct OfferChannel { pub cet_nsequence: u32, } -impl_dlc_writeable!(OfferChannel, { +impl_dlc_writeable!(OfferChannel, OFFER_CHANNEL_TYPE, { (protocol_version, writeable), (contract_flags, writeable), (chain_hash, writeable), @@ -193,7 +194,7 @@ pub struct AcceptChannel { pub negotiation_fields: Option, } -impl_dlc_writeable!(AcceptChannel, { +impl_dlc_writeable!(AcceptChannel, ACCEPT_CHANNEL_TYPE, { (temporary_channel_id, writeable), (accept_collateral, writeable), (funding_pubkey, writeable), @@ -240,7 +241,7 @@ pub struct SignChannel { pub funding_signatures: FundingSignatures, } -impl_dlc_writeable!(SignChannel, { +impl_dlc_writeable!(SignChannel, SIGN_CHANNEL_TYPE, { (channel_id, writeable), (cet_adaptor_signatures, writeable), (buffer_adaptor_signature, {cb_writeable, write_ecdsa_adaptor_signature, read_ecdsa_adaptor_signature}), @@ -272,7 +273,7 @@ pub struct SettleOffer { pub next_per_update_point: PublicKey, } -impl_dlc_writeable!(SettleOffer, { +impl_dlc_writeable!(SettleOffer, SETTLE_CHANNEL_OFFER_TYPE, { (channel_id, writeable), (counter_payout, writeable), (next_per_update_point, writeable) @@ -303,7 +304,7 @@ pub struct SettleAccept { pub settle_adaptor_signature: EcdsaAdaptorSignature, } -impl_dlc_writeable!(SettleAccept, { +impl_dlc_writeable!(SettleAccept, SETTLE_CHANNEL_ACCEPT_TYPE, { (channel_id, writeable), (next_per_update_point, writeable), (settle_adaptor_signature, {cb_writeable, write_ecdsa_adaptor_signature, read_ecdsa_adaptor_signature}) @@ -334,7 +335,7 @@ pub struct SettleConfirm { pub settle_adaptor_signature: EcdsaAdaptorSignature, } -impl_dlc_writeable!(SettleConfirm, { +impl_dlc_writeable!(SettleConfirm, SETTLE_CHANNEL_CONFIRM_TYPE, { (channel_id, writeable), (prev_per_update_secret, writeable), (settle_adaptor_signature, {cb_writeable, write_ecdsa_adaptor_signature, read_ecdsa_adaptor_signature}) @@ -362,7 +363,7 @@ pub struct SettleFinalize { pub prev_per_update_secret: SecretKey, } -impl_dlc_writeable!(SettleFinalize, { +impl_dlc_writeable!(SettleFinalize, SETTLE_CHANNEL_FINALIZE_TYPE, { (channel_id, writeable), (prev_per_update_secret, writeable) }); @@ -402,7 +403,7 @@ pub struct RenewOffer { pub cet_nsequence: u32, } -impl_dlc_writeable!(RenewOffer, { +impl_dlc_writeable!(RenewOffer, RENEW_CHANNEL_OFFER_TYPE, { (channel_id, writeable), (temporary_contract_id, writeable), (counter_payout, writeable), @@ -439,7 +440,7 @@ pub struct RenewAccept { pub refund_signature: Signature, } -impl_dlc_writeable!(RenewAccept, { +impl_dlc_writeable!(RenewAccept, RENEW_CHANNEL_ACCEPT_TYPE, { (channel_id, writeable), (next_per_update_point, writeable), (cet_adaptor_signatures, writeable), @@ -472,7 +473,7 @@ pub struct RenewConfirm { pub refund_signature: Signature, } -impl_dlc_writeable!(RenewConfirm, { +impl_dlc_writeable!(RenewConfirm, RENEW_CHANNEL_CONFIRM_TYPE, { (channel_id, writeable), (buffer_adaptor_signature, {cb_writeable, write_ecdsa_adaptor_signature, read_ecdsa_adaptor_signature}), (cet_adaptor_signatures, writeable), @@ -504,7 +505,7 @@ pub struct RenewFinalize { pub buffer_adaptor_signature: EcdsaAdaptorSignature, } -impl_dlc_writeable!(RenewFinalize, { +impl_dlc_writeable!(RenewFinalize, RENEW_CHANNEL_FINALIZE_TYPE, { (channel_id, writeable), (per_update_secret, writeable), (buffer_adaptor_signature, {cb_writeable, write_ecdsa_adaptor_signature, read_ecdsa_adaptor_signature}) @@ -533,7 +534,7 @@ pub struct RenewRevoke { pub per_update_secret: SecretKey, } -impl_dlc_writeable!(RenewRevoke, { +impl_dlc_writeable!(RenewRevoke, RENEW_CHANNEL_REVOKE_TYPE, { (channel_id, writeable), (per_update_secret, writeable) }); @@ -561,7 +562,7 @@ pub struct CollaborativeCloseOffer { pub close_signature: Signature, } -impl_dlc_writeable!(CollaborativeCloseOffer, { +impl_dlc_writeable!(CollaborativeCloseOffer, COLLABORATIVE_CLOSE_OFFER_TYPE, { (channel_id, writeable), (counter_payout, writeable), (close_signature, writeable) @@ -586,4 +587,4 @@ pub struct Reject { pub channel_id: [u8; 32], } -impl_dlc_writeable!(Reject, { (channel_id, writeable) }); +impl_dlc_writeable!(Reject, REJECT, { (channel_id, writeable) }); diff --git a/dlc-messages/src/lib.rs b/dlc-messages/src/lib.rs index 3585fec4..476f4cff 100644 --- a/dlc-messages/src/lib.rs +++ b/dlc-messages/src/lib.rs @@ -29,6 +29,7 @@ pub mod contract_msgs; pub mod message_handler; pub mod oracle_msgs; pub mod segmentation; +pub mod types; #[cfg(any(test, feature = "use-serde"))] pub mod serde_utils; @@ -36,6 +37,7 @@ pub mod serde_utils; use std::fmt::Display; use crate::ser_impls::{read_ecdsa_adaptor_signature, write_ecdsa_adaptor_signature}; +use crate::types::*; use bitcoin::{consensus::Decodable, OutPoint, Transaction}; use bitcoin::{Amount, ScriptBuf}; use channel::{ @@ -53,42 +55,6 @@ use secp256k1_zkp::Verification; use secp256k1_zkp::{ecdsa::Signature, EcdsaAdaptorSignature, PublicKey, Secp256k1}; use segmentation::{SegmentChunk, SegmentStart}; -macro_rules! impl_type { - ($const_name: ident, $type_name: ident, $type_val: expr) => { - /// The type prefix for an [`$type_name`] message. - pub const $const_name: u16 = $type_val; - - impl Type for $type_name { - fn type_id(&self) -> u16 { - $const_name - } - } - }; -} - -impl_type!(OFFER_TYPE, OfferDlc, 42778); -impl_type!(ACCEPT_TYPE, AcceptDlc, 42780); -impl_type!(SIGN_TYPE, SignDlc, 42782); -impl_type!(CLOSE_TYPE, CloseDlc, 42784); -impl_type!(OFFER_CHANNEL_TYPE, OfferChannel, 43000); -impl_type!(ACCEPT_CHANNEL_TYPE, AcceptChannel, 43002); -impl_type!(SIGN_CHANNEL_TYPE, SignChannel, 43004); -impl_type!(SETTLE_CHANNEL_OFFER_TYPE, SettleOffer, 43006); -impl_type!(SETTLE_CHANNEL_ACCEPT_TYPE, SettleAccept, 43008); -impl_type!(SETTLE_CHANNEL_CONFIRM_TYPE, SettleConfirm, 43010); -impl_type!(SETTLE_CHANNEL_FINALIZE_TYPE, SettleFinalize, 43012); -impl_type!(RENEW_CHANNEL_OFFER_TYPE, RenewOffer, 43014); -impl_type!(RENEW_CHANNEL_ACCEPT_TYPE, RenewAccept, 43016); -impl_type!(RENEW_CHANNEL_CONFIRM_TYPE, RenewConfirm, 43018); -impl_type!(RENEW_CHANNEL_FINALIZE_TYPE, RenewFinalize, 43020); -impl_type!(RENEW_CHANNEL_REVOKE_TYPE, RenewRevoke, 43026); -impl_type!( - COLLABORATIVE_CLOSE_OFFER_TYPE, - CollaborativeCloseOffer, - 43022 -); -impl_type!(REJECT, Reject, 43024); - #[derive(Clone, Debug, PartialEq, Eq)] #[cfg_attr( feature = "use-serde", @@ -429,7 +395,7 @@ impl OfferDlc { } } -impl_dlc_writeable!(OfferDlc, { +impl_dlc_writeable!(OfferDlc, OFFER_TYPE, { (protocol_version, writeable), (contract_flags, writeable), (chain_hash, writeable), @@ -492,7 +458,7 @@ pub struct AcceptDlc { pub negotiation_fields: Option, } -impl_dlc_writeable!(AcceptDlc, { +impl_dlc_writeable!(AcceptDlc, ACCEPT_TYPE, { (protocol_version, writeable), (temporary_contract_id, writeable), (accept_collateral, writeable), @@ -535,7 +501,7 @@ pub struct SignDlc { pub funding_signatures: FundingSignatures, } -impl_dlc_writeable!(SignDlc, { +impl_dlc_writeable!(SignDlc, SIGN_TYPE, { (protocol_version, writeable), (contract_id, writeable), (cet_adaptor_signatures, writeable), @@ -576,7 +542,7 @@ pub struct CloseDlc { pub funding_signatures: FundingSignatures, } -impl_dlc_writeable!(CloseDlc, { +impl_dlc_writeable!(CloseDlc, CLOSE_TYPE, { (protocol_version, writeable), (contract_id, writeable), (close_signature, writeable), diff --git a/dlc-messages/src/ser_macros.rs b/dlc-messages/src/ser_macros.rs index 2c883c61..6ee0b3aa 100644 --- a/dlc-messages/src/ser_macros.rs +++ b/dlc-messages/src/ser_macros.rs @@ -100,6 +100,34 @@ macro_rules! impl_dlc_writeable { } } }; + // Version with type_id - writes/reads type_id as first field + ($st:ident, $type_const:ident, {$(($field: ident, $fieldty: tt)), *} ) => { + impl Writeable for $st { + fn write(&self, w: &mut W) -> Result<(), ::lightning::io::Error> { + // Write type_id first + $type_const.write(w)?; + $( + field_write!(w, self.$field, $fieldty); + )* + Ok(()) + } + } + + impl Readable for $st { + fn read(r: &mut R) -> Result { + // Read and verify type_id first + let type_id: u16 = Readable::read(r)?; + if type_id != $type_const { + return Err(DecodeError::UnknownRequiredFeature); + } + Ok(Self { + $( + $field: field_read!(r, $fieldty), + )* + }) + } + } + }; } /// Implements the [`lightning::util::ser::Writeable`] trait for a struct external diff --git a/dlc-messages/src/types.rs b/dlc-messages/src/types.rs new file mode 100644 index 00000000..469d8620 --- /dev/null +++ b/dlc-messages/src/types.rs @@ -0,0 +1,154 @@ +//! Type definitions and constants for DLC messages + +use lightning::ln::wire::Type; + +// Define all type constants and implement Type trait using macro +macro_rules! impl_type { + ($const_name: ident, $type_name: ident, $type_val: expr) => { + /// The type prefix for a message. + pub const $const_name: u16 = $type_val; + + impl Type for $type_name { + fn type_id(&self) -> u16 { + $const_name + } + } + }; +} + +// Re-export the types that will get impl_type +pub use crate::{AcceptDlc, CloseDlc, OfferDlc, SignDlc}; + +pub use crate::channel::{ + AcceptChannel, CollaborativeCloseOffer, OfferChannel, Reject, RenewAccept, RenewConfirm, + RenewFinalize, RenewOffer, RenewRevoke, SettleAccept, SettleConfirm, SettleFinalize, + SettleOffer, SignChannel, +}; + +// DLC message types +impl_type!(OFFER_TYPE, OfferDlc, 42778); +impl_type!(ACCEPT_TYPE, AcceptDlc, 42780); +impl_type!(SIGN_TYPE, SignDlc, 42782); +impl_type!(CLOSE_TYPE, CloseDlc, 42784); + +// Channel message types +impl_type!(OFFER_CHANNEL_TYPE, OfferChannel, 43000); +impl_type!(ACCEPT_CHANNEL_TYPE, AcceptChannel, 43002); +impl_type!(SIGN_CHANNEL_TYPE, SignChannel, 43004); +impl_type!(SETTLE_CHANNEL_OFFER_TYPE, SettleOffer, 43006); +impl_type!(SETTLE_CHANNEL_ACCEPT_TYPE, SettleAccept, 43008); +impl_type!(SETTLE_CHANNEL_CONFIRM_TYPE, SettleConfirm, 43010); +impl_type!(SETTLE_CHANNEL_FINALIZE_TYPE, SettleFinalize, 43012); +impl_type!(RENEW_CHANNEL_OFFER_TYPE, RenewOffer, 43014); +impl_type!(RENEW_CHANNEL_ACCEPT_TYPE, RenewAccept, 43016); +impl_type!(RENEW_CHANNEL_CONFIRM_TYPE, RenewConfirm, 43018); +impl_type!(RENEW_CHANNEL_FINALIZE_TYPE, RenewFinalize, 43020); +impl_type!(RENEW_CHANNEL_REVOKE_TYPE, RenewRevoke, 43026); +impl_type!( + COLLABORATIVE_CLOSE_OFFER_TYPE, + CollaborativeCloseOffer, + 43022 +); +impl_type!(REJECT, Reject, 43024); + +#[cfg(test)] +mod tests { + use super::*; + use crate::contract_msgs::{ + ContractDescriptor, ContractInfo, ContractInfoInner, EnumeratedContractDescriptor, + }; + use crate::oracle_msgs::{OracleEvent, OracleInfo}; + use crate::OfferDlc; + use bitcoin::Amount; + use bitcoin::ScriptBuf; + use lightning::util::ser::{Readable, Writeable}; + use secp256k1_zkp::{rand, SECP256K1}; + + fn xonly_pubkey() -> secp256k1_zkp::XOnlyPublicKey { + secp256k1_zkp::Keypair::new(SECP256K1, &mut rand::thread_rng()) + .x_only_public_key() + .0 + } + + fn pubkey() -> secp256k1_zkp::PublicKey { + secp256k1_zkp::Keypair::new(SECP256K1, &mut rand::thread_rng()).public_key() + } + + #[test] + fn test_type_id_serialization() { + // Create a minimal OfferDlc for testing + let offer = OfferDlc { + protocol_version: 1, + contract_flags: 0, + chain_hash: [0u8; 32], + temporary_contract_id: [1u8; 32], + contract_info: ContractInfo::SingleContractInfo( + crate::contract_msgs::SingleContractInfo { + total_collateral: Amount::from_sat(100000), + contract_info: ContractInfoInner { + contract_descriptor: ContractDescriptor::EnumeratedContractDescriptor( + EnumeratedContractDescriptor { payouts: vec![] }, + ), + oracle_info: OracleInfo::Single(crate::oracle_msgs::SingleOracleInfo { + oracle_announcement: crate::oracle_msgs::OracleAnnouncement { + announcement_signature: + secp256k1_zkp::schnorr::Signature::from_slice(&[0u8; 64]) + .unwrap(), + oracle_public_key: xonly_pubkey(), + oracle_event: OracleEvent { + oracle_nonces: vec![xonly_pubkey()], + event_maturity_epoch: 1, + event_id: "oracle".to_string(), + event_descriptor: + crate::oracle_msgs::EventDescriptor::EnumEvent( + crate::oracle_msgs::EnumEventDescriptor { + outcomes: vec!["1".to_string(), "2".to_string()], + }, + ), + }, + }, + }), + }, + }, + ), + funding_pubkey: pubkey(), + payout_spk: ScriptBuf::new(), + payout_serial_id: 0, + offer_collateral: Amount::from_sat(50000), + funding_inputs: vec![], + change_spk: ScriptBuf::new(), + change_serial_id: 1, + fund_output_serial_id: 2, + fee_rate_per_vb: 1, + cet_locktime: 100, + refund_locktime: 200, + }; + + // Serialize the offer + let mut serialized = Vec::new(); + offer.write(&mut serialized).unwrap(); + + // Check that the first 2 bytes are the type_id + assert_eq!(&serialized[0..2], &OFFER_TYPE.to_be_bytes()); + + // Deserialize and check we get the same offer back + let deserialized = OfferDlc::read(&mut &serialized[..]).unwrap(); + assert_eq!(offer.protocol_version, deserialized.protocol_version); + assert_eq!( + offer.temporary_contract_id, + deserialized.temporary_contract_id + ); + } + + #[test] + fn test_wrong_type_id_fails() { + // Create a buffer with wrong type_id + let mut bad_data = Vec::new(); + bad_data.extend_from_slice(&9999u16.to_be_bytes()); // Wrong type_id + bad_data.extend_from_slice(&[0u8; 100]); // Some dummy data + + // Should fail to deserialize + let result = OfferDlc::read(&mut &bad_data[..]); + assert!(result.is_err()); + } +} From 4504635a855afb25e3dd43fbb323691ec32738cd Mon Sep 17 00:00:00 2001 From: bennyhodl Date: Mon, 25 Aug 2025 23:22:57 -0400 Subject: [PATCH 3/4] release: 1.0.0 with script --- .gitignore | 2 + release.js | 575 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 577 insertions(+) create mode 100644 release.js diff --git a/.gitignore b/.gitignore index 1a0cff1c..dfc5e33d 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ /ddk/.env .env + +/releases \ No newline at end of file diff --git a/release.js b/release.js new file mode 100644 index 00000000..14701dee --- /dev/null +++ b/release.js @@ -0,0 +1,575 @@ +#!/usr/bin/env node + +const { execSync } = require("child_process"); +const fs = require("fs"); +const path = require("path"); + +// Parse command line arguments +const args = process.argv.slice(2); +const dryRun = args.includes("--dry"); +const versionArg = args.find((arg) => !arg.startsWith("--")); + +if (!versionArg) { + console.error("Usage: node release.js [--dry]"); + console.error("Example: node release.js 0.22.0"); + console.error(" node release.js 0.22.0 --dry"); + process.exit(1); +} + +const version = versionArg; + +// Validate version format +if (!/^\d+\.\d+\.\d+(-.*)?$/.test(version)) { + console.error( + "Invalid version format. Use semantic versioning (e.g., 1.2.3 or 1.2.3-beta.1)" + ); + process.exit(1); +} + +if (dryRun) { + console.log("šŸ” DRY RUN MODE - No changes will be committed or published\n"); +} + +// Helper function to run commands +function run(command, options = {}) { + try { + if (options.dryRun && dryRun) { + console.log(` [DRY RUN] Would execute: ${command}`); + return ""; + } + return execSync(command, { encoding: "utf8", ...options }).trim(); + } catch (error) { + if (!options.allowFailure) { + console.error(`Command failed: ${command}`); + console.error(error.message); + process.exit(1); + } + return null; + } +} + +// Helper function to update version in Cargo.toml +function updateCargoVersion(cratePath, newVersion, isDryRun = false) { + const cargoPath = path.join(cratePath, "Cargo.toml"); + let content = fs.readFileSync(cargoPath, "utf8"); + const originalContent = content; + + // Update package version + content = content.replace(/^version = ".*"$/m, `version = "${newVersion}"`); + + // Update workspace dependencies to use the new version + const crateNames = [ + "ddk", + "ddk-dlc", + "ddk-messages", + "ddk-trie", + "ddk-manager", + "kormir", + "payouts", + "ddk-node", + ]; + + crateNames.forEach((crateName) => { + // Update path dependencies with version + const pathDepRegex = new RegExp( + `^${crateName} = \\{ version = "[^"]*"(.*path = "\\.\\./[^"]*".*)\\}$`, + "gm" + ); + content = content.replace( + pathDepRegex, + `${crateName} = { version = "${newVersion}"$1}` + ); + + // Also handle the ddk-* prefixed versions + if (crateName.startsWith("ddk-")) { + const regex = new RegExp( + `^${crateName.replace( + "ddk-", + "ddk-" + )} = \\{ version = "[^"]*"(.*path = "\\.\\./[^"]*".*)\\}$`, + "gm" + ); + content = content.replace( + regex, + `${crateName} = { version = "${newVersion}"$1}` + ); + } + }); + + if (isDryRun) { + if (content !== originalContent) { + console.log(` [DRY RUN] Would update ${cargoPath}`); + // Show what would change + const oldVersion = originalContent.match(/^version = "(.*)"$/m)?.[1]; + if (oldVersion && oldVersion !== newVersion) { + console.log(` Version: ${oldVersion} → ${newVersion}`); + } + } + } else { + fs.writeFileSync(cargoPath, content); + } +} + +// Check git status +function checkGitStatus() { + console.log("šŸ“‹ Checking git status..."); + + const status = run("git status --porcelain"); + if (status && !dryRun) { + console.error( + "āŒ Git working directory is not clean. Please commit or stash your changes." + ); + console.error("Uncommitted changes:"); + console.error(status); + process.exit(1); + } else if (status && dryRun) { + console.warn("āš ļø Git working directory is not clean (ignored in dry run)"); + } + + const branch = run("git rev-parse --abbrev-ref HEAD"); + console.log(`āœ… Git is clean on branch: ${branch}`); + + // Fetch latest from origin + console.log("šŸ”„ Fetching latest from origin..."); + run("git fetch origin master"); + + // Check if we're behind origin/master + const behind = run("git rev-list HEAD..origin/master --count"); + if (behind !== "0") { + if (!dryRun) { + console.error( + `āŒ Branch is ${behind} commits behind origin/master. Please pull latest changes.` + ); + process.exit(1); + } else { + console.warn( + `āš ļø Branch is ${behind} commits behind origin/master (ignored in dry run)` + ); + } + } else { + console.log("āœ… Branch is up to date with origin/master"); + } +} + +// Check GitHub Actions status +async function checkGitHubActions() { + console.log("šŸ” Checking GitHub Actions status..."); + + try { + // Get the repository info from git remote + const remoteUrl = run("git remote get-url origin"); + const match = remoteUrl.match(/github\.com[:/]([^/]+)\/([^.]+)/); + + if (!match) { + console.warn("āš ļø Could not parse GitHub repository from remote URL"); + return; + } + + const [, owner, repo] = match; + console.log(`Repository: ${owner}/${repo}`); + + // Check if gh CLI is available + const ghVersion = run("gh --version", { allowFailure: true }); + if (!ghVersion) { + console.warn("āš ļø GitHub CLI (gh) not found. Skipping workflow check."); + console.warn(" Install with: brew install gh"); + return; + } + + // Get the latest workflow run on master + const workflowRuns = run( + `gh run list --branch master --limit 1 --json status,conclusion,headSha` + ); + const runs = JSON.parse(workflowRuns); + + if (runs.length === 0) { + console.warn("āš ļø No workflow runs found on master branch"); + return; + } + + const latestRun = runs[0]; + console.log(`Latest workflow SHA: ${latestRun.headSha.substring(0, 7)}`); + + if ( + latestRun.status === "completed" && + latestRun.conclusion === "success" + ) { + console.log("āœ… Latest GitHub Actions workflow succeeded"); + } else if (latestRun.status === "in_progress") { + if (!dryRun) { + console.error( + "āŒ GitHub Actions workflow is still in progress. Please wait for it to complete." + ); + process.exit(1); + } else { + console.warn( + "āš ļø GitHub Actions workflow is still in progress (ignored in dry run)" + ); + } + } else { + if (!dryRun) { + console.error( + `āŒ Latest GitHub Actions workflow failed with status: ${latestRun.conclusion}` + ); + process.exit(1); + } else { + console.warn( + `āš ļø Latest GitHub Actions workflow failed with status: ${latestRun.conclusion} (ignored in dry run)` + ); + } + } + } catch (error) { + console.warn("āš ļø Could not check GitHub Actions status:", error.message); + } +} + +// Generate release notes +function generateReleaseNotes() { + console.log("\nšŸ“ Generating release notes..."); + + const releaseDir = "./releases"; + const releaseFile = path.join(releaseDir, `${version}-RELEASE.md`); + + // Check if release notes already exist + if (fs.existsSync(releaseFile)) { + console.log(`āœ… Release notes already exist at ${releaseFile}`); + return fs.readFileSync(releaseFile, "utf8"); + } + + // Create releases directory if it doesn't exist + if (!fs.existsSync(releaseDir)) { + if (!dryRun) { + fs.mkdirSync(releaseDir, { recursive: true }); + } else { + console.log(" [DRY RUN] Would create releases directory"); + } + } + + // Get the latest tag + const latestTag = + run("git describe --tags --abbrev=0", { allowFailure: true }) || ""; + console.log(` Latest tag: ${latestTag || "none"}`); + + // Get commit history since last tag + let commits = ""; + if (latestTag) { + commits = run(`git log ${latestTag}..HEAD --oneline`); + } else { + // If no tags, get last 20 commits + commits = run("git log --oneline -20"); + } + + // Get detailed git diff + let gitDiff = ""; + if (latestTag) { + gitDiff = run(`git diff ${latestTag}..HEAD --stat`); + } + + // Prepare the prompt for Claude + const prompt = `Generate professional release notes for version ${version} of the DLC DevKit (DDK) Rust workspace. + +Here are the commits since the last release (${latestTag || "initial release"}): +${commits} + +File changes summary: +${gitDiff} + +The workspace contains these crates that are all being released with version ${version}: +- ddk-trie: Trie data structure for DLC +- ddk-messages: DLC message protocol implementation +- kormir: Oracle implementation +- ddk-dlc: Core DLC functionality +- ddk-manager: DLC management and coordination +- ddk: Main DLC DevKit library +- payouts: Payout calculation utilities +- ddk-node: DLC node implementation + +Please create release notes with: +1. A brief summary of the release +2. Breaking changes (if any, look for BREAKING in commits or major API changes) +3. New features (commits starting with feat:) +4. Bug fixes (commits starting with fix:) +5. Other notable changes +6. Installation instructions showing how to add ddk = "${version}" to Cargo.toml + +Format as clean markdown suitable for a GitHub release. Be concise but informative.`; + + if (dryRun) { + console.log(" [DRY RUN] Would generate release notes using Claude"); + console.log(" Prompt preview:", prompt.substring(0, 200) + "..."); + + // Return a dummy content for dry run + const dummyContent = `# Release v${version}\n\n[DRY RUN - Release notes would be generated here using Claude]\n`; + return dummyContent; + } + + try { + // Use Claude to generate release notes + console.log(" Using Claude to generate release notes..."); + + // Write prompt to temporary file to avoid shell escaping issues + const tempPromptFile = `/tmp/release-prompt-${Date.now()}.txt`; + fs.writeFileSync(tempPromptFile, prompt); + + // Call Claude with the prompt + const claudeOutput = run(`claude -p "$(cat ${tempPromptFile})"`, { + allowFailure: false, + timeout: 60000, // 60 second timeout for Claude + }); + + // Clean up temp file + fs.unlinkSync(tempPromptFile); + + // Write release notes + fs.writeFileSync(releaseFile, claudeOutput); + console.log(`āœ… Release notes written to ${releaseFile}`); + + return claudeOutput; + } catch (error) { + console.error( + "āŒ Failed to generate release notes with Claude:", + error.message + ); + console.log(" Falling back to basic template..."); + + // Fallback to basic template if Claude fails + let content = `# Release v${version}\n\n`; + content += `Released: ${new Date().toISOString().split("T")[0]}\n\n`; + content += `## šŸ“¦ Published Crates\n\n`; + content += `All crates updated to version ${version}:\n\n`; + content += `- \`ddk-trie\`\n`; + content += `- \`ddk-messages\`\n`; + content += `- \`kormir\`\n`; + content += `- \`ddk-dlc\`\n`; + content += `- \`ddk-manager\`\n`; + content += `- \`ddk\`\n`; + content += `- \`payouts\`\n`; + content += `- \`ddk-node\`\n\n`; + content += `## šŸ“„ Installation\n\n`; + content += `\`\`\`toml\n`; + content += `# Add to your Cargo.toml\n`; + content += `ddk = "${version}"\n`; + content += `\`\`\`\n\n`; + content += `## Commits\n\n`; + content += `\`\`\`\n${commits}\n\`\`\`\n`; + + if (!dryRun) { + fs.writeFileSync(releaseFile, content); + console.log(`āœ… Basic release notes written to ${releaseFile}`); + } + + return content; + } +} + +// Create GitHub release +async function createGitHubRelease(releaseNotes) { + console.log("\nšŸš€ Creating GitHub release..."); + + // Check if gh CLI is available + const ghVersion = run("gh --version", { allowFailure: true }); + if (!ghVersion) { + console.warn( + "āš ļø GitHub CLI (gh) not found. Skipping GitHub release creation." + ); + console.warn(" Install with: brew install gh"); + return; + } + + if (dryRun) { + console.log(` [DRY RUN] Would create GitHub release for tag v${version}`); + return; + } + + try { + // Create the release using gh CLI + const releaseCmd = `gh release create v${version} --title "v${version}" --notes "${releaseNotes + .replace(/"/g, '\\"') + .replace(/\n/g, "\\n")}"`; + + // For very long release notes, use a file instead + const tempFile = `/tmp/release-notes-${version}.md`; + fs.writeFileSync(tempFile, releaseNotes); + + run( + `gh release create v${version} --title "v${version}" --notes-file ${tempFile}` + ); + + // Clean up temp file + fs.unlinkSync(tempFile); + + console.log(`āœ… GitHub release v${version} created successfully`); + console.log( + ` View at: https://github.com///releases/tag/v${version}` + ); + } catch (error) { + console.warn(`āš ļø Failed to create GitHub release: ${error.message}`); + console.log( + " You can create it manually at: https://github.com///releases/new" + ); + } +} + +// Define crate dependencies and publish order +const crateOrder = [ + // Level 0: No internal dependencies + { name: "ddk-dlc", path: "./dlc", package: "ddk-dlc" }, + + // Level 1: Depends on level 0 + { name: "ddk-trie", path: "./dlc-trie", package: "ddk-trie" }, + { name: "ddk-messages", path: "./dlc-messages", package: "ddk-messages" }, + { name: "kormir", path: "./kormir", package: "kormir" }, + + // Level 2: Depends on levels 0 and 1 + { name: "ddk-manager", path: "./ddk-manager", package: "ddk-manager" }, + + // Level 3: Depends on level 2 + { name: "ddk", path: "./ddk", package: "ddk" }, + { name: "payouts", path: "./payouts", package: "payouts" }, + + // Level 4: Depends on level 3 + { name: "ddk-node", path: "./ddk-node", package: "ddk-node" }, +]; + +// Main release process +async function release() { + console.log( + `šŸš€ Starting release process for version ${version}${ + dryRun ? " (DRY RUN)" : "" + }\n` + ); + + // Step 1: Check git status + checkGitStatus(); + + // Step 2: Check GitHub Actions + await checkGitHubActions(); + + // Step 3: Generate release notes (do this before updating versions) + const releaseNotes = generateReleaseNotes(); + + // Step 4: Update versions + console.log( + `\nšŸ“ ${dryRun ? "Checking" : "Updating"} versions to ${version}...` + ); + + for (const crate of crateOrder) { + console.log(` ${dryRun ? "Checking" : "Updating"} ${crate.name}...`); + updateCargoVersion(crate.path, version, dryRun); + } + + console.log(`āœ… All versions ${dryRun ? "checked" : "updated"}`); + + // Step 5: Build all crates to verify + console.log("\nšŸ”Ø Building all crates to verify changes..."); + if (!dryRun) { + run("cargo build --all"); + console.log("āœ… Build successful"); + } else { + console.log(" [DRY RUN] Would run: cargo build --all"); + } + + // Step 6: Commit version changes + console.log("\nšŸ“ Committing version changes..."); + run("git add .", { dryRun: true }); + run(`git commit -m "chore: release v${version}"`, { dryRun: true }); + if (!dryRun) { + console.log("āœ… Changes committed"); + } + + // Step 7: Create git tag + console.log("\nšŸ·ļø Creating git tag..."); + run(`git tag -a v${version} -m "Release v${version}"`, { dryRun: true }); + if (!dryRun) { + console.log(`āœ… Tag v${version} created`); + } + + // Step 8: Publish crates in order + console.log("\nšŸ“¦ Publishing crates to crates.io..."); + console.log(" Publishing in dependency order:\n"); + + for (const crate of crateOrder) { + console.log(`šŸ“¤ ${dryRun ? "Checking" : "Publishing"} ${crate.package}...`); + + if (dryRun) { + // In dry run, just do cargo publish --dry-run + console.log( + ` [DRY RUN] Would publish ${crate.package} from ${crate.path}` + ); + try { + run(`cargo publish --dry-run`, { cwd: crate.path }); + console.log(` āœ… ${crate.package} is ready to publish`); + } catch (error) { + console.error(` āŒ ${crate.package} would fail to publish`); + console.error(` ${error.message.split("\n")[0]}`); + } + } else { + try { + // Dry run first + run(`cargo publish --dry-run`, { cwd: crate.path }); + + // Actual publish + const publishOutput = run(`cargo publish`, { + cwd: crate.path, + allowFailure: true, + }); + + if (publishOutput === null) { + // Check if it's already published + const checkOutput = run(`cargo search ${crate.package} --limit 1`); + if (checkOutput.includes(`${crate.package} = "${version}"`)) { + console.log(` āœ… ${crate.package} v${version} already published`); + } else { + console.error(` āŒ Failed to publish ${crate.package}`); + console.error(" You may need to retry or publish manually"); + continue; + } + } else { + console.log(` āœ… ${crate.package} published successfully`); + } + + // Wait a bit between publishes to allow crates.io to update + if (crate !== crateOrder[crateOrder.length - 1]) { + console.log(" ā³ Waiting 30 seconds for crates.io to update..."); + await new Promise((resolve) => setTimeout(resolve, 30000)); + } + } catch (error) { + console.error( + ` āŒ Error publishing ${crate.package}: ${error.message}` + ); + console.error(" You may need to retry or publish manually"); + } + } + } + + // Step 9: Push changes and create GitHub release + if (!dryRun) { + console.log("\nšŸ“¤ Pushing changes to origin..."); + run("git push origin master --tags"); + console.log("āœ… Changes and tags pushed"); + + // Create GitHub release + await createGitHubRelease(releaseNotes); + } + + console.log(`\nšŸŽ‰ Release ${dryRun ? "validation" : "process"} complete!`); + + if (dryRun) { + console.log( + "\nšŸ“‹ Dry run complete. To perform the actual release, run without --dry flag:" + ); + console.log(` node release.js ${version}`); + } else { + console.log("\nšŸ“‹ Release completed successfully!"); + console.log(" - Version tags created and pushed"); + console.log(" - GitHub release created"); + console.log(" - All crates published to crates.io"); + console.log(` - Release notes saved in releases/${version}-RELEASE.md`); + } +} + +// Run the release process +release().catch((error) => { + console.error("āŒ Release failed:", error); + process.exit(1); +}); From 76754ac2d3c82f11eb8a0dbb7c5e740cfb3908c6 Mon Sep 17 00:00:00 2001 From: bennyhodl Date: Mon, 25 Aug 2025 23:46:26 -0400 Subject: [PATCH 4/4] clippy --- ddk-node/src/lib.rs | 2 +- ddk/examples/lightning.rs | 2 +- ddk/examples/nostr.rs | 2 +- ddk/examples/postgres.rs | 2 +- ddk/tests/nostr.rs | 1 + ddk/tests/test_util.rs | 1 + 6 files changed, 6 insertions(+), 4 deletions(-) diff --git a/ddk-node/src/lib.rs b/ddk-node/src/lib.rs index 2f0531dd..345a21df 100644 --- a/ddk-node/src/lib.rs +++ b/ddk-node/src/lib.rs @@ -75,7 +75,7 @@ impl DdkNode { let oracle = Arc::new(KormirOracleClient::new(&opts.oracle_host, None).await?); let mut builder = Builder::new(); - builder.set_seed_bytes(SeedConfig::Bytes(seed_bytes)); + builder.set_seed_bytes(SeedConfig::Bytes(seed_bytes))?; builder.set_esplora_host(opts.esplora_host); builder.set_network(network); builder.set_transport(transport.clone()); diff --git a/ddk/examples/lightning.rs b/ddk/examples/lightning.rs index c676cb8b..9bb6095c 100644 --- a/ddk/examples/lightning.rs +++ b/ddk/examples/lightning.rs @@ -14,7 +14,7 @@ async fn main() -> Result<(), ddk::error::Error> { let oracle_client = Arc::new(KormirOracleClient::new("host", None).await?); let mut builder = Builder::new(); - builder.set_seed_bytes(SeedConfig::Random); + builder.set_seed_bytes(SeedConfig::Random)?; builder.set_transport(transport.clone()); builder.set_storage(storage.clone()); builder.set_oracle(oracle_client.clone()); diff --git a/ddk/examples/nostr.rs b/ddk/examples/nostr.rs index f361fc6c..88031b80 100644 --- a/ddk/examples/nostr.rs +++ b/ddk/examples/nostr.rs @@ -21,7 +21,7 @@ async fn main() -> Result<(), ddk::error::Error> { let oracle_client = Arc::new(MemoryOracle::default()); let mut builder = Builder::new(); - builder.set_seed_bytes(SeedConfig::Bytes(seed_bytes)); + builder.set_seed_bytes(SeedConfig::Bytes(seed_bytes))?; builder.set_transport(transport.clone()); builder.set_storage(storage.clone()); builder.set_oracle(oracle_client.clone()); diff --git a/ddk/examples/postgres.rs b/ddk/examples/postgres.rs index 06fc21bc..b3f9f24b 100644 --- a/ddk/examples/postgres.rs +++ b/ddk/examples/postgres.rs @@ -27,7 +27,7 @@ async fn main() -> Result<(), ddk::error::Error> { .unwrap(); let mut builder = Builder::new(); - builder.set_seed_bytes(SeedConfig::Bytes(seed_bytes)); + builder.set_seed_bytes(SeedConfig::Bytes(seed_bytes))?; builder.set_transport(transport.clone()); builder.set_storage(storage.clone()); builder.set_oracle(oracle_client.clone()); diff --git a/ddk/tests/nostr.rs b/ddk/tests/nostr.rs index 1c06c5f2..b9380df3 100644 --- a/ddk/tests/nostr.rs +++ b/ddk/tests/nostr.rs @@ -33,6 +33,7 @@ mod nostr_test { let ddk: NostrDlcDevKit = Builder::new() .set_network(Network::Regtest) .set_seed_bytes(SeedConfig::Bytes(seed)) + .unwrap() .set_esplora_host(esplora_host) .set_name(name) .set_oracle(oracle) diff --git a/ddk/tests/test_util.rs b/ddk/tests/test_util.rs index d0916e7c..78416768 100644 --- a/ddk/tests/test_util.rs +++ b/ddk/tests/test_util.rs @@ -122,6 +122,7 @@ impl TestSuite { let ddk: TestDlcDevKit = Builder::new() .set_network(Network::Regtest) .set_seed_bytes(SeedConfig::Bytes(seed)) + .unwrap() .set_esplora_host(esplora_host) .set_name(name) .set_oracle(oracle)