Skip to content

Commit 1a56966

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 531ccf9 commit 1a56966

File tree

2 files changed

+176
-0
lines changed

2 files changed

+176
-0
lines changed
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
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,
50+
0x9f, 0xc0, 0x3b, 0x63, 0x7f, 0x2a, 0x4e, 0x1e,
51+
0x64, 0xe5, 0x90, 0xca, 0xc1, 0xbc, 0x3a, 0x6f,
52+
0x6d, 0x71, 0xaa, 0x44, 0x43, 0x65, 0x4c, 0x14,
53+
])
54+
.expect("valid asset id");
55+
56+
Ok(FlattenedOutputSpec {
57+
address,
58+
asset: default_asset,
59+
amount: elements::bitcoin::Amount::from_btc(amount)
60+
.result_context("parsing amount")?,
61+
})
62+
})),
63+
Self::Explicit {
64+
address,
65+
asset,
66+
amount,
67+
} => Box::new(
68+
Some(Ok(FlattenedOutputSpec {
69+
address,
70+
asset,
71+
amount,
72+
}))
73+
.into_iter(),
74+
),
75+
}
76+
}
77+
}
78+
79+
pub fn cmd<'a>() -> clap::App<'a, 'a> {
80+
cmd::subcommand("create", "create an empty PSET").args(&cmd::opts_networks()).args(&[
81+
cmd::arg(
82+
"inputs",
83+
"input outpoints (JSON array of objects containing txid, vout, sequence)",
84+
)
85+
.takes_value(true)
86+
.required(true),
87+
cmd::arg("outputs", "outputs (JSON array of objects containing address, asset, amount)")
88+
.takes_value(true)
89+
.required(true),
90+
])
91+
}
92+
93+
pub fn exec<'a>(matches: &clap::ArgMatches<'a>) {
94+
let inputs_json = matches.value_of("inputs").expect("inputs mandatory");
95+
let outputs_json = matches.value_of("outputs").expect("inputs mandatory");
96+
97+
match exec_inner(inputs_json, outputs_json) {
98+
Ok(info) => cmd::print_output(matches, &info),
99+
Err(e) => cmd::print_output(matches, &e),
100+
}
101+
}
102+
103+
fn exec_inner(inputs_json: &str, outputs_json: &str) -> Result<UpdatedPset, Error> {
104+
// Parse inputs JSON
105+
let input_specs: Vec<InputSpec> =
106+
serde_json::from_str(inputs_json).result_context("parsing inputs JSON")?;
107+
108+
// Parse outputs JSON - support both array and map formats
109+
let output_specs: Vec<OutputSpec> =
110+
serde_json::from_str(outputs_json).result_context("parsing outputs JSON")?;
111+
112+
// Create transaction inputs
113+
let mut inputs = Vec::new();
114+
for input_spec in &input_specs {
115+
let outpoint = OutPoint::new(input_spec.txid, input_spec.vout);
116+
let sequence = elements::Sequence(input_spec.sequence.unwrap_or(0xffffffff));
117+
118+
inputs.push(TxIn {
119+
previous_output: outpoint,
120+
script_sig: elements::Script::new(),
121+
sequence,
122+
asset_issuance: Default::default(),
123+
witness: Default::default(),
124+
is_pegin: false,
125+
});
126+
}
127+
128+
// Create transaction outputs
129+
let mut outputs = Vec::new();
130+
for output_spec in output_specs.into_iter().map(OutputSpec::flatten).flatten() {
131+
let output_spec = output_spec?; // serde has crappy error messages so we defer parsing and then have to unwrap errors
132+
133+
let script_pubkey = match output_spec.address.as_str() {
134+
"fee" => elements::Script::new(),
135+
x => {
136+
let addr = x.parse::<Address>().result_context("parsing address")?;
137+
if addr.is_blinded() {
138+
return Err("confidential addresses are not yet supported")
139+
.result_context("output address");
140+
}
141+
addr.script_pubkey()
142+
}
143+
};
144+
145+
outputs.push(TxOut {
146+
asset: confidential::Asset::Explicit(output_spec.asset),
147+
value: confidential::Value::Explicit(output_spec.amount.to_sat()),
148+
nonce: elements::confidential::Nonce::Null,
149+
script_pubkey,
150+
witness: elements::TxOutWitness::empty(),
151+
});
152+
}
153+
154+
// Create the transaction
155+
let tx = Transaction {
156+
version: 2,
157+
lock_time: elements::LockTime::ZERO,
158+
input: inputs,
159+
output: outputs,
160+
};
161+
162+
// Create PSET from transaction
163+
let pset = PartiallySignedTransaction::from_tx(tx);
164+
165+
Ok(UpdatedPset {
166+
pset: pset.to_string(),
167+
updated_values: vec![
168+
// FIXME we technically update a whole slew of fields; see the implementation
169+
// of PartiallySignedTransaction::from_tx. Should we attempt to exhaustively
170+
// list them here? Or list none? Or what?
171+
],
172+
})
173+
}

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)