hal_simplicity/actions/simplicity/pset/
create.rs

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