Skip to content

Commit cacd1f7

Browse files
simplicity: add 'simplicity pset create' command
This should be compatible with any non-pegin non-issuance calls to elements-cli createpsbt. Co-authored-by: aider (anthropic/claude-sonnet-4-20250514) <[email protected]>
1 parent d7e3b04 commit cacd1f7

File tree

2 files changed

+175
-0
lines changed

2 files changed

+175
-0
lines changed
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
// Copyright 2025 Andrew Poelstra
2+
// SPDX-License-Identifier: CC0-1.0
3+
4+
use super::super::{Error, ErrorExt as _};
5+
use super::UpdatedPset;
6+
use crate::cmd;
7+
8+
use elements::confidential;
9+
use elements::pset::PartiallySignedTransaction;
10+
use elements::{Address, AssetId, OutPoint, Transaction, TxIn, TxOut, Txid};
11+
use serde::Deserialize;
12+
13+
use std::collections::HashMap;
14+
15+
#[derive(Deserialize)]
16+
struct InputSpec {
17+
txid: Txid,
18+
vout: u32,
19+
#[serde(default)]
20+
sequence: Option<u32>,
21+
}
22+
23+
#[derive(Deserialize)]
24+
struct FlattenedOutputSpec {
25+
address: String,
26+
asset: AssetId,
27+
#[serde(with = "elements::bitcoin::amount::serde::as_btc")]
28+
amount: elements::bitcoin::Amount,
29+
}
30+
31+
#[derive(Deserialize)]
32+
#[serde(untagged)]
33+
enum OutputSpec {
34+
Explicit {
35+
address: String,
36+
asset: AssetId,
37+
#[serde(with = "elements::bitcoin::amount::serde::as_btc")]
38+
amount: elements::bitcoin::Amount,
39+
},
40+
Map(HashMap<String, f64>),
41+
}
42+
43+
impl OutputSpec {
44+
fn flatten(self) -> Box<dyn Iterator<Item = Result<FlattenedOutputSpec, Error>>> {
45+
match self {
46+
Self::Map(map) => Box::new(map.into_iter().map(|(address, amount)| {
47+
// Use liquid bitcoin asset as default for map format
48+
let default_asset = AssetId::from_slice(&[
49+
0x49, 0x9a, 0x81, 0x85, 0x45, 0xf6, 0xba, 0xe3, 0x9f, 0xc0, 0x3b, 0x63, 0x7f,
50+
0x2a, 0x4e, 0x1e, 0x64, 0xe5, 0x90, 0xca, 0xc1, 0xbc, 0x3a, 0x6f, 0x6d, 0x71,
51+
0xaa, 0x44, 0x43, 0x65, 0x4c, 0x14,
52+
])
53+
.expect("valid asset id");
54+
55+
Ok(FlattenedOutputSpec {
56+
address,
57+
asset: default_asset,
58+
amount: elements::bitcoin::Amount::from_btc(amount)
59+
.result_context("parsing amount")?,
60+
})
61+
})),
62+
Self::Explicit {
63+
address,
64+
asset,
65+
amount,
66+
} => Box::new(
67+
Some(Ok(FlattenedOutputSpec {
68+
address,
69+
asset,
70+
amount,
71+
}))
72+
.into_iter(),
73+
),
74+
}
75+
}
76+
}
77+
78+
pub fn cmd<'a>() -> clap::App<'a, 'a> {
79+
cmd::subcommand("create", "create an empty PSET").args(&cmd::opts_networks()).args(&[
80+
cmd::arg(
81+
"inputs",
82+
"input outpoints (JSON array of objects containing txid, vout, sequence)",
83+
)
84+
.takes_value(true)
85+
.required(true),
86+
cmd::arg("outputs", "outputs (JSON array of objects containing address, asset, amount)")
87+
.takes_value(true)
88+
.required(true),
89+
])
90+
}
91+
92+
pub fn exec<'a>(matches: &clap::ArgMatches<'a>) {
93+
let inputs_json = matches.value_of("inputs").expect("inputs mandatory");
94+
let outputs_json = matches.value_of("outputs").expect("inputs mandatory");
95+
96+
match exec_inner(inputs_json, outputs_json) {
97+
Ok(info) => cmd::print_output(matches, &info),
98+
Err(e) => cmd::print_output(matches, &e),
99+
}
100+
}
101+
102+
fn exec_inner(inputs_json: &str, outputs_json: &str) -> Result<UpdatedPset, Error> {
103+
// Parse inputs JSON
104+
let input_specs: Vec<InputSpec> =
105+
serde_json::from_str(inputs_json).result_context("parsing inputs JSON")?;
106+
107+
// Parse outputs JSON - support both array and map formats
108+
let output_specs: Vec<OutputSpec> =
109+
serde_json::from_str(outputs_json).result_context("parsing outputs JSON")?;
110+
111+
// Create transaction inputs
112+
let mut inputs = Vec::new();
113+
for input_spec in &input_specs {
114+
let outpoint = OutPoint::new(input_spec.txid, input_spec.vout);
115+
let sequence = elements::Sequence(input_spec.sequence.unwrap_or(0xffffffff));
116+
117+
inputs.push(TxIn {
118+
previous_output: outpoint,
119+
script_sig: elements::Script::new(),
120+
sequence,
121+
asset_issuance: Default::default(),
122+
witness: Default::default(),
123+
is_pegin: false,
124+
});
125+
}
126+
127+
// Create transaction outputs
128+
let mut outputs = Vec::new();
129+
for output_spec in output_specs.into_iter().flat_map(OutputSpec::flatten) {
130+
let output_spec = output_spec?; // serde has crappy error messages so we defer parsing and then have to unwrap errors
131+
132+
let script_pubkey = match output_spec.address.as_str() {
133+
"fee" => elements::Script::new(),
134+
x => {
135+
let addr = x.parse::<Address>().result_context("parsing address")?;
136+
if addr.is_blinded() {
137+
return Err("confidential addresses are not yet supported")
138+
.result_context("output address");
139+
}
140+
addr.script_pubkey()
141+
}
142+
};
143+
144+
outputs.push(TxOut {
145+
asset: confidential::Asset::Explicit(output_spec.asset),
146+
value: confidential::Value::Explicit(output_spec.amount.to_sat()),
147+
nonce: elements::confidential::Nonce::Null,
148+
script_pubkey,
149+
witness: elements::TxOutWitness::empty(),
150+
});
151+
}
152+
153+
// Create the transaction
154+
let tx = Transaction {
155+
version: 2,
156+
lock_time: elements::LockTime::ZERO,
157+
input: inputs,
158+
output: outputs,
159+
};
160+
161+
// Create PSET from transaction
162+
let pset = PartiallySignedTransaction::from_tx(tx);
163+
164+
Ok(UpdatedPset {
165+
pset: pset.to_string(),
166+
updated_values: vec![
167+
// FIXME we technically update a whole slew of fields; see the implementation
168+
// of PartiallySignedTransaction::from_tx. Should we attempt to exhaustively
169+
// list them here? Or list none? Or what?
170+
],
171+
})
172+
}

src/bin/hal-simplicity/cmd/simplicity/pset/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright 2025 Andrew Poelstra
22
// SPDX-License-Identifier: CC0-1.0
33

4+
mod create;
45
mod extract;
56
mod finalize;
67
mod update_input;
@@ -16,13 +17,15 @@ struct UpdatedPset {
1617

1718
pub fn cmd<'a>() -> clap::App<'a, 'a> {
1819
cmd::subcommand_group("pset", "manipulate PSETs for spending from Simplicity programs")
20+
.subcommand(self::create::cmd())
1921
.subcommand(self::extract::cmd())
2022
.subcommand(self::finalize::cmd())
2123
.subcommand(self::update_input::cmd())
2224
}
2325

2426
pub fn exec<'a>(matches: &clap::ArgMatches<'a>) {
2527
match matches.subcommand() {
28+
("create", Some(m)) => self::create::exec(m),
2629
("extract", Some(m)) => self::extract::exec(m),
2730
("finalize", Some(m)) => self::finalize::exec(m),
2831
("update-input", Some(m)) => self::update_input::exec(m),

0 commit comments

Comments
 (0)