ord/
lib.rs

1#![allow(
2  clippy::large_enum_variant,
3  clippy::result_large_err,
4  clippy::too_many_arguments,
5  clippy::type_complexity
6)]
7#![deny(
8  clippy::cast_lossless,
9  clippy::cast_possible_truncation,
10  clippy::cast_possible_wrap,
11  clippy::cast_sign_loss
12)]
13
14use {
15  self::{
16    arguments::Arguments,
17    blocktime::Blocktime,
18    decimal::Decimal,
19    deserialize_from_str::DeserializeFromStr,
20    index::BitcoinCoreRpcResultExt,
21    inscriptions::{
22      inscription_id,
23      media::{self, ImageRendering, Media},
24      teleburn, ParsedEnvelope,
25    },
26    into_usize::IntoUsize,
27    representation::Representation,
28    settings::Settings,
29    subcommand::{Subcommand, SubcommandResult},
30    tally::Tally,
31  },
32  anyhow::{anyhow, bail, ensure, Context, Error},
33  bip39::Mnemonic,
34  bitcoin::{
35    address::{Address, NetworkUnchecked},
36    blockdata::{
37      constants::{DIFFCHANGE_INTERVAL, MAX_SCRIPT_ELEMENT_SIZE, SUBSIDY_HALVING_INTERVAL},
38      locktime::absolute::LockTime,
39    },
40    consensus::{self, Decodable, Encodable},
41    hash_types::{BlockHash, TxMerkleNode},
42    hashes::Hash,
43    script, Amount, Block, Network, OutPoint, Script, ScriptBuf, Sequence, Transaction, TxIn,
44    TxOut, Txid, Witness,
45  },
46  bitcoincore_rpc::{Client, RpcApi},
47  chrono::{DateTime, TimeZone, Utc},
48  ciborium::Value,
49  clap::{ArgGroup, Parser},
50  html_escaper::{Escape, Trusted},
51  http::HeaderMap,
52  lazy_static::lazy_static,
53  ordinals::{
54    varint, Artifact, Charm, Edict, Epoch, Etching, Height, Pile, Rarity, Rune, RuneId, Runestone,
55    Sat, SatPoint, SpacedRune, Terms,
56  },
57  regex::Regex,
58  reqwest::Url,
59  serde::{Deserialize, Deserializer, Serialize},
60  serde_with::{DeserializeFromStr, SerializeDisplay},
61  std::{
62    cmp::{self, Reverse},
63    collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque},
64    env,
65    fmt::{self, Display, Formatter},
66    fs,
67    io::{self, Cursor, Read},
68    mem,
69    net::ToSocketAddrs,
70    path::{Path, PathBuf},
71    process::{self, Command, Stdio},
72    str::FromStr,
73    sync::{
74      atomic::{self, AtomicBool},
75      Arc, Mutex,
76    },
77    thread,
78    time::{Duration, Instant, SystemTime},
79  },
80  sysinfo::System,
81  tokio::{runtime::Runtime, task},
82};
83
84pub use self::{
85  chain::Chain,
86  fee_rate::FeeRate,
87  index::{Index, RuneEntry},
88  inscriptions::{Envelope, Inscription, InscriptionId},
89  object::Object,
90  options::Options,
91  wallet::transaction_builder::{Target, TransactionBuilder},
92};
93
94#[cfg(test)]
95#[macro_use]
96mod test;
97
98#[cfg(test)]
99use self::test::*;
100
101pub mod api;
102pub mod arguments;
103mod blocktime;
104pub mod chain;
105pub mod decimal;
106mod deserialize_from_str;
107mod fee_rate;
108pub mod index;
109pub mod inscriptions;
110mod into_usize;
111mod macros;
112mod object;
113pub mod options;
114pub mod outgoing;
115mod re;
116mod representation;
117pub mod runes;
118pub mod settings;
119pub mod subcommand;
120mod tally;
121pub mod templates;
122pub mod wallet;
123
124type Result<T = (), E = Error> = std::result::Result<T, E>;
125
126const TARGET_POSTAGE: Amount = Amount::from_sat(10_000);
127
128pub static SHUTTING_DOWN: AtomicBool = AtomicBool::new(false);
129pub static LISTENERS: Mutex<Vec<axum_server::Handle>> = Mutex::new(Vec::new());
130pub static INDEXER: Mutex<Option<thread::JoinHandle<()>>> = Mutex::new(None);
131
132#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
133fn fund_raw_transaction(
134  client: &Client,
135  fee_rate: FeeRate,
136  unfunded_transaction: &Transaction,
137) -> Result<Vec<u8>> {
138  let mut buffer = Vec::new();
139
140  {
141    unfunded_transaction.version.consensus_encode(&mut buffer)?;
142    unfunded_transaction.input.consensus_encode(&mut buffer)?;
143    unfunded_transaction.output.consensus_encode(&mut buffer)?;
144    unfunded_transaction
145      .lock_time
146      .consensus_encode(&mut buffer)?;
147  }
148
149  Ok(
150    client
151      .fund_raw_transaction(
152        &buffer,
153        Some(&bitcoincore_rpc::json::FundRawTransactionOptions {
154          // NB. This is `fundrawtransaction`'s `feeRate`, which is fee per kvB
155          // and *not* fee per vB. So, we multiply the fee rate given by the user
156          // by 1000.
157          fee_rate: Some(Amount::from_sat((fee_rate.n() * 1000.0).ceil() as u64)),
158          change_position: Some(unfunded_transaction.output.len().try_into()?),
159          ..default()
160        }),
161        Some(false),
162      )
163      .map_err(|err| {
164        if matches!(
165          err,
166          bitcoincore_rpc::Error::JsonRpc(bitcoincore_rpc::jsonrpc::Error::Rpc(
167            bitcoincore_rpc::jsonrpc::error::RpcError { code: -6, .. }
168          ))
169        ) {
170          anyhow!("not enough cardinal utxos")
171        } else {
172          err.into()
173        }
174      })?
175      .hex,
176  )
177}
178
179pub fn timestamp(seconds: u64) -> DateTime<Utc> {
180  Utc
181    .timestamp_opt(seconds.try_into().unwrap_or(i64::MAX), 0)
182    .unwrap()
183}
184
185fn target_as_block_hash(target: bitcoin::Target) -> BlockHash {
186  BlockHash::from_raw_hash(Hash::from_byte_array(target.to_le_bytes()))
187}
188
189pub fn unbound_outpoint() -> OutPoint {
190  OutPoint {
191    txid: Hash::all_zeros(),
192    vout: 0,
193  }
194}
195
196fn uncheck(address: &Address) -> Address<NetworkUnchecked> {
197  address.to_string().parse().unwrap()
198}
199
200fn default<T: Default>() -> T {
201  Default::default()
202}
203
204pub fn parse_ord_server_args(args: &str) -> (Settings, subcommand::server::Server) {
205  match Arguments::try_parse_from(args.split_whitespace()) {
206    Ok(arguments) => match arguments.subcommand {
207      Subcommand::Server(server) => (
208        Settings::merge(
209          arguments.options,
210          vec![("INTEGRATION_TEST".into(), "1".into())]
211            .into_iter()
212            .collect(),
213        )
214        .unwrap(),
215        server,
216      ),
217      subcommand => panic!("unexpected subcommand: {subcommand:?}"),
218    },
219    Err(err) => panic!("error parsing arguments: {err}"),
220  }
221}
222
223fn gracefully_shutdown_indexer() {
224  if let Some(indexer) = INDEXER.lock().unwrap().take() {
225    // We explicitly set this to true to notify the thread to not take on new work
226    SHUTTING_DOWN.store(true, atomic::Ordering::Release);
227    log::info!("Waiting for index thread to finish...");
228    if indexer.join().is_err() {
229      log::warn!("Index thread panicked; join failed");
230    }
231  }
232}
233
234pub fn main() {
235  env_logger::init();
236  ctrlc::set_handler(move || {
237    if SHUTTING_DOWN.fetch_or(true, atomic::Ordering::Acquire) {
238      process::exit(1);
239    }
240
241    eprintln!("Shutting down gracefully. Press <CTRL-C> again to shutdown immediately.");
242
243    LISTENERS
244      .lock()
245      .unwrap()
246      .iter()
247      .for_each(|handle| handle.graceful_shutdown(Some(Duration::from_millis(100))));
248
249    gracefully_shutdown_indexer();
250  })
251  .expect("Error setting <CTRL-C> handler");
252
253  let args = Arguments::parse();
254
255  let minify = args.options.minify;
256
257  match args.run() {
258    Err(err) => {
259      eprintln!("error: {err}");
260      err
261        .chain()
262        .skip(1)
263        .for_each(|cause| eprintln!("because: {cause}"));
264      if env::var_os("RUST_BACKTRACE")
265        .map(|val| val == "1")
266        .unwrap_or_default()
267      {
268        eprintln!("{}", err.backtrace());
269      }
270
271      gracefully_shutdown_indexer();
272
273      process::exit(1);
274    }
275    Ok(output) => {
276      if let Some(output) = output {
277        output.print_json(minify);
278      }
279      gracefully_shutdown_indexer();
280    }
281  }
282}