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 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 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}