1use crate::error::{TxError, Result};
4use crate::types::{Transaction, TxInput, TxOutput, Utxo};
5use crate::fee::{estimate_fee, is_dust, DUST_THRESHOLD_P2WPKH};
6use crate::coin_selection::select_coins;
7
8#[derive(Debug, Clone)]
10pub struct TxBuilder {
11 inputs: Vec<(Utxo, Option<[u8; 33]>)>, outputs: Vec<TxOutput>,
13 change_address: Option<String>,
14 fee_rate: u64,
15 version: i32,
16 locktime: u32,
17}
18
19impl Default for TxBuilder {
20 fn default() -> Self {
21 Self::new()
22 }
23}
24
25impl TxBuilder {
26 pub fn new() -> Self {
28 Self {
29 inputs: Vec::new(),
30 outputs: Vec::new(),
31 change_address: None,
32 fee_rate: 1, version: 2,
34 locktime: 0,
35 }
36 }
37
38 pub fn add_input(mut self, utxo: Utxo) -> Self {
40 self.inputs.push((utxo, None));
41 self
42 }
43
44 pub fn add_input_with_key(mut self, utxo: Utxo, pubkey: [u8; 33]) -> Self {
46 self.inputs.push((utxo, Some(pubkey)));
47 self
48 }
49
50 pub fn add_output_p2pkh(mut self, address: &str, value: u64) -> Result<Self> {
52 let script = address_to_script(address)?;
53 self.outputs.push(TxOutput::new(value, script));
54 Ok(self)
55 }
56
57 pub fn add_output(mut self, value: u64, script_pubkey: Vec<u8>) -> Self {
59 self.outputs.push(TxOutput::new(value, script_pubkey));
60 self
61 }
62
63 pub fn set_change_address(mut self, address: &str) -> Self {
65 self.change_address = Some(address.to_string());
66 self
67 }
68
69 pub fn set_fee_rate(mut self, sat_per_vb: u64) -> Self {
71 self.fee_rate = sat_per_vb;
72 self
73 }
74
75 pub fn set_version(mut self, version: i32) -> Self {
77 self.version = version;
78 self
79 }
80
81 pub fn set_locktime(mut self, locktime: u32) -> Self {
83 self.locktime = locktime;
84 self
85 }
86
87 pub fn build(self) -> Result<UnsignedTx> {
89 if self.inputs.is_empty() {
90 return Err(TxError::NoInputs);
91 }
92 if self.outputs.is_empty() {
93 return Err(TxError::NoOutputs);
94 }
95
96 for output in &self.outputs {
98 if is_dust(output.value, true) {
99 return Err(TxError::DustOutput(output.value));
100 }
101 }
102
103 let input_total: u64 = self.inputs.iter().map(|(u, _)| u.value).sum();
105 let output_total: u64 = self.outputs.iter().map(|o| o.value).sum();
106
107 let num_outputs = if self.change_address.is_some() {
109 self.outputs.len() + 1
110 } else {
111 self.outputs.len()
112 };
113 let fee = estimate_fee(self.inputs.len(), num_outputs, self.fee_rate);
114
115 let needed = output_total.saturating_add(fee);
117 if input_total < needed {
118 return Err(TxError::InsufficientFunds {
119 needed,
120 available: input_total,
121 });
122 }
123
124 let mut tx = Transaction {
126 version: self.version,
127 inputs: Vec::new(),
128 outputs: self.outputs.clone(),
129 locktime: self.locktime,
130 };
131
132 let mut input_info = Vec::new();
134 for (utxo, pubkey) in &self.inputs {
135 let input = TxInput::new(utxo.txid, utxo.vout);
136 tx.inputs.push(input);
137 input_info.push(InputInfo {
138 utxo: utxo.clone(),
139 pubkey: *pubkey,
140 });
141 }
142
143 let change = input_total - output_total - fee;
145 if change > DUST_THRESHOLD_P2WPKH {
146 if let Some(addr) = &self.change_address {
147 let script = address_to_script(addr)?;
148 tx.outputs.push(TxOutput::new(change, script));
149 }
150 }
151
152 Ok(UnsignedTx {
153 tx,
154 input_info,
155 fee,
156 })
157 }
158
159 pub fn build_with_coin_selection(
161 mut self,
162 utxos: &[Utxo],
163 ) -> Result<UnsignedTx> {
164 let output_total: u64 = self.outputs.iter().map(|o| o.value).sum();
165
166 let (selected, _) = select_coins(utxos, output_total, self.fee_rate)?;
168
169 for utxo in selected {
171 self = self.add_input(utxo);
172 }
173
174 self.build()
175 }
176}
177
178#[derive(Debug, Clone)]
180pub struct InputInfo {
181 pub utxo: Utxo,
183 pub pubkey: Option<[u8; 33]>,
185}
186
187#[derive(Debug, Clone)]
189pub struct UnsignedTx {
190 pub tx: Transaction,
192 pub input_info: Vec<InputInfo>,
194 pub fee: u64,
196}
197
198impl UnsignedTx {
199 pub fn transaction(&self) -> &Transaction {
201 &self.tx
202 }
203
204 pub fn fee(&self) -> u64 {
206 self.fee
207 }
208}
209
210fn address_to_script(address: &str) -> Result<Vec<u8>> {
212 if address.starts_with('1') || address.starts_with('m') || address.starts_with('n') {
214 let decoded = bs58::decode(address)
216 .into_vec()
217 .map_err(|e| TxError::InvalidAddress(e.to_string()))?;
218
219 if decoded.len() != 25 {
220 return Err(TxError::InvalidAddress("Invalid P2PKH address length".to_string()));
221 }
222
223 let mut pubkey_hash = [0u8; 20];
224 pubkey_hash.copy_from_slice(&decoded[1..21]);
225
226 Ok(crate::script::build_p2pkh_script(&pubkey_hash))
227 }
228 else if address.starts_with("bc1q") || address.starts_with("tb1q") {
230 let (_, data) = bech32_decode(address)
232 .map_err(TxError::InvalidAddress)?;
233
234 if data.len() != 20 {
235 return Err(TxError::InvalidAddress("Invalid P2WPKH address".to_string()));
236 }
237
238 let mut pubkey_hash = [0u8; 20];
239 pubkey_hash.copy_from_slice(&data);
240
241 Ok(crate::script::build_p2wpkh_script(&pubkey_hash))
242 }
243 else {
244 Err(TxError::InvalidAddress(format!("Unsupported address format: {}", address)))
245 }
246}
247
248fn bech32_decode(address: &str) -> std::result::Result<(String, Vec<u8>), String> {
250 let address = address.to_lowercase();
251 let pos = address.rfind('1').ok_or("No separator found")?;
252 let hrp = &address[..pos];
253 let data_part = &address[pos + 1..];
254
255 const CHARSET: &str = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
257 let mut values = Vec::new();
258 for c in data_part.chars() {
259 let idx = CHARSET.find(c).ok_or("Invalid character")?;
260 values.push(idx as u8);
261 }
262
263 if values.len() < 7 {
265 return Err("Too short".to_string());
266 }
267 let data_values = &values[1..values.len() - 6];
268
269 let mut result = Vec::new();
271 let mut acc = 0u32;
272 let mut bits = 0u32;
273 for &v in data_values {
274 acc = (acc << 5) | (v as u32);
275 bits += 5;
276 while bits >= 8 {
277 bits -= 8;
278 result.push((acc >> bits) as u8);
279 acc &= (1 << bits) - 1;
280 }
281 }
282
283 Ok((hrp.to_string(), result))
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289
290 fn make_utxo(value: u64) -> Utxo {
291 Utxo {
292 txid: [0u8; 32],
293 vout: 0,
294 value,
295 script_pubkey: vec![0x76, 0xa9, 0x14], address: "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa".to_string(),
297 }
298 }
299
300 #[test]
301 fn test_builder_no_inputs() {
302 let result = TxBuilder::new()
303 .add_output(50_000, vec![0x00])
304 .build();
305 assert!(matches!(result, Err(TxError::NoInputs)));
306 }
307
308 #[test]
309 fn test_builder_no_outputs() {
310 let result = TxBuilder::new()
311 .add_input(make_utxo(100_000))
312 .build();
313 assert!(matches!(result, Err(TxError::NoOutputs)));
314 }
315
316 #[test]
317 fn test_builder_dust_output() {
318 let result = TxBuilder::new()
319 .add_input(make_utxo(100_000))
320 .add_output(100, vec![0x00, 0x14]) .build();
322 assert!(matches!(result, Err(TxError::DustOutput(_))));
323 }
324
325 #[test]
326 fn test_builder_success() {
327 let result = TxBuilder::new()
328 .add_input(make_utxo(100_000))
329 .add_output(50_000, vec![0x76, 0xa9, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x88, 0xac])
330 .set_fee_rate(1)
331 .build();
332
333 assert!(result.is_ok());
334 let unsigned = result.unwrap();
335 assert_eq!(unsigned.tx.inputs.len(), 1);
336 assert_eq!(unsigned.tx.outputs.len(), 1);
337 }
338}