1use {super::*, crate::outgoing::Outgoing, base64::Engine, bitcoin::psbt::Psbt};
2
3#[derive(Debug, Parser)]
4pub(crate) struct Send {
5 #[arg(long, help = "Don't sign or broadcast transaction")]
6 pub(crate) dry_run: bool,
7 #[arg(long, help = "Use fee rate of <FEE_RATE> sats/vB")]
8 fee_rate: FeeRate,
9 #[arg(
10 long,
11 help = "Target <AMOUNT> postage with sent inscriptions. [default: 10000 sat]"
12 )]
13 pub(crate) postage: Option<Amount>,
14 address: Address<NetworkUnchecked>,
15 outgoing: Outgoing,
16}
17
18#[derive(Debug, Serialize, Deserialize)]
19pub struct Output {
20 pub txid: Txid,
21 pub psbt: String,
22 pub outgoing: Outgoing,
23 pub fee: u64,
24}
25
26impl Send {
27 pub(crate) fn run(self, wallet: Wallet) -> SubcommandResult {
28 let address = self
29 .address
30 .clone()
31 .require_network(wallet.chain().network())?;
32
33 let unsigned_transaction = match self.outgoing {
34 Outgoing::Amount(amount) => {
35 Self::create_unsigned_send_amount_transaction(&wallet, address, amount, self.fee_rate)?
36 }
37 Outgoing::Rune { decimal, rune } => Self::create_unsigned_send_runes_transaction(
38 &wallet,
39 address,
40 rune,
41 decimal,
42 self.fee_rate,
43 )?,
44 Outgoing::InscriptionId(id) => Self::create_unsigned_send_satpoint_transaction(
45 &wallet,
46 address,
47 wallet
48 .inscription_info()
49 .get(&id)
50 .ok_or_else(|| anyhow!("inscription {id} not found"))?
51 .satpoint,
52 self.postage,
53 self.fee_rate,
54 true,
55 )?,
56 Outgoing::SatPoint(satpoint) => Self::create_unsigned_send_satpoint_transaction(
57 &wallet,
58 address,
59 satpoint,
60 self.postage,
61 self.fee_rate,
62 false,
63 )?,
64 Outgoing::Sat(sat) => Self::create_unsigned_send_satpoint_transaction(
65 &wallet,
66 address,
67 wallet.find_sat_in_outputs(sat)?,
68 self.postage,
69 self.fee_rate,
70 true,
71 )?,
72 };
73
74 let unspent_outputs = wallet.utxos();
75
76 let (txid, psbt) = if self.dry_run {
77 let psbt = wallet
78 .bitcoin_client()
79 .wallet_process_psbt(
80 &base64::engine::general_purpose::STANDARD
81 .encode(Psbt::from_unsigned_tx(unsigned_transaction.clone())?.serialize()),
82 Some(false),
83 None,
84 None,
85 )?
86 .psbt;
87
88 (unsigned_transaction.txid(), psbt)
89 } else {
90 let psbt = wallet
91 .bitcoin_client()
92 .wallet_process_psbt(
93 &base64::engine::general_purpose::STANDARD
94 .encode(Psbt::from_unsigned_tx(unsigned_transaction.clone())?.serialize()),
95 Some(true),
96 None,
97 None,
98 )?
99 .psbt;
100
101 let signed_tx = wallet
102 .bitcoin_client()
103 .finalize_psbt(&psbt, None)?
104 .hex
105 .ok_or_else(|| anyhow!("unable to sign transaction"))?;
106
107 (
108 wallet.bitcoin_client().send_raw_transaction(&signed_tx)?,
109 psbt,
110 )
111 };
112
113 let mut fee = 0;
114 for txin in unsigned_transaction.input.iter() {
115 let Some(txout) = unspent_outputs.get(&txin.previous_output) else {
116 panic!("input {} not found in utxos", txin.previous_output);
117 };
118 fee += txout.value;
119 }
120
121 for txout in unsigned_transaction.output.iter() {
122 fee = fee.checked_sub(txout.value).unwrap();
123 }
124
125 Ok(Some(Box::new(Output {
126 txid,
127 psbt,
128 outgoing: self.outgoing,
129 fee,
130 })))
131 }
132
133 fn create_unsigned_send_amount_transaction(
134 wallet: &Wallet,
135 destination: Address,
136 amount: Amount,
137 fee_rate: FeeRate,
138 ) -> Result<Transaction> {
139 wallet.lock_non_cardinal_outputs()?;
140
141 let unfunded_transaction = Transaction {
142 version: 2,
143 lock_time: LockTime::ZERO,
144 input: Vec::new(),
145 output: vec![TxOut {
146 script_pubkey: destination.script_pubkey(),
147 value: amount.to_sat(),
148 }],
149 };
150
151 let unsigned_transaction = consensus::encode::deserialize(&fund_raw_transaction(
152 wallet.bitcoin_client(),
153 fee_rate,
154 &unfunded_transaction,
155 )?)?;
156
157 Ok(unsigned_transaction)
158 }
159
160 fn create_unsigned_send_satpoint_transaction(
161 wallet: &Wallet,
162 destination: Address,
163 satpoint: SatPoint,
164 postage: Option<Amount>,
165 fee_rate: FeeRate,
166 sending_inscription: bool,
167 ) -> Result<Transaction> {
168 if !sending_inscription {
169 for inscription_satpoint in wallet.inscriptions().keys() {
170 if satpoint == *inscription_satpoint {
171 bail!("inscriptions must be sent by inscription ID");
172 }
173 }
174 }
175
176 let runic_outputs = wallet.get_runic_outputs()?;
177
178 ensure!(
179 !runic_outputs.contains(&satpoint.outpoint),
180 "runic outpoints may not be sent by satpoint"
181 );
182
183 let change = [wallet.get_change_address()?, wallet.get_change_address()?];
184
185 let postage = if let Some(postage) = postage {
186 Target::ExactPostage(postage)
187 } else {
188 Target::Postage
189 };
190
191 Ok(
192 TransactionBuilder::new(
193 satpoint,
194 wallet.inscriptions().clone(),
195 wallet.utxos().clone(),
196 wallet.locked_utxos().clone().into_keys().collect(),
197 runic_outputs,
198 destination.clone(),
199 change,
200 fee_rate,
201 postage,
202 )
203 .build_transaction()?,
204 )
205 }
206
207 fn create_unsigned_send_runes_transaction(
208 wallet: &Wallet,
209 destination: Address,
210 spaced_rune: SpacedRune,
211 decimal: Decimal,
212 fee_rate: FeeRate,
213 ) -> Result<Transaction> {
214 ensure!(
215 wallet.has_rune_index(),
216 "sending runes with `ord send` requires index created with `--index-runes` flag",
217 );
218
219 let inscriptions = wallet.inscriptions();
220 let runic_outputs = wallet.get_runic_outputs()?;
221 let bitcoin_client = wallet.bitcoin_client();
222
223 wallet.lock_non_cardinal_outputs()?;
224
225 let (id, entry, _parent) = wallet
226 .get_rune(spaced_rune.rune)?
227 .with_context(|| format!("rune `{}` has not been etched", spaced_rune.rune))?;
228
229 let amount = decimal.to_integer(entry.divisibility)?;
230
231 let inscribed_outputs = inscriptions
232 .keys()
233 .map(|satpoint| satpoint.outpoint)
234 .collect::<HashSet<OutPoint>>();
235
236 let mut input_runes = 0;
237 let mut input = Vec::new();
238
239 for output in runic_outputs {
240 if inscribed_outputs.contains(&output) {
241 continue;
242 }
243
244 let balance = wallet.get_rune_balance_in_output(&output, entry.spaced_rune.rune)?;
245
246 if balance > 0 {
247 input_runes += balance;
248 input.push(output);
249 }
250
251 if input_runes >= amount {
252 break;
253 }
254 }
255
256 ensure! {
257 input_runes >= amount,
258 "insufficient `{}` balance, only {} in wallet",
259 spaced_rune,
260 Pile {
261 amount: input_runes,
262 divisibility: entry.divisibility,
263 symbol: entry.symbol
264 },
265 }
266
267 let runestone = Runestone {
268 edicts: vec![Edict {
269 amount,
270 id,
271 output: 2,
272 }],
273 ..default()
274 };
275
276 let unfunded_transaction = Transaction {
277 version: 2,
278 lock_time: LockTime::ZERO,
279 input: input
280 .into_iter()
281 .map(|previous_output| TxIn {
282 previous_output,
283 script_sig: ScriptBuf::new(),
284 sequence: Sequence::MAX,
285 witness: Witness::new(),
286 })
287 .collect(),
288 output: vec![
289 TxOut {
290 script_pubkey: runestone.encipher(),
291 value: 0,
292 },
293 TxOut {
294 script_pubkey: wallet.get_change_address()?.script_pubkey(),
295 value: TARGET_POSTAGE.to_sat(),
296 },
297 TxOut {
298 script_pubkey: destination.script_pubkey(),
299 value: TARGET_POSTAGE.to_sat(),
300 },
301 ],
302 };
303
304 let unsigned_transaction =
305 fund_raw_transaction(bitcoin_client, fee_rate, &unfunded_transaction)?;
306
307 let unsigned_transaction = consensus::encode::deserialize(&unsigned_transaction)?;
308
309 assert_eq!(
310 Runestone::decipher(&unsigned_transaction),
311 Some(Artifact::Runestone(runestone)),
312 );
313
314 Ok(unsigned_transaction)
315 }
316}