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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ All notable Labcoat CLI and test-harness changes are documented here. Releases u
### Added

- add `labcoat generate web`: deterministic TypeScript browser read artifacts (network manifest, typed ABI descriptors, and a dependency-free fetch client for indexed height, Alkanes balances, and ABI-typed simulate) derived from `labcoat.lock` and built ABIs with no network access; see docs/GENERATE-WEB.md
- add `labcoat exchange-plan --request <file>`: build the plan from a version-1 exchange request file (canonical-decimal-string JSON) instead of positional flags
- add `exchange.ts` to `labcoat generate web`: a browser-side exchange request builder that validates and serializes the request file and renders the `exchange-plan` → `wallet sign-psbt` → `exchange-settle` ceremony commands; no signing or transaction construction in the browser

## [0.2.0](https://github.com/jonatns/labcoat/compare/cli-v0.1.0...cli-v0.2.0) - 2026-07-31

Expand Down
15 changes: 8 additions & 7 deletions apps/web/src/content/docs/docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -378,17 +378,18 @@ Arguments and options:
Build an owner-partitioned exchange plan and unsigned PSBT

```text
exchange-plan --seller-address <SELLER_ADDRESS> --buyer-address <BUYER_ADDRESS> --plan-out <PLAN_OUT> --psbt-out <PSBT_OUT> <OFFERED> <OFFERED_AMOUNT> <PAYMENT> <PAYMENT_AMOUNT>
exchange-plan [OPTIONS] --plan-out <PLAN_OUT> --psbt-out <PSBT_OUT> [OFFERED] [OFFERED_AMOUNT] [PAYMENT] [PAYMENT_AMOUNT]
```

Arguments and options:

- `offered` (required)
- `offered_amount` (required)
- `payment` (required)
- `payment_amount` (required)
- `seller_address` (required)
- `buyer_address` (required)
- `offered` (optional): Asset sold by the seller: labcoat.lock name or block:tx id
- `offered_amount` (optional): Complete offered quantity delivered to the buyer
- `payment` (optional): Asset paid by the buyer: labcoat.lock name or block:tx id
- `payment_amount` (optional): Complete payment quantity delivered to the seller
- `request` (optional): Exchange request file (version 1 JSON, e.g. from a generated web client); replaces the positional assets and address options
- `seller_address` (optional)
- `buyer_address` (optional)
- `plan_out` (required)
- `psbt_out` (required)

Expand Down
28 changes: 17 additions & 11 deletions apps/web/src/generated/cli-reference.json
Original file line number Diff line number Diff line change
Expand Up @@ -608,40 +608,46 @@
{
"arguments": [
{
"description": "",
"description": "Asset sold by the seller: labcoat.lock name or block:tx id",
"id": "offered",
"possibleValues": [],
"required": true
"required": false
},
{
"description": "",
"description": "Complete offered quantity delivered to the buyer",
"id": "offered_amount",
"possibleValues": [],
"required": true
"required": false
},
{
"description": "",
"description": "Asset paid by the buyer: labcoat.lock name or block:tx id",
"id": "payment",
"possibleValues": [],
"required": true
"required": false
},
{
"description": "",
"description": "Complete payment quantity delivered to the seller",
"id": "payment_amount",
"possibleValues": [],
"required": true
"required": false
},
{
"description": "Exchange request file (version 1 JSON, e.g. from a generated web client); replaces the positional assets and address options",
"id": "request",
"possibleValues": [],
"required": false
},
{
"description": "",
"id": "seller_address",
"possibleValues": [],
"required": true
"required": false
},
{
"description": "",
"id": "buyer_address",
"possibleValues": [],
"required": true
"required": false
},
{
"description": "",
Expand All @@ -660,7 +666,7 @@
"name": "exchange-plan",
"path": "labcoat exchange-plan",
"subcommands": [],
"usage": "exchange-plan --seller-address <SELLER_ADDRESS> --buyer-address <BUYER_ADDRESS> --plan-out <PLAN_OUT> --psbt-out <PSBT_OUT> <OFFERED> <OFFERED_AMOUNT> <PAYMENT> <PAYMENT_AMOUNT>"
"usage": "exchange-plan [OPTIONS] --plan-out <PLAN_OUT> --psbt-out <PSBT_OUT> [OFFERED] [OFFERED_AMOUNT] [PAYMENT] [PAYMENT_AMOUNT]"
},
{
"arguments": [
Expand Down
59 changes: 44 additions & 15 deletions crates/labcoat-cli/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1109,32 +1109,61 @@ fn exchange_asset(
}

#[allow(clippy::too_many_arguments)]
/// Where an `exchange-plan` request comes from: the positional/flag form,
/// or a version-1 exchange request file (`--request`).
pub enum ExchangeRequestSource {
Flags {
offered: String,
offered_amount: u64,
payment: String,
payment_amount: u64,
seller_address: String,
buyer_address: String,
},
File {
path: String,
},
}

pub async fn exchange_plan(
ctx: &Ctx,
offered: &str,
offered_amount: u64,
payment: &str,
payment_amount: u64,
seller_address: &str,
buyer_address: &str,
source: ExchangeRequestSource,
plan_out: &str,
psbt_out: &str,
) -> (&'static str, CmdResult) {
let res = async {
let offered = exchange_asset(resolve(&ctx.config, offered)?, "offered asset")?;
let payment = exchange_asset(resolve(&ctx.config, payment)?, "payment asset")?;
let mut provider = ctx.wallet_provider().await?;
let plan = labcoat_core::atomic_exchange::build_exchange_plan(
&mut provider,
&ctx.config,
labcoat_core::atomic_exchange::AtomicExchangeRequest {
let request = match source {
ExchangeRequestSource::Flags {
offered,
offered_amount,
payment,
payment_amount,
seller_address: seller_address.to_string(),
buyer_address: buyer_address.to_string(),
seller_address,
buyer_address,
} => labcoat_core::atomic_exchange::AtomicExchangeRequest {
offered: exchange_asset(resolve(&ctx.config, &offered)?, "offered asset")?,
offered_amount,
payment: exchange_asset(resolve(&ctx.config, &payment)?, "payment asset")?,
payment_amount,
seller_address,
buyer_address,
},
ExchangeRequestSource::File { path } => {
let json = std::fs::read_to_string(&path).map_err(|e| {
labcoat_core::LabcoatError::new(
"CONFIG_INVALID",
format!("cannot read {path}: {e}"),
"pass an exchange request file produced by your application",
)
})?;
labcoat_core::atomic_exchange::AtomicExchangeRequest::from_request_file(&json)?
}
};
let mut provider = ctx.wallet_provider().await?;
let plan = labcoat_core::atomic_exchange::build_exchange_plan(
&mut provider,
&ctx.config,
request,
)
.await?;
let plan_json = serde_json::to_string_pretty(&plan).expect("serializable exchange plan");
Expand Down
52 changes: 33 additions & 19 deletions crates/labcoat-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,14 +222,26 @@ enum Commands {
},
/// Build an owner-partitioned exchange plan and unsigned PSBT.
ExchangePlan {
offered: String,
offered_amount: u64,
payment: String,
payment_amount: u64,
#[arg(long)]
seller_address: String,
/// Asset sold by the seller: labcoat.lock name or block:tx id
#[arg(required_unless_present = "request", conflicts_with = "request")]
offered: Option<String>,
/// Complete offered quantity delivered to the buyer
#[arg(required_unless_present = "request", conflicts_with = "request")]
offered_amount: Option<u64>,
/// Asset paid by the buyer: labcoat.lock name or block:tx id
#[arg(required_unless_present = "request", conflicts_with = "request")]
payment: Option<String>,
/// Complete payment quantity delivered to the seller
#[arg(required_unless_present = "request", conflicts_with = "request")]
payment_amount: Option<u64>,
/// Exchange request file (version 1 JSON, e.g. from a generated web
/// client); replaces the positional assets and address options
#[arg(long)]
buyer_address: String,
request: Option<String>,
#[arg(long, required_unless_present = "request", conflicts_with = "request")]
seller_address: Option<String>,
#[arg(long, required_unless_present = "request", conflicts_with = "request")]
buyer_address: Option<String>,
#[arg(long)]
plan_out: String,
#[arg(long)]
Expand Down Expand Up @@ -548,24 +560,26 @@ async fn run(cli: Cli) -> i32 {
offered_amount,
payment,
payment_amount,
request,
seller_address,
buyer_address,
plan_out,
psbt_out,
} => {
let progress = output::Progress::new("Building exchange plan…", !json);
let (cmd_name, res) = contract::exchange_plan(
&ctx,
&offered,
offered_amount,
&payment,
payment_amount,
&seller_address,
&buyer_address,
&plan_out,
&psbt_out,
)
.await;
let source = match request {
Some(path) => contract::ExchangeRequestSource::File { path },
// Clap enforces required_unless_present, so the flag form is complete here.
None => contract::ExchangeRequestSource::Flags {
offered: offered.expect("clap requires offered"),
offered_amount: offered_amount.expect("clap requires offered_amount"),
payment: payment.expect("clap requires payment"),
payment_amount: payment_amount.expect("clap requires payment_amount"),
seller_address: seller_address.expect("clap requires seller_address"),
buyer_address: buyer_address.expect("clap requires buyer_address"),
},
};
let (cmd_name, res) = contract::exchange_plan(&ctx, source, &plan_out, &psbt_out).await;
progress.finish();
output::finish_contract(json, cmd_name, res, output_options)
}
Expand Down
Loading
Loading