1use crate::config;
2use crate::error::{Context, Error, ensure};
3use crate::reveal;
4use crate::transaction::{decode, signed_vsize, spending, transaction};
5use crate::wallet::FundingInput;
6use bitcoin::{Amount, OutPoint, ScriptBuf, TxOut, consensus::serialize};
7use serde::{Deserialize, Serialize};
8use urma_chain::observation::Chain;
9use urma_core::{
10 envelope,
11 format::{PublicRecord, Urma},
12 multipart::MultipartRecord,
13};
14use urma_identity::identity::IdentitySigner;
15use urma_wallet::{
16 account::{Account, KeyPath},
17 funding::Funding,
18 wallet::{FeeBudget, SpendRequest, WalletError},
19};
20
21#[derive(Clone, Serialize, Deserialize)]
22#[serde(deny_unknown_fields)]
23pub struct PublicPlan {
24 pub version: u8,
25 pub chain: Chain,
26 pub funding: Vec<Funding>,
27 pub commit: String,
28 pub reveal: String,
29 pub fee_rate: u64,
30}
31
32pub fn prepare(
33 record: &PublicRecord,
34 author: &impl IdentitySigner,
35 funding: Vec<Funding>,
36 return_script: &ScriptBuf,
37 chain: Chain,
38 fee_rate: u64,
39) -> Result<PublicPlan, Error> {
40 prepare_bytes(
41 &record.encode()?,
42 author,
43 funding,
44 return_script,
45 chain,
46 fee_rate,
47 )
48}
49
50pub fn prepare_multipart(
51 record: &MultipartRecord,
52 author: &impl IdentitySigner,
53 funding: Vec<Funding>,
54 return_script: &ScriptBuf,
55 chain: Chain,
56 fee_rate: u64,
57) -> Result<PublicPlan, Error> {
58 prepare_bytes(
59 &record.encode()?,
60 author,
61 funding,
62 return_script,
63 chain,
64 fee_rate,
65 )
66}
67
68pub(crate) fn prevouts(funding: &[Funding]) -> Result<(Vec<OutPoint>, Vec<TxOut>, u64), Error> {
69 let mut outpoints = Vec::new();
70 let mut outputs = Vec::new();
71 let mut total = 0u64;
72 for source in funding {
73 let (outpoint, previous) = source.prevout()?;
74 total = total
75 .checked_add(previous.value.to_sat())
76 .context("funding sum overflow")?;
77 outpoints.push(outpoint);
78 outputs.push(previous);
79 }
80 Ok((outpoints, outputs, total))
81}
82
83pub fn prepare_bytes(
84 record: &[u8],
85 author: &impl IdentitySigner,
86 funding: Vec<Funding>,
87 return_script: &ScriptBuf,
88 chain: Chain,
89 fee_rate: u64,
90) -> Result<PublicPlan, Error> {
91 ensure!(
92 (1..=100).contains(&fee_rate),
93 "client fee rate must be 1..100 base units/vB"
94 );
95 ensure!(
96 (1..=config::WALLET_MAX_INPUTS).contains(&funding.len()),
97 "publication funding needs 1..{} inputs",
98 config::WALLET_MAX_INPUTS
99 );
100 ensure!(
101 return_script.is_p2wpkh(),
102 "publication return requires native P2WPKH"
103 );
104 let (outpoints, _, available) = prevouts(&funding)?;
105 let (script, info) =
106 envelope::build_for_author(record, author.public_key().inner.x_only_public_key().0)?;
107 let retained = config::publication_return(chain);
108 let return_output = TxOut {
109 value: Amount::from_sat(retained),
110 script_pubkey: return_script.clone(),
111 };
112 let mut reveal = reveal::preview(return_output.clone(), &script, &info)?;
113 let reveal_fee = reveal::fee(&reveal, fee_rate)?;
114 let publication_output = TxOut {
115 value: Amount::from_sat(retained + reveal_fee),
116 script_pubkey: ScriptBuf::new_p2tr_tweaked(info.output_key()),
117 };
118 let mut commit = spending(&outpoints, publication_output.clone());
119 commit.output.push(return_output);
120 let commit_fee = signed_vsize(&commit)?
121 .checked_mul(fee_rate)
122 .context("fee overflow")?;
123 let total_fee = commit_fee.checked_add(reveal_fee).context("fee overflow")?;
124 ensure!(
125 total_fee <= 500_000,
126 "client publication fee capacity exceeded"
127 );
128 let change = available
129 .checked_sub(total_fee + retained)
130 .context("insufficient funding")?;
131 ensure!(change >= retained, "funding must leave native change");
132 commit.output[1].value = Amount::from_sat(change);
133 reveal.input[0].previous_output = OutPoint {
134 txid: commit.compute_txid(),
135 vout: 0,
136 };
137 let signature = author.sign_author(reveal::digest(&reveal, &publication_output, &script)?)?;
138 reveal.input[0].witness = envelope::witness(signature.as_ref(), &script, &info)?;
139 envelope::verify_reveal(&reveal, &commit)?;
140 Ok(PublicPlan {
141 version: Urma::VERSION,
142 chain,
143 funding,
144 commit: hex::encode(serialize(&commit)),
145 reveal: hex::encode(serialize(&reveal)),
146 fee_rate,
147 })
148}
149
150pub fn quote_record(
151 record: &[u8],
152 author: bitcoin::XOnlyPublicKey,
153 return_script: &ScriptBuf,
154 fee_rate: u64,
155) -> Result<u64, Error> {
156 ensure!((1..=100).contains(&fee_rate), "invalid fee rate");
157 ensure!(return_script.is_p2wpkh(), "funding requires native P2WPKH");
158 let (script, info) = envelope::build_for_author(record, author)?;
159 let output = TxOut {
160 value: Amount::from_sat(1000),
161 script_pubkey: return_script.clone(),
162 };
163 let reveal = reveal::preview(output.clone(), &script, &info)?;
164 let mut commit = transaction(
165 OutPoint::null(),
166 TxOut {
167 value: Amount::from_sat(1000),
168 script_pubkey: ScriptBuf::new_p2tr_tweaked(info.output_key()),
169 },
170 );
171 commit.output.push(output);
172 signed_vsize(&commit)?
173 .checked_add(u64::try_from(reveal.vsize())?)
174 .and_then(|bytes| bytes.checked_mul(fee_rate))
175 .context("quote fee overflow")
176}
177
178pub struct SignedPublicPair {
179 pub plan: PublicPlan,
180 pub fee: u64,
181}
182
183pub fn prepare_signed_bytes(
184 record: &[u8],
185 author: &impl IdentitySigner,
186 funder: &Account,
187 inputs: &[FundingInput],
188 budget: FeeBudget,
189) -> Result<SignedPublicPair, Error> {
190 let chain = funder.chain();
191 ensure!(
192 budget.chain == chain.genesis()?,
193 "funding budget chain mismatch"
194 );
195 let funding: Vec<Funding> = inputs.iter().map(|input| input.funding.clone()).collect();
196 let paths: Vec<KeyPath> = inputs.iter().map(|input| input.path).collect();
197 let (_, previous, available) = prevouts(&funding)?;
198 let mut plan = prepare_bytes(
199 record,
200 author,
201 funding,
202 &funder.receive_script()?,
203 chain,
204 budget.rate.0.get(),
205 )?;
206 let commit = decode(
207 &plan.commit,
208 usize::try_from(config::STANDARD_TX_WEIGHT)? * 2,
209 )?;
210 let signed = funder.sign(
211 &SpendRequest {
212 transaction: &commit,
213 prevouts: &previous,
214 budget,
215 },
216 &paths,
217 )?;
218 let reveal = decode(
219 &plan.reveal,
220 usize::try_from(config::STANDARD_TX_WEIGHT)? * 2,
221 )?;
222 let outputs = signed.output[1]
223 .value
224 .to_sat()
225 .checked_add(reveal.output[0].value.to_sat())
226 .ok_or(WalletError::Overflow)?;
227 let fee = available
228 .checked_sub(outputs)
229 .ok_or(WalletError::InsufficientFunds)?;
230 if fee > budget.maximum_base_units {
231 return Err(WalletError::BudgetExceeded.into());
232 }
233 plan.commit = hex::encode(serialize(&signed));
234 Ok(SignedPublicPair { plan, fee })
235}