Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ hex = "0.4.3"
itertools = "0.15.0"
musig2 = { version = "0.4.1", features = ["secp256k1"] }
serde = { version = "1.0.229", features = ["derive"] }
serde_yaml = "0.9"
thiserror = "2.0.20"
tracing = "0.1.44"
tracing-subscriber = "0.3.23"
Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,26 @@ fn render(protocol: &Protocol) -> anyhow::Result<()> {

`GraphOptions::Default` renders each transaction as a node labeled with indexed inputs/outputs and their satoshi values. Use `GraphOptions::EdgeArrows` to include port-specific arrows that highlight which output feeds each downstream input.

### Export to BitVMX Protocol Studio

`Protocol::export_studio_yaml` returns the serialized YAML. Set an output directory
to also write the graph as `<protocol-name>.yaml`.

```rust
use protocol_builder::graph::studio_yaml::StudioExportSettings;

let yaml = protocol.export_studio_yaml(
StudioExportSettings::default().with_output_dir("studio-graphs"),
)?;
```

Applications may populate the directory from their own configuration. Setting
`PROTOCOL_BUILDER_EXPORT_DIR` also supplies it to `StudioExportSettings::default()`;
in that case, calls to `Protocol::visualize` automatically write the corresponding
Studio YAML file without changing visualization behavior if export fails. If the
environment variable is unset and `with_output_dir` is not called, YAML is only
returned to the caller and no file is written.

### Auto value outputs and fee estimation

`Protocol::compute_minimum_output_values` backfills outputs marked with `AUTO_AMOUNT` or `RECOVER_AMOUNT`. `AUTO_AMOUNT` placeholders are bumped up just enough for the downstream transaction to pay its own fee estimate (1 sat/vB plus a 5% buffer), while `RECOVER_AMOUNT` placeholders scoop up any leftover value from the parent subtree so no funds are stranded.
Expand Down
51 changes: 50 additions & 1 deletion src/builder/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ use storage_backend::storage::{KeyValueStore, Storage};

use crate::{
errors::ProtocolBuilderError,
graph::graph::{GraphOptions, TransactionGraph},
graph::{
graph::{GraphOptions, TransactionGraph},
studio_yaml::StudioExportSettings,
},
scripts::ProtocolScript,
types::{
connection::{ConnectionType, InputSpec, OutputSpec},
Expand All @@ -23,6 +26,7 @@ use crate::{
};

use super::check_params::{check_empty_connection_name, check_empty_transaction_name};
use tracing::{info, warn};

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Protocol {
Expand Down Expand Up @@ -63,6 +67,15 @@ impl Protocol {
Ok(self)
}

pub fn add_external_transaction_with_txid(
&mut self,
transaction_name: &str,
txid: Txid,
) -> Result<&mut Self, ProtocolBuilderError> {
self.get_or_create_transaction_with_txid(transaction_name, txid)?;
Ok(self)
}

pub fn add_unknown_outputs(
&mut self,
transaction_name: &str,
Expand Down Expand Up @@ -586,9 +599,25 @@ impl Protocol {
}

pub fn visualize(&self, options: GraphOptions) -> Result<String, ProtocolBuilderError> {
// Export alongside visualization when PROTOCOL_BUILDER_EXPORT_DIR is set.
// Never fail visualization on an optional diagnostic export.
let export_settings = StudioExportSettings::default();
if let Some(output_dir) = export_settings.output_dir.clone() {
match self.export_studio_yaml(export_settings) {
Ok(_) => info!("studio yaml exported under {}", output_dir.display()),
Err(error) => warn!("studio yaml export failed for '{}': {:?}", self.name, error),
}
}
Ok(self.graph.visualize(options)?)
}

pub fn export_studio_yaml(
&self,
settings: StudioExportSettings,
) -> Result<String, ProtocolBuilderError> {
Ok(self.graph.export_studio_yaml(&self.name, settings)?)
}

pub(crate) fn transaction_template() -> Transaction {
Transaction {
version: transaction::Version::TWO, // Post BIP-68.
Expand Down Expand Up @@ -618,6 +647,26 @@ impl Protocol {
.clone())
}

fn get_or_create_transaction_with_txid(
&mut self,
transaction_name: &str,
txid: Txid,
) -> Result<Transaction, ProtocolBuilderError> {
check_empty_transaction_name(transaction_name)?;

if !self.graph.contains_transaction(transaction_name) {
let transaction = Protocol::transaction_template();
self.graph
.add_transaction_with_txid(transaction_name, transaction, txid)?;
};

Ok(self
.graph
.get_transaction_by_name(transaction_name)
.unwrap()
.clone())
}

fn get_dependencies(
&self,
transaction_name: &str,
Expand Down
10 changes: 10 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,16 @@ pub enum GraphError {

#[error("Output amount is less than dust limit: {0} ")]
DustOutput(String),

#[error("Failed to serialize studio YAML: {0}")]
YamlSerialization(#[from] serde_yaml::Error),

#[error("Failed to write studio YAML to {path}: {source}")]
StudioYamlIo {
path: std::path::PathBuf,
#[source]
source: std::io::Error,
},
}

#[derive(Error, Debug)]
Expand Down
Loading
Loading