1use crate::config;
2use crate::error::{Context, Error, bail, ensure};
3use crate::node::Node;
4use crate::publish::{MempoolCheck, test_accept};
5use crate::transaction::transaction;
6use bitcoin::{Amount, OutPoint, ScriptBuf, Transaction, TxOut, Txid, Witness, consensus::serialize};
7use serde_json::json;
8use std::num::NonZeroU64;
9use urma_identity::identity::IdentitySigner;
10use urma_wallet::wallet::{FeeBudget, FeeRate, SpendRequest};
11
12pub struct TransferRequest {
13 pub destination: ScriptBuf,
14 pub amount: u64,
15 pub rate: NonZeroU64,
16 pub max_fee: u64,
17}
18
19pub struct Transfer {
20 pub transaction: Transaction,
21 pub fee: u64,
22 pub change: u64,
23}
24
25fn output(value: u64, script: &ScriptBuf) -> TxOut {
26 TxOut {
27 value: Amount::from_sat(value),
28 script_pubkey: script.clone(),
29 }
30}
31
32fn quote(request: &TransferRequest, own: &ScriptBuf, retained: u64) -> Result<u64, Error> {
33 let mut draft = transaction(OutPoint::null(), output(request.amount, &request.destination));
34 draft.output.push(output(retained, own));
35 draft.input[0].witness = Witness::from_slice(&[vec![0; 73], vec![0; 33]]);
36 let fee = u64::try_from(draft.vsize())?
37 .checked_mul(request.rate.get())
38 .context("transfer fee overflow")?;
39 ensure!(
40 fee <= request.max_fee,
41 "transfer fee {fee} exceeds the approved maximum {}",
42 request.max_fee
43 );
44 Ok(fee)
45}
46
47pub fn prepare(
48 node: &Node,
49 signer: &impl IdentitySigner,
50 request: &TransferRequest,
51) -> Result<Transfer, Error> {
52 let retained = config::publication_return(node.chain());
53 ensure!(
54 request.amount >= retained,
55 "amount below the dust-safe minimum of {retained} base units"
56 );
57 ensure!(
58 !request.destination.is_op_return(),
59 "destination cannot be a data output"
60 );
61 let own = urma_wallet::signing::script(signer)?;
62 let fee = quote(request, &own, retained)?;
63 let spent = request
64 .amount
65 .checked_add(fee)
66 .context("transfer amount overflow")?;
67 let minimum = spent.checked_add(retained).context("transfer amount overflow")?;
68 let funding = node.select_funding(signer, minimum)?;
69 let (outpoint, previous) = funding.prevout()?;
70 let change = previous
71 .value
72 .to_sat()
73 .checked_sub(spent)
74 .context("insufficient funding")?;
75 let mut unsigned = transaction(outpoint, output(request.amount, &request.destination));
76 unsigned.output.push(output(change, &own));
77 let signed = urma_wallet::signing::sign(
78 signer,
79 &SpendRequest {
80 transaction: &unsigned,
81 prevouts: std::slice::from_ref(&previous),
82 budget: FeeBudget {
83 chain: node.chain().genesis()?,
84 rate: FeeRate(request.rate),
85 maximum_base_units: request.max_fee,
86 },
87 },
88 )?;
89 Ok(Transfer {
90 transaction: signed,
91 fee,
92 change,
93 })
94}
95
96pub fn submit(node: &Node, transfer: &Transfer) -> Result<Txid, Error> {
97 let raw = hex::encode(serialize(&transfer.transaction));
98 match test_accept(node, &raw)? {
99 MempoolCheck::Allowed => {}
100 MempoolCheck::Rejected(reason) => {
101 bail!("node policy rejected the transfer: {reason}; nothing was broadcast")
102 }
103 MempoolCheck::Unavailable(reason) => {
104 tracing::warn!(%reason, "transfer broadcast without mempool preflight");
105 }
106 }
107 let txid = transfer.transaction.compute_txid();
108 let answer = node.call("sendrawtransaction", &[json!(raw)])?;
109 ensure!(
110 answer == json!(txid),
111 "broadcast returned an unexpected TXID"
112 );
113 Ok(txid)
114}