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