1use super::types::Utxo;
4use super::deployment::{
5 to_little_endian_32, to_little_endian_64, encode_varint, reverse_hex,
6 build_p2pkh_script_from_address,
7};
8
9pub struct ContractOutput {
11 pub script: String,
12 pub satoshis: i64,
13}
14
15pub struct AdditionalContractInput {
17 pub utxo: Utxo,
18 pub unlocking_script: String,
19}
20
21pub struct CallTxOptions {
23 pub contract_outputs: Option<Vec<ContractOutput>>,
25 pub additional_contract_inputs: Option<Vec<AdditionalContractInput>>,
27}
28
29pub fn build_call_transaction(
41 current_utxo: &Utxo,
42 unlocking_script: &str,
43 new_locking_script: Option<&str>,
44 new_satoshis: Option<i64>,
45 change_address: Option<&str>,
46 change_script: Option<&str>,
47 additional_utxos: Option<&[Utxo]>,
48 fee_rate: Option<i64>,
49) -> (String, usize, i64) {
50 build_call_transaction_ext(
51 current_utxo,
52 unlocking_script,
53 new_locking_script,
54 new_satoshis,
55 change_address,
56 change_script,
57 additional_utxos,
58 fee_rate,
59 None,
60 )
61}
62
63pub fn build_call_transaction_ext(
66 current_utxo: &Utxo,
67 unlocking_script: &str,
68 new_locking_script: Option<&str>,
69 new_satoshis: Option<i64>,
70 change_address: Option<&str>,
71 change_script: Option<&str>,
72 additional_utxos: Option<&[Utxo]>,
73 fee_rate: Option<i64>,
74 options: Option<&CallTxOptions>,
75) -> (String, usize, i64) {
76 let extra_contract_inputs = options
77 .and_then(|o| o.additional_contract_inputs.as_ref())
78 .map(|v| v.as_slice())
79 .unwrap_or(&[]);
80 let p2pkh_utxos = additional_utxos.unwrap_or(&[]);
81
82 let mut all_utxos = vec![current_utxo.clone()];
84 for ci in extra_contract_inputs {
85 all_utxos.push(ci.utxo.clone());
86 }
87 all_utxos.extend_from_slice(p2pkh_utxos);
88
89 let total_input: i64 = all_utxos.iter().map(|u| u.satoshis).sum();
90
91 let contract_outputs: Vec<ContractOutput> = if let Some(cos) = options.and_then(|o| o.contract_outputs.as_ref()) {
93 cos.iter().map(|co| ContractOutput { script: co.script.clone(), satoshis: co.satoshis }).collect()
95 } else if let Some(nls) = new_locking_script {
96 vec![ContractOutput {
97 script: nls.to_string(),
98 satoshis: new_satoshis.unwrap_or(current_utxo.satoshis),
99 }]
100 } else {
101 vec![]
102 };
103
104 let contract_output_sats: i64 = contract_outputs.iter().map(|o| o.satoshis).sum();
105
106 let unlock_byte_len = unlocking_script.len() / 2;
108 let input0_size = 32 + 4 + varint_byte_size(unlock_byte_len) + unlock_byte_len as i64 + 4;
109 let mut extra_contract_inputs_size: i64 = 0;
110 for ci in extra_contract_inputs {
111 let ci_byte_len = ci.unlocking_script.len() / 2;
112 extra_contract_inputs_size += 32 + 4 + varint_byte_size(ci_byte_len) + ci_byte_len as i64 + 4;
113 }
114 let p2pkh_inputs_size = p2pkh_utxos.len() as i64 * 148;
115 let inputs_size = input0_size + extra_contract_inputs_size + p2pkh_inputs_size;
116
117 let mut outputs_size: i64 = 0;
118 for co in &contract_outputs {
119 let co_byte_len = co.script.len() / 2;
120 outputs_size += 8 + varint_byte_size(co_byte_len) + co_byte_len as i64;
121 }
122 let has_change_target = change_address.is_some() || change_script.is_some();
123 if has_change_target {
124 outputs_size += 34; }
126 let estimated_size = 10 + inputs_size + outputs_size;
127 let rate = fee_rate.filter(|&r| r > 0).unwrap_or(100);
128 let fee = (estimated_size * rate + 999) / 1000;
129
130 let change = total_input - contract_output_sats - fee;
131
132 let mut tx = String::new();
134
135 tx.push_str(&to_little_endian_32(1));
137
138 tx.push_str(&encode_varint(all_utxos.len() as u64));
140
141 tx.push_str(&reverse_hex(¤t_utxo.txid));
143 tx.push_str(&to_little_endian_32(current_utxo.output_index));
144 tx.push_str(&encode_varint(unlock_byte_len as u64));
145 tx.push_str(unlocking_script);
146 tx.push_str("ffffffff");
147
148 for ci in extra_contract_inputs {
150 tx.push_str(&reverse_hex(&ci.utxo.txid));
151 tx.push_str(&to_little_endian_32(ci.utxo.output_index));
152 let ci_byte_len = ci.unlocking_script.len() / 2;
153 tx.push_str(&encode_varint(ci_byte_len as u64));
154 tx.push_str(&ci.unlocking_script);
155 tx.push_str("ffffffff");
156 }
157
158 for utxo in p2pkh_utxos {
160 tx.push_str(&reverse_hex(&utxo.txid));
161 tx.push_str(&to_little_endian_32(utxo.output_index));
162 tx.push_str("00"); tx.push_str("ffffffff");
164 }
165
166 let mut num_outputs = contract_outputs.len() as u64;
168 if change > 0 && has_change_target {
169 num_outputs += 1;
170 }
171 tx.push_str(&encode_varint(num_outputs));
172
173 for co in &contract_outputs {
175 tx.push_str(&to_little_endian_64(co.satoshis));
176 tx.push_str(&encode_varint((co.script.len() / 2) as u64));
177 tx.push_str(&co.script);
178 }
179
180 if change > 0 && has_change_target {
182 let actual_change_script = if let Some(cs) = change_script {
183 cs.to_string()
184 } else if let Some(addr) = change_address {
185 build_p2pkh_script_from_address(addr)
186 } else {
187 String::new()
188 };
189 tx.push_str(&to_little_endian_64(change));
190 tx.push_str(&encode_varint((actual_change_script.len() / 2) as u64));
191 tx.push_str(&actual_change_script);
192 }
193
194 tx.push_str(&to_little_endian_32(0));
196
197 let change_amount = if change > 0 { change } else { 0 };
198 (tx, all_utxos.len(), change_amount)
199}
200
201fn varint_byte_size(n: usize) -> i64 {
202 if n < 0xfd { 1 }
203 else if n <= 0xffff { 3 }
204 else if n <= 0xffff_ffff { 5 }
205 else { 9 }
206}
207
208#[cfg(test)]
213mod tests {
214 use super::*;
215
216 fn make_utxo(satoshis: i64, index: u32) -> Utxo {
217 Utxo {
218 txid: "aabbccdd".repeat(8),
219 output_index: index,
220 satoshis,
221 script: format!("76a914{}88ac", "00".repeat(20)),
222 }
223 }
224
225 fn parse_tx_hex(hex: &str) -> ParsedTx {
226 let mut offset = 0;
227
228 fn read_bytes<'a>(hex: &'a str, offset: &mut usize, n: usize) -> &'a str {
229 let start = *offset;
230 *offset += n * 2;
231 &hex[start..*offset]
232 }
233
234 fn read_u32_le(hex: &str, offset: &mut usize) -> u32 {
235 let h = read_bytes(hex, offset, 4);
236 let mut bytes = [0u8; 4];
237 for i in 0..4 {
238 bytes[i] = u8::from_str_radix(&h[i * 2..i * 2 + 2], 16).unwrap();
239 }
240 u32::from_le_bytes(bytes)
241 }
242
243 fn read_u64_le(hex: &str, offset: &mut usize) -> u64 {
244 let lo = read_u32_le(hex, offset) as u64;
245 let hi = read_u32_le(hex, offset) as u64;
246 lo | (hi << 32)
247 }
248
249 fn read_varint(hex: &str, offset: &mut usize) -> u64 {
250 let first = u8::from_str_radix(read_bytes(hex, offset, 1), 16).unwrap();
251 if first < 0xfd {
252 first as u64
253 } else if first == 0xfd {
254 let h = read_bytes(hex, offset, 2);
255 let lo = u8::from_str_radix(&h[0..2], 16).unwrap() as u64;
256 let hi = u8::from_str_radix(&h[2..4], 16).unwrap() as u64;
257 lo | (hi << 8)
258 } else {
259 panic!("unsupported varint");
260 }
261 }
262
263 let version = read_u32_le(hex, &mut offset);
264 let input_count = read_varint(hex, &mut offset) as usize;
265
266 let mut inputs = Vec::new();
267 for _ in 0..input_count {
268 let prev_txid = read_bytes(hex, &mut offset, 32).to_string();
269 let prev_index = read_u32_le(hex, &mut offset);
270 let script_len = read_varint(hex, &mut offset) as usize;
271 let script = read_bytes(hex, &mut offset, script_len).to_string();
272 let sequence = read_u32_le(hex, &mut offset);
273 inputs.push(ParsedInput {
274 prev_txid,
275 prev_index,
276 script,
277 sequence,
278 });
279 }
280
281 let output_count = read_varint(hex, &mut offset) as usize;
282 let mut outputs = Vec::new();
283 for _ in 0..output_count {
284 let satoshis = read_u64_le(hex, &mut offset) as i64;
285 let script_len = read_varint(hex, &mut offset) as usize;
286 let script = read_bytes(hex, &mut offset, script_len).to_string();
287 outputs.push(ParsedOutput { satoshis, script });
288 }
289
290 let locktime = read_u32_le(hex, &mut offset);
291
292 ParsedTx {
293 version,
294 input_count,
295 inputs,
296 output_count,
297 outputs,
298 locktime,
299 }
300 }
301
302 #[derive(Debug)]
303 #[allow(dead_code)]
304 struct ParsedTx {
305 version: u32,
306 input_count: usize,
307 inputs: Vec<ParsedInput>,
308 output_count: usize,
309 outputs: Vec<ParsedOutput>,
310 locktime: u32,
311 }
312
313 #[derive(Debug)]
314 #[allow(dead_code)]
315 struct ParsedInput {
316 prev_txid: String,
317 prev_index: u32,
318 script: String,
319 sequence: u32,
320 }
321
322 #[derive(Debug)]
323 #[allow(dead_code)]
324 struct ParsedOutput {
325 satoshis: i64,
326 script: String,
327 }
328
329 fn reverse_hex_helper(hex: &str) -> String {
330 let pairs: Vec<&str> = (0..hex.len()).step_by(2).map(|i| &hex[i..i + 2]).collect();
331 pairs.iter().rev().copied().collect()
332 }
333
334 #[test]
335 fn version_1_locktime_0() {
336 let utxo = make_utxo(100_000, 0);
337 let (tx_hex, _, _) = build_call_transaction(&utxo, "51", None, None, None, None, None, None);
338 let parsed = parse_tx_hex(&tx_hex);
339 assert_eq!(parsed.version, 1);
340 assert_eq!(parsed.locktime, 0);
341 }
342
343 #[test]
344 fn valid_hex_output() {
345 let utxo = make_utxo(100_000, 0);
346 let (tx_hex, _, _) = build_call_transaction(&utxo, "51", None, None, None, None, None, None);
347 assert!(!tx_hex.is_empty());
348 assert!(tx_hex.chars().all(|c| c.is_ascii_hexdigit()));
349 }
350
351 #[test]
352 fn embeds_unlocking_script_in_input_0() {
353 let utxo = make_utxo(100_000, 0);
354 let (tx_hex, _, _) = build_call_transaction(&utxo, "aabb", None, None, None, None, None, None);
355 let parsed = parse_tx_hex(&tx_hex);
356 assert_eq!(parsed.inputs[0].script, "aabb");
357 }
358
359 #[test]
360 fn all_sequences_ffffffff() {
361 let utxo = make_utxo(100_000, 0);
362 let additional = vec![make_utxo(50_000, 1), make_utxo(30_000, 2)];
363 let change_script = format!("76a914{}88ac", "ff".repeat(20));
364 let (tx_hex, _, _) = build_call_transaction(
365 &utxo, "51", None, None, Some("changeaddr"), Some(&change_script), Some(&additional), None,
366 );
367 let parsed = parse_tx_hex(&tx_hex);
368 for input in &parsed.inputs {
369 assert_eq!(input.sequence, 0xffff_ffff);
370 }
371 }
372
373 #[test]
374 fn reversed_txid_in_wire_format() {
375 let utxo = make_utxo(100_000, 0);
376 let (tx_hex, _, _) = build_call_transaction(&utxo, "51", None, None, None, None, None, None);
377 let parsed = parse_tx_hex(&tx_hex);
378 assert_eq!(parsed.inputs[0].prev_txid, reverse_hex_helper(&utxo.txid));
379 }
380
381 #[test]
382 fn single_input_no_additional() {
383 let utxo = make_utxo(100_000, 0);
384 let (tx_hex, input_count, _) = build_call_transaction(&utxo, "51", None, None, None, None, None, None);
385 let parsed = parse_tx_hex(&tx_hex);
386 assert_eq!(input_count, 1);
387 assert_eq!(parsed.input_count, 1);
388 }
389
390 #[test]
391 fn additional_utxos_have_empty_scriptsig() {
392 let utxo = make_utxo(100_000, 0);
393 let additional = vec![make_utxo(50_000, 1), make_utxo(30_000, 2)];
394 let change_script = format!("76a914{}88ac", "ff".repeat(20));
395 let (tx_hex, input_count, _) = build_call_transaction(
396 &utxo, "51", None, None, Some("changeaddr"), Some(&change_script), Some(&additional), None,
397 );
398 let parsed = parse_tx_hex(&tx_hex);
399 assert_eq!(input_count, 3);
400 assert_eq!(parsed.inputs[0].script, "51");
401 assert_eq!(parsed.inputs[1].script, "");
402 assert_eq!(parsed.inputs[2].script, "");
403 }
404
405 #[test]
406 fn correct_output_index_reference() {
407 let utxo = make_utxo(100_000, 3);
408 let (tx_hex, _, _) = build_call_transaction(&utxo, "51", None, None, None, None, None, None);
409 let parsed = parse_tx_hex(&tx_hex);
410 assert_eq!(parsed.inputs[0].prev_index, 3);
411 }
412
413 #[test]
414 fn stateful_output_with_new_locking_script() {
415 let utxo = make_utxo(100_000, 0);
416 let new_ls = format!("76a914{}88ac", "dd".repeat(20));
417 let change_script = format!("76a914{}88ac", "ff".repeat(20));
418 let (tx_hex, _, _) = build_call_transaction(
419 &utxo, "51", Some(&new_ls), Some(50_000), Some("changeaddr"), Some(&change_script), None, None,
420 );
421 let parsed = parse_tx_hex(&tx_hex);
422 assert_eq!(parsed.outputs[0].script, new_ls);
423 assert_eq!(parsed.outputs[0].satoshis, 50_000);
424 }
425
426 #[test]
427 fn defaults_to_current_utxo_satoshis() {
428 let utxo = make_utxo(75_000, 0);
429 let change_script = format!("76a914{}88ac", "ff".repeat(20));
430 let (tx_hex, _, _) = build_call_transaction(
431 &utxo, "00", Some("51"), None, Some("changeaddr"), Some(&change_script), None, None,
432 );
433 let parsed = parse_tx_hex(&tx_hex);
434 assert_eq!(parsed.outputs[0].satoshis, 75_000);
435 }
436
437 #[test]
438 fn change_calculation() {
439 let utxo = make_utxo(100_000, 0);
440 let change_script = format!("76a914{}88ac", "ff".repeat(20));
441 let (tx_hex, _, _) = build_call_transaction(
442 &utxo, "00", Some("51"), Some(50_000), Some("changeaddr"), Some(&change_script), None, None,
443 );
444 let parsed = parse_tx_hex(&tx_hex);
445 assert_eq!(parsed.output_count, 2);
449 assert_eq!(parsed.outputs[0].satoshis, 50_000);
450 assert_eq!(parsed.outputs[1].satoshis, 49_990);
451 assert_eq!(parsed.outputs[1].script, change_script);
452 }
453
454 #[test]
455 fn omits_change_when_zero() {
456 let utxo = make_utxo(50_010, 0);
459 let change_script = format!("76a914{}88ac", "ff".repeat(20));
460 let (tx_hex, _, _) = build_call_transaction(
461 &utxo, "00", Some("51"), Some(50_000), Some("changeaddr"), Some(&change_script), None, None,
462 );
463 let parsed = parse_tx_hex(&tx_hex);
464 assert_eq!(parsed.output_count, 1);
465 assert_eq!(parsed.outputs[0].satoshis, 50_000);
466 }
467
468 #[test]
469 fn stateless_change_only() {
470 let utxo = make_utxo(100_000, 0);
471 let change_script = format!("76a914{}88ac", "ff".repeat(20));
472 let (tx_hex, _, _) = build_call_transaction(
473 &utxo, "51", None, None, Some("changeaddr"), Some(&change_script), None, None,
474 );
475 let parsed = parse_tx_hex(&tx_hex);
476 assert_eq!(parsed.output_count, 1);
480 assert_eq!(parsed.outputs[0].script, change_script);
481 assert_eq!(parsed.outputs[0].satoshis, 99_991);
482 }
483
484 #[test]
485 fn stateless_no_outputs_when_change_zero() {
486 let utxo = make_utxo(9, 0);
489 let change_script = format!("76a914{}88ac", "ff".repeat(20));
490 let (tx_hex, _, _) = build_call_transaction(
491 &utxo, "51", None, None, Some("changeaddr"), Some(&change_script), None, None,
492 );
493 let parsed = parse_tx_hex(&tx_hex);
494 assert_eq!(parsed.output_count, 0);
495 }
496
497 #[test]
498 fn accumulates_additional_utxos() {
499 let utxo = make_utxo(50_000, 0);
500 let additional = vec![make_utxo(30_000, 1)];
501 let change_script = format!("76a914{}88ac", "ff".repeat(20));
502 let (tx_hex, _, _) = build_call_transaction(
503 &utxo, "00", Some("51"), Some(40_000), Some("changeaddr"), Some(&change_script), Some(&additional), None,
504 );
505 let parsed = parse_tx_hex(&tx_hex);
506 assert_eq!(parsed.output_count, 2);
510 assert_eq!(parsed.outputs[0].satoshis, 40_000);
511 assert_eq!(parsed.outputs[1].satoshis, 39_975);
512 }
513}