ord/subcommand/wallet/
send.rs

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}