diff --git a/src/index.rs b/src/index.rs index e4910d13b6..086ec34207 100644 --- a/src/index.rs +++ b/src/index.rs @@ -1,6 +1,7 @@ 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, + get_transferable_assets_by_account, get_transferable_assets_by_account_ticker, + get_transferable_assets_by_outpoint, }; use crate::okx::datastore::ord::redb::table::{ get_collection_inscription_id, get_collections_of_inscription, get_transaction_operations, @@ -95,13 +96,13 @@ 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_multimap_table! { COLLECTIONS_INSCRIPTION_ID_TO_KINDS, InscriptionIdValue, &[u8] } define_table! { BRC20_BALANCES, &str, &[u8] } define_table! { BRC20_TOKEN, &str, &[u8] } define_table! { BRC20_EVENTS, &TxidValue, &[u8] } -define_table! { BRC20_TRANSFERABLELOG, &str, &[u8] } -define_table! { BRC20_INSCRIBE_TRANSFER, InscriptionIdValue, &[u8] } +define_table! { BRC20_SATPOINT_TO_TRANSFERABLE_ASSETS, &SatPointValue, &[u8] } +define_multimap_table! { BRC20_ADDRESS_TICKER_TO_TRANSFERABLE_ASSETS, &str, &SatPointValue } #[derive(Debug, PartialEq)] pub enum List { @@ -359,14 +360,14 @@ impl Index { // 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)?; + tx.open_multimap_table(COLLECTIONS_INSCRIPTION_ID_TO_KINDS)?; // brc20 tables + tx.open_multimap_table(BRC20_ADDRESS_TICKER_TO_TRANSFERABLE_ASSETS)?; tx.open_table(BRC20_BALANCES)?; tx.open_table(BRC20_TOKEN)?; tx.open_table(BRC20_EVENTS)?; - tx.open_table(BRC20_TRANSFERABLELOG)?; - tx.open_table(BRC20_INSCRIBE_TRANSFER)?; + tx.open_table(BRC20_SATPOINT_TO_TRANSFERABLE_ASSETS)?; { let mut outpoint_to_sat_ranges = tx.open_table(OUTPOINT_TO_SAT_RANGES)?; @@ -637,7 +638,7 @@ impl Index { total_bytes, COLLECTIONS_KEY_TO_INSCRIPTION_ID, ); - insert_table_info( + insert_multimap_table_info( &mut tables, &wtx, total_bytes, @@ -646,8 +647,18 @@ impl Index { insert_table_info(&mut tables, &wtx, total_bytes, BRC20_BALANCES); insert_table_info(&mut tables, &wtx, total_bytes, BRC20_TOKEN); insert_table_info(&mut tables, &wtx, total_bytes, BRC20_EVENTS); - insert_table_info(&mut tables, &wtx, total_bytes, BRC20_TRANSFERABLELOG); - insert_table_info(&mut tables, &wtx, total_bytes, BRC20_INSCRIBE_TRANSFER); + insert_table_info( + &mut tables, + &wtx, + total_bytes, + BRC20_SATPOINT_TO_TRANSFERABLE_ASSETS, + ); + insert_multimap_table_info( + &mut tables, + &wtx, + total_bytes, + BRC20_ADDRESS_TICKER_TO_TRANSFERABLE_ASSETS, + ); for table in wtx.list_tables()? { assert!(tables.contains_key(table.name())); diff --git a/src/index/entry.rs b/src/index/entry.rs index 5b67c2a6cc..95bae50dc0 100644 --- a/src/index/entry.rs +++ b/src/index/entry.rs @@ -325,7 +325,7 @@ impl Entry for OutPoint { } } -pub(super) type SatPointValue = [u8; 44]; +pub(crate) type SatPointValue = [u8; 44]; impl Entry for SatPoint { type Value = SatPointValue; diff --git a/src/index/extend.rs b/src/index/extend.rs index 90d59e5b43..9c5779d149 100644 --- a/src/index/extend.rs +++ b/src/index/extend.rs @@ -156,7 +156,7 @@ impl Index { tick: brc20::Tick, script_key: ScriptKey, rtx: &Rtx, - ) -> Result>> { + ) -> Result>> { let transferable_utxo_assets = rtx.brc20_get_tick_transferable_by_address(&tick, script_key)?; if transferable_utxo_assets.is_empty() { @@ -267,4 +267,114 @@ impl Index { ) } } + + pub(crate) fn list_sat_range( + rtx: &Rtx, + outpoint: OutPoint, + index_sats: bool, + ) -> Result>> { + if !index_sats || outpoint == unbound_outpoint() { + return Ok(None); + } + + let sat_ranges = rtx.list_sat_range(outpoint.store())?; + + match sat_ranges { + Some(sat_ranges) => Ok(Some( + sat_ranges + .chunks_exact(11) + .map(|chunk| SatRange::load(chunk.try_into().unwrap())) + .collect(), + )), + None => Ok(None), + } + } + + pub(crate) fn calculate_rarity_for_sat_range(sat_range: SatRange) -> Vec<(Sat, Rarity)> { + let start_sat = Sat(sat_range.0); + let end_sat = Sat(sat_range.1); + + let start_height = if start_sat.third() > 0 { + start_sat.height().0 + 1 + } else { + start_sat.height().0 + }; + let end_height = if end_sat.third() > 0 { + end_sat.height().0 + } else { + end_sat.height().0 - 1 + }; + + let mut result = Vec::new(); + for height in start_height..=end_height { + let sat = Height(height).starting_sat(); + let rarity = sat.rarity(); + result.push((sat, rarity)); + } + result + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_calculate_rarity_for_sat_range_mythic() { + let sat_range: SatRange = (0, 100); + let rarity = Index::calculate_rarity_for_sat_range(sat_range); + assert_eq!(rarity, vec![(Sat(0), Rarity::Mythic)]); + let sat_range: SatRange = (1, 100); + let rarity = Index::calculate_rarity_for_sat_range(sat_range); + assert_eq!(rarity, vec![]); + } + #[test] + fn test_legendary_sat() { + let sat_range: SatRange = ( + Height(SUBSIDY_HALVING_INTERVAL * 6).starting_sat().0, + Height(SUBSIDY_HALVING_INTERVAL * 6).starting_sat().0 + 1, + ); + let rarity = Index::calculate_rarity_for_sat_range(sat_range); + assert_eq!(rarity, vec![(Sat(2067187500000000), Rarity::Legendary)]); + } + #[test] + fn test_epic_sat() { + let sat_range: SatRange = ( + Height(SUBSIDY_HALVING_INTERVAL).starting_sat().0, + Height(SUBSIDY_HALVING_INTERVAL).starting_sat().0 + 1, + ); + let rarity = Index::calculate_rarity_for_sat_range(sat_range); + assert_eq!(rarity, vec![(Sat(1050000000000000), Rarity::Epic)]); + } + + #[test] + fn test_rare_sat() { + let sat_range: SatRange = ( + Height(DIFFCHANGE_INTERVAL).starting_sat().0, + Height(DIFFCHANGE_INTERVAL).starting_sat().0 + 1, + ); + let rarity = Index::calculate_rarity_for_sat_range(sat_range); + assert_eq!(rarity, vec![(Sat(10080000000000), Rarity::Rare)]); + } + + #[test] + fn test_two_rarity_sat() { + let sat_range: SatRange = (0, 4999999999); + let rarity = Index::calculate_rarity_for_sat_range(sat_range); + assert_eq!(rarity, vec![(Sat(0), Rarity::Mythic)]); + let sat_range: SatRange = (0, 5000000000); + let rarity = Index::calculate_rarity_for_sat_range(sat_range); + assert_eq!(rarity, vec![(Sat(0), Rarity::Mythic)]); + let sat_range: SatRange = (0, 5000000001); + let rarity = Index::calculate_rarity_for_sat_range(sat_range); + assert_eq!( + rarity, + vec![ + (Sat(0), Rarity::Mythic), + (Sat(5000000000), Rarity::Uncommon) + ] + ); + let sat_range: SatRange = (1, 5000000001); + let rarity = Index::calculate_rarity_for_sat_range(sat_range); + assert_eq!(rarity, vec![(Sat(5000000000), Rarity::Uncommon)]); + } } diff --git a/src/index/rtx.rs b/src/index/rtx.rs index ed71e5a199..1f76f25a79 100644 --- a/src/index/rtx.rs +++ b/src/index/rtx.rs @@ -159,7 +159,9 @@ impl Rtx<'_> { &self, inscription_id: InscriptionId, ) -> Result>> { - let table = self.0.open_table(COLLECTIONS_INSCRIPTION_ID_TO_KINDS)?; + let table = self + .0 + .open_multimap_table(COLLECTIONS_INSCRIPTION_ID_TO_KINDS)?; get_collections_of_inscription(&table, &inscription_id) } @@ -219,16 +221,40 @@ impl Rtx<'_> { &self, tick: &brc20::Tick, script_key: ScriptKey, - ) -> Result> { - let table = self.0.open_table(BRC20_TRANSFERABLELOG)?; - get_transferable_by_tick(&table, &script_key, tick) + ) -> Result> { + let address_table = self + .0 + .open_multimap_table(BRC20_ADDRESS_TICKER_TO_TRANSFERABLE_ASSETS)?; + let satpoint_table = self.0.open_table(BRC20_SATPOINT_TO_TRANSFERABLE_ASSETS)?; + get_transferable_assets_by_account_ticker(&address_table, &satpoint_table, &script_key, tick) } pub(crate) fn brc20_get_all_transferable_by_address( &self, script_key: ScriptKey, - ) -> Result> { - let table = self.0.open_table(BRC20_TRANSFERABLELOG)?; - get_transferable(&table, &script_key) + ) -> Result> { + let address_table = self + .0 + .open_multimap_table(BRC20_ADDRESS_TICKER_TO_TRANSFERABLE_ASSETS)?; + let satpoint_table = self.0.open_table(BRC20_SATPOINT_TO_TRANSFERABLE_ASSETS)?; + get_transferable_assets_by_account(&address_table, &satpoint_table, &script_key) + } + + pub(crate) fn brc20_transferable_assets_on_output_with_satpoints( + &self, + outpoint: OutPoint, + ) -> Result> { + let satpoint_to_sequence_number = self.0.open_table(BRC20_SATPOINT_TO_TRANSFERABLE_ASSETS)?; + get_transferable_assets_by_outpoint(&satpoint_to_sequence_number, outpoint) + } + + pub(super) fn list_sat_range(&self, outpoint: OutPointValue) -> Result>> { + Ok( + self + .0 + .open_table(OUTPOINT_TO_SAT_RANGES)? + .get(&outpoint)? + .map(|outpoint| outpoint.value().to_vec()), + ) } } diff --git a/src/index/updater.rs b/src/index/updater.rs index 6c4de37267..600e0262d0 100644 --- a/src/index/updater.rs +++ b/src/index/updater.rs @@ -632,14 +632,16 @@ impl<'index> Updater<'_> { 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)?, + .open_multimap_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_table(BRC20_EVENTS)?, - BRC20_TRANSFERABLELOG: &mut wtx.open_table(BRC20_TRANSFERABLELOG)?, - BRC20_INSCRIBE_TRANSFER: &mut wtx.open_table(BRC20_INSCRIBE_TRANSFER)?, + BRC20_SATPOINT_TO_TRANSFERABLE_ASSETS: &mut wtx + .open_table(BRC20_SATPOINT_TO_TRANSFERABLE_ASSETS)?, + BRC20_ADDRESS_TICKER_TO_TRANSFERABLE_ASSETS: &mut wtx + .open_multimap_table(BRC20_ADDRESS_TICKER_TO_TRANSFERABLE_ASSETS)?, }; // Create a protocol manager to index the block of bitmap data. diff --git a/src/okx/datastore/brc20/mod.rs b/src/okx/datastore/brc20/mod.rs index 5618c4fc92..411e28de2d 100644 --- a/src/okx/datastore/brc20/mod.rs +++ b/src/okx/datastore/brc20/mod.rs @@ -4,16 +4,15 @@ 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, + transferable_log::TransferableLog, }; use super::ScriptKey; -use crate::{InscriptionId, Result}; -use bitcoin::Txid; +use crate::{Result, SatPoint}; +use bitcoin::{OutPoint, Txid}; use std::fmt::{Debug, Display}; pub trait Brc20Reader { @@ -31,22 +30,23 @@ pub trait Brc20Reader { fn get_transaction_receipts(&self, txid: &Txid) -> Result>, Self::Error>; - fn get_transferable(&self, script: &ScriptKey) -> Result, Self::Error>; - fn get_transferable_by_tick( + fn get_transferable_assets_by_satpoint( + &self, + satpoint: &SatPoint, + ) -> Result, Self::Error>; + fn get_transferable_assets_by_account( &self, script: &ScriptKey, - tick: &Tick, - ) -> Result, Self::Error>; - fn get_transferable_by_id( + ) -> Result, Self::Error>; + fn get_transferable_assets_by_account_ticker( &self, script: &ScriptKey, - inscription_id: &InscriptionId, - ) -> Result, Self::Error>; - - fn get_inscribe_transfer_inscription( + tick: &Tick, + ) -> Result, Self::Error>; + fn get_transferable_assets_by_outpoint( &self, - inscription_id: &InscriptionId, - ) -> Result, Self::Error>; + outpoint: OutPoint, + ) -> Result, Self::Error>; } pub trait Brc20ReaderWriter: Brc20Reader { @@ -71,28 +71,11 @@ pub trait Brc20ReaderWriter: Brc20Reader { receipt: &[Receipt], ) -> Result<(), Self::Error>; - fn insert_transferable( + fn insert_transferable_asset( &mut self, - script: &ScriptKey, - tick: &Tick, + satpoint: SatPoint, 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>; + fn remove_transferable_asset(&mut self, satpoint: SatPoint) -> Result<(), Self::Error>; } diff --git a/src/okx/datastore/brc20/redb/mod.rs b/src/okx/datastore/brc20/redb/mod.rs index 6421d2b885..48582eb12c 100644 --- a/src/okx/datastore/brc20/redb/mod.rs +++ b/src/okx/datastore/brc20/redb/mod.rs @@ -1,16 +1,6 @@ pub mod table; use super::{LowerTick, ScriptKey, Tick}; -use crate::inscriptions::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) diff --git a/src/okx/datastore/brc20/redb/table.rs b/src/okx/datastore/brc20/redb/table.rs index ab2a67b065..d4a23cff6b 100644 --- a/src/okx/datastore/brc20/redb/table.rs +++ b/src/okx/datastore/brc20/redb/table.rs @@ -1,19 +1,25 @@ -use crate::index::entry::Entry; -use crate::index::{InscriptionIdValue, TxidValue}; -use crate::inscriptions::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::{ + index::{ + entry::{Entry, SatPointValue}, + TxidValue, + }, + okx::datastore::{ + brc20::{ + redb::{ + max_script_tick_id_key, max_script_tick_key, min_script_tick_id_key, min_script_tick_key, + script_tick_key, + }, + Balance, Receipt, Tick, TokenInfo, TransferableLog, + }, + ScriptKey, + }, + Result, SatPoint, }; -use crate::okx::datastore::brc20::{ - Balance, Receipt, Tick, TokenInfo, TransferInfo, TransferableLog, -}; -use crate::okx::datastore::ScriptKey; -use bitcoin::Txid; -use redb::{ReadableTable, Table}; +use bitcoin::{OutPoint, Txid}; +use redb::{MultimapTable, ReadableMultimapTable, ReadableTable, Table}; // BRC20_BALANCES -pub fn get_balances(table: &T, script_key: &ScriptKey) -> crate::Result> +pub fn get_balances(table: &T, script_key: &ScriptKey) -> Result> where T: ReadableTable<&'static str, &'static [u8]>, { @@ -28,11 +34,7 @@ where } // BRC20_BALANCES -pub fn get_balance( - table: &T, - script_key: &ScriptKey, - tick: &Tick, -) -> crate::Result> +pub fn get_balance(table: &T, script_key: &ScriptKey, tick: &Tick) -> Result> where T: ReadableTable<&'static str, &'static [u8]>, { @@ -44,7 +46,7 @@ where } // BRC20_TOKEN -pub fn get_token_info(table: &T, tick: &Tick) -> crate::Result> +pub fn get_token_info(table: &T, tick: &Tick) -> Result> where T: ReadableTable<&'static str, &'static [u8]>, { @@ -56,7 +58,7 @@ where } // BRC20_TOKEN -pub fn get_tokens_info(table: &T) -> crate::Result> +pub fn get_tokens_info(table: &T) -> Result> where T: ReadableTable<&'static str, &'static [u8]>, { @@ -71,7 +73,7 @@ where } // BRC20_EVENTS -pub fn get_transaction_receipts(table: &T, txid: &Txid) -> crate::Result>> +pub fn get_transaction_receipts(table: &T, txid: &Txid) -> Result>> where T: ReadableTable<&'static TxidValue, &'static [u8]>, { @@ -82,73 +84,110 @@ where ) } -// BRC20_TRANSFERABLELOG -pub fn get_transferable(table: &T, script: &ScriptKey) -> crate::Result> +// BRC20_SATPOINT_TO_TRANSFERABLE_ASSETS +// BRC20_ADDRESS_TICKER_TO_TRANSFERABLE_ASSETS +pub fn get_transferable_assets_by_account( + address_table: &T, + satpoint_table: &S, + script: &ScriptKey, +) -> Result> where - T: ReadableTable<&'static str, &'static [u8]>, + T: ReadableMultimapTable<&'static str, &'static SatPointValue>, + S: ReadableTable<&'static SatPointValue, &'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(), - ) + let mut transferable_assets = Vec::new(); + + for range in address_table + .range(min_script_tick_key(script).as_str()..max_script_tick_key(script).as_str())? + { + let (_, satpoints) = range?; + for satpoint_guard in satpoints { + let satpoint = SatPoint::load(*satpoint_guard?.value()); + let entry = satpoint_table.get(&satpoint.store())?.unwrap(); + transferable_assets.push(( + satpoint, + rmp_serde::from_slice::(entry.value()).unwrap(), + )); + } + } + Ok(transferable_assets) } -// BRC20_TRANSFERABLELOG -pub fn get_transferable_by_tick( - table: &T, +// BRC20_SATPOINT_TO_TRANSFERABLE_ASSETS +// BRC20_ADDRESS_TICKER_TO_TRANSFERABLE_ASSETS +pub fn get_transferable_assets_by_account_ticker( + address_table: &T, + satpoint_table: &S, script: &ScriptKey, tick: &Tick, -) -> crate::Result> +) -> Result> where - T: ReadableTable<&'static str, &'static [u8]>, + T: ReadableMultimapTable<&'static str, &'static SatPointValue>, + S: ReadableTable<&'static SatPointValue, &'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(), - ) + let mut transferable_assets = Vec::new(); + + for range in address_table.range( + min_script_tick_id_key(script, tick).as_str()..max_script_tick_id_key(script, tick).as_str(), + )? { + let (_, satpoints) = range?; + for satpoint_guard in satpoints { + let satpoint = SatPoint::load(*satpoint_guard?.value()); + let entry = satpoint_table.get(&satpoint.store())?.unwrap(); + transferable_assets.push(( + satpoint, + rmp_serde::from_slice::(entry.value()).unwrap(), + )); + } + } + Ok(transferable_assets) } -// BRC20_TRANSFERABLELOG -pub fn get_transferable_by_id( +// BRC20_SATPOINT_TO_TRANSFERABLE_ASSETS +pub fn get_transferable_assets_by_satpoint( table: &T, - script: &ScriptKey, - inscription_id: &InscriptionId, -) -> crate::Result> + satpoint: &SatPoint, +) -> Result> where - T: ReadableTable<&'static str, &'static [u8]>, + T: ReadableTable<&'static SatPointValue, &'static [u8]>, { Ok( - get_transferable(table, script)? - .iter() - .find(|log| log.inscription_id == *inscription_id) - .cloned(), + table + .get(&satpoint.store())? + .map(|entry| rmp_serde::from_slice::(entry.value()).unwrap()), ) } -// BRC20_INSCRIBE_TRANSFER -pub fn get_inscribe_transfer_inscription( +// BRC20_SATPOINT_TO_TRANSFERABLE_ASSETS +pub fn get_transferable_assets_by_outpoint( table: &T, - inscription_id: &InscriptionId, -) -> crate::Result> + outpoint: OutPoint, +) -> Result> where - T: ReadableTable, + T: ReadableTable<&'static SatPointValue, &'static [u8]>, { - Ok( - table - .get(&inscription_id.store())? - .map(|v| rmp_serde::from_slice::(v.value()).unwrap()), - ) + let start = SatPoint { + outpoint, + offset: 0, + } + .store(); + + let end = SatPoint { + outpoint, + offset: u64::MAX, + } + .store(); + + let mut transferable_assets = Vec::new(); + for range in table.range::<&[u8; 44]>(&start..&end)? { + let (satpoint_guard, asset) = range?; + let satpoint = SatPoint::load(*satpoint_guard.value()); + transferable_assets.push(( + satpoint, + rmp_serde::from_slice::(asset.value()).unwrap(), + )); + } + Ok(transferable_assets) } // BRC20_BALANCES @@ -156,7 +195,7 @@ pub fn update_token_balance( table: &mut Table<'_, '_, &'static str, &'static [u8]>, script_key: &ScriptKey, new_balance: Balance, -) -> crate::Result<()> { +) -> Result<()> { table.insert( script_tick_key(script_key, &new_balance.tick).as_str(), rmp_serde::to_vec(&new_balance).unwrap().as_slice(), @@ -169,7 +208,7 @@ pub fn insert_token_info( table: &mut Table<'_, '_, &'static str, &'static [u8]>, tick: &Tick, new_info: &TokenInfo, -) -> crate::Result<()> { +) -> Result<()> { table.insert( tick.to_lowercase().hex().as_str(), rmp_serde::to_vec(new_info).unwrap().as_slice(), @@ -183,7 +222,7 @@ pub fn update_mint_token_info( tick: &Tick, minted_amt: u128, minted_block_number: u32, -) -> crate::Result<()> { +) -> Result<()> { let mut info = get_token_info(table, tick)?.unwrap_or_else(|| panic!("token {} not exist", tick.as_str())); @@ -202,7 +241,7 @@ pub fn save_transaction_receipts( table: &mut Table<'_, '_, &'static TxidValue, &'static [u8]>, txid: &Txid, receipts: &[Receipt], -) -> crate::Result<()> { +) -> Result<()> { table.insert( &txid.store(), rmp_serde::to_vec(receipts).unwrap().as_slice(), @@ -210,49 +249,38 @@ pub fn save_transaction_receipts( Ok(()) } -// BRC20_TRANSFERABLELOG -pub fn insert_transferable( - table: &mut Table<'_, '_, &'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(), +// BRC20_SATPOINT_TO_TRANSFERABLE_ASSETS +// BRC20_ADDRESS_TICKER_TO_TRANSFERABLE_ASSETS +pub fn insert_transferable_asset( + satpoint_table: &mut Table<'_, '_, &'static SatPointValue, &'static [u8]>, + address_table: &mut MultimapTable<'_, '_, &'static str, &'static SatPointValue>, + satpoint: SatPoint, + transferable_asset: &TransferableLog, +) -> Result<()> { + satpoint_table.insert( + &satpoint.store(), + rmp_serde::to_vec(&transferable_asset).unwrap().as_slice(), )?; - Ok(()) -} - -// BRC20_TRANSFERABLELOG -pub fn remove_transferable( - table: &mut Table<'_, '_, &'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( - table: &mut Table<'_, '_, 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(), + address_table.insert( + script_tick_key(&transferable_asset.owner, &transferable_asset.tick).as_str(), + &satpoint.store(), )?; Ok(()) } -// BRC20_INSCRIBE_TRANSFER -pub fn remove_inscribe_transfer_inscription( - table: &mut Table<'_, '_, InscriptionIdValue, &'static [u8]>, - inscription_id: &InscriptionId, -) -> crate::Result<()> { - table.remove(&inscription_id.store())?; +// BRC20_SATPOINT_TO_TRANSFERABLE_ASSETS +// BRC20_ADDRESS_TICKER_TO_TRANSFERABLE_ASSETS +pub fn remove_transferable_asset( + satpoint_table: &mut Table<'_, '_, &'static SatPointValue, &'static [u8]>, + address_table: &mut MultimapTable<'_, '_, &'static str, &'static SatPointValue>, + satpoint: SatPoint, +) -> Result<()> { + if let Some(guard) = satpoint_table.remove(&satpoint.store())? { + let transferable_asset = rmp_serde::from_slice::(guard.value()).unwrap(); + address_table.remove( + script_tick_key(&transferable_asset.owner, &transferable_asset.tick).as_str(), + &satpoint.store(), + )?; + } Ok(()) } diff --git a/src/okx/datastore/brc20/transfer.rs b/src/okx/datastore/brc20/transfer.rs deleted file mode 100644 index a290793a4b..0000000000 --- a/src/okx/datastore/brc20/transfer.rs +++ /dev/null @@ -1,7 +0,0 @@ -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/ord/collections.rs b/src/okx/datastore/ord/collections.rs index e0240707e9..196eceb929 100644 --- a/src/okx/datastore/ord/collections.rs +++ b/src/okx/datastore/ord/collections.rs @@ -5,6 +5,7 @@ use std::fmt::Display; #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub enum CollectionKind { BitMap, + BRC20, } impl Display for CollectionKind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -13,6 +14,7 @@ impl Display for CollectionKind { "{}", match self { CollectionKind::BitMap => String::from("bitmap"), + CollectionKind::BRC20 => String::from("brc20"), } ) } diff --git a/src/okx/datastore/ord/mod.rs b/src/okx/datastore/ord/mod.rs index d1f73b92ba..6c5f065636 100644 --- a/src/okx/datastore/ord/mod.rs +++ b/src/okx/datastore/ord/mod.rs @@ -57,9 +57,9 @@ pub trait OrdReaderWriter: OrdReader { inscription_id: &InscriptionId, ) -> Result<(), Self::Error>; - fn set_inscription_attributes( + fn add_inscription_attributes( &mut self, inscription_id: &InscriptionId, - kind: &[CollectionKind], + kind: CollectionKind, ) -> Result<(), Self::Error>; } diff --git a/src/okx/datastore/ord/redb/table.rs b/src/okx/datastore/ord/redb/table.rs index 080ec12e71..2f541867e5 100644 --- a/src/okx/datastore/ord/redb/table.rs +++ b/src/okx/datastore/ord/redb/table.rs @@ -5,7 +5,7 @@ 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 redb::{MultimapTable, ReadableMultimapTable, ReadableTable, Table}; use std::io; // COLLECTIONS_INSCRIPTION_ID_TO_KINDS @@ -14,13 +14,14 @@ pub fn get_collections_of_inscription( inscription_id: &InscriptionId, ) -> crate::Result>> where - T: ReadableTable, + T: ReadableMultimapTable, { - Ok( - table - .get(&inscription_id.store())? - .map(|v| rmp_serde::from_slice::>(v.value()).unwrap()), - ) + let mut values = Vec::new(); + + for v in table.get(&inscription_id.store())? { + values.push(rmp_serde::from_slice::(v?.value()).unwrap()); + } + Ok(Some(values)) } // COLLECTIONS_KEY_TO_INSCRIPTION_ID @@ -93,10 +94,10 @@ pub fn set_inscription_by_collection_key( } // COLLECTIONS_INSCRIPTION_ID_TO_KINDS -pub fn set_inscription_attributes( - table: &mut Table<'_, '_, InscriptionIdValue, &'static [u8]>, +pub fn add_inscription_attributes( + table: &mut MultimapTable<'_, '_, InscriptionIdValue, &'static [u8]>, inscription_id: &InscriptionId, - kind: &[CollectionKind], + kind: CollectionKind, ) -> crate::Result<()> { table.insert( inscription_id.store(), @@ -108,7 +109,7 @@ pub fn set_inscription_attributes( #[cfg(test)] mod tests { use super::*; - use crate::index::ORD_TX_TO_OPERATIONS; + use crate::index::{COLLECTIONS_INSCRIPTION_ID_TO_KINDS, ORD_TX_TO_OPERATIONS}; use crate::okx::datastore::ord::redb::table::{ get_transaction_operations, save_transaction_operations, }; @@ -118,6 +119,37 @@ mod tests { use std::str::FromStr; use tempfile::NamedTempFile; + #[test] + fn test_inscription_attributes() { + let dbfile = NamedTempFile::new().unwrap(); + let db = Database::create(dbfile.path()).unwrap(); + let wtx = db.begin_write().unwrap(); + let mut table = wtx + .open_multimap_table(COLLECTIONS_INSCRIPTION_ID_TO_KINDS) + .unwrap(); + let inscription_id = + InscriptionId::from_str("b61b0172d95e266c18aea0c624db987e971a5d6d4ebc2aaed85da4642d635735i0") + .unwrap(); + + add_inscription_attributes(&mut table, &inscription_id, CollectionKind::BitMap).unwrap(); + assert_eq!( + get_collections_of_inscription(&table, &inscription_id).unwrap(), + Some(vec![CollectionKind::BitMap]) + ); + + add_inscription_attributes(&mut table, &inscription_id, CollectionKind::BRC20).unwrap(); + assert_eq!( + get_collections_of_inscription(&table, &inscription_id).unwrap(), + Some(vec![CollectionKind::BRC20, CollectionKind::BitMap]) + ); + + add_inscription_attributes(&mut table, &inscription_id, CollectionKind::BRC20).unwrap(); + assert_eq!( + get_collections_of_inscription(&table, &inscription_id).unwrap(), + Some(vec![CollectionKind::BRC20, CollectionKind::BitMap]) + ); + } + #[test] fn test_transaction_to_operations() { let dbfile = NamedTempFile::new().unwrap(); diff --git a/src/okx/protocol/brc20/msg_executor.rs b/src/okx/protocol/brc20/msg_executor.rs index 8982e39340..b37adc5114 100644 --- a/src/okx/protocol/brc20/msg_executor.rs +++ b/src/okx/protocol/brc20/msg_executor.rs @@ -2,17 +2,19 @@ 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, InscribeTransferEvent, MintEvent, Receipt, Tick, - TokenInfo, TransferEvent, TransferInfo, TransferableLog, + datastore::{ + brc20::{ + BRC20Error, Balance, Brc20Reader, Brc20ReaderWriter, DeployEvent, Event, + InscribeTransferEvent, MintEvent, Receipt, Tick, TokenInfo, TransferEvent, TransferableLog, + }, + ord::OrdReader, + }, + protocol::{ + brc20::{Message, Mint, Operation}, + context::Context, }, - protocol::brc20::{Message, Mint, Operation}, }, Result, }; @@ -294,7 +296,7 @@ fn process_inscribe_transfer( .update_token_balance(&to_script_key, balance) .map_err(Error::LedgerError)?; - let inscription = TransferableLog { + let transferable_asset = TransferableLog { inscription_id: msg.inscription_id, inscription_number: msg.inscription_number, amount: amt, @@ -303,28 +305,18 @@ fn process_inscribe_transfer( }; context - .insert_transferable(&inscription.owner, &tick, &inscription) - .map_err(Error::LedgerError)?; - - context - .insert_inscribe_transfer_inscription( - &msg.inscription_id, - TransferInfo { - tick: token_info.tick, - amt, - }, - ) + .insert_transferable_asset(msg.new_satpoint, &transferable_asset) .map_err(Error::LedgerError)?; Ok(Event::InscribeTransfer(InscribeTransferEvent { - tick: inscription.tick, + tick: transferable_asset.tick, amount: amt, })) } fn process_transfer(context: &mut Context, msg: &ExecutionMessage) -> Result { let transferable = context - .get_transferable_by_id(&msg.from, &msg.inscription_id) + .get_transferable_assets_by_satpoint(&msg.old_satpoint) .map_err(Error::LedgerError)? .ok_or(BRC20Error::TransferableNotFound(msg.inscription_id))?; @@ -387,11 +379,7 @@ fn process_transfer(context: &mut Context, msg: &ExecutionMessage) -> Result( - table: &T, - new_inscriptions: &[Inscription], + pub(crate) fn resolve( op: &InscriptionOp, - ) -> Result> - where - T: ReadableTable, - { + new_inscriptions: &[Inscription], + transfer_assets_cache: HashMap, + ) -> Result> { log::debug!("BRC20 resolving the message from {:?}", op); let sat_in_outputs = op .new_satpoint @@ -35,34 +33,31 @@ impl Message { vindicated: false, inscription: _, } if sat_in_outputs => { - match deserialize_brc20_operation( + let Ok(brc20_opteration) = 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), - } + ) else { + return Ok(None); + }; + brc20_opteration } // 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, - )) + Action::Transfer => { + let Some(transfer_info) = transfer_assets_cache.get(&op.old_satpoint.store()) else { + return Ok(None); + }; + // If the inscription_id of the transfer operation is different from the inscription_id of the transferable log, it is invalid. + if transfer_info.inscription_id != op.inscription_id { + return Ok(None); } - _ => return Ok(None), - }, + Operation::Transfer(Transfer { + tick: transfer_info.tick.as_str().to_string(), + amount: transfer_info.amount.to_string(), + }) + } _ => return Ok(None), }; Ok(Some(Self { @@ -80,13 +75,9 @@ impl Message { #[cfg(test)] mod tests { use super::*; - use crate::index::BRC20_INSCRIBE_TRANSFER; - use crate::okx::datastore::brc20::redb::table::insert_inscribe_transfer_inscription; - use crate::okx::datastore::brc20::{Tick, TransferInfo}; - use bitcoin::OutPoint; - use redb::Database; + use crate::okx::datastore::brc20::{Tick, TransferableLog}; + use bitcoin::{Address, OutPoint}; use std::str::FromStr; - use tempfile::NamedTempFile; fn create_inscription(str: &str) -> Inscription { Inscription::new( @@ -158,23 +149,19 @@ mod tests { #[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 table = wtx.open_table(BRC20_INSCRIBE_TRANSFER).unwrap(); - + let transfer_assets_cache = HashMap::new(); let (inscriptions, op) = create_inscribe_operation( r#"{ "p": "brc-20s","op": "deploy", "tick": "ordi", "max": "1000", "lim": "10" }"#, ); - assert_matches!(Message::resolve(&table, &inscriptions, &op), Ok(None)); + assert_matches!( + Message::resolve(&op, &inscriptions, transfer_assets_cache), + 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 table = wtx.open_table(BRC20_INSCRIBE_TRANSFER).unwrap(); + let transfer_assets_cache = HashMap::new(); let (inscriptions, op) = create_inscribe_operation( r#"{ "p": "brc-20","op": "deploy", "tick": "ordi", "max": "1000", "lim": "10" }"#, @@ -188,7 +175,10 @@ mod tests { }, ..op }; - assert_matches!(Message::resolve(&table, &inscriptions, &op), Ok(None)); + assert_matches!( + Message::resolve(&op, &inscriptions, transfer_assets_cache.clone()), + Ok(None) + ); let op2 = InscriptionOp { action: Action::New { @@ -199,7 +189,10 @@ mod tests { }, ..op }; - assert_matches!(Message::resolve(&table, &inscriptions, &op2), Ok(None)); + assert_matches!( + Message::resolve(&op2, &inscriptions, transfer_assets_cache.clone()), + Ok(None) + ); let op3 = InscriptionOp { action: Action::New { cursed: true, @@ -209,16 +202,15 @@ mod tests { }, ..op }; - assert_matches!(Message::resolve(&table, &inscriptions, &op3), Ok(None)); + assert_matches!( + Message::resolve(&op3, &inscriptions, transfer_assets_cache), + 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 table = wtx.open_table(BRC20_INSCRIBE_TRANSFER).unwrap(); - + let transfer_assets_cache = HashMap::new(); let (inscriptions, op) = create_inscribe_operation( r#"{ "p": "brc-20","op": "deploy", "tick": "ordi", "max": "1000", "lim": "10" }"#, ); @@ -237,21 +229,21 @@ mod tests { sat_in_outputs: true, }; assert_matches!( - Message::resolve(&table, &inscriptions, &op), + Message::resolve(&op, &inscriptions, transfer_assets_cache), 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 table = wtx.open_table(BRC20_INSCRIBE_TRANSFER).unwrap(); + let transfer_assets_cache = HashMap::new(); // inscribe transfer not found let op = create_transfer_operation(); - assert_matches!(Message::resolve(&table, &[], &op), Ok(None)); + assert_matches!( + Message::resolve(&op, &[], transfer_assets_cache.clone()), + Ok(None) + ); // non-first transfer operations. let op1 = InscriptionOp { @@ -265,28 +257,27 @@ mod tests { }, ..op }; - assert_matches!(Message::resolve(&table, &[], &op1), Ok(None)); + assert_matches!(Message::resolve(&op1, &[], transfer_assets_cache), 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 mut table = wtx.open_table(BRC20_INSCRIBE_TRANSFER).unwrap(); - + let mut transfer_assets_cache = HashMap::new(); // inscribe transfer not found let op = create_transfer_operation(); - - insert_inscribe_transfer_inscription( - &mut table, - &op.inscription_id, - TransferInfo { + transfer_assets_cache.insert( + op.old_satpoint.store(), + TransferableLog { tick: Tick::from_str("ordi").unwrap(), - amt: 100, + amount: 100, + inscription_id: op.inscription_id, + inscription_number: op.inscription_number.unwrap(), + owner: ScriptKey::Address( + Address::from_str("bc1qhvd6suvqzjcu9pxjhrwhtrlj85ny3n2mqql5w4").unwrap(), + ), }, - ) - .unwrap(); + ); + let _msg = Message { txid: op.txid, sequence_number: op.sequence_number, @@ -300,6 +291,9 @@ mod tests { sat_in_outputs: true, }; - assert_matches!(Message::resolve(&table, &[], &op), Ok(Some(_msg))); + assert_matches!( + Message::resolve(&op, &[], transfer_assets_cache), + Ok(Some(_msg)) + ); } } diff --git a/src/okx/protocol/context.rs b/src/okx/protocol/context.rs index 8c9ff7e884..1ae2f19c9e 100644 --- a/src/okx/protocol/context.rs +++ b/src/okx/protocol/context.rs @@ -1,31 +1,40 @@ -use crate::index::{InscriptionEntryValue, InscriptionIdValue, OutPointValue, TxidValue}; -use crate::inscriptions::InscriptionId; -use crate::okx::datastore::brc20::redb::table::{ - 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, save_transaction_receipts, - update_mint_token_info, update_token_balance, +use crate::{ + index::{ + entry::SatPointValue, InscriptionEntryValue, InscriptionIdValue, OutPointValue, TxidValue, + }, + inscriptions::InscriptionId, + okx::{ + datastore::{ + brc20::{ + redb::table::{ + get_balance, get_balances, get_token_info, get_tokens_info, get_transaction_receipts, + get_transferable_assets_by_account, get_transferable_assets_by_account_ticker, + get_transferable_assets_by_outpoint, get_transferable_assets_by_satpoint, + insert_token_info, insert_transferable_asset, remove_transferable_asset, + save_transaction_receipts, update_mint_token_info, update_token_balance, + }, + Balance, Brc20Reader, Brc20ReaderWriter, Receipt, Tick, TokenInfo, TransferableLog, + }, + ord::{ + collections::CollectionKind, + redb::table::{ + add_inscription_attributes, get_collection_inscription_id, + get_collections_of_inscription, get_inscription_number_by_sequence_number, + get_transaction_operations, get_txout_by_outpoint, save_transaction_operations, + set_inscription_by_collection_key, + }, + InscriptionOp, OrdReader, OrdReaderWriter, + }, + ScriptKey, + }, + lru::SimpleLru, + protocol::BlockContext, + }, + SatPoint, }; -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::lru::SimpleLru; -use crate::okx::protocol::BlockContext; -use crate::SatPoint; use anyhow::anyhow; use bitcoin::{Network, OutPoint, TxOut, Txid}; -use redb::Table; +use redb::{MultimapTable, Table}; #[allow(non_snake_case)] pub struct Context<'a, 'db, 'txn> { @@ -39,7 +48,7 @@ pub struct Context<'a, 'db, 'txn> { 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]>, + &'a mut MultimapTable<'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]>, @@ -48,8 +57,10 @@ pub struct Context<'a, 'db, 'txn> { 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 Table<'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]>, + pub(crate) BRC20_SATPOINT_TO_TRANSFERABLE_ASSETS: + &'a mut Table<'db, 'txn, &'static SatPointValue, &'static [u8]>, + pub(crate) BRC20_ADDRESS_TICKER_TO_TRANSFERABLE_ASSETS: + &'a mut MultimapTable<'db, 'txn, &'static str, &'static SatPointValue>, } impl<'a, 'db, 'txn> OrdReader for Context<'a, 'db, 'txn> { @@ -129,12 +140,12 @@ impl<'a, 'db, 'txn> OrdReaderWriter for Context<'a, 'db, 'txn> { set_inscription_by_collection_key(self.COLLECTIONS_KEY_TO_INSCRIPTION_ID, key, inscription_id) } - fn set_inscription_attributes( + fn add_inscription_attributes( &mut self, inscription_id: &InscriptionId, - kind: &[CollectionKind], + kind: CollectionKind, ) -> crate::Result<(), Self::Error> { - set_inscription_attributes( + add_inscription_attributes( self.COLLECTIONS_INSCRIPTION_ID_TO_KINDS, inscription_id, kind, @@ -172,34 +183,42 @@ impl<'a, 'db, 'txn> Brc20Reader for Context<'a, 'db, 'txn> { get_transaction_receipts(self.BRC20_EVENTS, txid) } - fn get_transferable( + fn get_transferable_assets_by_account( &self, script: &ScriptKey, - ) -> crate::Result, Self::Error> { - get_transferable(self.BRC20_TRANSFERABLELOG, script) + ) -> crate::Result, Self::Error> { + get_transferable_assets_by_account( + self.BRC20_ADDRESS_TICKER_TO_TRANSFERABLE_ASSETS, + self.BRC20_SATPOINT_TO_TRANSFERABLE_ASSETS, + script, + ) } - fn get_transferable_by_tick( + fn get_transferable_assets_by_account_ticker( &self, script: &ScriptKey, tick: &Tick, - ) -> crate::Result, Self::Error> { - get_transferable_by_tick(self.BRC20_TRANSFERABLELOG, script, tick) + ) -> crate::Result, Self::Error> { + get_transferable_assets_by_account_ticker( + self.BRC20_ADDRESS_TICKER_TO_TRANSFERABLE_ASSETS, + self.BRC20_SATPOINT_TO_TRANSFERABLE_ASSETS, + script, + tick, + ) } - fn get_transferable_by_id( + fn get_transferable_assets_by_satpoint( &self, - script: &ScriptKey, - inscription_id: &InscriptionId, + satpoint: &SatPoint, ) -> crate::Result, Self::Error> { - get_transferable_by_id(self.BRC20_TRANSFERABLELOG, script, inscription_id) + get_transferable_assets_by_satpoint(self.BRC20_SATPOINT_TO_TRANSFERABLE_ASSETS, satpoint) } - fn get_inscribe_transfer_inscription( + fn get_transferable_assets_by_outpoint( &self, - inscription_id: &InscriptionId, - ) -> crate::Result, Self::Error> { - get_inscribe_transfer_inscription(self.BRC20_INSCRIBE_TRANSFER, inscription_id) + outpoint: OutPoint, + ) -> crate::Result, Self::Error> { + get_transferable_assets_by_outpoint(self.BRC20_SATPOINT_TO_TRANSFERABLE_ASSETS, outpoint) } } @@ -237,40 +256,24 @@ impl<'a, 'db, 'txn> Brc20ReaderWriter for Context<'a, 'db, 'txn> { save_transaction_receipts(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( + fn insert_transferable_asset( &mut self, - inscription_id: &InscriptionId, - transfer_info: TransferInfo, + satpoint: SatPoint, + transferable_asset: &TransferableLog, ) -> crate::Result<(), Self::Error> { - insert_inscribe_transfer_inscription( - self.BRC20_INSCRIBE_TRANSFER, - inscription_id, - transfer_info, + insert_transferable_asset( + self.BRC20_SATPOINT_TO_TRANSFERABLE_ASSETS, + self.BRC20_ADDRESS_TICKER_TO_TRANSFERABLE_ASSETS, + satpoint, + transferable_asset, ) } - fn remove_inscribe_transfer_inscription( - &mut self, - inscription_id: &InscriptionId, - ) -> crate::Result<(), Self::Error> { - remove_inscribe_transfer_inscription(self.BRC20_INSCRIBE_TRANSFER, inscription_id) + fn remove_transferable_asset(&mut self, satpoint: SatPoint) -> crate::Result<(), Self::Error> { + remove_transferable_asset( + self.BRC20_SATPOINT_TO_TRANSFERABLE_ASSETS, + self.BRC20_ADDRESS_TICKER_TO_TRANSFERABLE_ASSETS, + satpoint, + ) } } diff --git a/src/okx/protocol/execute_manager.rs b/src/okx/protocol/execute_manager.rs index 70d708e343..a4c7aad11f 100644 --- a/src/okx/protocol/execute_manager.rs +++ b/src/okx/protocol/execute_manager.rs @@ -1,11 +1,17 @@ -use crate::okx::datastore::brc20::Brc20ReaderWriter; -use crate::okx::protocol::context::Context; +use super::*; +use crate::{ + okx::{ + datastore::{ + brc20::Brc20ReaderWriter, + ord::{collections::CollectionKind, OrdReaderWriter}, + }, + protocol::{brc20 as brc20_proto, context::Context}, + }, + Result, +}; use anyhow::anyhow; use bitcoin::Txid; -use { - super::*, - crate::{okx::protocol::brc20 as brc20_proto, Result}, -}; +use std::collections::HashSet; pub struct CallManager {} @@ -32,6 +38,16 @@ impl CallManager { .save_transaction_receipts(txid, &receipts) .map_err(|e| anyhow!("failed to add transaction receipt to state! error: {e}"))?; + let brc20_inscriptions = receipts + .into_iter() + .map(|receipt| receipt.inscription_id) + .collect::>(); + + for inscription_id in brc20_inscriptions { + context + .add_inscription_attributes(&inscription_id, CollectionKind::BRC20) + .map_err(|e| anyhow!("failed to add inscription attributes to state! error: {e}"))?; + } Ok(()) } } diff --git a/src/okx/protocol/ord/bitmap.rs b/src/okx/protocol/ord/bitmap.rs index a436cab273..73c6d6c8bf 100644 --- a/src/okx/protocol/ord/bitmap.rs +++ b/src/okx/protocol/ord/bitmap.rs @@ -46,7 +46,7 @@ pub fn index_bitmap( { let key = district.to_collection_key(); context.set_inscription_by_collection_key(&key, &inscription_id)?; - context.set_inscription_attributes(&inscription_id, &[CollectionKind::BitMap])?; + context.add_inscription_attributes(&inscription_id, CollectionKind::BitMap)?; count += 1; } diff --git a/src/okx/protocol/resolve_manager.rs b/src/okx/protocol/resolve_manager.rs index 2a9d338440..5705f34ea7 100644 --- a/src/okx/protocol/resolve_manager.rs +++ b/src/okx/protocol/resolve_manager.rs @@ -1,12 +1,19 @@ -use crate::inscriptions::ParsedEnvelope; -use crate::okx::protocol::context::Context; use { super::*, crate::{ - okx::{datastore::ord::operation::InscriptionOp, protocol::Message}, + index::entry::{Entry, SatPointValue}, + inscriptions::ParsedEnvelope, + okx::{ + datastore::{ + brc20::{redb::table::get_transferable_assets_by_outpoint, TransferableLog}, + ord::operation::InscriptionOp, + }, + protocol::{context::Context, Message}, + }, Inscription, Result, }, bitcoin::Transaction, + std::collections::HashMap, }; pub struct MsgResolveManager { @@ -53,11 +60,18 @@ impl MsgResolveManager { .map(|height| context.chain.blockheight >= height) .unwrap_or(false) { - if let Some(msg) = brc20::Message::resolve( - context.BRC20_INSCRIBE_TRANSFER, - &new_inscriptions, - operation, - )? { + let satpoint_to_transfer_assets: HashMap = + get_transferable_assets_by_outpoint( + context.BRC20_SATPOINT_TO_TRANSFERABLE_ASSETS, + input.previous_output, + )? + .into_iter() + .map(|(satpoint, asset)| (satpoint.store(), asset)) + .collect(); + + if let Some(msg) = + brc20::Message::resolve(operation, &new_inscriptions, satpoint_to_transfer_assets)? + { log::debug!( "BRC20 resolved the message from {:?}, msg {:?}", operation, diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index cf89dbc599..8705e410c5 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -54,6 +54,7 @@ mod error; mod info; mod ord; mod response; +mod sat; mod types; mod utils; @@ -233,8 +234,8 @@ impl Server { brc20::ApiErrorEvent, brc20::ApiTxEvents, brc20::ApiBlockEvents, - brc20::TransferableInscription, - brc20::TransferableInscriptions, + brc20::ApiTransferableAsset, + brc20::ApiTransferableAssets, // BRC20 responses schemas response::ApiBRC20Tick, @@ -326,10 +327,22 @@ impl Server { "/brc20/address/:address/transferable", get(brc20::brc20_all_transferable), ) + .route( + "/brc20/outpoint/:outpoint/transferable", + get(brc20::brc20_outpoint), + ) .route("/brc20/tx/:txid/events", get(brc20::brc20_tx_events)) .route( "/brc20/block/:block_hash/events", get(brc20::brc20_block_events), + ) + .route( + "/sat/outpoint/:outpoint/info", + get(sat::sat_range_by_outpoint), + ) + .route( + "/sat/outpoint/:outpoint/rarity", + get(sat::sat_range_with_rarity_by_outpoint), ); let api_router = Router::new().nest("/v1", api_v1_router); diff --git a/src/subcommand/server/brc20/mod.rs b/src/subcommand/server/brc20/mod.rs index 40d36fc687..8d5887ce48 100644 --- a/src/subcommand/server/brc20/mod.rs +++ b/src/subcommand/server/brc20/mod.rs @@ -1,10 +1,11 @@ use super::{types::ScriptPubkey, *}; mod balance; +mod outpoint; mod receipt; mod ticker; mod transferable; -pub(super) use {balance::*, receipt::*, ticker::*, transferable::*}; +pub(super) use {balance::*, outpoint::*, receipt::*, ticker::*, transferable::*}; #[derive(Debug, thiserror::Error)] pub(super) enum BRC20ApiError { @@ -15,6 +16,9 @@ pub(super) enum BRC20ApiError { /// Thrown when a transaction receipt was requested but not matching transaction receipt exists #[error("transaction receipt {0} not found")] TransactionReceiptNotFound(Txid), + /// Thrown when an internal error occurs + #[error("internal error: {0}")] + Internal(String), } impl From for ApiError { @@ -23,6 +27,7 @@ impl From for ApiError { BRC20ApiError::InvalidTicker(_) => Self::bad_request(error.to_string()), BRC20ApiError::UnknownTicker(_) => Self::not_found(error.to_string()), BRC20ApiError::TransactionReceiptNotFound(_) => Self::not_found(error.to_string()), + BRC20ApiError::Internal(_) => Self::internal(error.to_string()), } } } diff --git a/src/subcommand/server/brc20/outpoint.rs b/src/subcommand/server/brc20/outpoint.rs new file mode 100644 index 0000000000..d1fa2ceff6 --- /dev/null +++ b/src/subcommand/server/brc20/outpoint.rs @@ -0,0 +1,71 @@ +use {super::*, utoipa::ToSchema}; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)] +#[schema(as = ord::ApiOutPointResult)] +#[serde(rename_all = "camelCase")] +pub struct ApiOutPointResult { + #[schema(value_type = Option)] + pub result: Option>, + pub latest_blockhash: String, + #[schema(format = "uint64")] + pub latest_height: u32, +} + +// /brc20/outpoint/:outpoint/transferable +/// Retrieve the outpoint brc20 transferable assets with the specified outpoint. +#[utoipa::path( + get, + path = "/api/v1/brc20/outpoint/{outpoint}/transferable", + params( + ("outpoint" = String, Path, description = "Outpoint") +), + responses( + (status = 200, description = "Obtain outpoint infomation", body = OrdOutPointData), + (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_outpoint( + Extension(index): Extension>, + Path(outpoint): Path, +) -> ApiResult { + log::debug!("rpc: get brc20_outpoint: {outpoint}"); + + let rtx = index.begin_read()?; + + let (latest_height, latest_blockhash) = rtx.latest_block()?.ok_or_api_err(|| { + BRC20ApiError::Internal("Failed to retrieve the latest block from the database.".to_string()) + .into() + })?; + + let transferable_assets_with_satpoints = + rtx.brc20_transferable_assets_on_output_with_satpoints(outpoint)?; + + // If there are no inscriptions on the output, return None and parsed block states. + if transferable_assets_with_satpoints.is_empty() { + return Ok(Json(ApiResponse::ok(ApiOutPointResult { + result: None, + latest_height: latest_height.n(), + latest_blockhash: latest_blockhash.to_string(), + }))); + } + + Ok(Json(ApiResponse::ok(ApiOutPointResult { + result: Some( + transferable_assets_with_satpoints + .into_iter() + .map(|(satpoint, asset)| ApiTransferableAsset { + inscription_id: asset.inscription_id.to_string(), + inscription_number: asset.inscription_number, + amount: asset.amount.to_string(), + tick: asset.tick.as_str().to_string(), + owner: asset.owner.to_string(), + location: satpoint, + }) + .collect(), + ), + latest_height: latest_height.n(), + latest_blockhash: latest_blockhash.to_string(), + }))) +} diff --git a/src/subcommand/server/brc20/transferable.rs b/src/subcommand/server/brc20/transferable.rs index 34d31a808d..5740da83e4 100644 --- a/src/subcommand/server/brc20/transferable.rs +++ b/src/subcommand/server/brc20/transferable.rs @@ -1,14 +1,9 @@ -use { - super::*, - crate::okx::datastore::brc20::{self as brc20_store, Tick}, - axum::Json, - utoipa::ToSchema, -}; +use {super::*, crate::okx::datastore::brc20::Tick, axum::Json, utoipa::ToSchema}; -#[derive(Default, Debug, Clone, Serialize, Deserialize, ToSchema)] -#[schema(as = brc20::TransferableInscription)] +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)] +#[schema(as = brc20::TransferableAsset)] #[serde(rename_all = "camelCase")] -pub struct TransferableInscription { +pub struct ApiTransferableAsset { /// The inscription id. pub inscription_id: String, /// The inscription number. @@ -20,18 +15,8 @@ pub struct TransferableInscription { pub tick: String, /// The address to which the transfer will be made. pub owner: String, -} - -impl From 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(), - } - } + /// The inscription location. + pub location: SatPoint, } /// Get the transferable inscriptions of the address. @@ -54,7 +39,7 @@ impl From for TransferableInscription { pub(crate) async fn brc20_transferable( Extension(index): Extension>, Path((tick, address)): Path<(String, String)>, -) -> ApiResult { +) -> ApiResult { log::debug!("rpc: get brc20_transferable: {tick} {address}"); let rtx = index.begin_read()?; @@ -64,29 +49,40 @@ pub(crate) async fn brc20_transferable( let script_key = utils::parse_and_validate_script_key_network(&address, network) .map_err(ApiError::bad_request)?; - let transferable_brc20_assets = + let brc20_transferable_assets = Index::get_brc20_transferable_utxo_by_tick_and_address(ticker, script_key, &rtx)? .ok_or(BRC20ApiError::UnknownTicker(tick.clone()))?; log::debug!( "rpc: get brc20_transferable: {tick} {address} {:?}", - transferable_brc20_assets + brc20_transferable_assets ); - Ok(Json(ApiResponse::ok(TransferableInscriptions { - inscriptions: transferable_brc20_assets - .into_iter() - .map(|t| t.into()) - .collect(), + let mut api_transferable_assets = Vec::new(); + for (satpoint, transferable_asset) in brc20_transferable_assets { + api_transferable_assets.push(ApiTransferableAsset { + inscription_id: transferable_asset.inscription_id.to_string(), + inscription_number: transferable_asset.inscription_number, + amount: transferable_asset.amount.to_string(), + tick: transferable_asset.tick.as_str().to_string(), + owner: transferable_asset.owner.to_string(), + location: satpoint, + }); + } + + api_transferable_assets.sort_by(|a, b| a.inscription_number.cmp(&b.inscription_number)); + + Ok(Json(ApiResponse::ok(ApiTransferableAssets { + inscriptions: api_transferable_assets, }))) } #[derive(Default, Debug, Clone, Serialize, Deserialize, ToSchema)] -#[schema(as = brc20::TransferableInscriptions)] +#[schema(as = brc20::ApiTransferableAssets)] #[serde(rename_all = "camelCase")] -pub struct TransferableInscriptions { - #[schema(value_type = Vec)] - pub inscriptions: Vec, +pub struct ApiTransferableAssets { + #[schema(value_type = Vec)] + pub inscriptions: Vec, } /// Get the balance of ticker of the address. @@ -108,7 +104,7 @@ pub struct TransferableInscriptions { pub(crate) async fn brc20_all_transferable( Extension(index): Extension>, Path(account): Path, -) -> ApiResult { +) -> ApiResult { log::debug!("rpc: get brc20_all_transferable: {account}"); let rtx = index.begin_read()?; @@ -117,13 +113,27 @@ pub(crate) async fn brc20_all_transferable( let script_key = utils::parse_and_validate_script_key_network(&account, network) .map_err(ApiError::bad_request)?; - let transferable = rtx.brc20_get_all_transferable_by_address(script_key)?; + let brc20_transferable_assets = rtx.brc20_get_all_transferable_by_address(script_key)?; log::debug!( "rpc: get brc20_all_transferable: {account} {:?}", - transferable + brc20_transferable_assets ); - Ok(Json(ApiResponse::ok(TransferableInscriptions { - inscriptions: transferable.into_iter().map(|t| t.into()).collect(), + let mut api_transferable_assets = Vec::new(); + for (satpoint, transferable_asset) in brc20_transferable_assets { + api_transferable_assets.push(ApiTransferableAsset { + inscription_id: transferable_asset.inscription_id.to_string(), + inscription_number: transferable_asset.inscription_number, + amount: transferable_asset.amount.to_string(), + tick: transferable_asset.tick.as_str().to_string(), + owner: transferable_asset.owner.to_string(), + location: satpoint, + }); + } + + api_transferable_assets.sort_by(|a, b| a.inscription_number.cmp(&b.inscription_number)); + + Ok(Json(ApiResponse::ok(ApiTransferableAssets { + inscriptions: api_transferable_assets, }))) } diff --git a/src/subcommand/server/response.rs b/src/subcommand/server/response.rs index bf551b9c43..957e8e4d51 100644 --- a/src/subcommand/server/response.rs +++ b/src/subcommand/server/response.rs @@ -10,7 +10,7 @@ use { ApiBRC20AllBalance = ApiResponse, ApiBRC20TxEvents = ApiResponse, ApiBRC20BlockEvents = ApiResponse, - ApiBRC20Transferable = ApiResponse, + ApiBRC20Transferable = ApiResponse, ApiOrdInscription = ApiResponse, ApiOrdOutPointData = ApiResponse, diff --git a/src/subcommand/server/sat.rs b/src/subcommand/server/sat.rs new file mode 100644 index 0000000000..adf218aa1b --- /dev/null +++ b/src/subcommand/server/sat.rs @@ -0,0 +1,203 @@ +use { + super::{error::ApiError, *}, + axum::Json, + utoipa::ToSchema, +}; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct ApiOutPointResult { + pub result: Option, + pub latest_blockhash: String, + #[schema(format = "uint64")] + pub latest_height: u32, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct ApiSatRanges { + /// The transaction id. + pub outpoint: OutPoint, + /// The script pubkey. + pub sat_ranges: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +#[serde(untagged)] +pub enum ApiSatRange { + Sketchy((u64, u64)), + #[serde(rename_all = "camelCase")] + ExactWithRarity { + first: u64, + last: u64, + rarity_sats: Vec, + }, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct RaritySat { + pub sat: Sat, + pub offset: u64, + pub rarity: Rarity, +} + +// /sat/outpoint/:outpoint/info +/// Retrieve the sat range of the outpoint. +#[utoipa::path( + get, + path = "/api/v1/sat/outpoint/{outpoint}/info", + params( + ("outpoint" = String, Path, description = "Outpoint") + ), + responses( + (status = 200, description = "Obtain outpoint infomation", body = OrdOutPointData), + (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 sat_range_by_outpoint( + Extension(index): Extension>, + Path(outpoint): Path, +) -> ApiResult { + log::debug!("rpc: get sat_outpoint_sat_range: {outpoint}"); + + let rtx = index.begin_read()?; + + let (latest_height, latest_blockhash) = rtx.latest_block()?.ok_or_api_err(|| { + ApiError::internal("Failed to retrieve the latest block from the database.".to_string()) + })?; + + let sat_ranges = Index::list_sat_range(&rtx, outpoint, index.has_sat_index())?; + + Ok(Json(ApiResponse::ok(ApiOutPointResult { + result: sat_ranges.map(|ranges| ApiSatRanges { + outpoint, + sat_ranges: ranges.into_iter().map(ApiSatRange::Sketchy).collect(), + }), + latest_height: latest_height.n(), + latest_blockhash: latest_blockhash.to_string(), + }))) +} + +pub(crate) async fn sat_range_with_rarity_by_outpoint( + Extension(index): Extension>, + Path(outpoint): Path, +) -> ApiResult { + log::debug!("rpc: get sat_outpoint_sat_range: {outpoint}"); + + let rtx = index.begin_read()?; + + let (latest_height, latest_blockhash) = rtx.latest_block()?.ok_or_api_err(|| { + ApiError::internal("Failed to retrieve the latest block from the database.".to_string()) + })?; + + let Some(sat_ranges) = Index::list_sat_range(&rtx, outpoint, index.has_sat_index())? else { + return Ok(Json(ApiResponse::ok(ApiOutPointResult { + result: None, + latest_height: latest_height.n(), + latest_blockhash: latest_blockhash.to_string(), + }))); + }; + + let mut exact_sat_ranges = Vec::new(); + let mut value = 0; + for sat_range in sat_ranges { + let rarity_sats = Index::calculate_rarity_for_sat_range(sat_range) + .into_iter() + .map(|(sat, rarity)| RaritySat { + sat, + offset: sat.0 - sat_range.0 + value, + rarity, + }) + .collect(); + exact_sat_ranges.push(ApiSatRange::ExactWithRarity { + first: sat_range.0, + last: sat_range.1, + rarity_sats, + }); + value += sat_range.1 - sat_range.0; + } + + Ok(Json(ApiResponse::ok(ApiOutPointResult { + result: Some(ApiSatRanges { + outpoint, + sat_ranges: exact_sat_ranges, + }), + latest_height: latest_height.n(), + latest_blockhash: latest_blockhash.to_string(), + }))) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_outpoint_sat_range_json_serialization() { + let outpoint = unbound_outpoint(); + let sat_ranges = vec![(0, 100), (100, 200)]; + let api_outpoint_sat_ranges = ApiSatRanges { + outpoint, + sat_ranges: sat_ranges.into_iter().map(ApiSatRange::Sketchy).collect(), + }; + let json = serde_json::to_string(&api_outpoint_sat_ranges).unwrap(); + assert_eq!( + json, + r#"{"outpoint":"0000000000000000000000000000000000000000000000000000000000000000:0","satRanges":[[0,100],[100,200]]}"# + ); + } + + #[test] + fn test_outpoint_sat_range_with_rarity_json_serialization() { + let outpoint = unbound_outpoint(); + let rarity_sats = vec![ + RaritySat { + sat: Sat(0), + offset: 0, + rarity: Rarity::Uncommon, + }, + RaritySat { + sat: Sat(1), + offset: 1, + rarity: Rarity::Epic, + }, + ]; + let api_outpoint_sat_ranges = ApiSatRanges { + outpoint, + sat_ranges: vec![ApiSatRange::ExactWithRarity { + first: 0, + last: 100, + rarity_sats, + }], + }; + let json = serde_json::to_string_pretty(&api_outpoint_sat_ranges).unwrap(); + assert_eq!( + json, + r##"{ + "outpoint": "0000000000000000000000000000000000000000000000000000000000000000:0", + "satRanges": [ + { + "first": 0, + "last": 100, + "raritySats": [ + { + "sat": 0, + "offset": 0, + "rarity": "uncommon" + }, + { + "sat": 1, + "offset": 1, + "rarity": "epic" + } + ] + } + ] +}"## + ); + } +}