ordi 0.1.0

Light indexer for Ordinals.
Documentation
use std::{path::PathBuf, thread, time::Duration};

use anyhow::Result;
use bitcoincore_rpc::{Client, RpcApi};
use log::info;
use rusty_leveldb::{Options, WriteBatch, DB};

use crate::block::{InscribeUpdater, TransferUpdater, Tx};
use crate::inscription::Inscription;
use crate::{
    bitcoin::index::{Index, FIRST_INSCRIPTION_HEIGHT},
    block::BlockUpdater,
};

pub mod bitcoin;
pub mod block;
pub mod epoch;
pub mod height;
pub mod inscription;

const ORDI_STATUS: &str = "status";
const ORDI_OUTPUT_VALUE: &str = "output_value";
const ORDI_ID_TO_INSCRIPTION: &str = "id_inscription";
const ORDI_INSCRIPTION_TO_OUTPUT: &str = "inscription_output";
const ORDI_OUTPUT_TO_INSCRIPTION: &str = "output_inscription";

pub struct Ordi {
    pub btc_rpc_client: Client,
    pub status: DB,
    pub output_value: DB,
    pub id_inscription: DB,
    pub inscription_output: DB,
    pub output_inscription: DB,
    pub index: Index,
    pub inscribe_updaters: Vec<InscribeUpdater>,
    pub transfer_updaters: Vec<TransferUpdater>,
}

impl Ordi {
    pub fn new(in_memory: bool) -> Result<Ordi> {
        let mut options = if in_memory {
            rusty_leveldb::in_memory()
        } else {
            Options::default()
        };
        options.max_file_size = 2 << 25;

        let base_path = PathBuf::from(std::env::var("ordi_data_dir")?.as_str());
        let status = DB::open(base_path.join(ORDI_STATUS), options.clone())?;
        let output_value = DB::open(base_path.join(ORDI_OUTPUT_VALUE), options.clone())?;
        let id_inscription = DB::open(base_path.join(ORDI_ID_TO_INSCRIPTION), options.clone())?;
        let inscription_output =
            DB::open(base_path.join(ORDI_INSCRIPTION_TO_OUTPUT), options.clone())?;
        let output_inscription = DB::open(base_path.join(ORDI_OUTPUT_TO_INSCRIPTION), options)?;

        let btc_data_dir = std::env::var("btc_data_dir")?;
        let btc_rpc_client = Client::new(
            std::env::var("btc_rpc_host")?.as_str(),
            bitcoincore_rpc::Auth::UserPass(
                std::env::var("btc_rpc_user")?,
                std::env::var("btc_rpc_pass")?,
            ),
        )?;
        let index = Index::new(PathBuf::from(btc_data_dir))?;

        Ok(Ordi {
            btc_rpc_client,
            status,
            output_value,
            id_inscription,
            inscription_output,
            output_inscription,
            index,
            inscribe_updaters: vec![],
            transfer_updaters: vec![],
        })
    }

    pub fn close(&mut self) {
        self.status.close().expect("Close status db.");
        self.output_value.close().expect("Close output_value db.");
        self.id_inscription
            .close()
            .expect("Close id_inscription db.");
        self.inscription_output
            .close()
            .expect("Close inscription_output db.");
        self.output_inscription
            .close()
            .expect("Close output_inscription db.");
    }

    pub fn start(&mut self) -> anyhow::Result<()> {
        // Catch up latest block.
        let mut next_height = self.index.max_height + 1;
        for height in FIRST_INSCRIPTION_HEIGHT..next_height {
            let block = self.index.catch_block(height)?;
            let mut block_updater = BlockUpdater::new(
                height,
                block,
                &self.btc_rpc_client,
                &mut self.status,
                &mut self.output_value,
                &mut self.id_inscription,
                &mut self.inscription_output,
                &mut self.output_inscription,
                &self.inscribe_updaters,
                &self.transfer_updaters,
            );

            block_updater.index_transactions()?;
        }

        let client = &self.btc_rpc_client;
        loop {
            match client.get_block_hash(next_height) {
                Ok(block_hash) => {
                    let mut block_updater = BlockUpdater::new(
                        next_height,
                        client.get_block(&block_hash)?.into(),
                        &self.btc_rpc_client,
                        &mut self.status,
                        &mut self.output_value,
                        &mut self.id_inscription,
                        &mut self.inscription_output,
                        &mut self.output_inscription,
                        &self.inscribe_updaters,
                        &self.transfer_updaters,
                    );

                    block_updater.index_transactions()?;
                    next_height += 1;
                }
                Err(_) => {
                    thread::sleep(Duration::from_secs(10));
                }
            };
        }
    }

    pub fn index_output_value(&mut self) -> Result<()> {
        for height in 0..FIRST_INSCRIPTION_HEIGHT {
            let block = self.index.catch_block(height)?;
            for (_tx_index, tx) in block.txs.iter().enumerate() {
                self.index_output_value_in_transaction(&tx)?;
            }
        }

        Ok(())
    }

    fn index_output_value_in_transaction(&mut self, tx: &Tx) -> Result<()> {
        let mut wb = WriteBatch::new();
        for (output_index, output) in tx.value.outputs.iter().enumerate() {
            let k = format!("{}:{}", tx.hash.to_string(), output_index);
            wb.put(k.as_bytes(), output.out.value.to_le_bytes().as_slice());
        }

        for input in tx.value.inputs.iter() {
            if input.outpoint.is_null() {
                continue;
            }

            let k = format!(
                "{}:{}",
                input.outpoint.txid.to_string(),
                input.outpoint.index
            );
            wb.delete(k.as_bytes())
        }

        self.output_value.write(wb, false)?;
        Ok(())
    }

    pub fn when_inscribe(&mut self, f: InscribeUpdater) {
        self.inscribe_updaters.push(f);
    }

    pub fn when_transfer(&mut self, f: TransferUpdater) {
        self.transfer_updaters.push(f);
    }
}

impl Drop for Ordi {
    fn drop(&mut self) {
        info!("Start closing Ordi instance.");
        self.close();
        info!("Closed Ordi instance.");
    }
}

#[derive(Clone)]
pub enum Origin {
    New {
        cursed: bool,
        unbound: bool,
        inscription: Inscription,
    },
    Old {
        old_output: String,
        old_offset: u64,
    },
}

#[derive(Clone)]
pub struct Flotsam {
    pub inscription_id: String,
    pub offset: u64,
    pub origin: Origin,
}