diff --git a/Cargo.lock b/Cargo.lock index 785d429ac4..dcb6960a68 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2323,12 +2323,15 @@ dependencies = [ "mime_guess", "miniscript", "mp4", + "once_cell", "ord-bitcoincore-rpc", "pretty_assertions", "pulldown-cmark", + "rayon", "redb", "regex", "reqwest", + "rmp-serde", "rss", "rust-embed", "rustls 0.22.1", @@ -2439,6 +2442,12 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + [[package]] name = "pem" version = "1.1.1" @@ -2818,6 +2827,28 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "rmp" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9860a6cc38ed1da53456442089b4dfa35e7cedaa326df63017af88385e6b20" +dependencies = [ + "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmp-serde" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bffea85eea980d8a74453e5d02a8d93028f3c34725de143085a844ebe953258a" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + [[package]] name = "rss" version = "2.0.6" diff --git a/Cargo.toml b/Cargo.toml index bc3b936ad4..b9d64d66c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,6 @@ axum-server = "0.5.0" base64 = "0.21.0" bech32 = "0.9.1" bigdecimal = "0.4.2" -bincode = "1.3.3" bip39 = "2.0.0" bitcoin = { version = "0.30.1", features = ["rand"] } boilerplate = { version = "1.0.0", features = ["axum"] } @@ -39,6 +38,7 @@ derive_more = "0.99.17" dirs = "5.0.0" env_logger = "0.10.0" futures = "0.3.21" +bincode = "1.3.3" hex = "0.4.3" html-escaper = "0.2.0" http = "0.2.6" @@ -72,6 +72,9 @@ tower-http = { version = "0.4.0", features = ["compression-br", "compression-gzi utoipa = "4.1.0" thiserror = "1.0.51" log4rs = { version = "1.2.0", features = ["gzip"] } +once_cell = "1.19.0" +rmp-serde = "1.1.2" +rayon = "1.8.0" [dev-dependencies] executable-path = "1.0.0" @@ -95,3 +98,7 @@ path = "tests/lib.rs" [build-dependencies] pulldown-cmark = "0.9.2" shadow-rs = "0.25.0" + +[features] +default = [] +cache = [] diff --git a/src/chain.rs b/src/chain.rs index 1186ac63ad..230687d29b 100644 --- a/src/chain.rs +++ b/src/chain.rs @@ -47,6 +47,15 @@ impl Chain { } } + pub(crate) fn first_brc20_height(self) -> u32 { + match self { + Self::Mainnet => 779832, + Self::Regtest => 0, + Self::Signet => 0, + Self::Testnet => 0, + } + } + pub(crate) fn first_rune_height(self) -> u32 { SUBSIDY_HALVING_INTERVAL * match self { diff --git a/src/index.rs b/src/index.rs index 32f0ddb1a7..b409255c0b 100644 --- a/src/index.rs +++ b/src/index.rs @@ -1,10 +1,18 @@ +use crate::okx::datastore::brc20::redb::table::{ + get_balance, get_balances, get_token_info, get_tokens_info, get_transaction_receipts, + get_transferable, get_transferable_by_tick, +}; +use crate::okx::datastore::ord::redb::table::{ + get_collection_inscription_id, get_collections_of_inscription, get_transaction_operations, + get_txout_by_outpoint, +}; +use crate::okx::datastore::{brc20, ScriptKey}; +use crate::okx::protocol::zeroindexer::datastore::get_zero_indexer_txs; +use crate::okx::protocol::zeroindexer::zerodata::ZeroData; use bitcoincore_rpc::bitcoincore_rpc_json::GetBlockResult; use { self::{ - entry::{ - BlockHashValue, Entry, InscriptionIdValue, OutPointValue, RuneEntryValue, RuneIdValue, - SatPointValue, SatRange, TxidValue, - }, + entry::{BlockHashValue, Entry, RuneEntryValue, RuneIdValue, SatPointValue, SatRange}, reorg::*, runes::{Rune, RuneId}, updater::Updater, @@ -17,10 +25,7 @@ use { chrono::SubsecRound, indicatif::{ProgressBar, ProgressStyle}, log::log_enabled, - okx::datastore::ord::{ - self, bitmap::District, collections::CollectionKind, redb::try_init_tables as try_init_ord, - DataStoreReadOnly, - }, + okx::datastore::ord::{self, bitmap::District, collections::CollectionKind}, redb::{ Database, DatabaseError, MultimapTable, MultimapTableDefinition, MultimapTableHandle, ReadableMultimapTable, ReadableTable, RedbKey, RedbValue, StorageError, Table, TableDefinition, @@ -34,14 +39,16 @@ use { }; pub(crate) use self::entry::RuneEntry; -pub(super) use self::entry::{InscriptionEntry, InscriptionEntryValue}; +pub(super) use self::entry::{ + InscriptionEntry, InscriptionEntryValue, InscriptionIdValue, OutPointValue, TxidValue, +}; pub(super) use self::updater::BlockData; pub(crate) mod entry; mod fetcher; mod reorg; mod rtx; -mod updater; +pub(crate) mod updater; #[cfg(test)] pub(crate) mod testing; @@ -56,7 +63,7 @@ macro_rules! define_table { macro_rules! define_multimap_table { ($name:ident, $key:ty, $value:ty) => { - const $name: MultimapTableDefinition<$key, $value> = + pub const $name: MultimapTableDefinition<$key, $value> = MultimapTableDefinition::new(stringify!($name)); }; } @@ -82,6 +89,20 @@ define_table! { STATISTIC_TO_COUNT, u64, u64 } define_table! { TRANSACTION_ID_TO_RUNE, &TxidValue, u128 } define_table! { WRITE_TRANSACTION_STARTING_BLOCK_COUNT_TO_TIMESTAMP, u32, u128 } +// new +define_table! { ORD_TX_TO_OPERATIONS, &TxidValue, &[u8] } +define_table! { COLLECTIONS_KEY_TO_INSCRIPTION_ID, &str, InscriptionIdValue } +define_table! { COLLECTIONS_INSCRIPTION_ID_TO_KINDS, InscriptionIdValue, &[u8] } + +define_table! { BRC20_BALANCES, &str, &[u8] } +define_table! { BRC20_TOKEN, &str, &[u8] } +define_multimap_table! { BRC20_EVENTS, &TxidValue, &[u8] } +define_table! { BRC20_TRANSFERABLELOG, &str, &[u8] } +define_table! { BRC20_INSCRIBE_TRANSFER, InscriptionIdValue, &[u8] } + +define_table! { ZERO_INSCRIPTION_ID_TO_INSCRIPTION, InscriptionIdValue, &[u8] } +define_table! { ZERO_HEIGHT_TO_TXS, u64, &[u8] } + #[derive(Debug, PartialEq)] pub enum List { Spent, @@ -312,6 +333,18 @@ impl Index { tx.open_table(TRANSACTION_ID_TO_RUNE)?; tx.open_table(WRITE_TRANSACTION_STARTING_BLOCK_COUNT_TO_TIMESTAMP)?; + // new ord tables + tx.open_table(ORD_TX_TO_OPERATIONS)?; + tx.open_table(COLLECTIONS_KEY_TO_INSCRIPTION_ID)?; + tx.open_table(COLLECTIONS_INSCRIPTION_ID_TO_KINDS)?; + + // brc20 tables + tx.open_table(BRC20_BALANCES)?; + tx.open_table(BRC20_TOKEN)?; + tx.open_multimap_table(BRC20_EVENTS)?; + tx.open_table(BRC20_TRANSFERABLELOG)?; + tx.open_table(BRC20_INSCRIBE_TRANSFER)?; + { let mut outpoint_to_sat_ranges = tx.open_table(OUTPOINT_TO_SAT_RANGES)?; let mut statistics = tx.open_table(STATISTIC_TO_COUNT)?; @@ -340,13 +373,7 @@ impl Index { Err(error) => bail!("failed to open index: {error}"), }; - { - let wtx = database.begin_write()?; - let rtx = database.begin_read()?; - try_init_ord(&wtx, &rtx)?; - wtx.commit()?; - log::info!("Options:\n{:#?}", options); - } + log::info!("Options:\n{:#?}", options); let genesis_block_coinbase_transaction = options.chain().genesis_block().coinbase().unwrap().clone(); @@ -700,7 +727,7 @@ impl Index { match updater.update_index() { Ok(ok) => return Ok(ok), Err(err) => { - log::info!("{}", err.to_string()); + log::error!("{}", err.to_string()); match err.downcast_ref() { Some(&ReorgError::Recoverable { height, depth }) => { @@ -1438,10 +1465,9 @@ impl Index { &self, inscription_id: InscriptionId, ) -> Result>> { - Ok( - ord::OrdDbReader::new(&self.database.begin_read()?) - .get_collections_of_inscription(inscription_id)?, - ) + let rtx = self.database.begin_read()?; + let table = rtx.open_table(COLLECTIONS_INSCRIPTION_ID_TO_KINDS)?; + get_collections_of_inscription(&table, &inscription_id) } pub(crate) fn ord_get_district_inscription_id( @@ -1449,10 +1475,9 @@ impl Index { number: u32, ) -> Result> { let district = District { number }; - Ok( - ord::OrdDbReader::new(&self.database.begin_read()?) - .get_collection_inscription_id(&district.to_collection_key())?, - ) + let rtx = self.database.begin_read()?; + let table = rtx.open_table(COLLECTIONS_KEY_TO_INSCRIPTION_ID)?; + get_collection_inscription_id(&table, &district.to_collection_key()) } pub(crate) fn get_inscription_by_id( @@ -1515,7 +1540,7 @@ impl Index { &self, outpoint: OutPoint, ) -> Result> { - Self::transaction_output_by_outpoint( + get_txout_by_outpoint( &self.database.begin_read()?.open_table(OUTPOINT_TO_ENTRY)?, &outpoint, ) @@ -1981,11 +2006,11 @@ impl Index { } } - fn inscriptions_on_output<'a: 'tx, 'tx>( + fn full_inscriptions_on_output<'a: 'tx, 'tx>( satpoint_to_sequence_number: &'a impl ReadableMultimapTable<&'static SatPointValue, u32>, sequence_number_to_inscription_entry: &'a impl ReadableTable, outpoint: OutPoint, - ) -> Result> { + ) -> Result> { let start = SatPoint { outpoint, offset: 0, @@ -2017,32 +2042,33 @@ impl Index { inscriptions.sort_by_key(|(sequence_number, _, _)| *sequence_number); - Ok( - inscriptions - .into_iter() - .map(|(_sequence_number, satpoint, inscription_id)| (satpoint, inscription_id)) - .collect(), - ) + Ok(inscriptions) } - pub(crate) fn transaction_output_by_outpoint( - outpoint_to_entry: &impl ReadableTable<&'static OutPointValue, &'static [u8]>, - outpoint: &OutPoint, - ) -> Result> { - Ok(if let Some(x) = outpoint_to_entry.get(&outpoint.store())? { - Some(TxOut::consensus_decode(&mut io::Cursor::new(x.value()))?) - } else { - None - }) + fn inscriptions_on_output<'a: 'tx, 'tx>( + satpoint_to_sequence_number: &'a impl ReadableMultimapTable<&'static SatPointValue, u32>, + sequence_number_to_inscription_entry: &'a impl ReadableTable, + outpoint: OutPoint, + ) -> Result> { + Ok( + Self::full_inscriptions_on_output( + satpoint_to_sequence_number, + sequence_number_to_inscription_entry, + outpoint, + )? + .into_iter() + .map(|(_sequence_number, satpoint, inscription_id)| (satpoint, inscription_id)) + .collect(), + ) } pub(crate) fn ord_txid_inscriptions( &self, txid: &Txid, ) -> Result>> { - let rtx = self.database.begin_read().unwrap(); - let ord_db = ord::OrdDbReader::new(&rtx); - let res = ord_db.get_transaction_operations(txid)?; + let rtx = self.database.begin_read()?; + let table = rtx.open_table(ORD_TX_TO_OPERATIONS)?; + let res = get_transaction_operations(&table, txid)?; if res.is_empty() { let tx = self.client.get_raw_transaction_info(txid, None)?; @@ -2064,10 +2090,10 @@ impl Index { txs: &Vec, ) -> Result)>> { let rtx = self.database.begin_read()?; - let ord_db = ord::OrdDbReader::new(&rtx); + let table = rtx.open_table(ORD_TX_TO_OPERATIONS)?; let mut result = Vec::new(); for txid in txs { - let inscriptions = ord_db.get_transaction_operations(txid)?; + let inscriptions = get_transaction_operations(&table, txid)?; if inscriptions.is_empty() { continue; } @@ -2075,6 +2101,113 @@ impl Index { } Ok(result) } + + pub(crate) fn brc20_get_tick_info(&self, name: &brc20::Tick) -> Result> { + let rtx = self.database.begin_read().unwrap(); + let table = rtx.open_table(BRC20_TOKEN)?; + let info = get_token_info(&table, name)?; + Ok(info) + } + + pub(crate) fn brc20_get_all_tick_info(&self) -> Result> { + let rtx = self.database.begin_read().unwrap(); + let table = rtx.open_table(BRC20_TOKEN)?; + let info = get_tokens_info(&table)?; + Ok(info) + } + + pub(crate) fn brc20_get_balance_by_address( + &self, + tick: &brc20::Tick, + address: &Address, + ) -> Result> { + let rtx = self.database.begin_read().unwrap(); + let table = rtx.open_table(BRC20_BALANCES)?; + let bal = get_balance(&table, &ScriptKey::from_address(address.clone()), tick)?; + Ok(bal) + } + + pub(crate) fn brc20_get_all_balance_by_address( + &self, + address: &bitcoin::Address, + ) -> Result> { + let rtx = self.database.begin_read().unwrap(); + let table = rtx.open_table(BRC20_BALANCES)?; + Ok(get_balances( + &table, + &ScriptKey::from_address(address.clone()), + )?) + } + + pub(crate) fn brc20_get_tx_events_by_txid( + &self, + txid: &bitcoin::Txid, + ) -> Result>> { + let rtx = self.database.begin_read().unwrap(); + let table = rtx.open_multimap_table(BRC20_EVENTS)?; + let res = get_transaction_receipts(&table, txid)?; + + if res.is_empty() { + let tx = self.client.get_raw_transaction_info(txid, None)?; + if let Some(tx_blockhash) = tx.blockhash { + let tx_bh = self.client.get_block_header_info(&tx_blockhash)?; + let parsed_height = self.begin_read()?.block_height()?; + if parsed_height.is_none() || tx_bh.height as u32 > parsed_height.unwrap().0 { + return Ok(None); + } + } else { + return Err(anyhow!("can't get tx block hash: {txid}")); + } + } + + Ok(Some(res)) + } + + pub(crate) fn brc20_get_txs_events( + &self, + txs: &Vec, + ) -> Result)>> { + let rtx = self.database.begin_read()?; + let table = rtx.open_multimap_table(BRC20_EVENTS)?; + let mut result = Vec::new(); + for txid in txs { + let tx_events = get_transaction_receipts(&table, txid)?; + if tx_events.is_empty() { + continue; + } + result.push((*txid, tx_events)); + } + Ok(result) + } + + pub(crate) fn brc20_get_tick_transferable_by_address( + &self, + tick: &brc20::Tick, + address: &bitcoin::Address, + ) -> Result> { + let rtx = self.database.begin_read().unwrap(); + let table = rtx.open_table(BRC20_TRANSFERABLELOG)?; + let res = get_transferable_by_tick(&table, &ScriptKey::from_address(address.clone()), tick)?; + Ok(res) + } + + pub(crate) fn brc20_get_all_transferable_by_address( + &self, + address: &bitcoin::Address, + ) -> Result> { + let rtx = self.database.begin_read().unwrap(); + let table = rtx.open_table(BRC20_TRANSFERABLELOG)?; + let res = get_transferable(&table, &ScriptKey::from_address(address.clone()))?; + + Ok(res) + } + + pub(crate) fn zero_indexer_get_txs(&self, height: u64) -> Result> { + let rtx = self.database.begin_read().unwrap(); + let table = rtx.open_table(ZERO_HEIGHT_TO_TXS)?; + let res = get_zero_indexer_txs(&table, height)?; + Ok(res) + } } #[cfg(test)] diff --git a/src/index/entry.rs b/src/index/entry.rs index f2347f98a3..7acefbc129 100644 --- a/src/index/entry.rs +++ b/src/index/entry.rs @@ -279,7 +279,7 @@ impl Entry for InscriptionId { } } -pub(super) type OutPointValue = [u8; 36]; +pub(crate) type OutPointValue = [u8; 36]; impl Entry for OutPoint { type Value = OutPointValue; @@ -338,7 +338,7 @@ impl Entry for SatRange { } } -pub(super) type TxidValue = [u8; 32]; +pub(crate) type TxidValue = [u8; 32]; impl Entry for Txid { type Value = TxidValue; diff --git a/src/index/updater.rs b/src/index/updater.rs index fe31f76e31..8e2c20972c 100644 --- a/src/index/updater.rs +++ b/src/index/updater.rs @@ -1,5 +1,5 @@ -use crate::okx::datastore::StateReadWrite; -use crate::okx::protocol::{BlockContext, ProtocolConfig, ProtocolManager}; +use crate::okx::protocol::{context::Context, BlockContext, ProtocolConfig, ProtocolManager}; +use std::sync::atomic::{AtomicUsize, Ordering}; use { self::{inscription_updater::InscriptionUpdater, rune_updater::RuneUpdater}, super::{fetcher::Fetcher, *}, @@ -8,7 +8,9 @@ use { tokio::sync::mpsc::{error::TryRecvError, Receiver, Sender}, }; -mod inscription_updater; +pub(crate) mod inscription_updater; +use crate::okx::lru::SimpleLru; + mod rune_updater; pub(crate) struct BlockData { @@ -89,13 +91,16 @@ impl<'index> Updater<'_> { let (mut outpoint_sender, mut tx_out_receiver) = Self::spawn_fetcher(self.index)?; let mut uncommitted = 0; + let mut tx_out_cache = SimpleLru::new(self.index.options.lru_size); while let Ok(block) = rx.recv() { + tx_out_cache.refresh(); self.index_block( self.index, &mut outpoint_sender, &mut tx_out_receiver, &mut wtx, block, + &mut tx_out_cache, )?; if let Some(progress_bar) = &mut progress_bar { @@ -316,6 +321,7 @@ impl<'index> Updater<'_> { tx_out_receiver: &mut Receiver, wtx: &mut WriteTransaction, block: BlockData, + tx_out_cache: &mut SimpleLru, ) -> Result<()> { Reorg::detect_reorg(&block, self.height, self.index)?; @@ -333,8 +339,11 @@ impl<'index> Updater<'_> { let index_inscriptions = self.height >= index.first_inscription_height; - let mut fetching_outputs_count = 0; - let mut total_outputs_count = 0; + let fetching_outputs_count = AtomicUsize::new(0); + let total_outputs_count = AtomicUsize::new(0); + let cache_outputs_count = AtomicUsize::new(0); + let miss_outputs_count = AtomicUsize::new(0); + let meet_outputs_count = AtomicUsize::new(0); if index_inscriptions { // Send all missing input outpoints to be fetched right away let txids = block @@ -342,27 +351,39 @@ impl<'index> Updater<'_> { .iter() .map(|(_, txid)| txid) .collect::>(); - for (tx, _) in &block.txdata { - for input in &tx.input { - total_outputs_count += 1u64; + use rayon::prelude::*; + let tx_outs = block + .txdata + .par_iter() + .flat_map(|(tx, _)| tx.input.par_iter()) + .filter_map(|input| { + total_outputs_count.fetch_add(1, Ordering::Relaxed); let prev_output = input.previous_output; // We don't need coinbase input value if prev_output.is_null() { - continue; - } - // We don't need input values from txs earlier in the block, since they'll be added to value_cache - // when the tx is indexed - if txids.contains(&prev_output.txid) { - continue; - } - // We don't need input values we already have in our outpoint_to_entry table from earlier blocks that - // were committed to db already - if outpoint_to_entry.get(&prev_output.store())?.is_some() { - continue; + None + } else if txids.contains(&prev_output.txid) { + meet_outputs_count.fetch_add(1, Ordering::Relaxed); + None + } else if tx_out_cache.contains(&prev_output) { + cache_outputs_count.fetch_add(1, Ordering::Relaxed); + None + } else if let Some(txout) = + get_txout_by_outpoint(&outpoint_to_entry, &prev_output).unwrap() + { + miss_outputs_count.fetch_add(1, Ordering::Relaxed); + Some((prev_output, Some(txout))) + } else { + fetching_outputs_count.fetch_add(1, Ordering::Relaxed); + Some((prev_output, None)) } - // We don't know the value of this tx input. Send this outpoint to background thread to be fetched - outpoint_sender.blocking_send(prev_output)?; - fetching_outputs_count += 1u64; + }) + .collect::>(); + for (out_point, value) in tx_outs.into_iter() { + if let Some(tx_out) = value { + tx_out_cache.insert(out_point, tx_out); + } else { + outpoint_sender.blocking_send(out_point).unwrap(); } } } @@ -370,12 +391,16 @@ impl<'index> Updater<'_> { let time = timestamp(block.header.time); log::info!( - "Block {} at {} with {} transactions, fetching previous outputs {}/{}…", + "Block {} at {} with {} transactions, fetching previous outputs {}/{}…, {},{},{}, cost:{}ms", self.height, time, block.txdata.len(), - fetching_outputs_count, - total_outputs_count, + fetching_outputs_count.load(Ordering::Relaxed), + total_outputs_count.load(Ordering::Relaxed), + miss_outputs_count.load(Ordering::Relaxed), + meet_outputs_count.load(Ordering::Relaxed), + cache_outputs_count.load(Ordering::Relaxed), + start.elapsed().as_millis(), ); let mut height_to_block_hash = wtx.open_table(HEIGHT_TO_BLOCK_HASH)?; @@ -420,8 +445,9 @@ impl<'index> Updater<'_> { .map(|(number, _id)| number.value() + 1) .unwrap_or(0); - let mut tx_out_cache = HashMap::new(); + let mut operations = HashMap::new(); let mut inscription_updater = InscriptionUpdater::new( + &mut operations, blessed_inscription_count, self.index.options.chain(), cursed_inscription_count, @@ -440,9 +466,10 @@ impl<'index> Updater<'_> { block.header.time, unbound_inscriptions, tx_out_receiver, - &mut tx_out_cache, + tx_out_cache, )?; + let start_time = Instant::now(); if self.index.index_sats { let mut sat_to_satpoint = wtx.open_table(SAT_TO_SATPOINT)?; let mut outpoint_to_sat_ranges = wtx.open_table(OUTPOINT_TO_SAT_RANGES)?; @@ -538,6 +565,7 @@ impl<'index> Updater<'_> { inscription_updater.index_envelopes(tx, *txid, None)?; } } + let ord_cost = start_time.elapsed().as_millis(); if index_inscriptions { height_to_last_sequence_number @@ -568,24 +596,36 @@ impl<'index> Updater<'_> { &inscription_updater.unbound_inscriptions, )?; - // Create a protocol manager to index the block of bitmap data. - let config = ProtocolConfig::new_with_options(&index.options); - ProtocolManager::new(&StateReadWrite::new(wtx), &config).index_block( - BlockContext { + inscription_updater.flush_cache()?; + + let mut context = Context { + chain: BlockContext { network: index.get_chain_network(), blockheight: self.height, blocktime: block.header.time, }, - &block, - &inscription_updater.operations, - )?; + tx_out_cache, + hit: 0, + miss: 0, + ORD_TX_TO_OPERATIONS: &mut wtx.open_table(ORD_TX_TO_OPERATIONS)?, + COLLECTIONS_KEY_TO_INSCRIPTION_ID: &mut wtx.open_table(COLLECTIONS_KEY_TO_INSCRIPTION_ID)?, + COLLECTIONS_INSCRIPTION_ID_TO_KINDS: &mut wtx + .open_table(COLLECTIONS_INSCRIPTION_ID_TO_KINDS)?, + SEQUENCE_NUMBER_TO_INSCRIPTION_ENTRY: &mut sequence_number_to_inscription_entry, + OUTPOINT_TO_ENTRY: &mut outpoint_to_entry, + BRC20_BALANCES: &mut wtx.open_table(BRC20_BALANCES)?, + BRC20_TOKEN: &mut wtx.open_table(BRC20_TOKEN)?, + BRC20_EVENTS: &mut wtx.open_multimap_table(BRC20_EVENTS)?, + BRC20_TRANSFERABLELOG: &mut wtx.open_table(BRC20_TRANSFERABLELOG)?, + BRC20_INSCRIBE_TRANSFER: &mut wtx.open_table(BRC20_INSCRIBE_TRANSFER)?, + ZERO_INSCRIPTION_ID_TO_INSCRIPTION: &mut wtx + .open_table(ZERO_INSCRIPTION_ID_TO_INSCRIPTION)?, + ZERO_HEIGHT_TO_TXS: &mut wtx.open_table(ZERO_HEIGHT_TO_TXS)?, + }; - // write tx_out to outpoint_to_entry table. - for (outpoint, tx_out) in tx_out_cache { - let mut entry = Vec::new(); - tx_out.consensus_encode(&mut entry)?; - outpoint_to_entry.insert(&outpoint.store(), entry.as_slice())?; - } + // Create a protocol manager to index the block of bitmap data. + let config = ProtocolConfig::new_with_options(&index.options); + ProtocolManager::new(config).index_block(&mut context, &block, operations)?; if index.index_runes && self.height >= self.index.options.first_rune_height() { let mut outpoint_to_rune_balances = wtx.open_table(OUTPOINT_TO_RUNE_BALANCES)?; @@ -624,8 +664,11 @@ impl<'index> Updater<'_> { self.outputs_traversed += outputs_in_block; log::info!( - "Wrote {sat_ranges_written} sat ranges from {outputs_in_block} outputs in {} ms", + "Wrote {sat_ranges_written} sat ranges from {outputs_in_block} outputs in {}/{} ms, hit miss: {}/{}", + ord_cost, (Instant::now() - start).as_millis(), + context.hit, + context.miss, ); Ok(()) @@ -735,3 +778,20 @@ impl<'index> Updater<'_> { Ok(()) } } + + +#[cfg(test)] +mod tests { + use rayon::prelude::*; + #[test] + fn parallel() { + let mut a: Vec<_> = (0..10000).into_par_iter().map(|x| { + x+1 + }).collect(); + + let b = a.clone(); + a.sort(); + assert_eq!(a, b); + println!("{:?}", a); + } +} \ No newline at end of file diff --git a/src/index/updater/inscription_updater.rs b/src/index/updater/inscription_updater.rs index ca04a6147a..c7950689e6 100644 --- a/src/index/updater/inscription_updater.rs +++ b/src/index/updater/inscription_updater.rs @@ -39,7 +39,7 @@ enum Origin { } pub(super) struct InscriptionUpdater<'a, 'db, 'tx> { - pub(super) operations: HashMap>, + pub(super) operations: &'a mut HashMap>, pub(super) blessed_inscription_count: u64, pub(super) chain: Chain, pub(super) cursed_inscription_count: u64, @@ -62,11 +62,13 @@ pub(super) struct InscriptionUpdater<'a, 'db, 'tx> { pub(super) timestamp: u32, pub(super) unbound_inscriptions: u64, pub(super) tx_out_receiver: &'a mut Receiver, - pub(super) tx_out_cache: &'a mut HashMap, + pub(super) tx_out_cache: &'a mut SimpleLru, + pub(super) new_outpoints: Vec, } impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { pub(super) fn new( + operations: &'a mut HashMap>, blessed_inscription_count: u64, chain: Chain, cursed_inscription_count: u64, @@ -85,10 +87,10 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { timestamp: u32, unbound_inscriptions: u64, tx_out_receiver: &'a mut Receiver, - tx_out_cache: &'a mut HashMap, + tx_out_cache: &'a mut SimpleLru, ) -> Result { Ok(Self { - operations: HashMap::new(), + operations, blessed_inscription_count, chain, cursed_inscription_count, @@ -111,6 +113,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { unbound_inscriptions, tx_out_receiver, tx_out_cache, + new_outpoints: vec![], }) } pub(super) fn index_envelopes( @@ -160,8 +163,6 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { let current_input_value = if let Some(tx_out) = self.tx_out_cache.get(&tx_in.previous_output) { tx_out.value - } else if let Some(data) = self.outpoint_to_entry.get(&tx_in.previous_output.store())? { - TxOut::consensus_decode(&mut io::Cursor::new(data.value()))?.value } else { let tx_out = self.tx_out_receiver.blocking_recv().ok_or_else(|| { anyhow!( @@ -169,6 +170,12 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { tx_in.previous_output.txid ) })?; + // received new tx out from chain node, add it to new_outpoints first and persist it in db later. + #[cfg(not(feature = "cache"))] + self.new_outpoints.push(tx_in.previous_output); + self + .tx_out_cache + .insert(tx_in.previous_output, tx_out.clone()); tx_out.value }; @@ -391,6 +398,28 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { } } + // write tx_out to outpoint_to_entry table + pub(super) fn flush_cache(self) -> Result { + let start = Instant::now(); + let persist = self.new_outpoints.len(); + let mut entry = Vec::new(); + for outpoint in self.new_outpoints.into_iter() { + let tx_out = self.tx_out_cache.get(&outpoint).unwrap(); + tx_out.consensus_encode(&mut entry)?; + self + .outpoint_to_entry + .insert(&outpoint.store(), entry.as_slice())?; + entry.clear(); + } + log::info!( + "flush cache, persist:{}, global:{} cost: {}ms", + persist, + self.tx_out_cache.len(), + start.elapsed().as_millis() + ); + Ok(()) + } + fn calculate_sat( input_sat_ranges: Option<&VecDeque<(u64, u64)>>, input_offset: u64, @@ -577,19 +606,12 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { .or_default() .push(InscriptionOp { txid: flotsam.txid, - inscription_number: { - if let Some(number) = self - .id_to_sequence_number - .get(&flotsam.inscription_id.store())? - { - self - .sequence_number_to_entry - .get(number.value())? - .map(|entry| InscriptionEntry::load(entry.value()).inscription_number) - } else { - None - } - }, + // TODO by yxq + sequence_number, + inscription_number: self + .sequence_number_to_entry + .get(sequence_number)? + .map(|entry| InscriptionEntry::load(entry.value()).inscription_number), inscription_id: flotsam.inscription_id, action: match flotsam.origin { Origin::Old => Action::Transfer, diff --git a/src/okx/datastore/brc20/balance.rs b/src/okx/datastore/brc20/balance.rs new file mode 100644 index 0000000000..f3b9d3df5e --- /dev/null +++ b/src/okx/datastore/brc20/balance.rs @@ -0,0 +1,18 @@ +use super::*; +use serde::{Deserialize, Serialize}; +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +pub struct Balance { + pub tick: Tick, + pub overall_balance: u128, + pub transferable_balance: u128, +} + +impl Balance { + pub fn new(tick: &Tick) -> Self { + Self { + tick: tick.clone(), + overall_balance: 0u128, + transferable_balance: 0u128, + } + } +} diff --git a/src/okx/datastore/brc20/errors.rs b/src/okx/datastore/brc20/errors.rs new file mode 100644 index 0000000000..4da0911610 --- /dev/null +++ b/src/okx/datastore/brc20/errors.rs @@ -0,0 +1,66 @@ +use crate::InscriptionId; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, thiserror::Error, Deserialize, Serialize)] +pub enum BRC20Error { + #[error("invalid number: {0}")] + InvalidNum(String), + + #[error("tick invalid supply {0}")] + InvalidSupply(String), + + #[error("tick: {0} has been existed")] + DuplicateTick(String), + + #[error("tick: {0} not found")] + TickNotFound(String), + + #[error("illegal tick length '{0}'")] + InvalidTickLen(String), + + #[error("decimals {0} too large")] + DecimalsTooLarge(u8), + + #[error("tick: {0} has been minted")] + TickMinted(String), + + #[error("tick: {0} mint limit out of range {0}")] + MintLimitOutOfRange(String, String), + + #[error("zero amount not allowed")] + InvalidZeroAmount, + + #[error("amount overflow: {0}")] + AmountOverflow(String), + + #[error("insufficient balance: {0} {1}")] + InsufficientBalance(String, String), + + #[error("amount exceed limit: {0}")] + AmountExceedLimit(String), + + #[error("transferable inscriptionId not found: {0}")] + TransferableNotFound(InscriptionId), + + #[error("invalid inscribe to coinbase")] + InscribeToCoinbase, + + #[error("transferable owner not match {0}")] + TransferableOwnerNotMatch(InscriptionId), + + /// an InternalError is an error that happens exceed our expect + /// and should not happen under normal circumstances + #[error("internal error: {0}")] + InternalError(String), + + // num error + #[error("{op} overflow: original: {org}, other: {other}")] + Overflow { + op: String, + org: String, + other: String, + }, + + #[error("invalid integer {0}")] + InvalidInteger(String), +} diff --git a/src/okx/datastore/brc20/events.rs b/src/okx/datastore/brc20/events.rs new file mode 100644 index 0000000000..0b3e0b51b7 --- /dev/null +++ b/src/okx/datastore/brc20/events.rs @@ -0,0 +1,118 @@ +use super::*; +use crate::{InscriptionId, SatPoint}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub enum OperationType { + Deploy, + Mint, + InscribeTransfer, + Transfer, +} +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct Receipt { + pub inscription_id: InscriptionId, + pub inscription_number: i32, + pub old_satpoint: SatPoint, + pub new_satpoint: SatPoint, + pub op: OperationType, + pub from: ScriptKey, + pub to: ScriptKey, + pub result: Result, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub enum Event { + Deploy(DeployEvent), + Mint(MintEvent), + InscribeTransfer(InscripbeTransferEvent), + Transfer(TransferEvent), +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct DeployEvent { + pub supply: u128, + pub limit_per_mint: u128, + pub decimal: u8, + pub tick: Tick, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct MintEvent { + pub tick: Tick, + pub amount: u128, + pub msg: Option, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct InscripbeTransferEvent { + pub tick: Tick, + pub amount: u128, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct TransferEvent { + pub tick: Tick, + pub amount: u128, + pub msg: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + use bitcoin::Address; + use std::str::FromStr; + + #[test] + fn action_receipt_serialize() { + let action_receipt = Receipt { + inscription_id: InscriptionId::from_str( + "9991111111111111111111111111111111111111111111111111111111111111i1", + ) + .unwrap(), + inscription_number: 1, + old_satpoint: SatPoint::from_str( + "1111111111111111111111111111111111111111111111111111111111111111:1:1", + ) + .unwrap(), + new_satpoint: SatPoint::from_str( + "2111111111111111111111111111111111111111111111111111111111111111:1:1", + ) + .unwrap(), + op: OperationType::Deploy, + from: ScriptKey::from_address( + Address::from_str("bc1qhvd6suvqzjcu9pxjhrwhtrlj85ny3n2mqql5w4") + .unwrap() + .assume_checked(), + ), + to: ScriptKey::from_address( + Address::from_str("bc1qhvd6suvqzjcu9pxjhrwhtrlj85ny3n2mqql5w4") + .unwrap() + .assume_checked(), + ), + result: Err(BRC20Error::InvalidTickLen("abcde".to_string())), + }; + println!("{}", serde_json::to_string_pretty(&action_receipt).unwrap()); + assert_eq!( + serde_json::to_string_pretty(&action_receipt).unwrap(), + r#"{ + "inscription_id": "9991111111111111111111111111111111111111111111111111111111111111i1", + "inscription_number": 1, + "old_satpoint": "1111111111111111111111111111111111111111111111111111111111111111:1:1", + "new_satpoint": "2111111111111111111111111111111111111111111111111111111111111111:1:1", + "op": "Deploy", + "from": { + "Address": "bc1qhvd6suvqzjcu9pxjhrwhtrlj85ny3n2mqql5w4" + }, + "to": { + "Address": "bc1qhvd6suvqzjcu9pxjhrwhtrlj85ny3n2mqql5w4" + }, + "result": { + "Err": { + "InvalidTickLen": "abcde" + } + } +}"# + ); + } +} diff --git a/src/okx/datastore/brc20/mod.rs b/src/okx/datastore/brc20/mod.rs new file mode 100644 index 0000000000..c039338794 --- /dev/null +++ b/src/okx/datastore/brc20/mod.rs @@ -0,0 +1,94 @@ +pub(super) mod balance; +pub(super) mod errors; +pub(super) mod events; +pub mod redb; +pub(super) mod tick; +pub(super) mod token_info; +pub(super) mod transfer; +pub(super) mod transferable_log; + +pub use self::{ + balance::Balance, errors::BRC20Error, events::Receipt, events::*, tick::*, token_info::TokenInfo, + transfer::TransferInfo, transferable_log::TransferableLog, +}; +use super::ScriptKey; +use crate::{InscriptionId, Result}; +use bitcoin::Txid; +use std::fmt::{Debug, Display}; + +pub trait Brc20Reader { + type Error: Debug + Display; + + fn get_balances(&self, script_key: &ScriptKey) -> Result, Self::Error>; + fn get_balance( + &self, + script_key: &ScriptKey, + tick: &Tick, + ) -> Result, Self::Error>; + + fn get_token_info(&self, tick: &Tick) -> Result, Self::Error>; + fn get_tokens_info(&self) -> Result, Self::Error>; + + fn get_transaction_receipts(&self, txid: &Txid) -> Result, Self::Error>; + + fn get_transferable(&self, script: &ScriptKey) -> Result, Self::Error>; + fn get_transferable_by_tick( + &self, + script: &ScriptKey, + tick: &Tick, + ) -> Result, Self::Error>; + fn get_transferable_by_id( + &self, + script: &ScriptKey, + inscription_id: &InscriptionId, + ) -> Result, Self::Error>; + + fn get_inscribe_transfer_inscription( + &self, + inscription_id: &InscriptionId, + ) -> Result, Self::Error>; +} + +pub trait Brc20ReaderWriter: Brc20Reader { + fn update_token_balance( + &mut self, + script_key: &ScriptKey, + new_balance: Balance, + ) -> Result<(), Self::Error>; + + fn insert_token_info(&mut self, tick: &Tick, new_info: &TokenInfo) -> Result<(), Self::Error>; + + fn update_mint_token_info( + &mut self, + tick: &Tick, + minted_amt: u128, + minted_block_number: u32, + ) -> Result<(), Self::Error>; + + fn add_transaction_receipt(&mut self, txid: &Txid, receipt: &Receipt) -> Result<(), Self::Error>; + + fn insert_transferable( + &mut self, + script: &ScriptKey, + tick: &Tick, + inscription: &TransferableLog, + ) -> Result<(), Self::Error>; + + fn remove_transferable( + &mut self, + script: &ScriptKey, + tick: &Tick, + inscription_id: &InscriptionId, + ) -> Result<(), Self::Error>; + + fn insert_inscribe_transfer_inscription( + &mut self, + inscription_id: &InscriptionId, + transfer_info: TransferInfo, + ) -> Result<(), Self::Error>; + + fn remove_inscribe_transfer_inscription( + &mut self, + inscription_id: &InscriptionId, + ) -> Result<(), Self::Error>; +} diff --git a/src/okx/datastore/brc20/redb/mod.rs b/src/okx/datastore/brc20/redb/mod.rs new file mode 100644 index 0000000000..37d40aaaae --- /dev/null +++ b/src/okx/datastore/brc20/redb/mod.rs @@ -0,0 +1,34 @@ +pub mod table; + +use super::{LowerTick, ScriptKey, Tick}; +use crate::inscription_id::InscriptionId; + +fn script_tick_id_key(script: &ScriptKey, tick: &Tick, inscription_id: &InscriptionId) -> String { + format!( + "{}_{}_{}", + script, + tick.to_lowercase().hex(), + inscription_id + ) +} + +fn min_script_tick_id_key(script: &ScriptKey, tick: &Tick) -> String { + script_tick_key(script, tick) +} + +fn max_script_tick_id_key(script: &ScriptKey, tick: &Tick) -> String { + // because hex format of `InscriptionId` will be 0~f, so `g` is greater than `InscriptionId.to_string()` in bytes order + format!("{}_{}_g", script, tick.to_lowercase().hex()) +} + +fn script_tick_key(script: &ScriptKey, tick: &Tick) -> String { + format!("{}_{}", script, tick.to_lowercase().hex()) +} + +fn min_script_tick_key(script: &ScriptKey) -> String { + format!("{}_{}", script, LowerTick::min_hex()) +} + +fn max_script_tick_key(script: &ScriptKey) -> String { + format!("{}_{}", script, LowerTick::max_hex()) +} diff --git a/src/okx/datastore/brc20/redb/table.rs b/src/okx/datastore/brc20/redb/table.rs new file mode 100644 index 0000000000..dd4174f319 --- /dev/null +++ b/src/okx/datastore/brc20/redb/table.rs @@ -0,0 +1,260 @@ +use crate::index::entry::Entry; +use crate::index::{InscriptionIdValue, TxidValue}; +use crate::inscription_id::InscriptionId; +use crate::okx::datastore::brc20::redb::{ + max_script_tick_id_key, max_script_tick_key, min_script_tick_id_key, min_script_tick_key, + script_tick_id_key, script_tick_key, +}; +use crate::okx::datastore::brc20::{ + Balance, Receipt, Tick, TokenInfo, TransferInfo, TransferableLog, +}; +use crate::okx::datastore::ScriptKey; +use bitcoin::Txid; +use redb::{MultimapTable, ReadableMultimapTable, ReadableTable, Table}; + +// BRC20_BALANCES +pub fn get_balances(table: &T, script_key: &ScriptKey) -> crate::Result> +where + T: ReadableTable<&'static str, &'static [u8]>, +{ + Ok( + table + .range(min_script_tick_key(script_key).as_str()..=max_script_tick_key(script_key).as_str())? + .flat_map(|result| { + result.map(|(_, data)| rmp_serde::from_slice::(data.value()).unwrap()) + }) + .collect(), + ) +} + +// BRC20_BALANCES +pub fn get_balance( + table: &T, + script_key: &ScriptKey, + tick: &Tick, +) -> crate::Result> +where + T: ReadableTable<&'static str, &'static [u8]>, +{ + Ok( + table + .get(script_tick_key(script_key, tick).as_str())? + .map(|v| rmp_serde::from_slice::(v.value()).unwrap()), + ) +} + +// BRC20_TOKEN +pub fn get_token_info(table: &T, tick: &Tick) -> crate::Result> +where + T: ReadableTable<&'static str, &'static [u8]>, +{ + Ok( + table + .get(tick.to_lowercase().hex().as_str())? + .map(|v| rmp_serde::from_slice::(v.value()).unwrap()), + ) +} + +// BRC20_TOKEN +pub fn get_tokens_info(table: &T) -> crate::Result> +where + T: ReadableTable<&'static str, &'static [u8]>, +{ + Ok( + table + .range::<&str>(..)? + .flat_map(|result| { + result.map(|(_, data)| rmp_serde::from_slice::(data.value()).unwrap()) + }) + .collect(), + ) +} + +// BRC20_EVENTS +pub fn get_transaction_receipts(table: &T, txid: &Txid) -> crate::Result> +where + T: ReadableMultimapTable<&'static TxidValue, &'static [u8]>, +{ + Ok( + table + .get(&txid.store())? + .into_iter() + .map(|x| rmp_serde::from_slice::(x.unwrap().value()).unwrap()) + .collect(), + ) +} + +// BRC20_TRANSFERABLELOG +pub fn get_transferable(table: &T, script: &ScriptKey) -> crate::Result> +where + T: ReadableTable<&'static str, &'static [u8]>, +{ + Ok( + table + .range(min_script_tick_key(script).as_str()..max_script_tick_key(script).as_str())? + .flat_map(|result| { + result.map(|(_, v)| rmp_serde::from_slice::(v.value()).unwrap()) + }) + .collect(), + ) +} + +// BRC20_TRANSFERABLELOG +pub fn get_transferable_by_tick( + table: &T, + script: &ScriptKey, + tick: &Tick, +) -> crate::Result> +where + T: ReadableTable<&'static str, &'static [u8]>, +{ + Ok( + table + .range( + min_script_tick_id_key(script, tick).as_str() + ..max_script_tick_id_key(script, tick).as_str(), + )? + .flat_map(|result| { + result.map(|(_, v)| rmp_serde::from_slice::(v.value()).unwrap()) + }) + .collect(), + ) +} + +// BRC20_TRANSFERABLELOG +pub fn get_transferable_by_id( + table: &T, + script: &ScriptKey, + inscription_id: &InscriptionId, +) -> crate::Result> +where + T: ReadableTable<&'static str, &'static [u8]>, +{ + Ok( + get_transferable(table, script)? + .iter() + .find(|log| log.inscription_id == *inscription_id) + .cloned(), + ) +} + +// BRC20_INSCRIBE_TRANSFER +pub fn get_inscribe_transfer_inscription( + table: &T, + inscription_id: &InscriptionId, +) -> crate::Result> +where + T: ReadableTable, +{ + Ok( + table + .get(&inscription_id.store())? + .map(|v| rmp_serde::from_slice::(v.value()).unwrap()), + ) +} + +// BRC20_BALANCES +pub fn update_token_balance<'db, 'txn>( + table: &mut Table<'db, 'txn, &'static str, &'static [u8]>, + script_key: &ScriptKey, + new_balance: Balance, +) -> crate::Result<()> { + table.insert( + script_tick_key(script_key, &new_balance.tick).as_str(), + rmp_serde::to_vec(&new_balance).unwrap().as_slice(), + )?; + Ok(()) +} + +// BRC20_TOKEN +pub fn insert_token_info<'db, 'txn>( + table: &mut Table<'db, 'txn, &'static str, &'static [u8]>, + tick: &Tick, + new_info: &TokenInfo, +) -> crate::Result<()> { + table.insert( + tick.to_lowercase().hex().as_str(), + rmp_serde::to_vec(new_info).unwrap().as_slice(), + )?; + Ok(()) +} + +// BRC20_TOKEN +pub fn update_mint_token_info<'db, 'txn>( + table: &mut Table<'db, 'txn, &'static str, &'static [u8]>, + tick: &Tick, + minted_amt: u128, + minted_block_number: u32, +) -> crate::Result<()> { + let mut info = + get_token_info(table, tick)?.unwrap_or_else(|| panic!("token {} not exist", tick.as_str())); + + info.minted = minted_amt; + info.latest_mint_number = minted_block_number; + + table.insert( + tick.to_lowercase().hex().as_str(), + rmp_serde::to_vec(&info).unwrap().as_slice(), + )?; + Ok(()) +} + +// BRC20_EVENTS +pub fn add_transaction_receipt<'db, 'txn>( + table: &mut MultimapTable<'db, 'txn, &'static TxidValue, &'static [u8]>, + txid: &Txid, + receipt: &Receipt, +) -> crate::Result<()> { + table.insert( + &txid.store(), + rmp_serde::to_vec(receipt).unwrap().as_slice(), + )?; + Ok(()) +} + +// BRC20_TRANSFERABLELOG +pub fn insert_transferable<'db, 'txn>( + table: &mut Table<'db, 'txn, &'static str, &'static [u8]>, + script: &ScriptKey, + tick: &Tick, + inscription: &TransferableLog, +) -> crate::Result<()> { + table.insert( + script_tick_id_key(script, tick, &inscription.inscription_id).as_str(), + rmp_serde::to_vec(&inscription).unwrap().as_slice(), + )?; + Ok(()) +} + +// BRC20_TRANSFERABLELOG +pub fn remove_transferable<'db, 'txn>( + table: &mut Table<'db, 'txn, &'static str, &'static [u8]>, + script: &ScriptKey, + tick: &Tick, + inscription_id: &InscriptionId, +) -> crate::Result<()> { + table.remove(script_tick_id_key(script, tick, inscription_id).as_str())?; + Ok(()) +} + +// BRC20_INSCRIBE_TRANSFER +pub fn insert_inscribe_transfer_inscription<'db, 'txn>( + table: &mut Table<'db, 'txn, InscriptionIdValue, &'static [u8]>, + inscription_id: &InscriptionId, + transfer_info: TransferInfo, +) -> crate::Result<()> { + table.insert( + &inscription_id.store(), + rmp_serde::to_vec(&transfer_info).unwrap().as_slice(), + )?; + Ok(()) +} + +// BRC20_INSCRIBE_TRANSFER +pub fn remove_inscribe_transfer_inscription<'db, 'txn>( + table: &mut Table<'db, 'txn, InscriptionIdValue, &'static [u8]>, + inscription_id: &InscriptionId, +) -> crate::Result<()> { + table.remove(&inscription_id.store())?; + Ok(()) +} diff --git a/src/okx/datastore/brc20/tick.rs b/src/okx/datastore/brc20/tick.rs new file mode 100644 index 0000000000..ed6533f055 --- /dev/null +++ b/src/okx/datastore/brc20/tick.rs @@ -0,0 +1,173 @@ +use super::*; +use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; +use std::{fmt::Formatter, str::FromStr}; + +pub const TICK_BYTE_COUNT: usize = 4; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct Tick([u8; TICK_BYTE_COUNT]); + +impl FromStr for Tick { + type Err = BRC20Error; + + fn from_str(s: &str) -> Result { + let bytes = s.as_bytes(); + + if bytes.len() != TICK_BYTE_COUNT { + return Err(BRC20Error::InvalidTickLen(s.to_string())); + } + + Ok(Self(bytes.try_into().unwrap())) + } +} + +impl Tick { + pub fn as_str(&self) -> &str { + // NOTE: Tick comes from &str by from_str, + // so it could be calling unwrap when convert to str + std::str::from_utf8(self.0.as_slice()).unwrap() + } + + pub fn to_lowercase(&self) -> LowerTick { + LowerTick::new(&self.as_str().to_lowercase()) + } +} + +impl Serialize for Tick { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.as_str().serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Tick { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Self::from_str(&String::deserialize(deserializer)?) + .map_err(|e| de::Error::custom(format!("deserialize tick error: {}", e))) + } +} + +impl Display for Tick { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct LowerTick(Box<[u8]>); + +impl LowerTick { + fn new(str: &str) -> Self { + LowerTick(str.as_bytes().to_vec().into_boxed_slice()) + } + + pub fn as_str(&self) -> &str { + std::str::from_utf8(&self.0).unwrap() + } + + pub fn hex(&self) -> String { + let mut data = [0u8; TICK_BYTE_COUNT * 4]; + data[..self.0.len()].copy_from_slice(&self.0); + hex::encode(data) + } + + pub fn min_hex() -> String { + hex::encode([0u8; TICK_BYTE_COUNT * 4]) + } + + pub fn max_hex() -> String { + hex::encode([0xffu8; TICK_BYTE_COUNT * 4]) + } +} + +impl Display for LowerTick { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_tick_length_case() { + assert!(Tick::from_str("XAİ").is_ok()); + assert!(Tick::from_str("XAİİ").is_err()); + assert!("XAİ".parse::().is_ok()); + assert!("XAİİ".parse::().is_err()); + assert!(Tick::from_str("X。").is_ok()); + assert!("X。".parse::().is_ok()); + assert!(Tick::from_str("aBc1").is_ok()); + assert!("aBc1".parse::().is_ok()); + assert!("ατ".parse::().is_ok()); + assert!("∑ii".parse::().is_err()); + assert!("∑i".parse::().is_ok()); + assert!("⊢i".parse::().is_ok()); + assert!("⊢ii".parse::().is_err()); + assert!("≯a".parse::().is_ok()); + assert!("a≯a".parse::().is_err()); + } + #[test] + fn test_tick_hex() { + assert_eq!( + Tick::from_str("XAİ").unwrap().to_lowercase().hex(), + "786169cc870000000000000000000000" + ); + assert_eq!( + Tick::from_str("aBc1").unwrap().to_lowercase().hex(), + "61626331000000000000000000000000" + ); + } + + #[test] + fn test_tick_unicode_lowercase() { + assert_eq!( + Tick::from_str("XAİ").unwrap().to_lowercase().as_str(), + "xai\u{307}" + ); + assert_eq!( + Tick::from_str("aBc1").unwrap().to_lowercase().as_str(), + "abc1", + ); + assert_eq!("ατ".parse::().unwrap().to_lowercase().as_str(), "ατ"); + assert_eq!("∑H".parse::().unwrap().to_lowercase().as_str(), "∑h"); + assert_eq!("⊢I".parse::().unwrap().to_lowercase().as_str(), "⊢i"); + assert_eq!("≯A".parse::().unwrap().to_lowercase().as_str(), "≯a"); + } + + #[test] + fn test_tick_compare_ignore_case() { + assert_ne!(Tick::from_str("aBc1"), Tick::from_str("AbC1")); + + assert_ne!(Tick::from_str("aBc1"), Tick::from_str("aBc2")); + + assert_eq!( + Tick::from_str("aBc1").unwrap().to_lowercase(), + Tick::from_str("AbC1").unwrap().to_lowercase(), + ); + assert_ne!( + Tick::from_str("aBc1").unwrap().to_lowercase(), + Tick::from_str("AbC2").unwrap().to_lowercase(), + ); + } + + #[test] + fn test_tick_serialize() { + let obj = Tick::from_str("Ab1;").unwrap(); + assert_eq!(serde_json::to_string(&obj).unwrap(), r#""Ab1;""#); + } + + #[test] + fn test_tick_deserialize() { + assert_eq!( + serde_json::from_str::(r#""Ab1;""#).unwrap(), + Tick::from_str("Ab1;").unwrap() + ); + } +} diff --git a/src/okx/datastore/brc20/token_info.rs b/src/okx/datastore/brc20/token_info.rs new file mode 100644 index 0000000000..bce89a90f3 --- /dev/null +++ b/src/okx/datastore/brc20/token_info.rs @@ -0,0 +1,17 @@ +use super::*; +use crate::InscriptionId; +use serde::{Deserialize, Serialize}; +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct TokenInfo { + pub tick: Tick, + pub inscription_id: InscriptionId, + pub inscription_number: i32, + pub supply: u128, + pub minted: u128, + pub limit_per_mint: u128, + pub decimal: u8, + pub deploy_by: ScriptKey, + pub deployed_number: u32, + pub deployed_timestamp: u32, + pub latest_mint_number: u32, +} diff --git a/src/okx/datastore/brc20/transfer.rs b/src/okx/datastore/brc20/transfer.rs new file mode 100644 index 0000000000..a290793a4b --- /dev/null +++ b/src/okx/datastore/brc20/transfer.rs @@ -0,0 +1,7 @@ +use super::*; +use serde::{Deserialize, Serialize}; +#[derive(Debug, PartialEq, Deserialize, Serialize)] +pub struct TransferInfo { + pub tick: Tick, + pub amt: u128, +} diff --git a/src/okx/datastore/brc20/transferable_log.rs b/src/okx/datastore/brc20/transferable_log.rs new file mode 100644 index 0000000000..dfaa25742d --- /dev/null +++ b/src/okx/datastore/brc20/transferable_log.rs @@ -0,0 +1,11 @@ +use super::*; +use crate::InscriptionId; +use serde::{Deserialize, Serialize}; +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +pub struct TransferableLog { + pub inscription_id: InscriptionId, + pub inscription_number: i32, + pub amount: u128, + pub tick: Tick, + pub owner: ScriptKey, +} diff --git a/src/okx/datastore/mod.rs b/src/okx/datastore/mod.rs index 9b50e6c345..d17e469105 100644 --- a/src/okx/datastore/mod.rs +++ b/src/okx/datastore/mod.rs @@ -1,28 +1,5 @@ +pub mod brc20; pub mod ord; -mod redb; mod script_key; -pub use self::{ - redb::{StateReadOnly, StateReadWrite}, - script_key::ScriptKey, -}; - -/// StateReader is a collection of multiple readonly storages. -/// -/// There are multiple categories in the storage, and they can be obtained separately. -pub trait StateReader { - type OrdReader: ord::DataStoreReadOnly; - - // Returns a reference to the readonly Ord store. - fn ord(&self) -> &Self::OrdReader; -} - -/// StateRWriter is a collection of multiple read-write storages. -/// -/// There are multiple categories in the storage, and they can be obtained separately. -pub trait StateRWriter { - type OrdRWriter: ord::DataStoreReadWrite; - - // Returns a reference to the read-write ord store. - fn ord(&self) -> &Self::OrdRWriter; -} +pub use self::script_key::ScriptKey; diff --git a/src/okx/datastore/ord/collections.rs b/src/okx/datastore/ord/collections.rs index b56928d08f..e0240707e9 100644 --- a/src/okx/datastore/ord/collections.rs +++ b/src/okx/datastore/ord/collections.rs @@ -1,14 +1,19 @@ use serde::{Deserialize, Serialize}; +use std::fmt::Display; // the act of marking an inscription. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub enum CollectionKind { BitMap, } -impl ToString for CollectionKind { - fn to_string(&self) -> String { - match self { - CollectionKind::BitMap => String::from("bitmap"), - } +impl Display for CollectionKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + CollectionKind::BitMap => String::from("bitmap"), + } + ) } } diff --git a/src/okx/datastore/ord/mod.rs b/src/okx/datastore/ord/mod.rs index 24e3318a70..ba79b054be 100644 --- a/src/okx/datastore/ord/mod.rs +++ b/src/okx/datastore/ord/mod.rs @@ -1,33 +1,38 @@ -pub use self::{ - operation::{Action, InscriptionOp}, - redb::{OrdDbReadWriter, OrdDbReader}, -}; +pub use self::operation::{Action, InscriptionOp}; +use bitcoin::Network; +use crate::okx::datastore::ScriptKey; +use crate::SatPoint; use { crate::{InscriptionId, Result}, - bitcoin::{OutPoint, TxOut, Txid}, + bitcoin::Txid, collections::CollectionKind, std::fmt::{Debug, Display}, }; + pub mod bitmap; pub mod collections; pub mod operation; pub mod redb; -pub trait DataStoreReadOnly { +pub trait OrdReader { type Error: Debug + Display; - fn get_number_by_inscription_id( + fn get_inscription_number_by_sequence_number( &self, - inscription_id: InscriptionId, - ) -> Result, Self::Error>; + sequence_number: u32, + ) -> Result; - fn get_outpoint_to_txout(&self, outpoint: OutPoint) -> Result, Self::Error>; + fn get_script_key_on_satpoint( + &mut self, + satpoint: &SatPoint, + network: Network, + ) -> Result; fn get_transaction_operations(&self, txid: &Txid) -> Result, Self::Error>; fn get_collections_of_inscription( &self, - inscription_id: InscriptionId, + inscription_id: &InscriptionId, ) -> Result>, Self::Error>; fn get_collection_inscription_id( @@ -36,24 +41,22 @@ pub trait DataStoreReadOnly { ) -> Result, Self::Error>; } -pub trait DataStoreReadWrite: DataStoreReadOnly { - fn set_outpoint_to_txout(&self, outpoint: OutPoint, tx_out: &TxOut) -> Result<(), Self::Error>; - +pub trait OrdReaderWriter: OrdReader { fn save_transaction_operations( - &self, + &mut self, txid: &Txid, operations: &[InscriptionOp], ) -> Result<(), Self::Error>; fn set_inscription_by_collection_key( - &self, + &mut self, key: &str, - inscription_id: InscriptionId, + inscription_id: &InscriptionId, ) -> Result<(), Self::Error>; fn set_inscription_attributes( - &self, - inscription_id: InscriptionId, + &mut self, + inscription_id: &InscriptionId, kind: &[CollectionKind], ) -> Result<(), Self::Error>; } diff --git a/src/okx/datastore/ord/operation.rs b/src/okx/datastore/ord/operation.rs index 6514b9febe..604b2f8f14 100644 --- a/src/okx/datastore/ord/operation.rs +++ b/src/okx/datastore/ord/operation.rs @@ -9,6 +9,7 @@ use { pub struct InscriptionOp { pub txid: Txid, pub action: Action, + pub sequence_number: u32, pub inscription_number: Option, pub inscription_id: InscriptionId, pub old_satpoint: SatPoint, diff --git a/src/okx/datastore/ord/redb/mod.rs b/src/okx/datastore/ord/redb/mod.rs index 7fc7248a81..13971b0a5d 100644 --- a/src/okx/datastore/ord/redb/mod.rs +++ b/src/okx/datastore/ord/redb/mod.rs @@ -1,15 +1 @@ -pub mod read_only; -pub mod read_write; - -pub use self::{ - read_only::OrdDbReader, - read_write::{try_init_tables, OrdDbReadWriter}, -}; -use {super::CollectionKind, redb::TableDefinition}; - -const ORD_TX_TO_OPERATIONS: TableDefinition<&str, &[u8]> = - TableDefinition::new("ORD_TX_TO_OPERATIONS"); -const COLLECTIONS_KEY_TO_INSCRIPTION_ID: TableDefinition<&str, &[u8; 36]> = - TableDefinition::new("COLLECTIONS_KEY_TO_INSCRIPTION_ID"); -const COLLECTIONS_INSCRIPTION_ID_TO_KINDS: TableDefinition<&[u8; 36], &[u8]> = - TableDefinition::new("COLLECTIONS_INSCRIPTION_ID_TO_KINDS"); +pub mod table; diff --git a/src/okx/datastore/ord/redb/read_only.rs b/src/okx/datastore/ord/redb/read_only.rs deleted file mode 100644 index 65fb447be3..0000000000 --- a/src/okx/datastore/ord/redb/read_only.rs +++ /dev/null @@ -1,160 +0,0 @@ -use crate::index::entry::Entry; -use { - super::*, - crate::{ - index::{ - INSCRIPTION_ID_TO_SEQUENCE_NUMBER, OUTPOINT_TO_ENTRY, SEQUENCE_NUMBER_TO_INSCRIPTION_ENTRY, - }, - okx::datastore::ord::{DataStoreReadOnly, InscriptionOp}, - Hash, InscriptionId, Result, - }, - bitcoin::{ - consensus::{Decodable, Encodable}, - OutPoint, TxOut, Txid, - }, - redb::{ - AccessGuard, ReadOnlyTable, ReadTransaction, ReadableTable, RedbKey, RedbValue, StorageError, - Table, TableDefinition, WriteTransaction, - }, - std::{borrow::Borrow, io}, -}; - -pub struct OrdDbReader<'db, 'a> { - wrapper: ReaderWrapper<'db, 'a>, -} - -pub(crate) fn new_with_wtx<'db, 'a>(wtx: &'a WriteTransaction<'db>) -> OrdDbReader<'db, 'a> { - OrdDbReader { - wrapper: ReaderWrapper::Wtx(wtx), - } -} - -impl<'db, 'a> OrdDbReader<'db, 'a> { - #[allow(dead_code)] - pub fn new(rtx: &'a ReadTransaction<'db>) -> Self { - Self { - wrapper: ReaderWrapper::Rtx(rtx), - } - } -} -#[allow(dead_code)] -enum ReaderWrapper<'db, 'a> { - Rtx(&'a ReadTransaction<'db>), - Wtx(&'a WriteTransaction<'db>), -} - -impl<'db, 'a> ReaderWrapper<'db, 'a> { - fn open_table( - &self, - definition: TableDefinition<'_, K, V>, - ) -> Result, redb::Error> { - match self { - Self::Rtx(rtx) => Ok(TableWrapper::RtxTable(rtx.open_table(definition)?)), - Self::Wtx(wtx) => Ok(TableWrapper::WtxTable(wtx.open_table(definition)?)), - } - } -} - -enum TableWrapper<'db, 'txn, K: RedbKey + 'static, V: RedbValue + 'static> { - RtxTable(ReadOnlyTable<'txn, K, V>), - WtxTable(Table<'db, 'txn, K, V>), -} - -impl<'db, 'txn, K: RedbKey + 'static, V: RedbValue + 'static> TableWrapper<'db, 'txn, K, V> { - fn get<'a>( - &self, - key: impl Borrow>, - ) -> Result>, StorageError> - where - K: 'a, - { - match self { - Self::RtxTable(rtx_table) => rtx_table.get(key), - Self::WtxTable(wtx_table) => wtx_table.get(key), - } - } -} - -impl<'db, 'a> DataStoreReadOnly for OrdDbReader<'db, 'a> { - type Error = redb::Error; - fn get_collections_of_inscription( - &self, - inscription_id: InscriptionId, - ) -> Result>, Self::Error> { - let mut key = [0; 36]; - let (txid, index) = key.split_at_mut(32); - txid.copy_from_slice(inscription_id.txid.as_ref()); - index.copy_from_slice(&inscription_id.index.to_be_bytes()); - - Ok( - self - .wrapper - .open_table(COLLECTIONS_INSCRIPTION_ID_TO_KINDS)? - .get(&key)? - .map(|v| bincode::deserialize::>(v.value()).unwrap()), - ) - } - - fn get_collection_inscription_id(&self, key: &str) -> Result, Self::Error> { - Ok( - self - .wrapper - .open_table(COLLECTIONS_KEY_TO_INSCRIPTION_ID)? - .get(key)? - .map(|v| { - let (txid, index) = v.value().split_at(32); - InscriptionId { - txid: Txid::from_raw_hash(Hash::from_slice(txid).unwrap()), - index: u32::from_be_bytes(index.try_into().unwrap()), - } - }), - ) - } - - fn get_number_by_inscription_id( - &self, - inscription_id: InscriptionId, - ) -> Result, Self::Error> { - let table = self.wrapper.open_table(INSCRIPTION_ID_TO_SEQUENCE_NUMBER)?; - - let sequence_number = table.get(inscription_id.store())?; - - if let Some(sequence_number) = sequence_number { - Ok( - self - .wrapper - .open_table(SEQUENCE_NUMBER_TO_INSCRIPTION_ENTRY)? - .get(sequence_number.value())? - .map(|entry| entry.value().4), - ) - } else { - Ok(None) - } - } - - fn get_outpoint_to_txout(&self, outpoint: OutPoint) -> Result, Self::Error> { - let mut value = [0; 36]; - outpoint - .consensus_encode(&mut value.as_mut_slice()) - .unwrap(); - Ok( - self - .wrapper - .open_table(OUTPOINT_TO_ENTRY)? - .get(&value)? - .map(|x| Decodable::consensus_decode(&mut io::Cursor::new(x.value())).unwrap()), - ) - } - - fn get_transaction_operations(&self, txid: &Txid) -> Result, Self::Error> { - Ok( - self - .wrapper - .open_table(ORD_TX_TO_OPERATIONS)? - .get(txid.to_string().as_str())? - .map_or(Vec::new(), |v| { - bincode::deserialize::>(v.value()).unwrap() - }), - ) - } -} diff --git a/src/okx/datastore/ord/redb/read_write.rs b/src/okx/datastore/ord/redb/read_write.rs deleted file mode 100644 index 39f497b4b3..0000000000 --- a/src/okx/datastore/ord/redb/read_write.rs +++ /dev/null @@ -1,198 +0,0 @@ -use { - super::*, - crate::{ - index::OUTPOINT_TO_ENTRY, - okx::datastore::ord::{DataStoreReadOnly, DataStoreReadWrite, InscriptionOp}, - InscriptionId, Result, - }, - bitcoin::{consensus::Encodable, OutPoint, TxOut, Txid}, - redb::{ReadTransaction, WriteTransaction}, -}; - -pub fn try_init_tables<'db, 'a>( - wtx: &'a WriteTransaction<'db>, - rtx: &'a ReadTransaction<'db>, -) -> Result { - if rtx.open_table(ORD_TX_TO_OPERATIONS).is_err() { - wtx.open_table(ORD_TX_TO_OPERATIONS)?; - wtx.open_table(COLLECTIONS_KEY_TO_INSCRIPTION_ID)?; - wtx.open_table(COLLECTIONS_INSCRIPTION_ID_TO_KINDS)?; - } - Ok(true) -} - -pub struct OrdDbReadWriter<'db, 'a> { - wtx: &'a WriteTransaction<'db>, -} - -impl<'db, 'a> OrdDbReadWriter<'db, 'a> -where - 'db: 'a, -{ - pub fn new(wtx: &'a WriteTransaction<'db>) -> Self { - Self { wtx } - } -} - -impl<'db, 'a> DataStoreReadOnly for OrdDbReadWriter<'db, 'a> { - type Error = redb::Error; - fn get_number_by_inscription_id( - &self, - inscription_id: InscriptionId, - ) -> Result, Self::Error> { - read_only::new_with_wtx(self.wtx).get_number_by_inscription_id(inscription_id) - } - - fn get_outpoint_to_txout(&self, outpoint: OutPoint) -> Result, Self::Error> { - read_only::new_with_wtx(self.wtx).get_outpoint_to_txout(outpoint) - } - - fn get_transaction_operations( - &self, - txid: &bitcoin::Txid, - ) -> Result, Self::Error> { - read_only::new_with_wtx(self.wtx).get_transaction_operations(txid) - } - // collections - fn get_collection_inscription_id(&self, key: &str) -> Result, Self::Error> { - read_only::new_with_wtx(self.wtx).get_collection_inscription_id(key) - } - fn get_collections_of_inscription( - &self, - inscription_id: InscriptionId, - ) -> Result>, Self::Error> { - read_only::new_with_wtx(self.wtx).get_collections_of_inscription(inscription_id) - } -} - -impl<'db, 'a> DataStoreReadWrite for OrdDbReadWriter<'db, 'a> { - // OUTPOINT_TO_SCRIPT - - fn set_outpoint_to_txout(&self, outpoint: OutPoint, tx_out: &TxOut) -> Result<(), Self::Error> { - let mut value = [0; 36]; - outpoint - .consensus_encode(&mut value.as_mut_slice()) - .unwrap(); - - let mut entry = Vec::new(); - tx_out.consensus_encode(&mut entry)?; - self - .wtx - .open_table(OUTPOINT_TO_ENTRY)? - .insert(&value, entry.as_slice())?; - Ok(()) - } - - fn save_transaction_operations( - &self, - txid: &Txid, - operations: &[InscriptionOp], - ) -> Result<(), Self::Error> { - self.wtx.open_table(ORD_TX_TO_OPERATIONS)?.insert( - txid.to_string().as_str(), - bincode::serialize(operations).unwrap().as_slice(), - )?; - Ok(()) - } - fn set_inscription_by_collection_key( - &self, - key: &str, - inscription_id: InscriptionId, - ) -> Result<(), Self::Error> { - let mut value = [0; 36]; - let (txid, index) = value.split_at_mut(32); - txid.copy_from_slice(inscription_id.txid.as_ref()); - index.copy_from_slice(&inscription_id.index.to_be_bytes()); - self - .wtx - .open_table(COLLECTIONS_KEY_TO_INSCRIPTION_ID)? - .insert(key, &value)?; - Ok(()) - } - - fn set_inscription_attributes( - &self, - inscription_id: InscriptionId, - kind: &[CollectionKind], - ) -> Result<(), Self::Error> { - let mut key = [0; 36]; - let (txid, index) = key.split_at_mut(32); - txid.copy_from_slice(inscription_id.txid.as_ref()); - index.copy_from_slice(&inscription_id.index.to_be_bytes()); - self - .wtx - .open_table(COLLECTIONS_INSCRIPTION_ID_TO_KINDS)? - .insert(&key, bincode::serialize(&kind).unwrap().as_slice())?; - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{inscription, okx::datastore::ord::Action, unbound_outpoint, SatPoint}; - use redb::Database; - use std::str::FromStr; - use tempfile::NamedTempFile; - - #[test] - fn test_outpoint_to_script() { - let dbfile = NamedTempFile::new().unwrap(); - let db = Database::create(dbfile.path()).unwrap(); - let wtx = db.begin_write().unwrap(); - let ord_db = OrdDbReadWriter::new(&wtx); - - let outpoint1 = unbound_outpoint(); - let tx_out = TxOut { - value: 100, - script_pubkey: bitcoin::Address::from_str("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa") - .unwrap() - .assume_checked() - .script_pubkey(), - }; - - ord_db.set_outpoint_to_txout(outpoint1, &tx_out).unwrap(); - - assert_eq!( - ord_db.get_outpoint_to_txout(outpoint1).unwrap().unwrap(), - tx_out - ); - } - - #[test] - fn test_transaction_to_operations() { - let dbfile = NamedTempFile::new().unwrap(); - let db = Database::create(dbfile.path()).unwrap(); - let wtx = db.begin_write().unwrap(); - let ord_db = OrdDbReadWriter::new(&wtx); - let txid = - Txid::from_str("b61b0172d95e266c18aea0c624db987e971a5d6d4ebc2aaed85da4642d635735").unwrap(); - let operation = InscriptionOp { - txid, - action: Action::New { - cursed: false, - unbound: false, - inscription: inscription("text/plain;charset=utf-8", "foobar"), - }, - inscription_number: Some(100), - inscription_id: InscriptionId { txid, index: 0 }, - old_satpoint: SatPoint::from_str( - "1111111111111111111111111111111111111111111111111111111111111111:1:1", - ) - .unwrap(), - new_satpoint: Some(SatPoint { - outpoint: OutPoint { txid, vout: 0 }, - offset: 1, - }), - }; - - ord_db - .save_transaction_operations(&txid, &[operation.clone()]) - .unwrap(); - - assert_eq!( - ord_db.get_transaction_operations(&txid).unwrap(), - vec![operation] - ); - } -} diff --git a/src/okx/datastore/ord/redb/table.rs b/src/okx/datastore/ord/redb/table.rs new file mode 100644 index 0000000000..517f3ce137 --- /dev/null +++ b/src/okx/datastore/ord/redb/table.rs @@ -0,0 +1,151 @@ +use crate::index::entry::Entry; +use crate::index::{InscriptionEntryValue, InscriptionIdValue, OutPointValue, TxidValue}; +use crate::inscription_id::InscriptionId; +use crate::okx::datastore::ord::collections::CollectionKind; +use crate::okx::datastore::ord::InscriptionOp; +use bitcoin::consensus::Decodable; +use bitcoin::{OutPoint, TxOut, Txid}; +use redb::{ReadableTable, Table}; +use std::io; + +// COLLECTIONS_INSCRIPTION_ID_TO_KINDS +pub fn get_collections_of_inscription( + table: &T, + inscription_id: &InscriptionId, +) -> crate::Result>> +where + T: ReadableTable, +{ + Ok( + table + .get(&inscription_id.store())? + .map(|v| rmp_serde::from_slice::>(v.value()).unwrap()), + ) +} + +// COLLECTIONS_KEY_TO_INSCRIPTION_ID +pub fn get_collection_inscription_id( + table: &T, + key: &str, +) -> crate::Result> +where + T: ReadableTable<&'static str, InscriptionIdValue>, +{ + Ok(table.get(key)?.map(|v| InscriptionId::load(v.value()))) +} + +// SEQUENCE_NUMBER_TO_INSCRIPTION_ENTRY +pub fn get_inscription_number_by_sequence_number( + table: &T, + sequence_number: u32, +) -> crate::Result> +where + T: ReadableTable, +{ + Ok(table.get(sequence_number)?.map(|value| value.value().4)) +} + +// OUTPOINT_TO_ENTRY +pub fn get_txout_by_outpoint(table: &T, outpoint: &OutPoint) -> crate::Result> +where + T: ReadableTable<&'static OutPointValue, &'static [u8]>, +{ + Ok( + table + .get(&outpoint.store())? + .map(|x| Decodable::consensus_decode(&mut io::Cursor::new(x.value())).unwrap()), + ) +} + +// ORD_TX_TO_OPERATIONS +pub fn get_transaction_operations(table: &T, txid: &Txid) -> crate::Result> +where + T: ReadableTable<&'static TxidValue, &'static [u8]>, +{ + Ok(table.get(&txid.store())?.map_or(Vec::new(), |v| { + rmp_serde::from_slice::>(v.value()).unwrap() + })) +} + +// ORD_TX_TO_OPERATIONS +pub fn save_transaction_operations<'db, 'txn>( + table: &mut Table<'db, 'txn, &'static TxidValue, &'static [u8]>, + txid: &Txid, + operations: &[InscriptionOp], +) -> crate::Result<()> { + table.insert(&txid.store(), rmp_serde::to_vec(operations)?.as_slice())?; + Ok(()) +} + +// COLLECTIONS_KEY_TO_INSCRIPTION_ID +pub fn set_inscription_by_collection_key<'db, 'txn>( + table: &mut Table<'db, 'txn, &'static str, InscriptionIdValue>, + key: &str, + inscription_id: &InscriptionId, +) -> crate::Result<()> { + table.insert(key, inscription_id.store())?; + Ok(()) +} + +// COLLECTIONS_INSCRIPTION_ID_TO_KINDS +pub fn set_inscription_attributes<'db, 'txn>( + table: &mut Table<'db, 'txn, InscriptionIdValue, &'static [u8]>, + inscription_id: &InscriptionId, + kind: &[CollectionKind], +) -> crate::Result<()> { + table.insert( + inscription_id.store(), + rmp_serde::to_vec(&kind).unwrap().as_slice(), + )?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::index::ORD_TX_TO_OPERATIONS; + use crate::okx::datastore::ord::redb::table::{ + get_transaction_operations, save_transaction_operations, + }; + use crate::okx::datastore::ord::InscriptionOp; + use crate::{inscription, okx::datastore::ord::Action, SatPoint}; + use redb::Database; + use std::str::FromStr; + use tempfile::NamedTempFile; + + #[test] + fn test_transaction_to_operations() { + let dbfile = NamedTempFile::new().unwrap(); + let db = Database::create(dbfile.path()).unwrap(); + let wtx = db.begin_write().unwrap(); + let mut table = wtx.open_table(ORD_TX_TO_OPERATIONS).unwrap(); + let txid = + Txid::from_str("b61b0172d95e266c18aea0c624db987e971a5d6d4ebc2aaed85da4642d635735").unwrap(); + let operation = InscriptionOp { + txid, + action: Action::New { + cursed: false, + unbound: false, + inscription: inscription("text/plain;charset=utf-8", "foobar"), + }, + sequence_number: 100, + inscription_number: Some(100), + inscription_id: InscriptionId { txid, index: 0 }, + old_satpoint: SatPoint::from_str( + "1111111111111111111111111111111111111111111111111111111111111111:1:1", + ) + .unwrap(), + new_satpoint: Some(SatPoint { + outpoint: OutPoint { txid, vout: 0 }, + offset: 1, + }), + }; + + save_transaction_operations(&mut table, &txid, &[operation.clone()]).unwrap(); + + assert_eq!( + get_transaction_operations(&table, &txid).unwrap(), + vec![operation] + ); + } +} diff --git a/src/okx/datastore/redb.rs b/src/okx/datastore/redb.rs deleted file mode 100644 index cbddf2c793..0000000000 --- a/src/okx/datastore/redb.rs +++ /dev/null @@ -1,50 +0,0 @@ -use { - super::{ - ord::redb::{OrdDbReadWriter as OrdStateRW, OrdDbReader as OrdStateReader}, - StateRWriter, StateReader, - }, - redb::{ReadTransaction, WriteTransaction}, -}; - -/// StateReadOnly, based on `redb`, is an implementation of the StateRWriter trait. -pub struct StateReadOnly<'db, 'a> { - ord: OrdStateReader<'db, 'a>, -} - -impl<'db, 'a> StateReadOnly<'db, 'a> { - #[allow(dead_code)] - pub fn new(rtx: &'a ReadTransaction<'db>) -> Self { - Self { - ord: OrdStateReader::new(rtx), - } - } -} - -impl<'db, 'a> StateReader for StateReadOnly<'db, 'a> { - type OrdReader = OrdStateReader<'db, 'a>; - - fn ord(&self) -> &Self::OrdReader { - &self.ord - } -} - -/// StateReadWrite, based on `redb`, is an implementation of the StateRWriter trait. -pub struct StateReadWrite<'db, 'a> { - ord: OrdStateRW<'db, 'a>, -} - -impl<'db, 'a> StateReadWrite<'db, 'a> { - pub fn new(wtx: &'a WriteTransaction<'db>) -> Self { - Self { - ord: OrdStateRW::new(wtx), - } - } -} - -impl<'db, 'a> StateRWriter for StateReadWrite<'db, 'a> { - type OrdRWriter = OrdStateRW<'db, 'a>; - - fn ord(&self) -> &Self::OrdRWriter { - &self.ord - } -} diff --git a/src/okx/datastore/script_key.rs b/src/okx/datastore/script_key.rs index 6dc2bb2800..91c22ef174 100644 --- a/src/okx/datastore/script_key.rs +++ b/src/okx/datastore/script_key.rs @@ -9,7 +9,6 @@ pub enum ScriptKey { } impl ScriptKey { - #[allow(dead_code)] pub fn from_address(address: Address) -> Self { ScriptKey::Address(Address::new(address.network, address.payload)) } diff --git a/src/okx/lru.rs b/src/okx/lru.rs new file mode 100644 index 0000000000..f2ebac3c89 --- /dev/null +++ b/src/okx/lru.rs @@ -0,0 +1,154 @@ +use std::borrow::Borrow; +use std::collections::HashMap; +use std::hash::Hash; +use std::mem; + +pub struct SimpleLru { + cache_size: usize, + new_cache: HashMap, + old_cache: HashMap, +} + +impl SimpleLru +where + K: Eq + Hash, +{ + pub fn new(cache_size: usize) -> SimpleLru { + Self { + cache_size, + new_cache: HashMap::with_capacity(cache_size), + old_cache: HashMap::new(), + } + } + + pub fn get(&self, key: &Q) -> Option<&V> + where + K: Borrow, + Q: Hash + Eq, + { + if let Some(v) = self.new_cache.get(key) { + Some(v) + } else { + self.old_cache.get(key) + } + } + + pub fn contains(&self, key: &Q) -> bool + where + K: Borrow, + Q: Hash + Eq, + { + if self.new_cache.contains_key(key) { + true + } else { + self.old_cache.contains_key(key) + } + } + + pub fn insert(&mut self, key: K, value: V) -> Option { + self.new_cache.insert(key, value) + } + + pub fn refresh(&mut self) { + if self.new_cache.len() >= self.cache_size { + self.old_cache.clear(); + mem::swap(&mut self.new_cache, &mut self.old_cache); + } + } + + pub fn len(&self) -> usize { + self.old_cache.len() + self.new_cache.len() + } +} + +#[cfg(test)] +mod tests { + use crate::okx::lru::SimpleLru; + + #[test] + fn lru_test() { + let mut lru = SimpleLru::new(2); + lru.insert(1, 11); + lru.insert(2, 22); + assert!(lru.get(&1).is_some()); + assert!(lru.get(&2).is_some()); + assert!(lru.contains(&1)); + assert!(lru.contains(&2)); + assert_eq!(2, lru.len()); + lru.refresh(); + + lru.insert(3, 33); + lru.insert(4, 44); + assert!(lru.contains(&1)); + assert!(lru.contains(&2)); + assert!(lru.contains(&3)); + assert!(lru.contains(&4)); + assert!(lru.get(&3).is_some()); + assert!(lru.get(&4).is_some()); + assert_eq!(4, lru.len()); + + lru.refresh(); + lru.insert(5, 55); + assert!(!lru.contains(&1)); + assert!(!lru.contains(&2)); + assert!(lru.contains(&3)); + assert!(lru.contains(&4)); + assert!(lru.contains(&5)); + assert!(lru.get(&1).is_none()); + assert!(lru.get(&2).is_none()); + assert!(lru.get(&3).is_some()); + assert!(lru.get(&4).is_some()); + assert!(lru.get(&5).is_some()); + assert_eq!(3, lru.len()); + + lru.refresh(); + lru.insert(6, 66); + assert!(lru.contains(&3)); + assert!(lru.contains(&4)); + assert!(lru.contains(&5)); + assert!(lru.contains(&6)); + assert!(lru.get(&3).is_some()); + assert!(lru.get(&4).is_some()); + assert!(lru.get(&5).is_some()); + assert!(lru.get(&6).is_some()); + assert_eq!(4, lru.len()); + + lru.refresh(); + lru.insert(7, 77); + assert!(!lru.contains(&3)); + assert!(!lru.contains(&4)); + assert!(lru.contains(&5)); + assert!(lru.contains(&6)); + assert!(lru.contains(&7)); + assert!(lru.get(&3).is_none()); + assert!(lru.get(&4).is_none()); + assert!(lru.get(&5).is_some()); + assert!(lru.get(&6).is_some()); + assert!(lru.get(&7).is_some()); + assert_eq!(3, lru.len()); + + lru.refresh(); + assert_eq!(55, *lru.get(&5).unwrap()); + assert_eq!(66, *lru.get(&6).unwrap()); + assert_eq!(77, *lru.get(&7).unwrap()); + } + + #[test] + fn lru_swap_test() { + const CACHE_SIZE: usize = 10000000; + let mut lru = SimpleLru::new(CACHE_SIZE); + for i in 0..CACHE_SIZE { + lru.insert(i, i); + } + assert_eq!(CACHE_SIZE, lru.len()); + lru.refresh(); + assert_eq!(CACHE_SIZE, lru.len()); + + for i in 0..CACHE_SIZE { + lru.insert(i, i); + } + assert_eq!(2 * CACHE_SIZE, lru.len()); + lru.refresh(); + assert_eq!(CACHE_SIZE, lru.len()); + } +} diff --git a/src/okx/mod.rs b/src/okx/mod.rs index c1137c5f7f..16c10e12a5 100644 --- a/src/okx/mod.rs +++ b/src/okx/mod.rs @@ -1,2 +1,3 @@ pub(crate) mod datastore; +pub(crate) mod lru; pub(crate) mod protocol; diff --git a/src/okx/protocol/brc20/error.rs b/src/okx/protocol/brc20/error.rs new file mode 100644 index 0000000000..9ce9747f05 --- /dev/null +++ b/src/okx/protocol/brc20/error.rs @@ -0,0 +1,44 @@ +use crate::okx::datastore::brc20::BRC20Error; +use redb::TableError; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("brc20 error: {0}")] + BRC20Error(BRC20Error), + + #[error("ledger error: {0}")] + LedgerError(anyhow::Error), + + #[error("table error: {0}")] + TableError(TableError), +} + +#[derive(Debug, PartialEq, thiserror::Error)] +pub enum JSONError { + #[error("invalid content type")] + InvalidContentType, + + #[error("unsupport content type")] + UnSupportContentType, + + #[error("invalid json string")] + InvalidJson, + + #[error("not brc20 json")] + NotBRC20Json, + + #[error("parse operation json error: {0}")] + ParseOperationJsonError(String), +} + +impl From for Error { + fn from(e: BRC20Error) -> Self { + Self::BRC20Error(e) + } +} + +impl From for Error { + fn from(error: TableError) -> Self { + Self::TableError(error) + } +} diff --git a/src/okx/protocol/brc20/mod.rs b/src/okx/protocol/brc20/mod.rs new file mode 100644 index 0000000000..1104403276 --- /dev/null +++ b/src/okx/protocol/brc20/mod.rs @@ -0,0 +1,35 @@ +use crate::{ + okx::datastore::{ + brc20::{BRC20Error, OperationType}, + ScriptKey, + }, + InscriptionId, Result, SatPoint, +}; +use bitcoin::Txid; + +mod error; +mod msg_executor; +mod msg_resolver; +mod num; +mod operation; +mod params; + +use self::error::Error; +pub(crate) use self::{ + error::JSONError, + msg_executor::{execute, ExecutionMessage}, + num::Num, + operation::{deserialize_brc20_operation, Deploy, Mint, Operation, Transfer}, +}; + +#[derive(Debug, Clone, PartialEq)] +pub struct Message { + pub txid: Txid, + pub sequence_number: u32, + pub inscription_id: InscriptionId, + pub old_satpoint: SatPoint, + // `new_satpoint` may be none when the transaction is not yet confirmed and the sat has not been bound to the current outputs. + pub new_satpoint: Option, + pub op: Operation, + pub sat_in_outputs: bool, +} diff --git a/src/okx/protocol/brc20/msg_executor.rs b/src/okx/protocol/brc20/msg_executor.rs new file mode 100644 index 0000000000..5c53fa0860 --- /dev/null +++ b/src/okx/protocol/brc20/msg_executor.rs @@ -0,0 +1,409 @@ +use super::{ + params::{BIGDECIMAL_TEN, MAXIMUM_SUPPLY, MAX_DECIMAL_WIDTH}, + *, +}; + +use crate::okx::datastore::brc20::{Brc20Reader, Brc20ReaderWriter}; +use crate::okx::datastore::ord::OrdReader; +use crate::okx::protocol::context::Context; +use crate::{ + okx::{ + datastore::brc20::{ + BRC20Error, Balance, DeployEvent, Event, InscripbeTransferEvent, MintEvent, Receipt, Tick, + TokenInfo, TransferEvent, TransferInfo, TransferableLog, + }, + protocol::brc20::{Message, Mint, Operation}, + }, + Result, +}; +use anyhow::anyhow; +use bigdecimal::num_bigint::Sign; +use bitcoin::Network; +use std::str::FromStr; + +#[derive(Debug, Clone, PartialEq)] +pub struct ExecutionMessage { + pub(self) txid: Txid, + pub(self) inscription_id: InscriptionId, + pub(self) inscription_number: i32, + pub(self) old_satpoint: SatPoint, + pub(self) new_satpoint: SatPoint, + pub(self) from: ScriptKey, + pub(self) to: Option, + pub(self) op: Operation, +} + +impl ExecutionMessage { + pub fn from_message(context: &mut Context, msg: &Message, network: Network) -> Result { + Ok(Self { + txid: msg.txid, + inscription_id: msg.inscription_id, + inscription_number: context.get_inscription_number_by_sequence_number(msg.sequence_number)?, + old_satpoint: msg.old_satpoint, + new_satpoint: msg + .new_satpoint + .ok_or(anyhow!("new satpoint cannot be None"))?, + from: context.get_script_key_on_satpoint(&msg.old_satpoint, network)?, + to: if msg.sat_in_outputs { + Some(context.get_script_key_on_satpoint(msg.new_satpoint.as_ref().unwrap(), network)?) + } else { + None + }, + op: msg.op.clone(), + }) + } +} + +pub fn execute(context: &mut Context, msg: &ExecutionMessage) -> Result> { + log::debug!("BRC20 execute message: {:?}", msg); + let event = match &msg.op { + Operation::Deploy(deploy) => process_deploy(context, msg, deploy.clone()), + Operation::Mint(mint) => process_mint(context, msg, mint.clone()), + Operation::InscribeTransfer(transfer) => { + process_inscribe_transfer(context, msg, transfer.clone()) + } + Operation::Transfer(_) => process_transfer(context, msg), + }; + + let receipt = Receipt { + inscription_id: msg.inscription_id, + inscription_number: msg.inscription_number, + old_satpoint: msg.old_satpoint, + new_satpoint: msg.new_satpoint, + from: msg.from.clone(), + // redirect receiver to sender if transfer to conibase. + to: msg.to.clone().map_or(msg.from.clone(), |v| v), + op: msg.op.op_type(), + result: match event { + Ok(event) => Ok(event), + Err(Error::BRC20Error(e)) => Err(e), + Err(e) => return Err(anyhow!("BRC20 execute exception: {e}")), + }, + }; + + log::debug!("BRC20 message receipt: {:?}", receipt); + context + .add_transaction_receipt(&msg.txid, &receipt) + .map_err(|e| anyhow!("failed to add transaction receipt to state! error: {e}"))?; + + Ok(Some(receipt)) +} + +fn process_deploy( + context: &mut Context, + msg: &ExecutionMessage, + deploy: Deploy, +) -> Result { + // ignore inscribe inscription to coinbase. + let to_script_key = msg.to.clone().ok_or(BRC20Error::InscribeToCoinbase)?; + + let tick = deploy.tick.parse::()?; + + if let Some(stored_tick_info) = context + .get_token_info(&tick) + .map_err(|e| Error::LedgerError(e))? + { + return Err(Error::BRC20Error(BRC20Error::DuplicateTick( + stored_tick_info.tick.to_string(), + ))); + } + + let dec = Num::from_str(&deploy.decimals.map_or(MAX_DECIMAL_WIDTH.to_string(), |v| v))? + .checked_to_u8()?; + if dec > MAX_DECIMAL_WIDTH { + return Err(Error::BRC20Error(BRC20Error::DecimalsTooLarge(dec))); + } + let base = BIGDECIMAL_TEN.checked_powu(u64::from(dec))?; + + let supply = Num::from_str(&deploy.max_supply)?; + + if supply.sign() == Sign::NoSign + || supply > MAXIMUM_SUPPLY.to_owned() + || supply.scale() > i64::from(dec) + { + return Err(Error::BRC20Error(BRC20Error::InvalidSupply( + supply.to_string(), + ))); + } + + let limit = Num::from_str(&deploy.mint_limit.map_or(deploy.max_supply, |v| v))?; + + if limit.sign() == Sign::NoSign + || limit > MAXIMUM_SUPPLY.to_owned() + || limit.scale() > i64::from(dec) + { + return Err(Error::BRC20Error(BRC20Error::MintLimitOutOfRange( + tick.to_lowercase().to_string(), + limit.to_string(), + ))); + } + + let supply = supply.checked_mul(&base)?.checked_to_u128()?; + let limit = limit.checked_mul(&base)?.checked_to_u128()?; + + let new_info = TokenInfo { + inscription_id: msg.inscription_id, + inscription_number: msg.inscription_number, + tick: tick.clone(), + decimal: dec, + supply, + limit_per_mint: limit, + minted: 0u128, + deploy_by: to_script_key, + deployed_number: context.chain.blockheight, + latest_mint_number: context.chain.blockheight, + deployed_timestamp: context.chain.blocktime, + }; + context + .insert_token_info(&tick, &new_info) + .map_err(|e| Error::LedgerError(e))?; + + Ok(Event::Deploy(DeployEvent { + supply, + limit_per_mint: limit, + decimal: dec, + tick: new_info.tick, + })) +} + +fn process_mint(context: &mut Context, msg: &ExecutionMessage, mint: Mint) -> Result { + // ignore inscribe inscription to coinbase. + let to_script_key = msg.to.clone().ok_or(BRC20Error::InscribeToCoinbase)?; + + let tick = mint.tick.parse::()?; + + let token_info = context + .get_token_info(&tick) + .map_err(|e| Error::LedgerError(e))? + .ok_or(BRC20Error::TickNotFound(tick.to_string()))?; + + let base = BIGDECIMAL_TEN.checked_powu(u64::from(token_info.decimal))?; + + let mut amt = Num::from_str(&mint.amount)?; + + if amt.scale() > i64::from(token_info.decimal) { + return Err(Error::BRC20Error(BRC20Error::AmountOverflow( + amt.to_string(), + ))); + } + + amt = amt.checked_mul(&base)?; + if amt.sign() == Sign::NoSign { + return Err(Error::BRC20Error(BRC20Error::InvalidZeroAmount)); + } + if amt > Into::::into(token_info.limit_per_mint) { + return Err(Error::BRC20Error(BRC20Error::AmountExceedLimit( + amt.to_string(), + ))); + } + let minted = Into::::into(token_info.minted); + let supply = Into::::into(token_info.supply); + + if minted >= supply { + return Err(Error::BRC20Error(BRC20Error::TickMinted( + token_info.tick.to_string(), + ))); + } + + // cut off any excess. + let mut out_msg = None; + amt = if amt.checked_add(&minted)? > supply { + let new = supply.checked_sub(&minted)?; + out_msg = Some(format!( + "amt has been cut off to fit the supply! origin: {}, now: {}", + amt, new + )); + new + } else { + amt + }; + + // get or initialize user balance. + let mut balance = context + .get_balance(&to_script_key, &tick) + .map_err(|e| Error::LedgerError(e))? + .map_or(Balance::new(&tick), |v| v); + + // add amount to available balance. + balance.overall_balance = Into::::into(balance.overall_balance) + .checked_add(&amt)? + .checked_to_u128()?; + + // store to database. + context + .update_token_balance(&to_script_key, balance) + .map_err(|e| Error::LedgerError(e))?; + + // update token minted. + let minted = minted.checked_add(&amt)?.checked_to_u128()?; + context + .update_mint_token_info(&tick, minted, context.chain.blockheight) + .map_err(|e| Error::LedgerError(e))?; + + Ok(Event::Mint(MintEvent { + tick: token_info.tick, + amount: amt.checked_to_u128()?, + msg: out_msg, + })) +} + +fn process_inscribe_transfer( + context: &mut Context, + msg: &ExecutionMessage, + transfer: Transfer, +) -> Result { + // ignore inscribe inscription to coinbase. + let to_script_key = msg.to.clone().ok_or(BRC20Error::InscribeToCoinbase)?; + + let tick = transfer.tick.parse::()?; + + let token_info = context + .get_token_info(&tick) + .map_err(|e| Error::LedgerError(e))? + .ok_or(BRC20Error::TickNotFound(tick.to_string()))?; + + let base = BIGDECIMAL_TEN.checked_powu(u64::from(token_info.decimal))?; + + let mut amt = Num::from_str(&transfer.amount)?; + + if amt.scale() > i64::from(token_info.decimal) { + return Err(Error::BRC20Error(BRC20Error::AmountOverflow( + amt.to_string(), + ))); + } + + amt = amt.checked_mul(&base)?; + if amt.sign() == Sign::NoSign || amt > Into::::into(token_info.supply) { + return Err(Error::BRC20Error(BRC20Error::AmountOverflow( + amt.to_string(), + ))); + } + + let mut balance = context + .get_balance(&to_script_key, &tick) + .map_err(|e| Error::LedgerError(e))? + .map_or(Balance::new(&tick), |v| v); + + let overall = Into::::into(balance.overall_balance); + let transferable = Into::::into(balance.transferable_balance); + let available = overall.checked_sub(&transferable)?; + if available < amt { + return Err(Error::BRC20Error(BRC20Error::InsufficientBalance( + available.to_string(), + amt.to_string(), + ))); + } + + balance.transferable_balance = transferable.checked_add(&amt)?.checked_to_u128()?; + + let amt = amt.checked_to_u128()?; + context + .update_token_balance(&to_script_key, balance) + .map_err(|e| Error::LedgerError(e))?; + + let inscription = TransferableLog { + inscription_id: msg.inscription_id, + inscription_number: msg.inscription_number, + amount: amt, + tick: token_info.tick.clone(), + owner: to_script_key, + }; + + context + .insert_transferable(&inscription.owner, &tick, &inscription) + .map_err(|e| Error::LedgerError(e))?; + + context + .insert_inscribe_transfer_inscription( + &msg.inscription_id, + TransferInfo { + tick: token_info.tick, + amt, + }, + ) + .map_err(|e| Error::LedgerError(e))?; + + Ok(Event::InscribeTransfer(InscripbeTransferEvent { + tick: inscription.tick, + amount: amt, + })) +} + +fn process_transfer(context: &mut Context, msg: &ExecutionMessage) -> Result { + let transferable = context + .get_transferable_by_id(&msg.from, &msg.inscription_id) + .map_err(|e| Error::LedgerError(e))? + .ok_or(BRC20Error::TransferableNotFound(msg.inscription_id))?; + + let amt = Into::::into(transferable.amount); + + if transferable.owner != msg.from { + return Err(Error::BRC20Error(BRC20Error::TransferableOwnerNotMatch( + msg.inscription_id, + ))); + } + + let tick = transferable.tick; + + let token_info = context + .get_token_info(&tick) + .map_err(|e| Error::LedgerError(e))? + .ok_or(BRC20Error::TickNotFound(tick.to_string()))?; + + // update from key balance. + let mut from_balance = context + .get_balance(&msg.from, &tick) + .map_err(|e| Error::LedgerError(e))? + .map_or(Balance::new(&tick), |v| v); + + let from_overall = Into::::into(from_balance.overall_balance); + let from_transferable = Into::::into(from_balance.transferable_balance); + + let from_overall = from_overall.checked_sub(&amt)?.checked_to_u128()?; + let from_transferable = from_transferable.checked_sub(&amt)?.checked_to_u128()?; + + from_balance.overall_balance = from_overall; + from_balance.transferable_balance = from_transferable; + + context + .update_token_balance(&msg.from, from_balance) + .map_err(|e| Error::LedgerError(e))?; + + // redirect receiver to sender if transfer to conibase. + let mut out_msg = None; + + let to_script_key = if msg.to.clone().is_none() { + out_msg = + Some("redirect receiver to sender, reason: transfer inscription to coinbase".to_string()); + msg.from.clone() + } else { + msg.to.clone().unwrap() + }; + + // update to key balance. + let mut to_balance = context + .get_balance(&to_script_key, &tick) + .map_err(|e| Error::LedgerError(e))? + .map_or(Balance::new(&tick), |v| v); + + let to_overall = Into::::into(to_balance.overall_balance); + to_balance.overall_balance = to_overall.checked_add(&amt)?.checked_to_u128()?; + + context + .update_token_balance(&to_script_key, to_balance) + .map_err(|e| Error::LedgerError(e))?; + + context + .remove_transferable(&msg.from, &tick, &msg.inscription_id) + .map_err(|e| Error::LedgerError(e))?; + + context + .remove_inscribe_transfer_inscription(&msg.inscription_id) + .map_err(|e| Error::LedgerError(e))?; + + Ok(Event::Transfer(TransferEvent { + msg: out_msg, + tick: token_info.tick, + amount: amt.checked_to_u128()?, + })) +} diff --git a/src/okx/protocol/brc20/msg_resolver.rs b/src/okx/protocol/brc20/msg_resolver.rs new file mode 100644 index 0000000000..84937dbeb5 --- /dev/null +++ b/src/okx/protocol/brc20/msg_resolver.rs @@ -0,0 +1,298 @@ +use super::*; +use crate::index::InscriptionIdValue; +use crate::okx::datastore::brc20::redb::table::get_inscribe_transfer_inscription; +use crate::{ + inscription::Inscription, + okx::{ + datastore::ord::{Action, InscriptionOp}, + protocol::brc20::{deserialize_brc20_operation, Operation}, + }, + Result, +}; +use anyhow::anyhow; +use redb::ReadableTable; + +impl Message { + pub(crate) fn resolve( + table: &T, + new_inscriptions: &[Inscription], + op: &InscriptionOp, + ) -> Result> + where + T: ReadableTable, + { + log::debug!("BRC20 resolving the message from {:?}", op); + let sat_in_outputs = op + .new_satpoint + .map(|satpoint| satpoint.outpoint.txid == op.txid) + .unwrap_or(false); + + let brc20_operation = match op.action { + // New inscription is not `cursed` or `unbound`. + Action::New { + cursed: false, + unbound: false, + inscription: _, + } if sat_in_outputs => { + match deserialize_brc20_operation( + new_inscriptions + .get(usize::try_from(op.inscription_id.index).unwrap()) + .unwrap(), + &op.action, + ) { + Ok(brc20_operation) => brc20_operation, + _ => return Ok(None), + } + } + // Transfered inscription operation. + // Attempt to retrieve the `InscribeTransfer` Inscription information from the data store of BRC20S. + Action::Transfer => match get_inscribe_transfer_inscription(table, &op.inscription_id) { + // Ignore non-first transfer operations. + Ok(Some(transfer_info)) if op.inscription_id.txid == op.old_satpoint.outpoint.txid => { + Operation::Transfer(Transfer { + tick: transfer_info.tick.as_str().to_string(), + amount: transfer_info.amt.to_string(), + }) + } + Err(e) => { + return Err(anyhow!( + "failed to get inscribe transfer inscription for {}! error: {e}", + op.inscription_id, + )) + } + _ => return Ok(None), + }, + _ => return Ok(None), + }; + Ok(Some(Self { + txid: op.txid, + sequence_number: op.sequence_number, + inscription_id: op.inscription_id, + old_satpoint: op.old_satpoint, + new_satpoint: op.new_satpoint, + op: brc20_operation, + sat_in_outputs, + })) + } +} +// #[cfg(test)] +// mod tests { +// use super::*; +// use crate::okx::datastore::brc20::{Brc20ReaderWriter, Tick, TransferInfo}; +// use bitcoin::OutPoint; +// use redb::Database; +// use std::str::FromStr; +// use tempfile::NamedTempFile; +// fn create_inscription(str: &str) -> Inscription { +// Inscription::new( +// Some("text/plain;charset=utf-8".as_bytes().to_vec()), +// Some(str.as_bytes().to_vec()), +// ) +// } +// +// fn create_inscribe_operation(str: &str) -> (Vec, InscriptionOp) { +// let inscriptions = vec![create_inscription(str)]; +// let txid = +// Txid::from_str("b61b0172d95e266c18aea0c624db987e971a5d6d4ebc2aaed85da4642d635735").unwrap(); +// let op = InscriptionOp { +// txid, +// action: Action::New { +// cursed: false, +// unbound: false, +// inscription: inscriptions.get(0).unwrap().clone(), +// }, +// inscription_number: Some(1), +// inscription_id: InscriptionId { txid, index: 0 }, +// old_satpoint: SatPoint { +// outpoint: OutPoint { +// txid: Txid::from_str("2111111111111111111111111111111111111111111111111111111111111111") +// .unwrap(), +// vout: 0, +// }, +// offset: 0, +// }, +// new_satpoint: Some(SatPoint { +// outpoint: OutPoint { txid, vout: 0 }, +// offset: 0, +// }), +// }; +// (inscriptions, op) +// } +// +// fn create_transfer_operation() -> InscriptionOp { +// let txid = +// Txid::from_str("b61b0172d95e266c18aea0c624db987e971a5d6d4ebc2aaed85da4642d635735").unwrap(); +// +// let inscription_id = InscriptionId { +// txid: Txid::from_str("2111111111111111111111111111111111111111111111111111111111111111") +// .unwrap(), +// index: 0, +// }; +// +// InscriptionOp { +// txid, +// action: Action::Transfer, +// inscription_number: Some(1), +// inscription_id, +// old_satpoint: SatPoint { +// outpoint: OutPoint { +// txid: inscription_id.txid, +// vout: 0, +// }, +// offset: 0, +// }, +// new_satpoint: Some(SatPoint { +// outpoint: OutPoint { txid, vout: 0 }, +// offset: 0, +// }), +// } +// } +// +// #[test] +// fn test_invalid_protocol() { +// let db_file = NamedTempFile::new().unwrap(); +// let db = Database::create(db_file.path()).unwrap(); +// let wtx = db.begin_write().unwrap(); +// let brc20_store = DataStore::new(&wtx); +// +// let (inscriptions, op) = create_inscribe_operation( +// r#"{ "p": "brc-20s","op": "deploy", "tick": "ordi", "max": "1000", "lim": "10" }"#, +// ); +// assert_matches!(Message::resolve(&brc20_store, &inscriptions, &op), Ok(None)); +// } +// +// #[test] +// fn test_cursed_or_unbound_inscription() { +// let db_file = NamedTempFile::new().unwrap(); +// let db = Database::create(db_file.path()).unwrap(); +// let wtx = db.begin_write().unwrap(); +// let brc20_store = DataStore::new(&wtx); +// +// let (inscriptions, op) = create_inscribe_operation( +// r#"{ "p": "brc-20","op": "deploy", "tick": "ordi", "max": "1000", "lim": "10" }"#, +// ); +// let op = InscriptionOp { +// action: Action::New { +// cursed: true, +// unbound: false, +// inscription: inscriptions.get(0).unwrap().clone(), +// }, +// ..op +// }; +// assert_matches!(Message::resolve(&brc20_store, &inscriptions, &op), Ok(None)); +// +// let op2 = InscriptionOp { +// action: Action::New { +// cursed: false, +// unbound: true, +// inscription: inscriptions.get(0).unwrap().clone(), +// }, +// ..op +// }; +// assert_matches!( +// Message::resolve(&brc20_store, &inscriptions, &op2), +// Ok(None) +// ); +// let op3 = InscriptionOp { +// action: Action::New { +// cursed: true, +// unbound: true, +// inscription: inscriptions.get(0).unwrap().clone(), +// }, +// ..op +// }; +// assert_matches!( +// Message::resolve(&brc20_store, &inscriptions, &op3), +// Ok(None) +// ); +// } +// +// #[test] +// fn test_valid_inscribe_operation() { +// let db_file = NamedTempFile::new().unwrap(); +// let db = Database::create(db_file.path()).unwrap(); +// let wtx = db.begin_write().unwrap(); +// let brc20_store = DataStore::new(&wtx); +// +// let (inscriptions, op) = create_inscribe_operation( +// r#"{ "p": "brc-20","op": "deploy", "tick": "ordi", "max": "1000", "lim": "10" }"#, +// ); +// let _result_msg = Message { +// txid: op.txid, +// inscription_id: op.inscription_id, +// old_satpoint: op.old_satpoint, +// new_satpoint: op.new_satpoint, +// op: Operation::Deploy(Deploy { +// tick: "ordi".to_string(), +// max_supply: "1000".to_string(), +// mint_limit: Some("10".to_string()), +// decimals: None, +// }), +// sat_in_outputs: true, +// }; +// assert_matches!( +// Message::resolve(&brc20_store, &inscriptions, &op), +// Ok(Some(_result_msg)) +// ); +// } +// +// #[test] +// fn test_invalid_transfer() { +// let db_file = NamedTempFile::new().unwrap(); +// let db = Database::create(db_file.path()).unwrap(); +// let wtx = db.begin_write().unwrap(); +// let brc20_store = DataStore::new(&wtx); +// +// // inscribe transfer not found +// let op = create_transfer_operation(); +// assert_matches!(Message::resolve(&brc20_store, &[], &op), Ok(None)); +// +// // non-first transfer operations. +// let op1 = InscriptionOp { +// old_satpoint: SatPoint { +// outpoint: OutPoint { +// txid: Txid::from_str("3111111111111111111111111111111111111111111111111111111111111111") +// .unwrap(), +// vout: 0, +// }, +// offset: 0, +// }, +// ..op +// }; +// assert_matches!(Message::resolve(&brc20_store, &[], &op1), Ok(None)); +// } +// +// #[test] +// fn test_valid_transfer() { +// let db_file = NamedTempFile::new().unwrap(); +// let db = Database::create(db_file.path()).unwrap(); +// let wtx = db.begin_write().unwrap(); +// let brc20_store = DataStore::new(&wtx); +// +// // inscribe transfer not found +// let op = create_transfer_operation(); +// +// brc20_store +// .insert_inscribe_transfer_inscription( +// op.inscription_id, +// TransferInfo { +// tick: Tick::from_str("ordi").unwrap(), +// amt: 100, +// }, +// ) +// .unwrap(); +// let _msg = Message { +// txid: op.txid, +// inscription_id: op.inscription_id, +// old_satpoint: op.old_satpoint, +// new_satpoint: op.new_satpoint, +// op: Operation::Transfer(Transfer { +// tick: "ordi".to_string(), +// amount: "100".to_string(), +// }), +// sat_in_outputs: true, +// }; +// +// assert_matches!(Message::resolve(&brc20_store, &[], &op), Ok(Some(_msg))); +// } +// } diff --git a/src/okx/protocol/brc20/num.rs b/src/okx/protocol/brc20/num.rs new file mode 100644 index 0000000000..4ba88d619a --- /dev/null +++ b/src/okx/protocol/brc20/num.rs @@ -0,0 +1,435 @@ +use super::{params::MAX_DECIMAL_WIDTH, BRC20Error}; +use bigdecimal::{ + num_bigint::{BigInt, Sign, ToBigInt}, + BigDecimal, One, ToPrimitive, +}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::{ + fmt::{Display, Formatter}, + str::FromStr, +}; + +#[derive(PartialEq, PartialOrd, Debug, Clone)] +pub struct Num(BigDecimal); + +impl Num { + // TODO check overflow + pub fn checked_add(&self, other: &Num) -> Result { + Ok(Self(self.0.clone() + &other.0)) + } + + pub fn checked_sub(&self, other: &Num) -> Result { + if self.0 < other.0 { + return Err(BRC20Error::Overflow { + op: String::from("checked_sub"), + org: self.clone().to_string(), + other: other.clone().to_string(), + }); + } + + Ok(Self(self.0.clone() - &other.0)) + } + + // TODO check overflow + pub fn checked_mul(&self, other: &Num) -> Result { + Ok(Self(self.0.clone() * &other.0)) + } + + pub fn checked_powu(&self, exp: u64) -> Result { + match exp { + 0 => Ok(Self(BigDecimal::one())), + 1 => Ok(Self(self.0.clone())), + exp => { + let mut result = self.0.clone(); + for _ in 1..exp { + result *= &self.0; + } + + Ok(Self(result)) + } + } + } + + pub fn checked_to_u8(&self) -> Result { + if !self.0.is_integer() { + return Err(BRC20Error::InvalidInteger(self.clone().to_string())); + } + self.0.clone().to_u8().ok_or(BRC20Error::Overflow { + op: String::from("to_u8"), + org: self.clone().to_string(), + other: Self(BigDecimal::from(u8::MAX)).to_string(), + }) + } + + pub fn sign(&self) -> Sign { + self.0.sign() + } + + pub fn scale(&self) -> i64 { + let (_, scale) = self.0.as_bigint_and_exponent(); + scale + } + + pub fn checked_to_u128(&self) -> Result { + if !self.0.is_integer() { + return Err(BRC20Error::InvalidInteger(self.clone().to_string())); + } + self + .0 + .to_bigint() + .ok_or(BRC20Error::InternalError(format!( + "convert {} to bigint failed", + self.0 + )))? + .to_u128() + .ok_or(BRC20Error::Overflow { + op: String::from("to_u128"), + org: self.clone().to_string(), + other: Self(BigDecimal::from(BigInt::from(u128::MAX))).to_string(), // TODO: change overflow error to others + }) + } +} + +impl From for Num { + fn from(n: u64) -> Self { + Self(BigDecimal::from(n)) + } +} + +impl From for Num { + fn from(n: u128) -> Self { + Self(BigDecimal::from(BigInt::from(n))) + } +} + +impl FromStr for Num { + type Err = BRC20Error; + fn from_str(s: &str) -> Result { + if s.starts_with('.') || s.ends_with('.') || s.find(['e', 'E', '+', '-']).is_some() { + return Err(BRC20Error::InvalidNum(s.to_string())); + } + let num = BigDecimal::from_str(s).map_err(|_| BRC20Error::InvalidNum(s.to_string()))?; + + let (_, scale) = num.as_bigint_and_exponent(); + if scale > i64::from(MAX_DECIMAL_WIDTH) { + return Err(BRC20Error::InvalidNum(s.to_string())); + } + + Ok(Self(num)) + } +} + +impl Display for Num { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl Serialize for Num { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let s = self.to_string(); + serializer.serialize_str(&s) + } +} + +impl<'de> Deserialize<'de> for Num { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Ok(Self( + BigDecimal::from_str(&s).map_err(serde::de::Error::custom)?, + )) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use bigdecimal::FromPrimitive; + #[test] + fn test_num_from_str2() { + assert_eq!( + Num::from_str("001").unwrap(), + Num(BigDecimal::new(BigInt::from(1), 0)), + ); + assert_eq!( + Num::from_str("00.1").unwrap(), + Num(BigDecimal::new(BigInt::from(1), 1)), + ); + assert_eq!( + Num::from_str("0.0").unwrap(), + Num(BigDecimal::new(BigInt::from(0), 0)), + ); + assert_eq!( + Num::from_str("0.100").unwrap(), + Num(BigDecimal::new(BigInt::from(1), 1)), + ); + assert_eq!( + Num::from_str("0").unwrap(), + Num(BigDecimal::new(BigInt::from(0), 0)), + ); + assert_eq!( + Num::from_str("00.00100").unwrap(), + Num(BigDecimal::new(BigInt::from(1), 3)), + ); + } + + #[test] + fn test_num_from_str() { + assert!(Num::from_str(".1").is_err()); + assert_eq!( + Num(BigDecimal::new(BigInt::from(0), 0)), + Num::from_str("0").unwrap() + ); + assert_eq!( + Num(BigDecimal::new(BigInt::from(1), 0)), + Num::from_str("001").unwrap() + ); + assert_eq!( + Num(BigDecimal::new(BigInt::from(1), 1)), + Num::from_str("00.1").unwrap() + ); + + assert_eq!( + Num(BigDecimal::new(BigInt::from(0), 0)), + Num::from_str("0.0").unwrap() + ); + assert_eq!( + Num(BigDecimal::new(BigInt::from(1), 1)), + Num::from_str("0.100").unwrap() + ); + assert_eq!( + Num(BigDecimal::new(BigInt::from(1), 3)), + Num::from_str("00.00100").unwrap() + ); + assert_eq!( + Num(BigDecimal::new(BigInt::from(11), 1)), + Num::from_str("1.1").unwrap() + ); + assert_eq!( + Num(BigDecimal::new(BigInt::from(11), 1)), + Num::from_str("1.1000").unwrap() + ); + assert_eq!( + Num(BigDecimal::new(BigInt::from(101), 2)), + Num::from_str("1.01").unwrap() + ); + + // can not be negative + assert!(Num::from_str("-1.1").is_err()); + + // number of decimal fractional can not exceed 18 + assert_eq!( + Num(BigDecimal::new( + BigInt::from(1_000_000_000_000_000_001_u64), + 18 + )), + Num::from_str("1.000000000000000001").unwrap() + ); + assert!(Num::from_str("1.0000000000000000001").is_err()); + } + + #[test] + fn test_invalid_num() { + assert!(Num::from_str("").is_err()); + assert!(Num::from_str(" ").is_err()); + assert!(Num::from_str(".").is_err()); + assert!(Num::from_str(" 123.456").is_err()); + assert!(Num::from_str(".456").is_err()); + assert!(Num::from_str(".456 ").is_err()); + assert!(Num::from_str(" .456 ").is_err()); + assert!(Num::from_str(" 456").is_err()); + assert!(Num::from_str("456 ").is_err()); + assert!(Num::from_str("45 6").is_err()); + assert!(Num::from_str("123. 456").is_err()); + assert!(Num::from_str("123.-456").is_err()); + assert!(Num::from_str("123.+456").is_err()); + assert!(Num::from_str("+123.456").is_err()); + assert!(Num::from_str("123.456.789").is_err()); + assert!(Num::from_str("123456789.").is_err()); + assert!(Num::from_str("123456789.12345678901234567891").is_err()); + } + + #[test] + fn test_num_serialize() { + let num = Num::from_str("1.01").unwrap(); + let json = serde_json::to_string(&num).unwrap(); + assert_eq!(json.as_str(), "\"1.01\""); + } + + #[test] + fn test_num_deserialize() { + let num = serde_json::from_str::("\"1.11\"").unwrap(); + assert_eq!(Num::from_str("1.11").unwrap(), num); + } + + #[test] + fn test_num_checked_add() { + assert_eq!( + Num::from_str("2"), + Num::from_str("1") + .unwrap() + .checked_add(&Num::from_str("1").unwrap()) + ); + assert_eq!( + Num::from_str("2.1"), + Num::from_str("1") + .unwrap() + .checked_add(&Num::from_str("1.1").unwrap()) + ); + assert_eq!( + Num::from_str("2.1"), + Num::from_str("1.1") + .unwrap() + .checked_add(&Num::from_str("1").unwrap()) + ); + assert_eq!( + Num::from_str("2.222"), + Num::from_str("1.101") + .unwrap() + .checked_add(&Num::from_str("1.121").unwrap()) + ); + } + + #[test] + fn test_num_checked_sub() { + assert_eq!( + Num::from_str("2"), + Num::from_str("3") + .unwrap() + .checked_sub(&Num::from_str("1").unwrap()) + ); + assert_eq!( + Num::from_str("2.1"), + Num::from_str("3") + .unwrap() + .checked_sub(&Num::from_str("0.9").unwrap()) + ); + assert_eq!( + Num::from_str("2.1"), + Num::from_str("3.1") + .unwrap() + .checked_sub(&Num::from_str("1").unwrap()) + ); + assert_eq!( + Num::from_str("2.222"), + Num::from_str("3.303") + .unwrap() + .checked_sub(&Num::from_str("1.081").unwrap()) + ); + } + + #[test] + fn test_to_u8() { + assert_eq!(Num::from_str("2").unwrap().checked_to_u8().unwrap(), 2); + assert_eq!(Num::from_str("255").unwrap().checked_to_u8().unwrap(), 255); + assert_eq!( + Num::from_str("256").unwrap().checked_to_u8().unwrap_err(), + BRC20Error::Overflow { + op: String::from("to_u8"), + org: Num::from_str("256").unwrap().to_string(), + other: Num(BigDecimal::from_u8(u8::MAX).unwrap()).to_string(), + } + ); + + let n = Num::from_str("15.00").unwrap(); + assert_eq!(n.checked_to_u8().unwrap(), 15u8); + } + + #[test] + fn test_max_value() { + // brc20 protocol stipulate that a max integer value is 64 bit, and decimal has 18 numbers at most. + let max = format!("{}.999999999999999999", u64::MAX); + + BigDecimal::from_str(&max).unwrap(); + } + + #[test] + fn test_checked_powu_floatpoint() { + let n = Num::from_str("3.7").unwrap(); + assert_eq!(n.checked_powu(0).unwrap(), Num::from_str("1").unwrap()); + assert_eq!(n.checked_powu(1).unwrap(), n); + assert_eq!(n.checked_powu(2).unwrap(), Num::from_str("13.69").unwrap()); + assert_eq!(n.checked_powu(3).unwrap(), Num::from_str("50.653").unwrap()); + assert_eq!( + n.checked_powu(5).unwrap(), + Num::from_str("693.43957").unwrap() + ); + assert_eq!( + n.checked_powu(18).unwrap(), + Num::from_str("16890053810.563300749953435929").unwrap() + ); + } + + #[test] + fn test_checked_powu_integer() { + let n = Num::from_str("10").unwrap(); + assert_eq!(n.checked_powu(0).unwrap(), Num::from_str("1").unwrap()); + assert_eq!(n.checked_powu(1).unwrap(), n); + assert_eq!(n.checked_powu(2).unwrap(), Num::from_str("100").unwrap()); + assert_eq!(n.checked_powu(3).unwrap(), Num::from_str("1000").unwrap()); + assert_eq!(n.checked_powu(5).unwrap(), Num::from_str("100000").unwrap()); + assert_eq!( + n.checked_powu(18).unwrap(), + Num::from_str("1000000000000000000").unwrap() + ); + } + + #[test] + fn test_checked_to_u128() { + let n = Num::from_str(&format!("{}", u128::MAX)).unwrap(); + assert_eq!(n.checked_to_u128().unwrap(), u128::MAX); + + let n = Num::from_str("0").unwrap(); + assert_eq!(n.checked_to_u128().unwrap(), 0); + + let n = Num::from_str(&format!("{}{}", u128::MAX, 1)).unwrap(); + assert_eq!( + n.checked_to_u128().unwrap_err(), + BRC20Error::Overflow { + op: String::from("to_u128"), + org: n.to_string(), + other: Num::from(u128::MAX).to_string(), + } + ); + + let n = Num::from_str(&format!("{}.{}", u128::MAX - 1, "33333")).unwrap(); + assert_eq!( + n.checked_to_u128().unwrap_err(), + BRC20Error::InvalidInteger(n.to_string()) + ); + + let n = Num::from_str(&format!("{}.{}", 0, "33333")).unwrap(); + assert_eq!( + n.checked_to_u128().unwrap_err(), + BRC20Error::InvalidInteger(n.to_string()) + ); + let a = BigDecimal::from_str("0.333").unwrap().to_bigint().unwrap(); + + assert_eq!(a.to_u128().unwrap(), 0_u128); + + let n = Num::from_str("3140000000000000000.00").unwrap(); + assert_eq!(n.checked_to_u128().unwrap(), 3140000000000000000u128); + + let n = Num::from_str(&format!("{}.{}", u128::MAX - 1, "33333")).unwrap(); + assert_eq!(n.scale(), 5_i64); + assert_eq!( + Num::from_str("1e2").unwrap_err(), + BRC20Error::InvalidNum("1e2".to_string()) + ); + assert_eq!( + Num::from_str("0e2").unwrap_err(), + BRC20Error::InvalidNum("0e2".to_string()) + ); + + assert_eq!( + Num::from_str("100E2").unwrap_err(), + BRC20Error::InvalidNum("100E2".to_string()) + ); + } +} diff --git a/src/okx/protocol/brc20/operation/deploy.rs b/src/okx/protocol/brc20/operation/deploy.rs new file mode 100644 index 0000000000..f9d3f36e7c --- /dev/null +++ b/src/okx/protocol/brc20/operation/deploy.rs @@ -0,0 +1,117 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +pub struct Deploy { + #[serde(rename = "tick")] + pub tick: String, + #[serde(rename = "max")] + pub max_supply: String, + #[serde(rename = "lim")] + pub mint_limit: Option, + #[serde(rename = "dec")] + pub decimals: Option, +} + +#[cfg(test)] +mod tests { + use super::super::*; + use super::*; + + #[test] + fn test_serialize() { + let obj = Deploy { + tick: "abcd".to_string(), + max_supply: "12000".to_string(), + mint_limit: Some("12".to_string()), + decimals: Some("11".to_string()), + }; + + assert_eq!( + serde_json::to_string(&obj).unwrap(), + format!( + r##"{{"tick":"{}","max":"{}","lim":"{}","dec":"{}"}}"##, + obj.tick, + obj.max_supply, + obj.mint_limit.unwrap(), + obj.decimals.unwrap() + ) + ) + } + + #[test] + fn test_deserialize() { + assert_eq!( + deserialize_brc20( + r#"{"p":"brc-20","op":"deploy","tick":"abcd","max":"12000","lim":"12","dec":"11"}"# + ) + .unwrap(), + RawOperation::Deploy(Deploy { + tick: "abcd".to_string(), + max_supply: "12000".to_string(), + mint_limit: Some("12".to_string()), + decimals: Some("11".to_string()), + }) + ); + } + + #[test] + fn test_loss_require_key() { + assert_eq!( + deserialize_brc20(r#"{"p":"brc-20","op":"deploy","tick":"11","lim":"22","dec":"11"}"#) + .unwrap_err(), + JSONError::ParseOperationJsonError("missing field `max`".to_string()) + ); + } + + #[test] + fn test_loss_option_key() { + // loss lim + assert_eq!( + deserialize_brc20(r#"{"p":"brc-20","op":"deploy","tick":"smol","max":"100","dec":"10"}"#) + .unwrap(), + RawOperation::Deploy(Deploy { + tick: "smol".to_string(), + max_supply: "100".to_string(), + mint_limit: None, + decimals: Some("10".to_string()), + }) + ); + + // loss dec + assert_eq!( + deserialize_brc20(r#"{"p":"brc-20","op":"deploy","tick":"smol","max":"100","lim":"10"}"#) + .unwrap(), + RawOperation::Deploy(Deploy { + tick: "smol".to_string(), + max_supply: "100".to_string(), + mint_limit: Some("10".to_string()), + decimals: None, + }) + ); + + // loss all option + assert_eq!( + deserialize_brc20(r#"{"p":"brc-20","op":"deploy","tick":"smol","max":"100"}"#).unwrap(), + RawOperation::Deploy(Deploy { + tick: "smol".to_string(), + max_supply: "100".to_string(), + mint_limit: None, + decimals: None, + }) + ); + } + + #[test] + fn test_duplicate_key() { + let json_str = r#"{"p":"brc-20","op":"deploy","tick":"smol","max":"100","lim":"10","dec":"17","max":"200","lim":"20","max":"300"}"#; + assert_eq!( + deserialize_brc20(json_str).unwrap(), + RawOperation::Deploy(Deploy { + tick: "smol".to_string(), + max_supply: "300".to_string(), + mint_limit: Some("20".to_string()), + decimals: Some("17".to_string()), + }) + ); + } +} diff --git a/src/okx/protocol/brc20/operation/mint.rs b/src/okx/protocol/brc20/operation/mint.rs new file mode 100644 index 0000000000..ae0d9cb8f3 --- /dev/null +++ b/src/okx/protocol/brc20/operation/mint.rs @@ -0,0 +1,58 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +pub struct Mint { + #[serde(rename = "tick")] + pub tick: String, + #[serde(rename = "amt")] + pub amount: String, +} + +#[cfg(test)] +mod tests { + use super::super::*; + use super::*; + + #[test] + fn test_serialize() { + let obj = Mint { + tick: "abcd".to_string(), + amount: "22".to_string(), + }; + assert_eq!( + serde_json::to_string(&obj).unwrap(), + r#"{"tick":"abcd","amt":"22"}"# + ); + } + + #[test] + fn test_deserialize() { + assert_eq!( + deserialize_brc20(r#"{"p":"brc-20","op":"mint","tick":"abcd","amt":"12000"}"#).unwrap(), + RawOperation::Mint(Mint { + tick: "abcd".to_string(), + amount: "12000".to_string() + }) + ); + } + + #[test] + fn test_loss_require_key() { + assert_eq!( + deserialize_brc20(r#"{"p":"brc-20","op":"mint","tick":"abcd"}"#).unwrap_err(), + JSONError::ParseOperationJsonError("missing field `amt`".to_string()) + ); + } + + #[test] + fn test_duplicate_key() { + let json_str = r#"{"p":"brc-20","op":"mint","tick":"smol","amt":"100","tick":"hhaa","amt":"200","tick":"actt"}"#; + assert_eq!( + deserialize_brc20(json_str).unwrap(), + RawOperation::Mint(Mint { + tick: "actt".to_string(), + amount: "200".to_string(), + }) + ); + } +} diff --git a/src/okx/protocol/brc20/operation/mod.rs b/src/okx/protocol/brc20/operation/mod.rs new file mode 100644 index 0000000000..6e2778b960 --- /dev/null +++ b/src/okx/protocol/brc20/operation/mod.rs @@ -0,0 +1,289 @@ +mod deploy; +mod mint; +mod transfer; + +use super::{params::*, *}; +use crate::{okx::datastore::ord::Action, Inscription}; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; + +pub use self::{deploy::Deploy, mint::Mint, transfer::Transfer}; + +#[derive(Debug, Clone, PartialEq)] +pub enum Operation { + Deploy(Deploy), + Mint(Mint), + InscribeTransfer(Transfer), + Transfer(Transfer), +} + +impl Operation { + pub fn op_type(&self) -> OperationType { + match self { + Operation::Deploy(_) => OperationType::Deploy, + Operation::Mint(_) => OperationType::Mint, + Operation::InscribeTransfer(_) => OperationType::InscribeTransfer, + Operation::Transfer(_) => OperationType::Transfer, + } + } +} + +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[serde(tag = "op")] +enum RawOperation { + #[serde(rename = "deploy")] + Deploy(Deploy), + #[serde(rename = "mint")] + Mint(Mint), + #[serde(rename = "transfer")] + Transfer(Transfer), +} + +pub(crate) fn deserialize_brc20_operation( + inscription: &Inscription, + action: &Action, +) -> Result { + let content_body = std::str::from_utf8(inscription.body().ok_or(JSONError::InvalidJson)?)?; + if content_body.len() < 40 { + return Err(JSONError::NotBRC20Json.into()); + } + + let content_type = inscription + .content_type() + .ok_or(JSONError::InvalidContentType)?; + + if content_type != "text/plain" + && content_type != "text/plain;charset=utf-8" + && content_type != "text/plain;charset=UTF-8" + && content_type != "application/json" + && !content_type.starts_with("text/plain;") + { + return Err(JSONError::UnSupportContentType.into()); + } + let raw_operation = match deserialize_brc20(content_body) { + Ok(op) => op, + Err(e) => { + return Err(e.into()); + } + }; + + match action { + Action::New { .. } => match raw_operation { + RawOperation::Deploy(deploy) => Ok(Operation::Deploy(deploy)), + RawOperation::Mint(mint) => Ok(Operation::Mint(mint)), + RawOperation::Transfer(transfer) => Ok(Operation::InscribeTransfer(transfer)), + }, + Action::Transfer => match raw_operation { + RawOperation::Transfer(transfer) => Ok(Operation::Transfer(transfer)), + _ => Err(JSONError::NotBRC20Json.into()), + }, + } +} + +fn deserialize_brc20(s: &str) -> Result { + let value: Value = serde_json::from_str(s).map_err(|_| JSONError::InvalidJson)?; + if value.get("p") != Some(&json!(PROTOCOL_LITERAL)) { + return Err(JSONError::NotBRC20Json); + } + + serde_json::from_value(value).map_err(|e| JSONError::ParseOperationJsonError(e.to_string())) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::okx::datastore::ord::Action; + + #[test] + fn test_deploy_deserialize() { + let max_supply = "21000000".to_string(); + let mint_limit = "1000".to_string(); + + let json_str = format!( + r##"{{ + "p": "brc-20", + "op": "deploy", + "tick": "ordi", + "max": "{max_supply}", + "lim": "{mint_limit}" +}}"## + ); + + assert_eq!( + deserialize_brc20(&json_str).unwrap(), + RawOperation::Deploy(Deploy { + tick: "ordi".to_string(), + max_supply, + mint_limit: Some(mint_limit), + decimals: None + }) + ); + } + + #[test] + fn test_mint_deserialize() { + let amount = "1000".to_string(); + + let json_str = format!( + r##"{{ + "p": "brc-20", + "op": "mint", + "tick": "ordi", + "amt": "{amount}" +}}"## + ); + + assert_eq!( + deserialize_brc20(&json_str).unwrap(), + RawOperation::Mint(Mint { + tick: "ordi".to_string(), + amount, + }) + ); + } + + #[test] + fn test_transfer_deserialize() { + let amount = "100".to_string(); + + let json_str = format!( + r##"{{ + "p": "brc-20", + "op": "transfer", + "tick": "ordi", + "amt": "{amount}" +}}"## + ); + + assert_eq!( + deserialize_brc20(&json_str).unwrap(), + RawOperation::Transfer(Transfer { + tick: "ordi".to_string(), + amount, + }) + ); + } + #[test] + fn test_json_duplicate_field() { + let json_str = r#"{"p":"brc-20","op":"mint","tick":"smol","amt":"333","amt":"33"}"#; + assert_eq!( + deserialize_brc20(json_str).unwrap(), + RawOperation::Mint(Mint { + tick: String::from("smol"), + amount: String::from("33"), + }) + ) + } + + #[test] + fn test_json_non_string() { + let json_str = r#"{"p":"brc-20","op":"mint","tick":"smol","amt":33}"#; + assert!(deserialize_brc20(json_str).is_err()) + } + + #[test] + fn test_deserialize_case_insensitive() { + let max_supply = "21000000".to_string(); + let mint_limit = "1000".to_string(); + + let json_str = format!( + r##"{{ + "P": "brc-20", + "Op": "deploy", + "Tick": "ordi", + "mAx": "{max_supply}", + "Lim": "{mint_limit}" +}}"## + ); + + assert_eq!(deserialize_brc20(&json_str), Err(JSONError::NotBRC20Json)); + } + #[test] + fn test_ignore_non_transfer_brc20() { + let content_type = "text/plain;charset=utf-8"; + let inscription = crate::inscription( + content_type, + r#"{"p":"brc-20","op":"deploy","tick":"abcd","max":"12000","lim":"12","dec":"11"}"#, + ); + assert_eq!( + deserialize_brc20_operation( + &inscription, + &Action::New { + cursed: false, + unbound: false, + inscription: inscription.clone() + }, + ) + .unwrap(), + Operation::Deploy(Deploy { + tick: "abcd".to_string(), + max_supply: "12000".to_string(), + mint_limit: Some("12".to_string()), + decimals: Some("11".to_string()), + }), + ); + let inscription = crate::inscription( + content_type, + r#"{"p":"brc-20","op":"mint","tick":"abcd","amt":"12000"}"#, + ); + + assert_eq!( + deserialize_brc20_operation( + &inscription, + &Action::New { + cursed: false, + unbound: false, + inscription: inscription.clone() + }, + ) + .unwrap(), + Operation::Mint(Mint { + tick: "abcd".to_string(), + amount: "12000".to_string() + }) + ); + let inscription = crate::inscription( + content_type, + r#"{"p":"brc-20","op":"transfer","tick":"abcd","amt":"12000"}"#, + ); + + assert_eq!( + deserialize_brc20_operation( + &inscription, + &Action::New { + cursed: false, + unbound: false, + inscription: inscription.clone() + }, + ) + .unwrap(), + Operation::InscribeTransfer(Transfer { + tick: "abcd".to_string(), + amount: "12000".to_string() + }) + ); + + let inscription = crate::inscription( + content_type, + r#"{"p":"brc-20","op":"deploy","tick":"abcd","max":"12000","lim":"12","dec":"11"}"#, + ); + assert!(deserialize_brc20_operation(&inscription, &Action::Transfer).is_err()); + + let inscription = crate::inscription( + content_type, + r#"{"p":"brc-20","op":"mint","tick":"abcd","amt":"12000"}"#, + ); + assert!(deserialize_brc20_operation(&inscription, &Action::Transfer).is_err()); + let inscription = crate::inscription( + content_type, + r#"{"p":"brc-20","op":"transfer","tick":"abcd","amt":"12000"}"#, + ); + assert_eq!( + deserialize_brc20_operation(&inscription, &Action::Transfer).unwrap(), + Operation::Transfer(Transfer { + tick: "abcd".to_string(), + amount: "12000".to_string() + }) + ); + } +} diff --git a/src/okx/protocol/brc20/operation/transfer.rs b/src/okx/protocol/brc20/operation/transfer.rs new file mode 100644 index 0000000000..2026e31837 --- /dev/null +++ b/src/okx/protocol/brc20/operation/transfer.rs @@ -0,0 +1,58 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +pub struct Transfer { + #[serde(rename = "tick")] + pub tick: String, + #[serde(rename = "amt")] + pub amount: String, +} + +#[cfg(test)] +mod tests { + use super::super::*; + use super::*; + + #[test] + fn test_serialize() { + let obj = Transfer { + tick: "abcd".to_string(), + amount: "333".to_string(), + }; + assert_eq!( + serde_json::to_string(&obj).unwrap(), + r#"{"tick":"abcd","amt":"333"}"# + ); + } + + #[test] + fn test_deserialize() { + assert_eq!( + deserialize_brc20(r#"{"p":"brc-20","op":"transfer","tick":"abcd","amt":"12000"}"#).unwrap(), + RawOperation::Transfer(Transfer { + tick: "abcd".to_string(), + amount: "12000".to_string() + }) + ); + } + + #[test] + fn test_loss_require_key() { + assert_eq!( + deserialize_brc20(r#"{"p":"brc-20","op":"transfer","tick":"abcd"}"#).unwrap_err(), + JSONError::ParseOperationJsonError("missing field `amt`".to_string()) + ); + } + + #[test] + fn test_duplicate_key() { + let json_str = r#"{"p":"brc-20","op":"transfer","tick":"smol","amt":"100","tick":"hhaa","amt":"200","tick":"actt"}"#; + assert_eq!( + deserialize_brc20(json_str).unwrap(), + RawOperation::Transfer(Transfer { + tick: "actt".to_string(), + amount: "200".to_string(), + }) + ); + } +} diff --git a/src/okx/protocol/brc20/params.rs b/src/okx/protocol/brc20/params.rs new file mode 100644 index 0000000000..5bd326c99c --- /dev/null +++ b/src/okx/protocol/brc20/params.rs @@ -0,0 +1,14 @@ +use super::num::Num; +use once_cell::sync::Lazy; + +pub const PROTOCOL_LITERAL: &str = "brc-20"; +pub const MAX_DECIMAL_WIDTH: u8 = 18; + +pub static MAXIMUM_SUPPLY: Lazy = Lazy::new(|| Num::from(u64::MAX)); + +pub static BIGDECIMAL_TEN: Lazy = Lazy::new(|| Num::from(10u64)); + +#[allow(dead_code)] +pub const fn default_decimals() -> u8 { + MAX_DECIMAL_WIDTH +} diff --git a/src/okx/protocol/context.rs b/src/okx/protocol/context.rs new file mode 100644 index 0000000000..e266ed1c90 --- /dev/null +++ b/src/okx/protocol/context.rs @@ -0,0 +1,344 @@ +use crate::index::{ + entry::Entry, InscriptionEntryValue, InscriptionIdValue, OutPointValue, TxidValue, +}; +use crate::inscription_id::InscriptionId; +use crate::okx::datastore::brc20::redb::table::{ + add_transaction_receipt, get_balance, get_balances, get_inscribe_transfer_inscription, + get_token_info, get_tokens_info, get_transaction_receipts, get_transferable, + get_transferable_by_id, get_transferable_by_tick, insert_inscribe_transfer_inscription, + insert_token_info, insert_transferable, remove_inscribe_transfer_inscription, + remove_transferable, update_mint_token_info, update_token_balance, +}; +use crate::okx::datastore::brc20::{ + Balance, Brc20Reader, Brc20ReaderWriter, Receipt, Tick, TokenInfo, TransferInfo, TransferableLog, +}; +use crate::okx::datastore::ord::collections::CollectionKind; +use crate::okx::datastore::ord::redb::table::{ + get_collection_inscription_id, get_collections_of_inscription, get_transaction_operations, + get_txout_by_outpoint, set_inscription_attributes, set_inscription_by_collection_key, +}; +use crate::okx::datastore::ord::redb::table::{ + get_inscription_number_by_sequence_number, save_transaction_operations, +}; +use crate::okx::datastore::ord::{InscriptionOp, OrdReader, OrdReaderWriter}; +use crate::okx::datastore::ScriptKey; +use crate::okx::protocol::zeroindexer::{ + datastore::{ZeroIndexerReader, ZeroIndexerReaderWriter}, + zerodata::ZeroData, +}; +use crate::okx::lru::SimpleLru; +use crate::okx::protocol::BlockContext; +use crate::{Inscription, SatPoint}; +use anyhow::anyhow; +use bitcoin::{Network, OutPoint, TxOut, Txid}; +use redb::{MultimapTable, ReadableTable, Table}; + +#[allow(non_snake_case)] +pub struct Context<'a, 'db, 'txn> { + pub(crate) chain: BlockContext, + pub(crate) tx_out_cache: &'a mut SimpleLru, + pub(crate) hit: u64, + pub(crate) miss: u64, + + // ord tables + pub(crate) ORD_TX_TO_OPERATIONS: &'a mut Table<'db, 'txn, &'static TxidValue, &'static [u8]>, + pub(crate) COLLECTIONS_KEY_TO_INSCRIPTION_ID: + &'a mut Table<'db, 'txn, &'static str, InscriptionIdValue>, + pub(crate) COLLECTIONS_INSCRIPTION_ID_TO_KINDS: + &'a mut Table<'db, 'txn, InscriptionIdValue, &'static [u8]>, + pub(crate) SEQUENCE_NUMBER_TO_INSCRIPTION_ENTRY: + &'a mut Table<'db, 'txn, u32, InscriptionEntryValue>, + pub(crate) OUTPOINT_TO_ENTRY: &'a mut Table<'db, 'txn, &'static OutPointValue, &'static [u8]>, + + // BRC20 tables + pub(crate) BRC20_BALANCES: &'a mut Table<'db, 'txn, &'static str, &'static [u8]>, + pub(crate) BRC20_TOKEN: &'a mut Table<'db, 'txn, &'static str, &'static [u8]>, + pub(crate) BRC20_EVENTS: &'a mut MultimapTable<'db, 'txn, &'static TxidValue, &'static [u8]>, + pub(crate) BRC20_TRANSFERABLELOG: &'a mut Table<'db, 'txn, &'static str, &'static [u8]>, + pub(crate) BRC20_INSCRIBE_TRANSFER: &'a mut Table<'db, 'txn, InscriptionIdValue, &'static [u8]>, + + //zero-indexer tables + pub(crate) ZERO_INSCRIPTION_ID_TO_INSCRIPTION: + &'a mut Table<'db, 'txn, InscriptionIdValue, &'static [u8]>, + pub(crate) ZERO_HEIGHT_TO_TXS: &'a mut Table<'db, 'txn, u64, &'static [u8]>, +} + +impl<'a, 'db, 'txn> OrdReader for Context<'a, 'db, 'txn> { + type Error = anyhow::Error; + + fn get_inscription_number_by_sequence_number( + &self, + sequence_number: u32, + ) -> crate::Result { + get_inscription_number_by_sequence_number( + self.SEQUENCE_NUMBER_TO_INSCRIPTION_ENTRY, + sequence_number, + ) + .map_err(|e| anyhow!("failed to get inscription number from state! error: {e}"))? + .ok_or(anyhow!( + "failed to get inscription number! error: sequence number {} not found", + sequence_number + )) + } + + fn get_script_key_on_satpoint( + &mut self, + satpoint: &SatPoint, + network: Network, + ) -> crate::Result { + if let Some(tx_out) = self.tx_out_cache.get(&satpoint.outpoint) { + self.hit += 1; + Ok(ScriptKey::from_script(&tx_out.script_pubkey, network)) + } else if let Some(tx_out) = get_txout_by_outpoint(self.OUTPOINT_TO_ENTRY, &satpoint.outpoint)? + { + self.miss += 1; + Ok(ScriptKey::from_script(&tx_out.script_pubkey, network)) + } else { + Err(anyhow!( + "failed to get tx out! error: outpoint {} not found", + &satpoint.outpoint + )) + } + } + + fn get_transaction_operations( + &self, + txid: &Txid, + ) -> crate::Result, Self::Error> { + get_transaction_operations(self.ORD_TX_TO_OPERATIONS, txid) + } + + fn get_collections_of_inscription( + &self, + inscription_id: &InscriptionId, + ) -> crate::Result>, Self::Error> { + get_collections_of_inscription(self.COLLECTIONS_INSCRIPTION_ID_TO_KINDS, inscription_id) + } + + fn get_collection_inscription_id( + &self, + collection_key: &str, + ) -> crate::Result, Self::Error> { + get_collection_inscription_id(self.COLLECTIONS_KEY_TO_INSCRIPTION_ID, collection_key) + } +} + +impl<'a, 'db, 'txn> OrdReaderWriter for Context<'a, 'db, 'txn> { + fn save_transaction_operations( + &mut self, + txid: &Txid, + operations: &[InscriptionOp], + ) -> crate::Result<(), Self::Error> { + save_transaction_operations(self.ORD_TX_TO_OPERATIONS, txid, operations) + } + + fn set_inscription_by_collection_key( + &mut self, + key: &str, + inscription_id: &InscriptionId, + ) -> crate::Result<(), Self::Error> { + set_inscription_by_collection_key(self.COLLECTIONS_KEY_TO_INSCRIPTION_ID, key, inscription_id) + } + + fn set_inscription_attributes( + &mut self, + inscription_id: &InscriptionId, + kind: &[CollectionKind], + ) -> crate::Result<(), Self::Error> { + set_inscription_attributes( + self.COLLECTIONS_INSCRIPTION_ID_TO_KINDS, + inscription_id, + kind, + ) + } +} + +impl<'a, 'db, 'txn> Brc20Reader for Context<'a, 'db, 'txn> { + type Error = anyhow::Error; + + fn get_balances(&self, script_key: &ScriptKey) -> crate::Result, Self::Error> { + get_balances(self.BRC20_BALANCES, script_key) + } + + fn get_balance( + &self, + script_key: &ScriptKey, + tick: &Tick, + ) -> crate::Result, Self::Error> { + get_balance(self.BRC20_BALANCES, script_key, tick) + } + + fn get_token_info(&self, tick: &Tick) -> crate::Result, Self::Error> { + get_token_info(self.BRC20_TOKEN, tick) + } + + fn get_tokens_info(&self) -> crate::Result, Self::Error> { + get_tokens_info(self.BRC20_TOKEN) + } + + fn get_transaction_receipts(&self, txid: &Txid) -> crate::Result, Self::Error> { + get_transaction_receipts(self.BRC20_EVENTS, txid) + } + + fn get_transferable( + &self, + script: &ScriptKey, + ) -> crate::Result, Self::Error> { + get_transferable(self.BRC20_TRANSFERABLELOG, script) + } + + fn get_transferable_by_tick( + &self, + script: &ScriptKey, + tick: &Tick, + ) -> crate::Result, Self::Error> { + get_transferable_by_tick(self.BRC20_TRANSFERABLELOG, script, tick) + } + + fn get_transferable_by_id( + &self, + script: &ScriptKey, + inscription_id: &InscriptionId, + ) -> crate::Result, Self::Error> { + get_transferable_by_id(self.BRC20_TRANSFERABLELOG, script, inscription_id) + } + + fn get_inscribe_transfer_inscription( + &self, + inscription_id: &InscriptionId, + ) -> crate::Result, Self::Error> { + get_inscribe_transfer_inscription(self.BRC20_INSCRIBE_TRANSFER, inscription_id) + } +} + +impl<'a, 'db, 'txn> Brc20ReaderWriter for Context<'a, 'db, 'txn> { + fn update_token_balance( + &mut self, + script_key: &ScriptKey, + new_balance: Balance, + ) -> crate::Result<(), Self::Error> { + update_token_balance(self.BRC20_BALANCES, script_key, new_balance) + } + + fn insert_token_info( + &mut self, + tick: &Tick, + new_info: &TokenInfo, + ) -> crate::Result<(), Self::Error> { + insert_token_info(self.BRC20_TOKEN, tick, new_info) + } + + fn update_mint_token_info( + &mut self, + tick: &Tick, + minted_amt: u128, + minted_block_number: u32, + ) -> crate::Result<(), Self::Error> { + update_mint_token_info(self.BRC20_TOKEN, tick, minted_amt, minted_block_number) + } + + fn add_transaction_receipt( + &mut self, + txid: &Txid, + receipt: &Receipt, + ) -> crate::Result<(), Self::Error> { + add_transaction_receipt(self.BRC20_EVENTS, txid, receipt) + } + + fn insert_transferable( + &mut self, + script: &ScriptKey, + tick: &Tick, + inscription: &TransferableLog, + ) -> crate::Result<(), Self::Error> { + insert_transferable(self.BRC20_TRANSFERABLELOG, script, tick, inscription) + } + + fn remove_transferable( + &mut self, + script: &ScriptKey, + tick: &Tick, + inscription_id: &InscriptionId, + ) -> crate::Result<(), Self::Error> { + remove_transferable(self.BRC20_TRANSFERABLELOG, script, tick, inscription_id) + } + + fn insert_inscribe_transfer_inscription( + &mut self, + inscription_id: &InscriptionId, + transfer_info: TransferInfo, + ) -> crate::Result<(), Self::Error> { + insert_inscribe_transfer_inscription( + self.BRC20_INSCRIBE_TRANSFER, + inscription_id, + transfer_info, + ) + } + + fn remove_inscribe_transfer_inscription( + &mut self, + inscription_id: &InscriptionId, + ) -> crate::Result<(), Self::Error> { + remove_inscribe_transfer_inscription(self.BRC20_INSCRIBE_TRANSFER, inscription_id) + } +} + +impl<'a, 'db, 'txn> ZeroIndexerReader for Context<'a, 'db, 'txn> { + type Error = anyhow::Error; + + fn get_inscription( + &self, + inscription_id: &InscriptionId, + ) -> crate::Result, Self::Error> { + Ok( + self + .ZERO_INSCRIPTION_ID_TO_INSCRIPTION + .get(&inscription_id.store())? + .map(|x| bincode::deserialize::(x.value()).unwrap()), + ) + } + + fn get_zero_indexer_txs(&self, height: u64) -> crate::Result, Self::Error> { + Ok( + self + .ZERO_HEIGHT_TO_TXS + .get(height)? + .map(|x| bincode::deserialize::(x.value()).unwrap()), + ) + } +} + +impl<'a, 'db, 'txn> ZeroIndexerReaderWriter for Context<'a, 'db, 'txn> { + fn insert_inscription( + &mut self, + inscription_id: &InscriptionId, + inscription: &Inscription, + ) -> crate::Result<(), Self::Error> { + self.ZERO_INSCRIPTION_ID_TO_INSCRIPTION.insert( + &inscription_id.store(), + bincode::serialize(inscription).unwrap().as_slice(), + )?; + Ok(()) + } + + fn remove_inscription( + &mut self, + inscription_id: &InscriptionId, + ) -> crate::Result<(), Self::Error> { + self + .ZERO_INSCRIPTION_ID_TO_INSCRIPTION + .remove(&inscription_id.store())?; + Ok(()) + } + + fn insert_zero_indexer_txs( + &mut self, + height: u64, + data: &ZeroData, + ) -> crate::Result<(), Self::Error> { + self + .ZERO_HEIGHT_TO_TXS + .insert(height, bincode::serialize(data).unwrap().as_slice())?; + Ok(()) + } +} diff --git a/src/okx/protocol/execute_manager.rs b/src/okx/protocol/execute_manager.rs new file mode 100644 index 0000000000..1829ce5b97 --- /dev/null +++ b/src/okx/protocol/execute_manager.rs @@ -0,0 +1,26 @@ +use crate::okx::protocol::context::Context; +use { + super::*, + crate::{okx::protocol::brc20 as brc20_proto, Result}, +}; + +pub struct CallManager {} + +impl CallManager { + pub fn new() -> Self { + Self {} + } + + pub fn execute_message(&self, context: &mut Context, msg: &Message) -> Result { + // execute message + match msg { + Message::BRC20(brc_msg) => { + let msg = + brc20_proto::ExecutionMessage::from_message(context, brc_msg, context.chain.network)?; + brc20_proto::execute(context, &msg).map(|v| v.map(Receipt::BRC20))? + } + }; + + Ok(()) + } +} diff --git a/src/okx/protocol/message.rs b/src/okx/protocol/message.rs new file mode 100644 index 0000000000..5d1b7e5bea --- /dev/null +++ b/src/okx/protocol/message.rs @@ -0,0 +1,12 @@ +use crate::okx::datastore::brc20 as brc20_store; +use crate::okx::protocol::brc20 as brc20_proto; + +#[allow(clippy::upper_case_acronyms)] +pub enum Message { + BRC20(brc20_proto::Message), +} + +#[allow(clippy::upper_case_acronyms)] +pub enum Receipt { + BRC20(brc20_store::Receipt), +} diff --git a/src/okx/protocol/mod.rs b/src/okx/protocol/mod.rs index 3f09a6c0f3..49fb33be22 100644 --- a/src/okx/protocol/mod.rs +++ b/src/okx/protocol/mod.rs @@ -1,8 +1,23 @@ +pub(crate) mod brc20; +pub(crate) mod context; +pub(crate) mod execute_manager; +pub(crate) mod message; pub(crate) mod ord; pub(crate) mod protocol_manager; +pub(crate) mod resolve_manager; +pub(crate) mod zeroindexer; pub use self::protocol_manager::ProtocolManager; -use {crate::Options, bitcoin::Network}; + +use { + self::{ + execute_manager::CallManager, + message::{Message, Receipt}, + resolve_manager::MsgResolveManager, + }, + crate::Options, + bitcoin::Network, +}; #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct BlockContext { @@ -10,9 +25,10 @@ pub struct BlockContext { pub blockheight: u32, pub blocktime: u32, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] pub struct ProtocolConfig { first_inscription_height: u32, + first_brc20_height: Option, enable_ord_receipts: bool, enable_index_bitmap: bool, } @@ -21,6 +37,11 @@ impl ProtocolConfig { pub(crate) fn new_with_options(options: &Options) -> Self { Self { first_inscription_height: options.first_inscription_height(), + first_brc20_height: if options.enable_index_brc20 { + Some(options.first_brc20_height()) + } else { + None + }, enable_ord_receipts: options.enable_save_ord_receipts, enable_index_bitmap: options.enable_index_bitmap, } diff --git a/src/okx/protocol/ord/bitmap.rs b/src/okx/protocol/ord/bitmap.rs index 5d16d1a631..843aa50caf 100644 --- a/src/okx/protocol/ord/bitmap.rs +++ b/src/okx/protocol/ord/bitmap.rs @@ -1,23 +1,21 @@ +use crate::okx::datastore::ord::{OrdReader, OrdReaderWriter}; +use crate::okx::protocol::context::Context; use { - super::*, crate::{ - okx::{ - datastore::ord::{ - bitmap::District, - collections::CollectionKind, - operation::{Action, InscriptionOp}, - }, - protocol::BlockContext, + okx::datastore::ord::{ + bitmap::District, + collections::CollectionKind, + operation::{Action, InscriptionOp}, }, Inscription, InscriptionId, Result, }, + anyhow::anyhow, bitcoin::Txid, std::collections::HashMap, }; -pub fn index_bitmap( - ord_store: &O, - context: BlockContext, +pub fn index_bitmap( + context: &mut Context, operations: &HashMap>, ) -> Result { let mut count = 0; @@ -43,17 +41,12 @@ pub fn index_bitmap( inscription, } => { if let Some((inscription_id, district)) = - index_district(ord_store, context, inscription, op.inscription_id)? + index_district(context, inscription, op.inscription_id)? { let key = district.to_collection_key(); - ord_store - .set_inscription_by_collection_key(&key, inscription_id) - .map_err(|e| anyhow!("failed to store collection! key: {key}, error: {e}"))?; - ord_store - .set_inscription_attributes(inscription_id, &[CollectionKind::BitMap]) - .map_err(|e| { - anyhow!("failed to store inscription attributes! id: {inscription_id} error: {e}") - })?; + context.set_inscription_by_collection_key(&key, &inscription_id)?; + context.set_inscription_attributes(&inscription_id, &[CollectionKind::BitMap])?; + count += 1; } } @@ -63,19 +56,19 @@ pub fn index_bitmap( Ok(count) } -fn index_district( - ord_store: &O, - context: BlockContext, +fn index_district( + context: &mut Context, inscription: Inscription, inscription_id: InscriptionId, ) -> Result> { if let Some(content) = inscription.body() { if let Ok(district) = District::parse(content) { - if district.number > context.blockheight { + if district.number > context.chain.blockheight { return Ok(None); } let collection_key = district.to_collection_key(); - if ord_store + + if context .get_collection_inscription_id(&collection_key) .map_err(|e| { anyhow!("failed to get collection inscription! key: {collection_key} error: {e}") diff --git a/src/okx/protocol/ord/mod.rs b/src/okx/protocol/ord/mod.rs index 641f768402..163f8968d3 100644 --- a/src/okx/protocol/ord/mod.rs +++ b/src/okx/protocol/ord/mod.rs @@ -1,19 +1 @@ -use { - crate::{ - okx::datastore::ord::{DataStoreReadWrite, InscriptionOp}, - Result, - }, - anyhow::anyhow, - bitcoin::Txid, -}; pub mod bitmap; - -pub fn save_transaction_operations( - ord_store: &O, - txid: &Txid, - tx_operations: &[InscriptionOp], -) -> Result<()> { - ord_store - .save_transaction_operations(txid, tx_operations) - .map_err(|e| anyhow!("failed to set transaction ordinals operations to state! error: {e}")) -} diff --git a/src/okx/protocol/protocol_manager.rs b/src/okx/protocol/protocol_manager.rs index 5fffc990b2..6b22dc270e 100644 --- a/src/okx/protocol/protocol_manager.rs +++ b/src/okx/protocol/protocol_manager.rs @@ -1,63 +1,132 @@ +use crate::okx::datastore::ord::OrdReaderWriter; +use crate::okx::protocol::context::Context; +use crate::okx::protocol::zeroindexer::datastore::ZeroIndexerReaderWriter; +use crate::okx::protocol::zeroindexer::resolve_zero_inscription; +use crate::okx::protocol::zeroindexer::zerodata::{ZeroData, ZeroIndexerTx}; use { super::*, crate::{ index::BlockData, - okx::{ - datastore::{ord::operation::InscriptionOp, StateRWriter}, - protocol::ord as ord_proto, - }, + okx::{datastore::ord::operation::InscriptionOp, protocol::ord as ord_proto}, Instant, Result, }, bitcoin::Txid, std::collections::HashMap, }; -pub struct ProtocolManager<'a, RW: StateRWriter> { - state_store: &'a RW, - config: &'a ProtocolConfig, +pub struct ProtocolManager { + config: ProtocolConfig, + call_man: CallManager, + resolve_man: MsgResolveManager, } -impl<'a, RW: StateRWriter> ProtocolManager<'a, RW> { +impl ProtocolManager { // Need three datastore, and they're all in the same write transaction. - pub fn new(state_store: &'a RW, config: &'a ProtocolConfig) -> Self { + pub fn new(config: ProtocolConfig) -> Self { Self { - state_store, config, + call_man: CallManager::new(), + resolve_man: MsgResolveManager::new(config), } } pub(crate) fn index_block( &self, - context: BlockContext, + context: &mut Context, block: &BlockData, - operations: &HashMap>, + operations: HashMap>, ) -> Result { let start = Instant::now(); let mut inscriptions_size = 0; + let mut messages_size = 0; + let mut cost1 = 0u128; + let mut cost2 = 0u128; + let mut cost3 = 0u128; + let mut zero_indexer_txs: Vec = Vec::new(); // skip the coinbase transaction. - for (_, txid) in block.txdata.iter().skip(1) { + for (tx, txid) in block.txdata.iter() { + // skip coinbase transaction. + if tx + .input + .first() + .is_some_and(|tx_in| tx_in.previous_output.is_null()) + { + continue; + } + // index inscription operations. if let Some(tx_operations) = operations.get(txid) { // save all transaction operations to ord database. if self.config.enable_ord_receipts - && context.blockheight >= self.config.first_inscription_height + && context.chain.blockheight >= self.config.first_inscription_height { - ord_proto::save_transaction_operations(self.state_store.ord(), txid, tx_operations)?; + let start = Instant::now(); + context.save_transaction_operations(txid, tx_operations)?; inscriptions_size += tx_operations.len(); + cost1 += start.elapsed().as_micros(); + } + + let start = Instant::now(); + // Resolve and execute messages. + let messages = self + .resolve_man + .resolve_message(context, tx, tx_operations)?; + cost2 += start.elapsed().as_micros(); + + let start = Instant::now(); + for msg in messages.iter() { + self.call_man.execute_message(context, msg)?; + } + cost3 += start.elapsed().as_micros(); + messages_size += messages.len(); + + if context.chain.blockheight >= 779832 { + match resolve_zero_inscription(context, &block.header.block_hash(), tx, tx_operations) { + Ok(mut results) => zero_indexer_txs.append(&mut results), + Err(e) => { + log::error!("resolve_zero_inscription error:{}", e); + return Err(e); + } + }; } } } + + let bitmap_start = Instant::now(); let mut bitmap_count = 0; if self.config.enable_index_bitmap { - bitmap_count = ord_proto::bitmap::index_bitmap(self.state_store.ord(), context, &operations)?; + bitmap_count = ord_proto::bitmap::index_bitmap(context, &operations)?; } + let cost4 = bitmap_start.elapsed().as_millis(); + if context.chain.blockheight >= 779832 { + match context.insert_zero_indexer_txs( + context.chain.blockheight as u64, + &ZeroData { + block_height: context.chain.blockheight as u64, + block_hash: block.header.block_hash().to_string(), + prev_block_hash: block.header.prev_blockhash.to_string(), + block_time: block.header.time, + txs: zero_indexer_txs, + }, + ) { + Ok(_) => {} + Err(e) => { + log::info!("insert_zer_indexer_tx failed: {e}") + } + }; + } log::info!( - "Protocol Manager indexed block {} with ord inscriptions {}, bitmap {} in {} ms", - context.blockheight, + "Protocol Manager indexed block {} with ord inscriptions {}, messages {}, bitmap {} in {} ms, {}/{}/{}/{}", + context.chain.blockheight, inscriptions_size, + messages_size, bitmap_count, - (Instant::now() - start).as_millis(), + start.elapsed().as_millis(), + cost1/1000, + cost2/1000, + cost3/1000, + cost4, ); Ok(()) } diff --git a/src/okx/protocol/resolve_manager.rs b/src/okx/protocol/resolve_manager.rs new file mode 100644 index 0000000000..54f205e9c3 --- /dev/null +++ b/src/okx/protocol/resolve_manager.rs @@ -0,0 +1,74 @@ +use crate::envelope::ParsedEnvelope; +use crate::okx::protocol::context::Context; +use { + super::*, + crate::{ + okx::{datastore::ord::operation::InscriptionOp, protocol::Message}, + Inscription, Result, + }, + bitcoin::Transaction, +}; + +pub struct MsgResolveManager { + config: ProtocolConfig, +} + +impl MsgResolveManager { + pub fn new(config: ProtocolConfig) -> Self { + Self { config } + } + + pub fn resolve_message( + &self, + context: &Context, + tx: &Transaction, + operations: &[InscriptionOp], + ) -> Result> { + log::debug!( + "Resolve Manager indexed transaction {}, operations size: {}, data: {:?}", + tx.txid(), + operations.len(), + operations + ); + let mut messages = Vec::new(); + let mut operation_iter = operations.iter().peekable(); + let new_inscriptions = ParsedEnvelope::from_transaction(tx) + .into_iter() + .map(|v| v.payload) + .collect::>(); + + for input in &tx.input { + // "operations" is a list of all the operations in the current block, and they are ordered. + // We just need to find the operation corresponding to the current transaction here. + while let Some(operation) = operation_iter.peek() { + if operation.old_satpoint.outpoint != input.previous_output { + break; + } + let operation = operation_iter.next().unwrap(); + + // Parse BRC20 message through inscription operation. + if self + .config + .first_brc20_height + .map(|height| context.chain.blockheight >= height) + .unwrap_or(false) + { + if let Some(msg) = brc20::Message::resolve( + context.BRC20_INSCRIBE_TRANSFER, + &new_inscriptions, + operation, + )? { + log::debug!( + "BRC20 resolved the message from {:?}, msg {:?}", + operation, + msg + ); + messages.push(Message::BRC20(msg)); + continue; + } + } + } + } + Ok(messages) + } +} diff --git a/src/okx/protocol/zeroindexer.rs b/src/okx/protocol/zeroindexer.rs new file mode 100644 index 0000000000..9468745349 --- /dev/null +++ b/src/okx/protocol/zeroindexer.rs @@ -0,0 +1,231 @@ +use crate::{ + envelope::ParsedEnvelope, + okx::{ + datastore::{ + ord::{Action, InscriptionOp, OrdReader}, + ScriptKey, + }, + protocol::{ + context::Context, + zeroindexer::{ + datastore::{ZeroIndexerReader, ZeroIndexerReaderWriter}, + error::JSONError, + error::LedgerError, + zerodata::{InscriptionContext, ZeroIndexerTx}, + }, + }, + }, + Inscription, Result, +}; +use anyhow::anyhow; +use bitcoin::{BlockHash, Network, OutPoint, Transaction}; +use serde_json::Value; + +pub(crate) mod datastore; +pub(crate) mod error; +pub(crate) mod zerodata; + +pub fn resolve_zero_inscription( + context: &mut Context, + block_hash: &BlockHash, + tx: &Transaction, + operations: &Vec, +) -> Result> { + log::debug!( + "Resolve Inscription indexed transaction {}, operations size: {}, data: {:?}", + tx.txid(), + operations.len(), + operations + ); + let mut zero_indexer_txs: Vec = Vec::new(); + let mut operation_iter = operations.into_iter().peekable(); + let new_inscriptions = ParsedEnvelope::from_transaction(&tx); + for input in &tx.input { + // "operations" is a list of all the operations in the current block, and they are ordered. + // We just need to find the operation corresponding to the current transaction here. + while let Some(operation) = operation_iter.peek() { + if operation.old_satpoint.outpoint != input.previous_output { + break; + } + let operation = operation_iter.next().unwrap(); + + let sat_in_outputs = operation + .new_satpoint + .map(|satpoint| satpoint.outpoint.txid == operation.txid) + .unwrap_or(false); + + let mut is_transfer = false; + let mut sender = "".to_string(); + let inscription = match operation.action { + // New inscription is not `cursed` or `unbound`. + Action::New { + cursed: false, + unbound: false, + .. + } => { + let inscription_struct = new_inscriptions + .get(usize::try_from(operation.inscription_id.index).unwrap()) + .unwrap() + .clone() + .payload; + let des_res = deserialize_zeroindexer_inscription(&inscription_struct); + match des_res { + Ok((content, _, inscription_op)) => { + if inscription_op == "transfer" { + context + .insert_inscription(&operation.inscription_id, &inscription_struct) + .map_err(|e| LedgerError::LedgerError(e))?; + } + content + } + Err(_) => { + continue; + } + } + } + // Transfer inscription operation. + Action::Transfer => { + if operation.inscription_id.txid == operation.old_satpoint.outpoint.txid + && operation.inscription_id.index == operation.old_satpoint.outpoint.vout + { + is_transfer = true; + + let inscription_struct = match context.get_inscription(&operation.inscription_id) { + Ok(inner) => match inner { + None => continue, + Some(inner) => { + context + .remove_inscription(&operation.inscription_id) + .map_err(|e| LedgerError::LedgerError(e))?; + inner + } + }, + Err(err) => { + return Err(anyhow!( + "failed to get inscription because btc is down:{}", + err + )); + } + }; + let des_res = deserialize_zeroindexer_inscription(&inscription_struct); + match des_res { + Ok((content, _, _)) => { + sender = context + .get_script_key_on_satpoint(&operation.old_satpoint, context.chain.network)? + .to_string(); + content + } + Err(_) => { + continue; + } + } + } else { + continue; + } + } + _ => { + continue; + } + }; + + let new_sat_point = match operation.new_satpoint { + None => "".to_string(), + Some(sat_point) => sat_point.to_string(), + }; + let receiver = if sat_in_outputs { + match operation.new_satpoint { + None => "".to_string(), + Some(sat_point) => { + match get_script_key_from_transaction(&tx, &sat_point.outpoint, context.chain.network) { + None => "".to_string(), + Some(script_key) => script_key.to_string(), + } + } + } + } else { + "".to_string() + }; + let inscription_context = InscriptionContext { + txid: operation.txid.to_string(), + inscription_id: operation.inscription_id.to_string(), + inscription_number: 0, + old_sat_point: operation.old_satpoint.to_string(), + new_sat_point, + sender, + receiver, + is_transfer, + block_height: context.chain.blockheight as u64, + block_time: context.chain.blocktime, + block_hash: block_hash.to_string(), + }; + zero_indexer_txs.push(ZeroIndexerTx { + protocol_name: "brc-20".to_string(), + inscription, + inscription_context: serde_json::to_string(&inscription_context).unwrap(), + btc_txid: operation.txid.to_string(), + btc_fee: "10000000".to_string(), + }); + } + } + Ok(zero_indexer_txs) +} + +//Some(ScriptKey::from_script(&tx_out.script_pubkey,network)) +fn get_script_key_from_transaction( + tx: &Transaction, + outpoint: &OutPoint, + network: Network, +) -> Option { + if !tx.txid().eq(&outpoint.txid) { + return None; + } + match tx.output.get(outpoint.vout as usize) { + None => None, + Some(tx_out) => Some(ScriptKey::from_script(&tx_out.script_pubkey, network)), + } +} + +fn deserialize_zeroindexer_inscription( + inscription: &Inscription, +) -> Result<(String, String, String)> { + let content_body = std::str::from_utf8(inscription.body().ok_or(JSONError::InvalidJson)?)?; + if content_body.len() == 0 { + return Err(JSONError::InvalidJson.into()); + } + + let content_type = inscription + .content_type() + .ok_or(JSONError::InvalidContentType)?; + + if content_type != "text/plain" + && content_type != "text/plain;charset=utf-8" + && content_type != "text/plain;charset=UTF-8" + && content_type != "application/json" + && !content_type.starts_with("text/plain;") + { + return Err(JSONError::UnSupportContentType.into()); + } + + let value: Value = serde_json::from_str(content_body).map_err(|_| JSONError::InvalidJson)?; + if value.get("p") == None || !value["p"].is_string() { + return Err(JSONError::InvalidJson.into()); + } + let protocol_name = match value.get("p") { + None => return Err(JSONError::NotZeroIndexerJson.into()), + Some(v) => v.to_string().replace("\"", ""), + }; + if protocol_name != "brc-20".to_string() { + return Err(JSONError::InvalidJson.into()); + } + + let protocol_op = match value.get("op") { + None => return Err(JSONError::OpNotExist.into()), + Some(op) => op.to_string().replace("\"", ""), + }; + + return Ok(( + serde_json::to_string(&value).unwrap(), + protocol_name, + protocol_op, + )); +} diff --git a/src/okx/protocol/zeroindexer/datastore.rs b/src/okx/protocol/zeroindexer/datastore.rs new file mode 100644 index 0000000000..e047f19c26 --- /dev/null +++ b/src/okx/protocol/zeroindexer/datastore.rs @@ -0,0 +1,45 @@ +use crate::inscription_id::InscriptionId; +use crate::okx::protocol::zeroindexer::zerodata::ZeroData; +use crate::Inscription; +use redb::ReadableTable; +use std::fmt::{Debug, Display}; + +pub trait ZeroIndexerReader { + type Error: Debug + Display; + fn get_inscription( + &self, + inscription_id: &InscriptionId, + ) -> crate::Result, Self::Error>; + + fn get_zero_indexer_txs(&self, height: u64) -> crate::Result, Self::Error>; +} + +pub trait ZeroIndexerReaderWriter: ZeroIndexerReader { + fn insert_inscription( + &mut self, + inscription_id: &InscriptionId, + inscription: &Inscription, + ) -> crate::Result<(), Self::Error>; + + fn remove_inscription( + &mut self, + inscription_id: &InscriptionId, + ) -> crate::Result<(), Self::Error>; + + fn insert_zero_indexer_txs( + &mut self, + height: u64, + data: &ZeroData, + ) -> crate::Result<(), Self::Error>; +} + +pub fn get_zero_indexer_txs(table: &T, height: u64) -> crate::Result> +where + T: ReadableTable, +{ + Ok( + table + .get(height)? + .map(|v| bincode::deserialize::(v.value()).unwrap()), + ) +} diff --git a/src/okx/protocol/zeroindexer/error.rs b/src/okx/protocol/zeroindexer/error.rs new file mode 100644 index 0000000000..006118da28 --- /dev/null +++ b/src/okx/protocol/zeroindexer/error.rs @@ -0,0 +1,23 @@ +#[derive(Debug, PartialEq, thiserror::Error)] +pub enum JSONError { + #[error("invalid content type")] + InvalidContentType, + + #[error("unsupport content type")] + UnSupportContentType, + + #[error("invalid json string")] + InvalidJson, + + #[error("not zeroindexer json")] + NotZeroIndexerJson, + + #[error("op is not exist")] + OpNotExist, +} + +#[derive(Debug, thiserror::Error)] +pub enum LedgerError { + #[error("ledger error: {0}")] + LedgerError(anyhow::Error), +} diff --git a/src/okx/protocol/zeroindexer/zerodata.rs b/src/okx/protocol/zeroindexer/zerodata.rs new file mode 100644 index 0000000000..5802b723d1 --- /dev/null +++ b/src/okx/protocol/zeroindexer/zerodata.rs @@ -0,0 +1,34 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +pub struct ZeroData { + pub block_height: u64, + pub block_hash: String, + pub prev_block_hash: String, + pub block_time: u32, + pub txs: Vec, +} + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +pub struct ZeroIndexerTx { + pub protocol_name: String, + pub inscription: String, + pub inscription_context: String, + pub btc_txid: String, + pub btc_fee: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct InscriptionContext { + pub txid: String, + pub inscription_id: String, + pub inscription_number: i64, + pub old_sat_point: String, + pub new_sat_point: String, + pub sender: String, + pub receiver: String, + pub is_transfer: bool, + pub block_height: u64, + pub block_time: u32, + pub block_hash: String, +} diff --git a/src/options.rs b/src/options.rs index 20a587c817..ecf5604cb2 100644 --- a/src/options.rs +++ b/src/options.rs @@ -37,6 +37,12 @@ pub(crate) struct Options { help = "Set index cache to bytes. By default takes 1/4 of available RAM." )] pub(crate) db_cache_size: Option, + #[arg( + long, + default_value = "10000000", + help = "Set lru cache to . By default 10000000" + )] + pub(crate) lru_size: usize, #[arg( long, help = "Don't look for inscriptions below ." @@ -67,6 +73,14 @@ pub(crate) struct Options { pub(crate) enable_save_ord_receipts: bool, #[arg(long, help = "Enable Index Bitmap Collection.")] pub(crate) enable_index_bitmap: bool, + // OKX defined options. + #[arg(long, help = "Enable Index all of BRC20 Protocol")] + pub(crate) enable_index_brc20: bool, + #[arg( + long, + help = "Don't look for BRC20 messages below ." + )] + pub(crate) first_brc20_height: Option, } #[derive(Debug, Clone)] @@ -114,6 +128,18 @@ impl Options { } } + pub(crate) fn first_brc20_height(&self) -> u32 { + if self.chain() == Chain::Regtest { + self.first_brc20_height.unwrap_or(0) + } else if integration_test() { + 0 + } else { + self + .first_brc20_height + .unwrap_or_else(|| self.chain().first_brc20_height()) + } + } + pub(crate) fn first_rune_height(&self) -> u32 { if integration_test() { 0 diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 3e6105c504..8da14c23c0 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -49,6 +49,7 @@ use { mod accept_encoding; mod accept_json; mod api; +mod brc20; mod error; mod info; mod ord; @@ -191,14 +192,49 @@ impl Server { #[derive(OpenApi)] #[openapi( paths( + brc20::brc20_balance, + brc20::brc20_all_balance, + brc20::brc20_tick_info, + brc20::brc20_all_tick_info, + brc20::brc20_tx_events, + brc20::brc20_block_events, + brc20::brc20_transferable, + brc20::brc20_all_transferable, + ord::ord_inscription_id, ord::ord_inscription_number, ord::ord_outpoint, ord::ord_txid_inscriptions, ord::ord_block_inscriptions, + info::node_info, ), components(schemas( + // BRC20 schemas + brc20::TickInfo, + brc20::AllTickInfo, + brc20::Balance, + brc20::AllBalance, + brc20::TxEvent, + brc20::DeployEvent, + brc20::MintEvent, + brc20::InscribeTransferEvent, + brc20::TransferEvent, + brc20::ErrorEvent, + brc20::TxEvents, + brc20::BlockEvents, + brc20::TransferableInscription, + brc20::TransferableInscriptions, + + // BRC20 responses schemas + response::BRC20Tick, + response::BRC20AllTick, + response::BRC20Balance, + response::BRC20AllBalance, + response::BRC20TxEvents, + response::BRC20BlockEvents, + response::BRC20Transferable, + // Ord schemas ord::OrdInscription, ord::InscriptionDigest, @@ -208,11 +244,13 @@ impl Server { ord::TxInscription, ord::TxInscriptions, ord::BlockInscriptions, + // Ord responses schemas response::OrdOrdInscription, response::OrdTxInscriptions, response::OrdBlockInscriptions, response::OrdOutPointResult, + // Node Info schemas info::NodeInfo, info::ChainInfo, @@ -250,14 +288,49 @@ impl Server { .route( "/ord/tx/:txid/inscriptions", get(ord::ord_txid_inscriptions), - ) + ).route( + "/ord/tx/:txid/inscriptions_op", + get(ord::ord_txid_inscriptions_op), + ) .route( "/ord/block/:blockhash/inscriptions", get(ord::ord_block_inscriptions), ) + .route( + "/crawler/zeroindexer/:height", + get(ord::crawler_zeroindexer), + ) + .route("/crawler/height", get(ord::crawler_height)) + .route( + "/crawler/zeroindexer_new/:height", + get(ord::crawler_zeroindexer_new), + ) .route( "/ord/debug/bitmap/district/:number", get(ord::ord_debug_bitmap_district), + ) + .route("/brc20/tick/:tick", get(brc20::brc20_tick_info)) + .route("/brc20/tick", get(brc20::brc20_all_tick_info)) + .route( + "/brc20/tick/:tick/address/:address/balance", + get(brc20::brc20_balance), + ) + .route( + "/brc20/address/:address/balance", + get(brc20::brc20_all_balance), + ) + .route( + "/brc20/tick/:tick/address/:address/transferable", + get(brc20::brc20_transferable), + ) + .route( + "/brc20/address/:address/transferable", + get(brc20::brc20_all_transferable), + ) + .route("/brc20/tx/:txid/events", get(brc20::brc20_tx_events)) + .route( + "/brc20/block/:block_hash/events", + get(brc20::brc20_block_events), ); let api_router = Router::new().nest("/v1", api_v1_router); diff --git a/src/subcommand/server/brc20/balance.rs b/src/subcommand/server/brc20/balance.rs new file mode 100644 index 0000000000..9e66e30221 --- /dev/null +++ b/src/subcommand/server/brc20/balance.rs @@ -0,0 +1,115 @@ +use {super::*, crate::okx::datastore::brc20::Tick, axum::Json, utoipa::ToSchema}; + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +#[schema(as = brc20::Balance)] +pub struct Balance { + /// Name of the ticker. + pub tick: String, + /// Available balance. + #[schema(format = "uint64")] + pub available_balance: String, + /// Transferable balance. + #[schema(format = "uint64")] + pub transferable_balance: String, + /// Overall balance. + #[schema(format = "uint64")] + pub overall_balance: String, +} + +/// Get the ticker balance of the address. +/// +/// Retrieve the asset balance of the 'ticker' for the address. +#[utoipa::path( + get, + path = "/api/v1/brc20/tick/{ticker}/address/{address}/balance", + params( + ("ticker" = String, Path, description = "Token ticker", min_length = 4, max_length = 4), + ("address" = String, Path, description = "Address") + ), + responses( + (status = 200, description = "Obtain account balance by query ticker.", body = BRC20Balance), + (status = 400, description = "Bad query.", body = ApiError, example = json!(&ApiError::bad_request("bad request"))), + (status = 404, description = "Not found.", body = ApiError, example = json!(&ApiError::not_found("not found"))), + (status = 500, description = "Internal server error.", body = ApiError, example = json!(&ApiError::internal("internal error"))), + ) + )] +pub(crate) async fn brc20_balance( + Extension(index): Extension>, + Path((tick, address)): Path<(String, String)>, +) -> ApiResult { + log::debug!("rpc: get brc20_balance: {} {}", tick, address); + + let tick = + Tick::from_str(&tick).map_err(|_| ApiError::bad_request(BRC20Error::IncorrectTickFormat))?; + + let address: bitcoin::Address = Address::from_str(&address) + .and_then(|address| address.require_network(index.get_chain_network())) + .map_err(ApiError::bad_request)?; + + let balance = index + .brc20_get_balance_by_address(&tick, &address)? + .ok_or_api_not_found(BRC20Error::BalanceNotFound)?; + + let available_balance = balance.overall_balance - balance.transferable_balance; + + log::debug!("rpc: get brc20_balance: {} {} {:?}", tick, address, balance); + + Ok(Json(ApiResponse::ok(Balance { + tick: balance.tick.to_string(), + available_balance: available_balance.to_string(), + transferable_balance: balance.transferable_balance.to_string(), + overall_balance: balance.overall_balance.to_string(), + }))) +} + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +#[schema(as = brc20::AllBalance)] +pub struct AllBalance { + #[schema(value_type = Vec)] + pub balance: Vec, +} + +/// Get all ticker balances of the address. +/// +/// Retrieve all BRC20 protocol asset balances associated with a address. +#[utoipa::path( + get, + path = "/api/v1/brc20/address/{address}/balance", + params( + ("address" = String, Path, description = "Address") + ), + responses( + (status = 200, description = "Obtain account balances by query address.", body = BRC20AllBalance), + (status = 400, description = "Bad query.", body = ApiError, example = json!(&ApiError::bad_request("bad request"))), + (status = 404, description = "Not found.", body = ApiError, example = json!(&ApiError::not_found("not found"))), + (status = 500, description = "Internal server error.", body = ApiError, example = json!(&ApiError::internal("internal error"))), + ) + )] +pub(crate) async fn brc20_all_balance( + Extension(index): Extension>, + Path(address): Path, +) -> ApiResult { + log::debug!("rpc: get brc20_all_balance: {}", address); + + let address: bitcoin::Address = Address::from_str(&address) + .and_then(|address| address.require_network(index.get_chain_network())) + .map_err(ApiError::bad_request)?; + + let all_balance = index.brc20_get_all_balance_by_address(&address)?; + + log::debug!("rpc: get brc20_all_balance: {} {:?}", address, all_balance); + + Ok(Json(ApiResponse::ok(AllBalance { + balance: all_balance + .iter() + .map(|bal| Balance { + tick: bal.tick.to_string(), + available_balance: (bal.overall_balance - bal.transferable_balance).to_string(), + transferable_balance: bal.transferable_balance.to_string(), + overall_balance: bal.overall_balance.to_string(), + }) + .collect(), + }))) +} diff --git a/src/subcommand/server/brc20/mod.rs b/src/subcommand/server/brc20/mod.rs new file mode 100644 index 0000000000..fe56c181ce --- /dev/null +++ b/src/subcommand/server/brc20/mod.rs @@ -0,0 +1,22 @@ +use super::{types::ScriptPubkey, *}; +mod balance; +mod receipt; +mod ticker; +mod transaction; +mod transferable; + +#[derive(Debug, thiserror::Error)] +pub(super) enum BRC20Error { + #[error("ticker must be 4 bytes length")] + IncorrectTickFormat, + #[error("tick not found")] + TickNotFound, + #[error("balance not found")] + BalanceNotFound, + #[error("events not found")] + EventsNotFound, + #[error("block not found")] + BlockNotFound, +} + +pub(super) use {balance::*, receipt::*, ticker::*, transferable::*}; diff --git a/src/subcommand/server/brc20/receipt.rs b/src/subcommand/server/brc20/receipt.rs new file mode 100644 index 0000000000..6c7b340ff1 --- /dev/null +++ b/src/subcommand/server/brc20/receipt.rs @@ -0,0 +1,359 @@ +use {super::*, crate::okx::datastore::brc20 as brc20_store, axum::Json, utoipa::ToSchema}; + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[schema(as = brc20::TxEvent)] +#[serde(untagged)] +#[serde(rename_all = "camelCase")] +pub enum TxEvent { + /// Event generated by deployed ticker. + #[schema(value_type = brc20::DeployEvent)] + Deploy(DeployEvent), + /// Event generated by mining. + #[schema(value_type = brc20::MintEvent)] + Mint(MintEvent), + /// Event generated by pretransfer. + #[schema(value_type = brc20::InscribeTransferEvent)] + InscribeTransfer(InscribeTransferEvent), + #[schema(value_type = brc20::TransferEvent)] + /// Event generated by transfer. + Transfer(TransferEvent), + /// Event generated by the execution has failed. + #[schema(value_type = brc20::ErrorEvent)] + Error(ErrorEvent), +} + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[schema(as = brc20::ErrorEvent)] +#[serde(rename_all = "camelCase")] +pub struct ErrorEvent { + /// Event type. + #[serde(rename = "type")] + pub event: String, + /// The inscription id. + pub inscription_id: String, + /// The inscription number. + pub inscription_number: i32, + /// The inscription satpoint of the transaction input. + pub old_satpoint: String, + /// The inscription satpoint of the transaction output. + pub new_satpoint: String, + /// The message sender which is an address or script pubkey hash. + pub from: ScriptPubkey, + /// The message receiver which is an address or script pubkey hash. + pub to: ScriptPubkey, + /// Executed state. + pub valid: bool, + /// Error message. + pub msg: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[schema(as = brc20::DeployEvent)] +#[serde(rename_all = "camelCase")] +pub struct DeployEvent { + /// Event type. + #[serde(rename = "type")] + pub event: String, + /// The ticker deployed. + pub tick: String, + /// The inscription id. + pub inscription_id: String, + /// The inscription number. + pub inscription_number: i32, + /// The inscription satpoint of the transaction input. + pub old_satpoint: String, + /// The inscription satpoint of the transaction output. + pub new_satpoint: String, + /// The total supply of the deployed ticker. + pub supply: String, + /// The limit per mint of the deployed ticker. + pub limit_per_mint: String, + /// The decimal of the deployed ticker. + pub decimal: u8, + /// The message sender which is an address or script pubkey hash. + pub from: ScriptPubkey, + /// The message receiver which is an address or script pubkey hash. + pub to: ScriptPubkey, + /// Executed state. + pub valid: bool, + /// Message generated during execution. + pub msg: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[schema(as = brc20::MintEvent)] +#[serde(rename_all = "camelCase")] +pub struct MintEvent { + #[serde(rename = "type")] + /// Event type. + pub event: String, + /// The ticker minted. + pub tick: String, + /// The inscription id. + pub inscription_id: String, + /// The inscription number. + pub inscription_number: i32, + /// The inscription satpoint of the transaction input. + pub old_satpoint: String, + /// The inscription satpoint of the transaction output. + pub new_satpoint: String, + /// The amount minted. + pub amount: String, + /// The message sender which is an address or script pubkey hash. + pub from: ScriptPubkey, + /// The message receiver which is an address or script pubkey hash. + pub to: ScriptPubkey, + /// Executed state. + pub valid: bool, + /// Message generated during execution. + pub msg: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[schema(as = brc20::InscribeTransferEvent)] +#[serde(rename_all = "camelCase")] +pub struct InscribeTransferEvent { + /// Event type. + #[serde(rename = "type")] + pub event: String, + /// The ticker of pretransfer. + pub tick: String, + /// The inscription id. + pub inscription_id: String, + /// The inscription number. + pub inscription_number: i32, + /// The inscription satpoint of the transaction input. + pub old_satpoint: String, + /// The inscription satpoint of the transaction output. + pub new_satpoint: String, + /// The amount of pretransfer. + pub amount: String, + /// The message sender which is an address or script pubkey hash. + pub from: ScriptPubkey, + /// The message receiver which is an address or script pubkey hash. + pub to: ScriptPubkey, + /// Executed state. + pub valid: bool, + /// Message generated during execution. + pub msg: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[schema(as = brc20::TransferEvent)] +#[serde(rename_all = "camelCase")] +pub struct TransferEvent { + /// Event type. + #[serde(rename = "type")] + pub event: String, + /// The ticker of transfer. + pub tick: String, + /// The inscription id. + pub inscription_id: String, + /// The inscription number. + pub inscription_number: i32, + /// The inscription satpoint of the transaction input. + pub old_satpoint: String, + /// The inscription satpoint of the transaction output. + pub new_satpoint: String, + /// The amount of transfer. + pub amount: String, + /// The message sender which is an address or script pubkey hash. + pub from: ScriptPubkey, + /// The message receiver which is an address or script pubkey hash. + pub to: ScriptPubkey, + /// Executed state. + pub valid: bool, + /// Message generated during execution. + pub msg: String, +} + +impl From<&brc20_store::Receipt> for TxEvent { + fn from(event: &brc20_store::Receipt) -> Self { + match &event.result { + Ok(brc20_store::Event::Deploy(deploy_event)) => Self::Deploy(DeployEvent { + tick: deploy_event.tick.to_string(), + inscription_id: event.inscription_id.to_string(), + inscription_number: event.inscription_number, + old_satpoint: event.old_satpoint.to_string(), + new_satpoint: event.new_satpoint.to_string(), + supply: deploy_event.supply.to_string(), + limit_per_mint: deploy_event.limit_per_mint.to_string(), + decimal: deploy_event.decimal, + from: event.from.clone().into(), + to: event.to.clone().into(), + valid: true, + msg: "ok".to_string(), + event: "deploy".to_string(), + }), + Ok(brc20_store::Event::Mint(mint_event)) => Self::Mint(MintEvent { + tick: mint_event.tick.to_string(), + inscription_id: event.inscription_id.to_string(), + inscription_number: event.inscription_number, + old_satpoint: event.old_satpoint.to_string(), + new_satpoint: event.new_satpoint.to_string(), + amount: mint_event.amount.to_string(), + from: event.from.clone().into(), + to: event.to.clone().into(), + valid: true, + msg: mint_event.msg.clone().unwrap_or("ok".to_string()), + event: "mint".to_string(), + }), + Ok(brc20_store::Event::InscribeTransfer(trans1)) => { + Self::InscribeTransfer(InscribeTransferEvent { + tick: trans1.tick.to_string(), + inscription_id: event.inscription_id.to_string(), + inscription_number: event.inscription_number, + old_satpoint: event.old_satpoint.to_string(), + new_satpoint: event.new_satpoint.to_string(), + amount: trans1.amount.to_string(), + from: event.from.clone().into(), + to: event.to.clone().into(), + valid: true, + msg: "ok".to_string(), + event: "inscribeTransfer".to_string(), + }) + } + Ok(brc20_store::Event::Transfer(trans2)) => Self::Transfer(TransferEvent { + tick: trans2.tick.to_string(), + inscription_id: event.inscription_id.to_string(), + inscription_number: event.inscription_number, + old_satpoint: event.old_satpoint.to_string(), + new_satpoint: event.new_satpoint.to_string(), + amount: trans2.amount.to_string(), + from: event.from.clone().into(), + to: event.to.clone().into(), + valid: true, + msg: trans2.msg.clone().unwrap_or("ok".to_string()), + event: "transfer".to_string(), + }), + Err(err) => Self::Error(ErrorEvent { + inscription_id: event.inscription_id.to_string(), + inscription_number: event.inscription_number, + old_satpoint: event.old_satpoint.to_string(), + new_satpoint: event.new_satpoint.to_string(), + valid: false, + from: event.from.clone().into(), + to: event.to.clone().into(), + msg: err.to_string(), + event: match event.op { + brc20_store::OperationType::Deploy => "deploy".to_string(), + brc20_store::OperationType::Mint => "mint".to_string(), + brc20_store::OperationType::InscribeTransfer => "inscribeTransfer".to_string(), + brc20_store::OperationType::Transfer => "transfer".to_string(), + }, + }), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[schema(as = brc20::TxEvents)] +#[serde(rename_all = "camelCase")] +pub struct TxEvents { + #[schema(value_type = Vec)] + pub events: Vec, + pub txid: String, +} + +/// Get transaction events by txid. +/// +/// Retrieve all BRC20 events associated with a transaction. +#[utoipa::path( + get, + path = "/api/v1/brc20/tx/{txid}/events", + params( + ("txid" = String, Path, description = "transaction ID") + ), + responses( + (status = 200, description = "Obtain transaction events by txid", body = BRC20TxEvents), + (status = 400, description = "Bad query.", body = ApiError, example = json!(&ApiError::bad_request("bad request"))), + (status = 404, description = "Not found.", body = ApiError, example = json!(&ApiError::not_found("not found"))), + (status = 500, description = "Internal server error.", body = ApiError, example = json!(&ApiError::internal("internal error"))), + ) + )] +pub(crate) async fn brc20_tx_events( + Extension(index): Extension>, + Path(txid): Path, +) -> ApiResult { + log::debug!("rpc: get brc20_tx_events: {}", txid); + let txid = bitcoin::Txid::from_str(&txid).map_err(|e| ApiError::bad_request(e.to_string()))?; + let tx_events = index + .brc20_get_tx_events_by_txid(&txid)? + .ok_or_api_not_found(BRC20Error::EventsNotFound)?; + + log::debug!("rpc: get brc20_tx_events: {} {:?}", txid, tx_events); + + Ok(Json(ApiResponse::ok(TxEvents { + txid: txid.to_string(), + events: tx_events.iter().map(|e| e.into()).collect(), + }))) +} + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[schema(as = brc20::BlockEvents)] +#[serde(rename_all = "camelCase")] +pub struct BlockEvents { + #[schema(value_type = Vec)] + pub block: Vec, +} + +/// Get block events by blockhash. +/// +/// Retrieve all BRC20 events associated with a block. +#[utoipa::path( + get, + path = "/api/v1/brc20/block/{blockhash}/events", + params( + ("blockhash" = String, Path, description = "block hash") + ), + responses( + (status = 200, description = "Obtain block events by block hash", body = BRC20BlockEvents), + (status = 400, description = "Bad query.", body = ApiError, example = json!(&ApiError::bad_request("bad request"))), + (status = 404, description = "Not found.", body = ApiError, example = json!(&ApiError::not_found("not found"))), + (status = 500, description = "Internal server error.", body = ApiError, example = json!(&ApiError::internal("internal error"))), + ) + )] +pub(crate) async fn brc20_block_events( + Extension(index): Extension>, + Path(blockhash): Path, +) -> ApiResult { + log::debug!("rpc: get brc20_block_events: {}", blockhash); + + let blockhash = bitcoin::BlockHash::from_str(&blockhash).map_err(ApiError::bad_request)?; + // get block from btc client. + let blockinfo = index + .get_block_info_by_hash(blockhash) + .map_err(ApiError::internal)? + .ok_or_api_not_found(BRC20Error::BlockNotFound)?; + + // get blockhash from redb. + let blockhash = index + .block_hash(Some(blockinfo.height as u32)) + .map_err(ApiError::internal)? + .ok_or_api_not_found(BRC20Error::BlockNotFound)?; + + // check blockhash. + if blockinfo.hash != blockhash { + return Err(ApiError::NotFound(BRC20Error::BlockNotFound.to_string())); + } + + let block_events = index + .brc20_get_txs_events(&blockinfo.tx) + .map_err(ApiError::internal)?; + + log::debug!( + "rpc: get brc20_block_events: {} {:?}", + blockhash, + block_events + ); + + Ok(Json(ApiResponse::ok(BlockEvents { + block: block_events + .iter() + .map(|(txid, events)| TxEvents { + txid: txid.to_string(), + events: events.iter().map(|e| e.into()).collect(), + }) + .collect(), + }))) +} diff --git a/src/subcommand/server/brc20/ticker.rs b/src/subcommand/server/brc20/ticker.rs new file mode 100644 index 0000000000..5c846855cb --- /dev/null +++ b/src/subcommand/server/brc20/ticker.rs @@ -0,0 +1,136 @@ +use { + super::*, + crate::okx::datastore::brc20::{Tick, TokenInfo}, + axum::Json, + utoipa::ToSchema, +}; + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[schema(as = brc20::TickInfo)] +#[serde(rename_all = "camelCase")] +/// Description of a BRC20 ticker. +pub struct TickInfo { + /// Name of the ticker. + pub tick: String, + /// Inscription ID of the ticker deployed. + pub inscription_id: String, + /// Inscription number of the ticker deployed. + pub inscription_number: i32, + /// The total supply of the ticker.
+ /// Maximum supply cannot exceed uint64_max. + /// + /// A string containing a 64-bit unsigned integer.
+ /// We represent u64 values as a string to ensure compatibility with languages such as JavaScript that do not parse u64s in JSON natively. + #[schema(format = "uint64")] + pub supply: String, + /// The maximum amount of each mining. + #[schema(format = "uint64")] + pub limit_per_mint: String, + /// The amount of the ticker that has been minted. + #[schema(format = "uint64")] + pub minted: String, + /// The decimal of the ticker.
+ /// Number of decimals cannot exceed 18 (default). + #[schema( + example = 18, + default = 18, + maximum = 18, + minimum = 0, + format = "uint8" + )] + pub decimal: u8, + pub deploy_by: ScriptPubkey, + /// A hex encoded 32 byte transaction ID that the ticker deployed. + /// + /// This is represented in a string as adding a prefix 0x to a 64 character hex string. + pub txid: String, + /// The height of the block that the ticker deployed. + #[schema(format = "uint32")] + pub deploy_height: u32, + /// The timestamp of the block that the ticker deployed. + #[schema(format = "uint32")] + pub deploy_blocktime: u32, +} + +impl From for TickInfo { + fn from(tick_info: TokenInfo) -> Self { + Self { + tick: tick_info.tick.to_string(), + inscription_id: tick_info.inscription_id.to_string(), + inscription_number: tick_info.inscription_number, + supply: tick_info.supply.to_string(), + limit_per_mint: tick_info.limit_per_mint.to_string(), + minted: tick_info.minted.to_string(), + decimal: tick_info.decimal, + deploy_by: tick_info.deploy_by.clone().into(), + txid: tick_info.inscription_id.txid.to_string(), + deploy_height: tick_info.deployed_number, + deploy_blocktime: tick_info.deployed_timestamp, + } + } +} + +/// Get the ticker info. +/// +/// Retrieve detailed information about the ticker. +#[utoipa::path( + get, + path = "/api/v1/brc20/tick/{ticker}", + params( + ("ticker" = String, Path, description = "Token ticker", min_length = 4, max_length = 4) + ), + responses( + (status = 200, description = "Obtain matching BRC20 ticker by query.", body = BRC20Tick), + (status = 400, description = "Bad query.", body = ApiError, example = json!(&ApiError::bad_request(BRC20Error::IncorrectTickFormat))), + (status = 404, description = "Ticker not found.", body = ApiError, example = json!(&ApiError::not_found(BRC20Error::TickNotFound))), + (status = 500, description = "Internal server error.", body = ApiError, example = json!(&ApiError::internal("internal error"))), + ) + )] +pub(crate) async fn brc20_tick_info( + Extension(index): Extension>, + Path(tick): Path, +) -> ApiResult { + log::debug!("rpc: get brc20_tick_info: {}", tick); + let tick = + Tick::from_str(&tick).map_err(|_| ApiError::bad_request(BRC20Error::IncorrectTickFormat))?; + let tick_info = index + .brc20_get_tick_info(&tick)? + .ok_or_api_not_found(BRC20Error::TickNotFound)?; + + log::debug!("rpc: get brc20_tick_info: {:?} {:?}", tick, tick_info); + + Ok(Json(ApiResponse::ok(tick_info.into()))) +} + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[schema(as = brc20::AllTickInfo)] +#[serde(rename_all = "camelCase")] +pub struct AllTickInfo { + #[schema(value_type = Vec)] + pub tokens: Vec, +} + +/// Get all tickers info. +/// +/// Retrieve detailed information about all tickers. +#[utoipa::path( + get, + path = "/api/v1/brc20/tick", + responses( + (status = 200, description = "Obtain matching all BRC20 tickers.", body = BRC20AllTick), + (status = 400, description = "Bad query.", body = ApiError, example = json!(&ApiError::bad_request("bad request"))), + (status = 404, description = "Not found.", body = ApiError, example = json!(&ApiError::not_found("not found"))), + (status = 500, description = "Internal server error.", body = ApiError, example = json!(&ApiError::internal("internal error"))), + ) + )] +pub(crate) async fn brc20_all_tick_info( + Extension(index): Extension>, +) -> ApiResult { + log::debug!("rpc: get brc20_all_tick_info"); + let all_tick_info = index.brc20_get_all_tick_info()?; + log::debug!("rpc: get brc20_all_tick_info: {:?}", all_tick_info); + + Ok(Json(ApiResponse::ok(AllTickInfo { + tokens: all_tick_info.into_iter().map(|t| t.into()).collect(), + }))) +} diff --git a/src/subcommand/server/brc20/transaction.rs b/src/subcommand/server/brc20/transaction.rs new file mode 100644 index 0000000000..2d5d9daf9a --- /dev/null +++ b/src/subcommand/server/brc20/transaction.rs @@ -0,0 +1,198 @@ +use {super::*, crate::okx::protocol::brc20 as brc20_proto}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum ActionType { + Transfer, + Inscribe, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct InscriptionInfo { + pub action: ActionType, + // if the transaction not committed to the blockchain, the following fields are None + pub inscription_number: Option, + pub inscription_id: String, + pub from: ScriptPubkey, + pub to: Option, + pub old_satpoint: String, + // if transfer to coinbase new_satpoint is None + pub new_satpoint: Option, + pub operation: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(untagged)] +pub enum RawOperation { + Brc20Operation(Brc20RawOperation), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type")] +#[serde(rename_all = "camelCase")] +pub enum Brc20RawOperation { + Deploy(Deploy), + Mint(Mint), + InscribeTransfer(Transfer), + Transfer(Transfer), +} + +// action to raw operation +impl From for Brc20RawOperation { + fn from(op: brc20_proto::Operation) -> Self { + match op { + brc20_proto::Operation::Deploy(deploy) => Brc20RawOperation::Deploy(deploy.into()), + brc20_proto::Operation::Mint(mint) => Brc20RawOperation::Mint(mint.into()), + brc20_proto::Operation::InscribeTransfer(transfer) => { + Brc20RawOperation::InscribeTransfer(transfer.into()) + } + brc20_proto::Operation::Transfer(transfer) => Brc20RawOperation::Transfer(transfer.into()), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Deploy { + pub tick: String, + pub max: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub lim: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub dec: Option, +} + +impl From for Deploy { + fn from(deploy: brc20_proto::Deploy) -> Self { + Deploy { + tick: deploy.tick, + max: deploy.max_supply, + lim: deploy.mint_limit, + dec: deploy.decimals, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Mint { + pub tick: String, + pub amt: String, +} + +impl From for Mint { + fn from(mint: brc20_proto::Mint) -> Self { + Mint { + tick: mint.tick, + amt: mint.amount, + } + } +} +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Transfer { + pub tick: String, + pub amt: String, +} + +impl From for Transfer { + fn from(transfer: brc20_proto::Transfer) -> Self { + Transfer { + tick: transfer.tick, + amt: transfer.amount, + } + } +} + +#[cfg(test)] +mod tests { + + use super::*; + #[test] + fn serialize_deploy() { + let deploy = Deploy { + tick: "ordi".to_string(), + max: "1000".to_string(), + lim: Some("1000".to_string()), + dec: Some("18".to_string()), + }; + assert_eq!( + serde_json::to_string(&deploy).unwrap(), + r#"{"tick":"ordi","max":"1000","lim":"1000","dec":"18"}"# + ); + let deploy = Deploy { + tick: "ordi".to_string(), + max: "1000".to_string(), + lim: None, + dec: None, + }; + assert_eq!( + serde_json::to_string(&deploy).unwrap(), + r#"{"tick":"ordi","max":"1000"}"# + ); + } + + #[test] + fn serialize_mint() { + let mint = Mint { + tick: "ordi".to_string(), + amt: "1000".to_string(), + }; + assert_eq!( + serde_json::to_string(&mint).unwrap(), + r#"{"tick":"ordi","amt":"1000"}"# + ); + } + + #[test] + fn serialize_transfer() { + let transfer = Transfer { + tick: "ordi".to_string(), + amt: "1000".to_string(), + }; + assert_eq!( + serde_json::to_string(&transfer).unwrap(), + r#"{"tick":"ordi","amt":"1000"}"# + ); + } + + #[test] + fn serialize_raw_operation() { + let deploy = Brc20RawOperation::Deploy(Deploy { + tick: "ordi".to_string(), + max: "1000".to_string(), + lim: Some("1000".to_string()), + dec: Some("18".to_string()), + }); + assert_eq!( + serde_json::to_string(&deploy).unwrap(), + r#"{"type":"deploy","tick":"ordi","max":"1000","lim":"1000","dec":"18"}"# + ); + let mint = Brc20RawOperation::Mint(Mint { + tick: "ordi".to_string(), + amt: "1000".to_string(), + }); + assert_eq!( + serde_json::to_string(&mint).unwrap(), + r#"{"type":"mint","tick":"ordi","amt":"1000"}"# + ); + let inscribe_transfer = Brc20RawOperation::InscribeTransfer(Transfer { + tick: "ordi".to_string(), + amt: "1000".to_string(), + }); + assert_eq!( + serde_json::to_string(&inscribe_transfer).unwrap(), + r#"{"type":"inscribeTransfer","tick":"ordi","amt":"1000"}"# + ); + let transfer = Brc20RawOperation::Transfer(Transfer { + tick: "ordi".to_string(), + amt: "1000".to_string(), + }); + assert_eq!( + serde_json::to_string(&transfer).unwrap(), + r#"{"type":"transfer","tick":"ordi","amt":"1000"}"# + ); + } +} diff --git a/src/subcommand/server/brc20/transferable.rs b/src/subcommand/server/brc20/transferable.rs new file mode 100644 index 0000000000..6f7d7cbe30 --- /dev/null +++ b/src/subcommand/server/brc20/transferable.rs @@ -0,0 +1,117 @@ +use {super::*, crate::okx::datastore::brc20 as brc20_store, axum::Json, utoipa::ToSchema}; + +#[derive(Default, Debug, Clone, Serialize, Deserialize, ToSchema)] +#[schema(as = brc20::TransferableInscription)] +#[serde(rename_all = "camelCase")] +pub struct TransferableInscription { + /// The inscription id. + pub inscription_id: String, + /// The inscription number. + pub inscription_number: i32, + /// The amount of the ticker that will be transferred. + #[schema(format = "uint64")] + pub amount: String, + /// The ticker name that will be transferred. + pub tick: String, + /// The address to which the transfer will be made. + pub owner: String, +} + +impl From<&brc20_store::TransferableLog> for TransferableInscription { + fn from(trans: &brc20_store::TransferableLog) -> Self { + Self { + inscription_id: trans.inscription_id.to_string(), + inscription_number: trans.inscription_number, + amount: trans.amount.to_string(), + tick: trans.tick.as_str().to_string(), + owner: trans.owner.to_string(), + } + } +} + +/// Get the transferable inscriptions of the address. +/// +/// Retrieve the transferable inscriptions with the ticker from the given address. +#[utoipa::path( + get, + path = "/api/v1/brc20/tick/{ticker}/address/{address}/transferable", + params( + ("ticker" = String, Path, description = "Token ticker", min_length = 4, max_length = 4), + ("address" = String, Path, description = "Address") +), + responses( + (status = 200, description = "Obtain account transferable inscriptions of ticker.", body = BRC20Transferable), + (status = 400, description = "Bad query.", body = ApiError, example = json!(&ApiError::bad_request("bad request"))), + (status = 404, description = "Not found.", body = ApiError, example = json!(&ApiError::not_found("not found"))), + (status = 500, description = "Internal server error.", body = ApiError, example = json!(&ApiError::internal("internal error"))), + ) +)] +pub(crate) async fn brc20_transferable( + Extension(index): Extension>, + Path((tick, address)): Path<(String, String)>, +) -> ApiResult { + log::debug!("rpc: get brc20_transferable: {tick} {address}"); + + let tick = brc20_store::Tick::from_str(&tick) + .map_err(|_| ApiError::bad_request(BRC20Error::IncorrectTickFormat))?; + + let address: bitcoin::Address = Address::from_str(&address) + .and_then(|address| address.require_network(index.get_chain_network())) + .map_err(ApiError::bad_request)?; + + let transferable = index.brc20_get_tick_transferable_by_address(&tick, &address)?; + + log::debug!( + "rpc: get brc20_transferable: {tick} {address} {:?}", + transferable + ); + + Ok(Json(ApiResponse::ok(TransferableInscriptions { + inscriptions: transferable.iter().map(|trans| trans.into()).collect(), + }))) +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize, ToSchema)] +#[schema(as = brc20::TransferableInscriptions)] +#[serde(rename_all = "camelCase")] +pub struct TransferableInscriptions { + #[schema(value_type = Vec)] + pub inscriptions: Vec, +} + +/// Get the balance of ticker of the address. +/// +/// Retrieve the balance of the ticker from the given address. +#[utoipa::path( + get, + path = "/api/v1/brc20/address/{address}/transferable", + params( + ("address" = String, Path, description = "Address") +), + responses( + (status = 200, description = "Obtain account all transferable inscriptions.", body = BRC20Transferable), + (status = 400, description = "Bad query.", body = ApiError, example = json!(&ApiError::bad_request("bad request"))), + (status = 404, description = "Not found.", body = ApiError, example = json!(&ApiError::not_found("not found"))), + (status = 500, description = "Internal server error.", body = ApiError, example = json!(&ApiError::internal("internal error"))), + ) +)] +pub(crate) async fn brc20_all_transferable( + Extension(index): Extension>, + Path(address): Path, +) -> ApiResult { + log::debug!("rpc: get brc20_all_transferable: {address}"); + + let address: bitcoin::Address = Address::from_str(&address) + .and_then(|address| address.require_network(index.get_chain_network())) + .map_err(ApiError::bad_request)?; + + let transferable = index.brc20_get_all_transferable_by_address(&address)?; + log::debug!( + "rpc: get brc20_all_transferable: {address} {:?}", + transferable + ); + + Ok(Json(ApiResponse::ok(TransferableInscriptions { + inscriptions: transferable.iter().map(|trans| trans.into()).collect(), + }))) +} diff --git a/src/subcommand/server/ord/mod.rs b/src/subcommand/server/ord/mod.rs index a8a6d1716c..b84abe035e 100644 --- a/src/subcommand/server/ord/mod.rs +++ b/src/subcommand/server/ord/mod.rs @@ -3,8 +3,9 @@ use super::*; mod inscription; mod outpoint; mod transaction; +mod zeroindexer; -pub(super) use {inscription::*, outpoint::*, transaction::*}; +pub(super) use {inscription::*, outpoint::*, transaction::*, zeroindexer::*}; #[derive(Debug, thiserror::Error)] pub enum OrdError { diff --git a/src/subcommand/server/ord/transaction.rs b/src/subcommand/server/ord/transaction.rs index 3a5bbee806..788fe410f9 100644 --- a/src/subcommand/server/ord/transaction.rs +++ b/src/subcommand/server/ord/transaction.rs @@ -156,6 +156,37 @@ pub(crate) async fn ord_txid_inscriptions( }))) } +// ord/tx/:txid/inscriptions +/// Retrieve the inscription actions from the given transaction. +#[utoipa::path( +get, +path = "/api/v1/ord/tx/{txid}/inscriptionsop", +params( +("txid" = String, Path, description = "transaction ID") +), +responses( +(status = 200, description = "Obtain inscription actions by txid", body = OrdTxInscriptions), +(status = 400, description = "Bad query.", body = ApiError, example = json!(&ApiError::bad_request("bad request"))), +(status = 404, description = "Not found.", body = ApiError, example = json!(&ApiError::not_found("not found"))), +(status = 500, description = "Internal server error.", body = ApiError, example = json!(&ApiError::internal("internal error"))), +) +)] +pub(crate) async fn ord_txid_inscriptions_op( + Extension(index): Extension>, + Path(txid): Path, +) -> ApiResult> { + log::debug!("rpc: get ord_txid_inscriptions: {}", txid); + let txid = Txid::from_str(&txid).map_err(ApiError::bad_request)?; + + let ops = index + .ord_txid_inscriptions(&txid)? + .ok_or_api_not_found(OrdError::OperationNotFound)?; + + log::debug!("rpc: get ord_txid_inscriptions: {:?}", ops); + + Ok(Json(ApiResponse::ok(ops))) +} + // ord/block/:blockhash/inscriptions /// Retrieve the inscription actions from the given block. #[utoipa::path( diff --git a/src/subcommand/server/ord/zeroindexer.rs b/src/subcommand/server/ord/zeroindexer.rs new file mode 100644 index 0000000000..bb3123bdab --- /dev/null +++ b/src/subcommand/server/ord/zeroindexer.rs @@ -0,0 +1,358 @@ +use crate::okx::protocol::BlockContext; +use { + super::{error::ApiError, *}, + crate::okx::datastore::{ + ord::{Action, InscriptionOp}, + ScriptKey, + }, + crate::okx::protocol::zeroindexer::{ + error::JSONError, + zerodata::{InscriptionContext, ZeroData, ZeroIndexerTx}, + }, + axum::Json, + serde_json::Value, +}; + +// ord/block/:blockhash/inscriptions +/// Retrieve the inscription actions from the given block. +#[utoipa::path( +get, +path = "/api/v1/crawler/zeroindexer/:height", +params( +("height" = u32, Path, description = "block height") +), +responses( +(status = 200, description = "Obtain inscription actions by blockhash", body = OrdBlockInscriptions), +(status = 400, description = "Bad query.", body = ApiError, example = json!(&ApiError::bad_request("bad request"))), +(status = 404, description = "Not found.", body = ApiError, example = json!(&ApiError::not_found("not found"))), +(status = 500, description = "Internal server error.", body = ApiError, example = json!(&ApiError::internal("internal error"))), +) +)] +pub(crate) async fn crawler_zeroindexer_new( + Extension(index): Extension>, + Path(height): Path, +) -> ApiResult { + log::debug!("rpc: get crawler_zeroindexer: {}", height); + + let blockhash = index + .block_hash(Some(height)) + .map_err(ApiError::internal)? + .ok_or_api_not_found(OrdError::BlockNotFound)?; + + // get block from btc client. + let blockinfo = index + .get_block_info_by_hash(blockhash) + .map_err(ApiError::internal)? + .ok_or_api_not_found(OrdError::BlockNotFound)?; + + // get blockhash from redb. + let blockhash = index + .block_hash(Some(u32::try_from(blockinfo.height).unwrap())) + .map_err(ApiError::internal)? + .ok_or_api_not_found(OrdError::BlockNotFound)?; + + // check of conflicting block. + if blockinfo.hash != blockhash { + return Err(ApiError::NotFound(OrdError::BlockNotFound.to_string())); + } + + let block_inscriptions = index + .ord_get_txs_inscriptions(&blockinfo.tx) + .map_err(ApiError::internal)?; + + log::debug!("rpc: get ord_block_inscriptions: {:?}", block_inscriptions); + let mut txs: Vec = Vec::new(); + for (txid, ops) in block_inscriptions { + match resolve_zero_inscription( + BlockContext { + network: index.get_chain_network(), + blockheight: height, + blocktime: blockinfo.time as u32, + }, + &blockinfo.hash, + txid, + ops, + &index, + ) { + Ok(mut message) => txs.append(&mut message), + Err(e) => return Err(ApiError::internal(e)), + }; + } + let zero_data = ZeroData { + block_height: height as u64, + block_hash: blockinfo.hash.to_string(), + prev_block_hash: match blockinfo.previousblockhash { + None => "".to_string(), + Some(hash) => hash.to_string(), + }, + block_time: blockinfo.time as u32, + txs, + }; + Ok(Json(ApiResponse::ok(zero_data))) +} + +// ord/block/:blockhash/inscriptions +/// Retrieve the inscription actions from the given block. +#[utoipa::path( +get, +path = "/api/v1/crawler/zeroindexer/:height", +params( +("height" = u64, Path, description = "block height") +), +responses( +(status = 200, description = "Obtain inscription actions by blockhash", body = OrdBlockInscriptions), +(status = 400, description = "Bad query.", body = ApiError, example = json!(&ApiError::bad_request("bad request"))), +(status = 404, description = "Not found.", body = ApiError, example = json!(&ApiError::not_found("not found"))), +(status = 500, description = "Internal server error.", body = ApiError, example = json!(&ApiError::internal("internal error"))), +) +)] +pub(crate) async fn crawler_zeroindexer( + Extension(index): Extension>, + Path(height): Path, +) -> ApiResult { + log::debug!("rpc: get crawler_zeroindexer: {}", height); + + match index.zero_indexer_get_txs(height) { + Ok(data) => match data { + Some(data) => Ok(Json(ApiResponse::ok(data))), + None => Ok(Json(ApiResponse::err("block not found".to_string()))), + }, + Err(e) => Err(ApiError::not_found(e)), + } +} + +fn resolve_zero_inscription( + context: BlockContext, + block_hash: &BlockHash, + txid: Txid, + operations: Vec, + index: &Arc, +) -> Result> { + log::debug!( + "Resolve Inscription indexed transaction {}, operations size: {}, data: {:?}", + txid, + operations.len(), + operations + ); + let tx = match index.get_transaction(txid) { + Ok(tx) => match tx { + Some(tx) => tx, + None => return Err(anyhow!("tx not found")), + }, + Err(e) => return Err(e), + }; + let mut zero_indexer_txs: Vec = Vec::new(); + let mut operation_iter = operations.into_iter().peekable(); + let new_inscriptions = ParsedEnvelope::from_transaction(&tx); + for input in &tx.input { + // "operations" is a list of all the operations in the current block, and they are ordered. + // We just need to find the operation corresponding to the current transaction here. + while let Some(operation) = operation_iter.peek() { + if operation.old_satpoint.outpoint != input.previous_output { + break; + } + let operation = operation_iter.next().unwrap(); + + let sat_in_outputs = operation + .new_satpoint + .map(|satpoint| satpoint.outpoint.txid == operation.txid) + .unwrap_or(false); + + let mut is_transfer = false; + let mut sender = "".to_string(); + let inscription = match operation.action { + // New inscription is not `cursed` or `unbound`. + Action::New { + cursed: false, + unbound: false, + .. + } => { + let inscription_struct = new_inscriptions + .get(usize::try_from(operation.inscription_id.index).unwrap()) + .unwrap() + .clone() + .payload; + let des_res = deserialize_inscription(&inscription_struct); + match des_res { + Ok(content) => content, + Err(_) => { + continue; + } + } + } + // Transfer inscription operation. + Action::Transfer => { + if operation.inscription_id.txid == operation.old_satpoint.outpoint.txid + && operation.inscription_id.index == operation.old_satpoint.outpoint.vout + { + is_transfer = true; + + let inscription_struct = match index.get_inscription_by_id(operation.inscription_id) { + Ok(inner) => match inner { + None => continue, + Some(innner) => innner, + }, + Err(err) => { + return Err(anyhow!( + "failed to get inscription because btc is down:{}", + err + )); + } + }; + let des_res = deserialize_inscription(&inscription_struct); + match des_res { + Ok(content) => { + sender = + get_script_key_on_outpoint(&index, operation.old_satpoint.outpoint)?.to_string(); + content + } + Err(_) => { + continue; + } + } + } else { + continue; + } + } + _ => { + continue; + } + }; + + let new_sat_point = match operation.new_satpoint { + None => "".to_string(), + Some(sat_point) => sat_point.to_string(), + }; + let receiver = if sat_in_outputs { + match operation.new_satpoint { + None => "".to_string(), + Some(sat_point) => { + match get_script_key_from_transaction( + &tx, + &sat_point.outpoint, + index.get_chain_network(), + ) { + None => "".to_string(), + Some(script_key) => script_key.to_string(), + } + } + } + } else { + "".to_string() + }; + let inscription_context = InscriptionContext { + txid: operation.txid.to_string(), + inscription_id: operation.inscription_id.to_string(), + inscription_number: 0, + old_sat_point: operation.old_satpoint.to_string(), + new_sat_point, + sender, + receiver, + is_transfer, + block_height: context.blockheight as u64, + block_time: context.blocktime, + block_hash: block_hash.to_string(), + }; + zero_indexer_txs.push(ZeroIndexerTx { + protocol_name: "brc-20".to_string(), + inscription, + inscription_context: serde_json::to_string(&inscription_context).unwrap(), + btc_txid: operation.txid.to_string(), + btc_fee: "10000000".to_string(), + }); + } + } + Ok(zero_indexer_txs) +} + +#[utoipa::path( +get, +path = "/api/v1/crawler/height", +responses( +(status = 200, description = "Obtain inscription actions by blockhash", body = OrdBlockInscriptions), +(status = 400, description = "Bad query.", body = ApiError, example = json!(&ApiError::bad_request("bad request"))), +(status = 404, description = "Not found.", body = ApiError, example = json!(&ApiError::not_found("not found"))), +(status = 500, description = "Internal server error.", body = ApiError, example = json!(&ApiError::internal("internal error"))), +) +)] +pub(crate) async fn crawler_height( + Extension(index): Extension>, +) -> ApiResult { + log::debug!("rpc: get crawler_height"); + + + let (ord_height, _) = index.height_btc(false)?; + + let fast_sync_info = FastSyncInfo { + crawler_height: ord_height.map(|h| h.0 as u64 ), + }; + + Ok(Json(ApiResponse::ok(fast_sync_info))) +} + +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct FastSyncInfo { + #[schema(format = "uint64")] + pub crawler_height: Option, +} + +fn get_script_key_on_outpoint(index: &Arc, outpoint: OutPoint) -> Result { + match index.get_outpoint_entry(outpoint) { + Ok(tx_out) => match tx_out { + None => Err(anyhow!("failed get_script_key_on_outpoint is none ")), + Some(tx) => Ok(ScriptKey::from_script( + &tx.script_pubkey, + index.get_chain_network(), + )), + }, + Err(e) => Err(anyhow!("failed get_script_key_on_outpoint.error : {e} ")), + } +} +//Some(ScriptKey::from_script(&tx_out.script_pubkey,network)) +fn get_script_key_from_transaction( + tx: &Transaction, + outpoint: &OutPoint, + network: Network, +) -> Option { + if !tx.txid().eq(&outpoint.txid) { + return None; + } + match tx.output.get(outpoint.vout as usize) { + None => None, + Some(tx_out) => Some(ScriptKey::from_script(&tx_out.script_pubkey, network)), + } +} + +fn deserialize_inscription(inscription: &Inscription) -> Result { + let content_body = std::str::from_utf8(inscription.body().ok_or(JSONError::InvalidJson)?)?; + if content_body.len() == 0 { + return Err(JSONError::InvalidJson.into()); + } + + let content_type = inscription + .content_type() + .ok_or(JSONError::InvalidContentType)?; + + if content_type != "text/plain" + && content_type != "text/plain;charset=utf-8" + && content_type != "text/plain;charset=UTF-8" + && content_type != "application/json" + && !content_type.starts_with("text/plain;") + { + return Err(JSONError::UnSupportContentType.into()); + } + + let value: Value = serde_json::from_str(content_body).map_err(|_| JSONError::InvalidJson)?; + if value.get("p") == None || !value["p"].is_string() { + return Err(JSONError::InvalidJson.into()); + } + let protocol_name = match value.get("p") { + None => return Err(JSONError::NotZeroIndexerJson.into()), + Some(v) => v.to_string().replace("\"", ""), + }; + if protocol_name != "brc-20".to_string() { + return Err(JSONError::InvalidJson.into()); + } + + return Ok(serde_json::to_string(&value).unwrap()); +} diff --git a/src/subcommand/server/response.rs b/src/subcommand/server/response.rs index 0b9cc666cd..7e122f4fbb 100644 --- a/src/subcommand/server/response.rs +++ b/src/subcommand/server/response.rs @@ -4,6 +4,14 @@ use { }; #[derive(Default, Debug, Clone, Serialize, Deserialize, ToSchema)] #[aliases( + BRC20Tick = ApiResponse, + BRC20AllTick = ApiResponse, + BRC20Balance = ApiResponse, + BRC20AllBalance = ApiResponse, + BRC20TxEvents = ApiResponse, + BRC20BlockEvents = ApiResponse, + BRC20Transferable = ApiResponse, + OrdOrdInscription = ApiResponse, OrdOutPointData = ApiResponse, OrdOutPointResult = ApiResponse, @@ -17,18 +25,21 @@ pub(crate) struct ApiResponse { /// ok #[schema(example = "ok")] pub msg: String, - pub data: T, + pub data: Option, } impl ApiResponse where T: Serialize, { - fn new(code: i32, msg: String, data: T) -> Self { + fn new(code: i32, msg: String, data: Option) -> Self { Self { code, msg, data } } pub fn ok(data: T) -> Self { - Self::new(0, "ok".to_string(), data) + Self::new(0, "ok".to_string(), Some(data)) + } + pub fn err(msg: String) -> Self { + Self::new(1, msg.to_string(), None) } }