mirror of
https://github.com/alexgo-io/bitcoin-indexer.git
synced 2026-01-12 16:52:57 +08:00
feat: add BRC-20 indexing (#284)
* feat: first version of brc20 parser * feat: verifier * fix: parse with tests * test: extra parser stuff * feat: verifier with tests, basic wiring * test: transfer sends * fix: add deploy op to ledger * chore: upgrade to latest chainhook * feat: start adapting for scan * feat: wire up scan * feat: self minted tokens * feat: self mint and drop data * fix: transaction commit and rollback * chore: update dependencies * chore: update rust * chore: cascade chainhook changes in ordhook * fix: ordinal-sdk-js build * chore: update deps * fix: centralize db init * fix: check brc20 activation height * chore: downgrade brc-20 warns to debug * feat: cache tokens and minted supplies per block * feat: memory cache * feat: cache module * feat: use LRU cache for brc20 * feat: configurable lru size * fix: blacklist invalid transfers * chore: block cache struct * chore: config meta protocols * fix: use cache across all blocks * chore: formatting * test: cache and utils * fix: address balance cache miss * fix: flush db caches on miss * fix: check db cache length before flushing * chore: tweak log msgs * test: transfer full balance * fix: cache miss on avail balance deltas * fix: save tx_index in ledger table * test: null bytes * fix: remove unique index ledger --------- Co-authored-by: Ludo Galabru <ludo@hiro.so> Co-authored-by: Ludo Galabru <ludovic@galabru.com>
This commit is contained in:
1996
Cargo.lock
generated
1996
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,9 @@ use crate::config::generator::generate_config;
|
||||
use clap::{Parser, Subcommand};
|
||||
use hiro_system_kit;
|
||||
use ordhook::chainhook_sdk::bitcoincore_rpc::{Auth, Client, RpcApi};
|
||||
use ordhook::chainhook_sdk::chainhooks::types::{BitcoinChainhookSpecification, HttpHook};
|
||||
use ordhook::chainhook_sdk::chainhooks::types::{
|
||||
BitcoinChainhookSpecification, HttpHook, InscriptionFeedData,
|
||||
};
|
||||
use ordhook::chainhook_sdk::chainhooks::types::{
|
||||
BitcoinPredicateType, ChainhookFullSpecification, HookAction, OrdinalOperations,
|
||||
};
|
||||
@@ -22,15 +24,14 @@ use ordhook::core::pipeline::processors::start_inscription_indexing_processor;
|
||||
use ordhook::core::protocol::inscription_parsing::parse_inscriptions_and_standardize_block;
|
||||
use ordhook::core::protocol::satoshi_numbering::compute_satoshi_number;
|
||||
use ordhook::db::{
|
||||
delete_data_in_ordhook_db, find_all_inscription_transfers, find_all_inscriptions_in_block,
|
||||
find_all_transfers_in_block, find_block_bytes_at_block_height, find_inscription_with_id,
|
||||
find_last_block_inserted, find_latest_inscription_block_height, find_missing_blocks,
|
||||
get_default_ordhook_db_file_path, initialize_ordhook_db, open_ordhook_db_conn_rocks_db_loop,
|
||||
open_readonly_ordhook_db_conn, open_readonly_ordhook_db_conn_rocks_db,
|
||||
open_readwrite_ordhook_db_conn, BlockBytesCursor,
|
||||
delete_data_in_ordhook_db, find_all_inscriptions_in_block, find_all_transfers_in_block,
|
||||
find_block_bytes_at_block_height, find_inscription_with_id, find_last_block_inserted,
|
||||
find_latest_inscription_block_height, find_missing_blocks, get_default_ordhook_db_file_path,
|
||||
open_ordhook_db_conn_rocks_db_loop, open_readonly_ordhook_db_conn,
|
||||
open_readonly_ordhook_db_conn_rocks_db, BlockBytesCursor,
|
||||
};
|
||||
use ordhook::download::download_ordinals_dataset_if_required;
|
||||
use ordhook::hex;
|
||||
use ordhook::{hex, initialize_db};
|
||||
use ordhook::scan::bitcoin::scan_bitcoin_chainstate_via_rpc_using_predicate;
|
||||
use ordhook::service::observers::initialize_observers_db;
|
||||
use ordhook::service::{start_observer_forwarding, Service};
|
||||
@@ -258,7 +259,7 @@ impl RepairStorageCommand {
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
blocks.into()
|
||||
blocks.unwrap().into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -540,7 +541,10 @@ async fn handle_command(opts: Opts, ctx: &Context) -> Result<(), String> {
|
||||
// If post-to:
|
||||
// - Replay that requires connection to bitcoind
|
||||
let block_heights = parse_blocks_heights_spec(&cmd.blocks_interval, &cmd.blocks);
|
||||
let mut block_range = block_heights.get_sorted_entries();
|
||||
let mut block_range = block_heights
|
||||
.get_sorted_entries()
|
||||
.map_err(|_e| format!("Block start / end block spec invalid"))?;
|
||||
|
||||
if let Some(ref post_to) = cmd.post_to {
|
||||
info!(ctx.expect_logger(), "A fully synchronized bitcoind node is required for retrieving inscriptions content.");
|
||||
info!(
|
||||
@@ -579,8 +583,8 @@ async fn handle_command(opts: Opts, ctx: &Context) -> Result<(), String> {
|
||||
let mut total_inscriptions = 0;
|
||||
let mut total_transfers = 0;
|
||||
|
||||
let inscriptions_db_conn =
|
||||
initialize_ordhook_db(&config.expected_cache_path(), ctx);
|
||||
let db_connections = initialize_db(&config, ctx);
|
||||
let inscriptions_db_conn = db_connections.ordhook;
|
||||
while let Some(block_height) = block_range.pop_front() {
|
||||
let inscriptions =
|
||||
find_all_inscriptions_in_block(&block_height, &inscriptions_db_conn, ctx);
|
||||
@@ -654,18 +658,6 @@ async fn handle_command(opts: Opts, ctx: &Context) -> Result<(), String> {
|
||||
inscription.inscription_number.jubilee,
|
||||
inscription.ordinal_number
|
||||
);
|
||||
let transfers = find_all_inscription_transfers(
|
||||
&inscription.get_inscription_id(),
|
||||
&inscriptions_db_conn,
|
||||
ctx,
|
||||
);
|
||||
for (transfer, block_height) in transfers.iter().skip(1) {
|
||||
println!(
|
||||
"\t→ Transferred in transaction {} (block #{block_height})",
|
||||
transfer.transaction_identifier_location.hash
|
||||
);
|
||||
}
|
||||
println!("Number of transfers: {}", transfers.len() - 1);
|
||||
}
|
||||
Command::Scan(ScanCommand::Transaction(cmd)) => {
|
||||
let config: Config =
|
||||
@@ -710,7 +702,7 @@ async fn handle_command(opts: Opts, ctx: &Context) -> Result<(), String> {
|
||||
let config =
|
||||
ConfigFile::default(cmd.regtest, cmd.testnet, cmd.mainnet, &cmd.config_path)?;
|
||||
|
||||
let _ = initialize_ordhook_db(&config.expected_cache_path(), ctx);
|
||||
initialize_db(&config, ctx);
|
||||
|
||||
let inscriptions_db_conn =
|
||||
open_readonly_ordhook_db_conn(&config.expected_cache_path(), ctx)?;
|
||||
@@ -790,7 +782,7 @@ async fn handle_command(opts: Opts, ctx: &Context) -> Result<(), String> {
|
||||
},
|
||||
Command::Db(OrdhookDbCommand::New(cmd)) => {
|
||||
let config = ConfigFile::default(false, false, false, &cmd.config_path)?;
|
||||
initialize_ordhook_db(&config.expected_cache_path(), ctx);
|
||||
initialize_db(&config, ctx);
|
||||
open_ordhook_db_conn_rocks_db_loop(
|
||||
true,
|
||||
&config.expected_cache_path(),
|
||||
@@ -801,7 +793,7 @@ async fn handle_command(opts: Opts, ctx: &Context) -> Result<(), String> {
|
||||
}
|
||||
Command::Db(OrdhookDbCommand::Sync(cmd)) => {
|
||||
let config = ConfigFile::default(false, false, false, &cmd.config_path)?;
|
||||
initialize_ordhook_db(&config.expected_cache_path(), ctx);
|
||||
initialize_db(&config, ctx);
|
||||
let service = Service::new(config, ctx.clone());
|
||||
service.update_state(None).await?;
|
||||
}
|
||||
@@ -914,15 +906,6 @@ async fn handle_command(opts: Opts, ctx: &Context) -> Result<(), String> {
|
||||
}
|
||||
Command::Db(OrdhookDbCommand::Drop(cmd)) => {
|
||||
let config = ConfigFile::default(false, false, false, &cmd.config_path)?;
|
||||
let blocks_db = open_ordhook_db_conn_rocks_db_loop(
|
||||
true,
|
||||
&config.expected_cache_path(),
|
||||
config.resources.ulimit,
|
||||
config.resources.memory_available,
|
||||
ctx,
|
||||
);
|
||||
let inscriptions_db_conn_rw =
|
||||
open_readwrite_ordhook_db_conn(&config.expected_cache_path(), ctx)?;
|
||||
|
||||
println!(
|
||||
"{} blocks will be deleted. Confirm? [Y/n]",
|
||||
@@ -934,13 +917,7 @@ async fn handle_command(opts: Opts, ctx: &Context) -> Result<(), String> {
|
||||
return Err("Deletion aborted".to_string());
|
||||
}
|
||||
|
||||
delete_data_in_ordhook_db(
|
||||
cmd.start_block,
|
||||
cmd.end_block,
|
||||
&blocks_db,
|
||||
&inscriptions_db_conn_rw,
|
||||
ctx,
|
||||
)?;
|
||||
delete_data_in_ordhook_db(cmd.start_block, cmd.end_block, &config, ctx)?;
|
||||
info!(
|
||||
ctx.expect_logger(),
|
||||
"Cleaning ordhook_db: {} blocks dropped",
|
||||
@@ -1011,7 +988,11 @@ pub fn build_predicate_from_cli(
|
||||
include_witness: false,
|
||||
expired_at: None,
|
||||
enabled: true,
|
||||
predicate: BitcoinPredicateType::OrdinalsProtocol(OrdinalOperations::InscriptionFeed),
|
||||
predicate: BitcoinPredicateType::OrdinalsProtocol(OrdinalOperations::InscriptionFeed(
|
||||
InscriptionFeedData {
|
||||
meta_protocols: None,
|
||||
},
|
||||
)),
|
||||
action: HookAction::HttpPost(HttpHook {
|
||||
url: post_to.to_string(),
|
||||
authorization_header: format!("Bearer {}", auth_token.unwrap_or("".to_string())),
|
||||
|
||||
@@ -4,9 +4,9 @@ use ordhook::chainhook_sdk::types::{
|
||||
BitcoinBlockSignaling, BitcoinNetwork, StacksNetwork, StacksNodeConfig,
|
||||
};
|
||||
use ordhook::config::{
|
||||
Config, LogConfig, PredicatesApi, PredicatesApiConfig, ResourcesConfig, SnapshotConfig,
|
||||
StorageConfig, DEFAULT_BITCOIND_RPC_THREADS, DEFAULT_BITCOIND_RPC_TIMEOUT,
|
||||
DEFAULT_CONTROL_PORT, DEFAULT_MEMORY_AVAILABLE, DEFAULT_ULIMIT,
|
||||
Config, LogConfig, MetaProtocolsConfig, PredicatesApi, PredicatesApiConfig, ResourcesConfig,
|
||||
SnapshotConfig, StorageConfig, DEFAULT_BITCOIND_RPC_THREADS, DEFAULT_BITCOIND_RPC_TIMEOUT,
|
||||
DEFAULT_BRC20_LRU_CACHE_SIZE, DEFAULT_CONTROL_PORT, DEFAULT_MEMORY_AVAILABLE, DEFAULT_ULIMIT,
|
||||
};
|
||||
use std::fs::File;
|
||||
use std::io::{BufReader, Read};
|
||||
@@ -19,6 +19,7 @@ pub struct ConfigFile {
|
||||
pub network: NetworkConfigFile,
|
||||
pub logs: Option<LogConfigFile>,
|
||||
pub snapshot: Option<SnapshotConfigFile>,
|
||||
pub meta_protocols: Option<MetaProtocolsConfigFile>,
|
||||
}
|
||||
|
||||
impl ConfigFile {
|
||||
@@ -94,6 +95,10 @@ impl ConfigFile {
|
||||
.resources
|
||||
.expected_observers_count
|
||||
.unwrap_or(1),
|
||||
brc20_lru_cache_size: config_file
|
||||
.resources
|
||||
.brc20_lru_cache_size
|
||||
.unwrap_or(DEFAULT_BRC20_LRU_CACHE_SIZE),
|
||||
},
|
||||
network: IndexerConfig {
|
||||
bitcoind_rpc_url: config_file.network.bitcoind_rpc_url.to_string(),
|
||||
@@ -123,6 +128,13 @@ impl ConfigFile {
|
||||
.and_then(|l| l.chainhook_internals)
|
||||
.unwrap_or(true),
|
||||
},
|
||||
meta_protocols: MetaProtocolsConfig {
|
||||
brc20: config_file
|
||||
.meta_protocols
|
||||
.as_ref()
|
||||
.and_then(|l| l.brc20)
|
||||
.unwrap_or(false),
|
||||
},
|
||||
};
|
||||
Ok(config)
|
||||
}
|
||||
@@ -168,6 +180,11 @@ pub struct SnapshotConfigFile {
|
||||
pub download_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct MetaProtocolsConfigFile {
|
||||
pub brc20: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct ResourcesConfigFile {
|
||||
pub ulimit: Option<usize>,
|
||||
@@ -176,6 +193,7 @@ pub struct ResourcesConfigFile {
|
||||
pub bitcoind_rpc_threads: Option<usize>,
|
||||
pub bitcoind_rpc_timeout: Option<u32>,
|
||||
pub expected_observers_count: Option<usize>,
|
||||
pub brc20_lru_cache_size: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
|
||||
@@ -10,8 +10,9 @@ serde_json = "1"
|
||||
serde_derive = "1"
|
||||
hex = "0.4.3"
|
||||
rand = "0.8.5"
|
||||
chainhook-sdk = { version = "=0.12.5", features = ["zeromq"] }
|
||||
# chainhook-sdk = { version = "=0.12.1", path = "../../../chainhook/components/chainhook-sdk", features = ["zeromq"] }
|
||||
lru = "0.12.3"
|
||||
chainhook-sdk = { version = "=0.12.8", features = ["zeromq"] }
|
||||
# chainhook-sdk = { version = "=0.12.5", path = "../../../chainhook/components/chainhook-sdk", features = ["zeromq"] }
|
||||
hiro-system-kit = "0.3.1"
|
||||
reqwest = { version = "0.11", default-features = false, features = [
|
||||
"stream",
|
||||
@@ -29,10 +30,10 @@ crossbeam-channel = "0.5.8"
|
||||
uuid = { version = "1.3.0", features = ["v4", "fast-rng"] }
|
||||
threadpool = "1.8.1"
|
||||
rocket_okapi = "0.8.0-rc.3"
|
||||
rocket = { version = "=0.5.0-rc.3", features = ["json"] }
|
||||
rocket = { version = "0.5.0", features = ["json"] }
|
||||
dashmap = "5.4.0"
|
||||
fxhash = "0.2.1"
|
||||
rusqlite = { version = "0.27.0", features = ["bundled"] }
|
||||
rusqlite = { version = "0.28.0", features = ["bundled"] }
|
||||
anyhow = { version = "1.0.56", features = ["backtrace"] }
|
||||
schemars = { version = "0.8.10", git = "https://github.com/hirosystems/schemars.git", branch = "feat-chainhook-fixes" }
|
||||
progressing = '3'
|
||||
@@ -44,6 +45,10 @@ pprof = { version = "0.13.0", features = ["flamegraph"], optional = true }
|
||||
hyper = { version = "=0.14.27" }
|
||||
lazy_static = { version = "1.4.0" }
|
||||
ciborium = "0.2.1"
|
||||
regex = "1.10.3"
|
||||
|
||||
[dev-dependencies]
|
||||
test-case = "3.1.0"
|
||||
|
||||
# [profile.release]
|
||||
# debug = true
|
||||
|
||||
@@ -15,6 +15,7 @@ pub const DEFAULT_ULIMIT: usize = 2048;
|
||||
pub const DEFAULT_MEMORY_AVAILABLE: usize = 8;
|
||||
pub const DEFAULT_BITCOIND_RPC_THREADS: usize = 4;
|
||||
pub const DEFAULT_BITCOIND_RPC_TIMEOUT: u32 = 15;
|
||||
pub const DEFAULT_BRC20_LRU_CACHE_SIZE: usize = 50_000;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Config {
|
||||
@@ -23,9 +24,15 @@ pub struct Config {
|
||||
pub resources: ResourcesConfig,
|
||||
pub network: IndexerConfig,
|
||||
pub snapshot: SnapshotConfig,
|
||||
pub meta_protocols: MetaProtocolsConfig,
|
||||
pub logs: LogConfig,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MetaProtocolsConfig {
|
||||
pub brc20: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct LogConfig {
|
||||
pub ordinals_internals: bool,
|
||||
@@ -73,6 +80,7 @@ pub struct ResourcesConfig {
|
||||
pub bitcoind_rpc_threads: usize,
|
||||
pub bitcoind_rpc_timeout: u32,
|
||||
pub expected_observers_count: usize,
|
||||
pub brc20_lru_cache_size: usize,
|
||||
}
|
||||
|
||||
impl ResourcesConfig {
|
||||
@@ -103,6 +111,7 @@ impl Config {
|
||||
BitcoinNetwork::Signet => 112402,
|
||||
},
|
||||
logs: self.logs.clone(),
|
||||
meta_protocols: self.meta_protocols.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +128,7 @@ impl Config {
|
||||
cache_path: self.storage.working_dir.clone(),
|
||||
bitcoin_network: self.network.bitcoin_network.clone(),
|
||||
stacks_network: self.network.stacks_network.clone(),
|
||||
prometheus_monitoring_port: None,
|
||||
data_handler_tx: None,
|
||||
}
|
||||
}
|
||||
@@ -172,6 +182,7 @@ impl Config {
|
||||
bitcoind_rpc_threads: DEFAULT_BITCOIND_RPC_THREADS,
|
||||
bitcoind_rpc_timeout: DEFAULT_BITCOIND_RPC_TIMEOUT,
|
||||
expected_observers_count: 1,
|
||||
brc20_lru_cache_size: DEFAULT_BRC20_LRU_CACHE_SIZE,
|
||||
},
|
||||
network: IndexerConfig {
|
||||
bitcoind_rpc_url: "http://0.0.0.0:18443".into(),
|
||||
@@ -187,6 +198,7 @@ impl Config {
|
||||
ordinals_internals: true,
|
||||
chainhook_internals: false,
|
||||
},
|
||||
meta_protocols: MetaProtocolsConfig { brc20: false },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,6 +216,7 @@ impl Config {
|
||||
bitcoind_rpc_threads: DEFAULT_BITCOIND_RPC_THREADS,
|
||||
bitcoind_rpc_timeout: DEFAULT_BITCOIND_RPC_TIMEOUT,
|
||||
expected_observers_count: 1,
|
||||
brc20_lru_cache_size: DEFAULT_BRC20_LRU_CACHE_SIZE,
|
||||
},
|
||||
network: IndexerConfig {
|
||||
bitcoind_rpc_url: "http://0.0.0.0:18332".into(),
|
||||
@@ -219,6 +232,7 @@ impl Config {
|
||||
ordinals_internals: true,
|
||||
chainhook_internals: false,
|
||||
},
|
||||
meta_protocols: MetaProtocolsConfig { brc20: false },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,6 +250,7 @@ impl Config {
|
||||
bitcoind_rpc_threads: DEFAULT_BITCOIND_RPC_THREADS,
|
||||
bitcoind_rpc_timeout: DEFAULT_BITCOIND_RPC_TIMEOUT,
|
||||
expected_observers_count: 1,
|
||||
brc20_lru_cache_size: DEFAULT_BRC20_LRU_CACHE_SIZE,
|
||||
},
|
||||
network: IndexerConfig {
|
||||
bitcoind_rpc_url: "http://0.0.0.0:8332".into(),
|
||||
@@ -251,6 +266,7 @@ impl Config {
|
||||
ordinals_internals: true,
|
||||
chainhook_internals: false,
|
||||
},
|
||||
meta_protocols: MetaProtocolsConfig { brc20: false },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
546
components/ordhook-core/src/core/meta_protocols/brc20/cache.rs
Normal file
546
components/ordhook-core/src/core/meta_protocols/brc20/cache.rs
Normal file
@@ -0,0 +1,546 @@
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use chainhook_sdk::{
|
||||
types::{BlockIdentifier, OrdinalInscriptionRevealData, OrdinalInscriptionTransferData},
|
||||
utils::Context,
|
||||
};
|
||||
use lru::LruCache;
|
||||
use rusqlite::{Connection, Transaction};
|
||||
|
||||
use crate::core::meta_protocols::brc20::db::get_unsent_token_transfer;
|
||||
|
||||
use super::{
|
||||
db::{
|
||||
get_token, get_token_available_balance_for_address, get_token_minted_supply,
|
||||
insert_ledger_rows, insert_token_rows, Brc20DbLedgerRow, Brc20DbTokenRow,
|
||||
},
|
||||
verifier::{VerifiedBrc20BalanceData, VerifiedBrc20TokenDeployData, VerifiedBrc20TransferData},
|
||||
};
|
||||
|
||||
/// Keeps BRC20 DB rows before they're inserted into SQLite. Use `flush` to insert.
|
||||
pub struct Brc20DbCache {
|
||||
ledger_rows: Vec<Brc20DbLedgerRow>,
|
||||
token_rows: Vec<Brc20DbTokenRow>,
|
||||
}
|
||||
|
||||
impl Brc20DbCache {
|
||||
fn new() -> Self {
|
||||
Brc20DbCache {
|
||||
ledger_rows: Vec::new(),
|
||||
token_rows: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn flush(&mut self, db_tx: &Transaction, ctx: &Context) {
|
||||
if self.token_rows.len() > 0 {
|
||||
insert_token_rows(&self.token_rows, db_tx, ctx);
|
||||
self.token_rows.clear();
|
||||
}
|
||||
if self.ledger_rows.len() > 0 {
|
||||
insert_ledger_rows(&self.ledger_rows, db_tx, ctx);
|
||||
self.ledger_rows.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// In-memory cache that keeps verified token data to avoid excessive reads to the database.
|
||||
pub struct Brc20MemoryCache {
|
||||
tokens: LruCache<String, Brc20DbTokenRow>,
|
||||
token_minted_supplies: LruCache<String, f64>,
|
||||
token_addr_avail_balances: LruCache<String, f64>, // key format: "tick:address"
|
||||
unsent_transfers: LruCache<u64, Brc20DbLedgerRow>,
|
||||
ignored_inscriptions: LruCache<u64, bool>,
|
||||
pub db_cache: Brc20DbCache,
|
||||
}
|
||||
|
||||
impl Brc20MemoryCache {
|
||||
pub fn new(lru_size: usize) -> Self {
|
||||
Brc20MemoryCache {
|
||||
tokens: LruCache::new(NonZeroUsize::new(lru_size).unwrap()),
|
||||
token_minted_supplies: LruCache::new(NonZeroUsize::new(lru_size).unwrap()),
|
||||
token_addr_avail_balances: LruCache::new(NonZeroUsize::new(lru_size).unwrap()),
|
||||
unsent_transfers: LruCache::new(NonZeroUsize::new(lru_size).unwrap()),
|
||||
ignored_inscriptions: LruCache::new(NonZeroUsize::new(lru_size).unwrap()),
|
||||
db_cache: Brc20DbCache::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_token(
|
||||
&mut self,
|
||||
tick: &str,
|
||||
db_tx: &Transaction,
|
||||
ctx: &Context,
|
||||
) -> Option<Brc20DbTokenRow> {
|
||||
if let Some(token) = self.tokens.get(&tick.to_string()) {
|
||||
return Some(token.clone());
|
||||
}
|
||||
self.handle_cache_miss(db_tx, ctx);
|
||||
match get_token(tick, db_tx, ctx) {
|
||||
Some(db_token) => {
|
||||
self.tokens.put(tick.to_string(), db_token.clone());
|
||||
return Some(db_token);
|
||||
}
|
||||
None => return None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_token_minted_supply(
|
||||
&mut self,
|
||||
tick: &str,
|
||||
db_tx: &Transaction,
|
||||
ctx: &Context,
|
||||
) -> Option<f64> {
|
||||
if let Some(minted) = self.token_minted_supplies.get(&tick.to_string()) {
|
||||
return Some(minted.clone());
|
||||
}
|
||||
self.handle_cache_miss(db_tx, ctx);
|
||||
if let Some(minted_supply) = get_token_minted_supply(tick, db_tx, ctx) {
|
||||
self.token_minted_supplies
|
||||
.put(tick.to_string(), minted_supply);
|
||||
return Some(minted_supply);
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
pub fn get_token_address_avail_balance(
|
||||
&mut self,
|
||||
tick: &str,
|
||||
address: &str,
|
||||
db_tx: &Transaction,
|
||||
ctx: &Context,
|
||||
) -> Option<f64> {
|
||||
let key = format!("{}:{}", tick, address);
|
||||
if let Some(balance) = self.token_addr_avail_balances.get(&key) {
|
||||
return Some(balance.clone());
|
||||
}
|
||||
self.handle_cache_miss(db_tx, ctx);
|
||||
if let Some(balance) = get_token_available_balance_for_address(tick, address, db_tx, ctx) {
|
||||
self.token_addr_avail_balances.put(key, balance);
|
||||
return Some(balance);
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
pub fn get_unsent_token_transfer(
|
||||
&mut self,
|
||||
ordinal_number: u64,
|
||||
db_tx: &Transaction,
|
||||
ctx: &Context,
|
||||
) -> Option<Brc20DbLedgerRow> {
|
||||
// Use `get` instead of `contains` so we promote this value in the LRU.
|
||||
if let Some(_) = self.ignored_inscriptions.get(&ordinal_number) {
|
||||
return None;
|
||||
}
|
||||
if let Some(row) = self.unsent_transfers.get(&ordinal_number) {
|
||||
return Some(row.clone());
|
||||
}
|
||||
self.handle_cache_miss(db_tx, ctx);
|
||||
match get_unsent_token_transfer(ordinal_number, db_tx, ctx) {
|
||||
Some(row) => {
|
||||
self.unsent_transfers.put(ordinal_number, row.clone());
|
||||
return Some(row);
|
||||
}
|
||||
None => {
|
||||
// Inscription is not relevant for BRC20.
|
||||
self.ignore_inscription(ordinal_number);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Marks an ordinal number as ignored so we don't bother computing its transfers for BRC20 purposes.
|
||||
pub fn ignore_inscription(&mut self, ordinal_number: u64) {
|
||||
self.ignored_inscriptions.put(ordinal_number, true);
|
||||
}
|
||||
|
||||
pub fn insert_token_deploy(
|
||||
&mut self,
|
||||
data: &VerifiedBrc20TokenDeployData,
|
||||
reveal: &OrdinalInscriptionRevealData,
|
||||
block_identifier: &BlockIdentifier,
|
||||
tx_index: u64,
|
||||
_db_tx: &Connection,
|
||||
_ctx: &Context,
|
||||
) {
|
||||
let token = Brc20DbTokenRow {
|
||||
inscription_id: reveal.inscription_id.clone(),
|
||||
inscription_number: reveal.inscription_number.jubilee as u64,
|
||||
block_height: block_identifier.index,
|
||||
tick: data.tick.clone(),
|
||||
max: data.max,
|
||||
lim: data.lim,
|
||||
dec: data.dec,
|
||||
address: data.address.clone(),
|
||||
self_mint: data.self_mint,
|
||||
};
|
||||
self.tokens.put(token.tick.clone(), token.clone());
|
||||
self.token_minted_supplies.put(token.tick.clone(), 0.0);
|
||||
self.token_addr_avail_balances
|
||||
.put(format!("{}:{}", token.tick, data.address), 0.0);
|
||||
self.db_cache.token_rows.push(token);
|
||||
self.db_cache.ledger_rows.push(Brc20DbLedgerRow {
|
||||
inscription_id: reveal.inscription_id.clone(),
|
||||
inscription_number: reveal.inscription_number.jubilee as u64,
|
||||
ordinal_number: reveal.ordinal_number,
|
||||
block_height: block_identifier.index,
|
||||
tx_index,
|
||||
tick: data.tick.clone(),
|
||||
address: data.address.clone(),
|
||||
avail_balance: 0.0,
|
||||
trans_balance: 0.0,
|
||||
operation: "deploy".to_string(),
|
||||
});
|
||||
self.ignore_inscription(reveal.ordinal_number);
|
||||
}
|
||||
|
||||
pub fn insert_token_mint(
|
||||
&mut self,
|
||||
data: &VerifiedBrc20BalanceData,
|
||||
reveal: &OrdinalInscriptionRevealData,
|
||||
block_identifier: &BlockIdentifier,
|
||||
tx_index: u64,
|
||||
db_tx: &Transaction,
|
||||
ctx: &Context,
|
||||
) {
|
||||
let Some(minted) = self.get_token_minted_supply(&data.tick, db_tx, ctx) else {
|
||||
unreachable!("BRC-20 deployed token should have a minted supply entry");
|
||||
};
|
||||
self.token_minted_supplies
|
||||
.put(data.tick.clone(), minted + data.amt);
|
||||
let balance = self
|
||||
.get_token_address_avail_balance(&data.tick, &data.address, db_tx, ctx)
|
||||
.unwrap_or(0.0);
|
||||
self.token_addr_avail_balances.put(
|
||||
format!("{}:{}", data.tick, data.address),
|
||||
balance + data.amt, // Increase for minter.
|
||||
);
|
||||
self.db_cache.ledger_rows.push(Brc20DbLedgerRow {
|
||||
inscription_id: reveal.inscription_id.clone(),
|
||||
inscription_number: reveal.inscription_number.jubilee as u64,
|
||||
ordinal_number: reveal.ordinal_number,
|
||||
block_height: block_identifier.index,
|
||||
tx_index,
|
||||
tick: data.tick.clone(),
|
||||
address: data.address.clone(),
|
||||
avail_balance: data.amt,
|
||||
trans_balance: 0.0,
|
||||
operation: "mint".to_string(),
|
||||
});
|
||||
self.ignore_inscription(reveal.ordinal_number);
|
||||
}
|
||||
|
||||
pub fn insert_token_transfer(
|
||||
&mut self,
|
||||
data: &VerifiedBrc20BalanceData,
|
||||
reveal: &OrdinalInscriptionRevealData,
|
||||
block_identifier: &BlockIdentifier,
|
||||
tx_index: u64,
|
||||
db_tx: &Transaction,
|
||||
ctx: &Context,
|
||||
) {
|
||||
let Some(balance) =
|
||||
self.get_token_address_avail_balance(&data.tick, &data.address, db_tx, ctx)
|
||||
else {
|
||||
unreachable!("BRC-20 transfer insert attempted for an address with no balance");
|
||||
};
|
||||
self.token_addr_avail_balances.put(
|
||||
format!("{}:{}", data.tick, data.address),
|
||||
balance - data.amt, // Decrease for sender.
|
||||
);
|
||||
let ledger_row = Brc20DbLedgerRow {
|
||||
inscription_id: reveal.inscription_id.clone(),
|
||||
inscription_number: reveal.inscription_number.jubilee as u64,
|
||||
ordinal_number: reveal.ordinal_number,
|
||||
block_height: block_identifier.index,
|
||||
tx_index,
|
||||
tick: data.tick.clone(),
|
||||
address: data.address.clone(),
|
||||
avail_balance: data.amt * -1.0,
|
||||
trans_balance: data.amt,
|
||||
operation: "transfer".to_string(),
|
||||
};
|
||||
self.unsent_transfers
|
||||
.put(reveal.ordinal_number, ledger_row.clone());
|
||||
self.db_cache.ledger_rows.push(ledger_row);
|
||||
self.ignored_inscriptions.pop(&reveal.ordinal_number); // Just in case.
|
||||
}
|
||||
|
||||
pub fn insert_token_transfer_send(
|
||||
&mut self,
|
||||
data: &VerifiedBrc20TransferData,
|
||||
transfer: &OrdinalInscriptionTransferData,
|
||||
block_identifier: &BlockIdentifier,
|
||||
tx_index: u64,
|
||||
db_tx: &Transaction,
|
||||
ctx: &Context,
|
||||
) {
|
||||
let transfer_row = self.get_unsent_transfer_row(transfer.ordinal_number, db_tx, ctx);
|
||||
self.db_cache.ledger_rows.push(Brc20DbLedgerRow {
|
||||
inscription_id: transfer_row.inscription_id.clone(),
|
||||
inscription_number: transfer_row.inscription_number,
|
||||
ordinal_number: transfer.ordinal_number,
|
||||
block_height: block_identifier.index,
|
||||
tx_index,
|
||||
tick: data.tick.clone(),
|
||||
address: data.sender_address.clone(),
|
||||
avail_balance: 0.0,
|
||||
trans_balance: data.amt * -1.0,
|
||||
operation: "transfer_send".to_string(),
|
||||
});
|
||||
self.db_cache.ledger_rows.push(Brc20DbLedgerRow {
|
||||
inscription_id: transfer_row.inscription_id.clone(),
|
||||
inscription_number: transfer_row.inscription_number,
|
||||
ordinal_number: transfer.ordinal_number,
|
||||
block_height: block_identifier.index,
|
||||
tx_index,
|
||||
tick: data.tick.clone(),
|
||||
address: data.receiver_address.clone(),
|
||||
avail_balance: data.amt,
|
||||
trans_balance: 0.0,
|
||||
operation: "transfer_receive".to_string(),
|
||||
});
|
||||
let balance = self
|
||||
.get_token_address_avail_balance(&data.tick, &data.receiver_address, db_tx, ctx)
|
||||
.unwrap_or(0.0);
|
||||
self.token_addr_avail_balances.put(
|
||||
format!("{}:{}", data.tick, data.receiver_address),
|
||||
balance + data.amt, // Increase for receiver.
|
||||
);
|
||||
// We're not interested in further transfers.
|
||||
self.unsent_transfers.pop(&transfer.ordinal_number);
|
||||
self.ignore_inscription(transfer.ordinal_number);
|
||||
}
|
||||
|
||||
//
|
||||
//
|
||||
//
|
||||
|
||||
fn get_unsent_transfer_row(
|
||||
&mut self,
|
||||
ordinal_number: u64,
|
||||
db_tx: &Transaction,
|
||||
ctx: &Context,
|
||||
) -> Brc20DbLedgerRow {
|
||||
if let Some(transfer) = self.unsent_transfers.get(&ordinal_number) {
|
||||
return transfer.clone();
|
||||
}
|
||||
self.handle_cache_miss(db_tx, ctx);
|
||||
let Some(transfer) = get_unsent_token_transfer(ordinal_number, db_tx, ctx) else {
|
||||
unreachable!("Invalid transfer ordinal number {}", ordinal_number)
|
||||
};
|
||||
self.unsent_transfers.put(ordinal_number, transfer.clone());
|
||||
return transfer;
|
||||
}
|
||||
|
||||
fn handle_cache_miss(&mut self, db_tx: &Transaction, ctx: &Context) {
|
||||
// TODO: Measure this event somewhere
|
||||
self.db_cache.flush(db_tx, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use chainhook_sdk::types::{BitcoinNetwork, BlockIdentifier};
|
||||
use test_case::test_case;
|
||||
|
||||
use crate::core::meta_protocols::brc20::{
|
||||
db::initialize_brc20_db,
|
||||
parser::{ParsedBrc20BalanceData, ParsedBrc20Operation},
|
||||
test_utils::{get_test_ctx, Brc20RevealBuilder},
|
||||
verifier::{
|
||||
verify_brc20_operation, VerifiedBrc20BalanceData, VerifiedBrc20Operation,
|
||||
VerifiedBrc20TokenDeployData,
|
||||
},
|
||||
};
|
||||
|
||||
use super::Brc20MemoryCache;
|
||||
|
||||
#[test]
|
||||
fn test_brc20_memory_cache_transfer_miss() {
|
||||
let ctx = get_test_ctx();
|
||||
let mut conn = initialize_brc20_db(None, &ctx);
|
||||
let tx = conn.transaction().unwrap();
|
||||
// LRU size as 1 so we can test a miss.
|
||||
let mut cache = Brc20MemoryCache::new(1);
|
||||
cache.insert_token_deploy(
|
||||
&VerifiedBrc20TokenDeployData {
|
||||
tick: "pepe".to_string(),
|
||||
max: 21000000.0,
|
||||
lim: 1000.0,
|
||||
dec: 18,
|
||||
address: "324A7GHA2azecbVBAFy4pzEhcPT1GjbUAp".to_string(),
|
||||
self_mint: false,
|
||||
},
|
||||
&Brc20RevealBuilder::new().inscription_number(0).build(),
|
||||
&BlockIdentifier {
|
||||
index: 800000,
|
||||
hash: "00000000000000000002d8ba402150b259ddb2b30a1d32ab4a881d4653bceb5b"
|
||||
.to_string(),
|
||||
},
|
||||
0,
|
||||
&tx,
|
||||
&ctx,
|
||||
);
|
||||
let block = BlockIdentifier {
|
||||
index: 800002,
|
||||
hash: "00000000000000000002d8ba402150b259ddb2b30a1d32ab4a881d4653bceb5b".to_string(),
|
||||
};
|
||||
let address1 = "324A7GHA2azecbVBAFy4pzEhcPT1GjbUAp".to_string();
|
||||
let address2 = "bc1pngjqgeamkmmhlr6ft5yllgdmfllvcvnw5s7ew2ler3rl0z47uaesrj6jte".to_string();
|
||||
cache.insert_token_mint(
|
||||
&VerifiedBrc20BalanceData {
|
||||
tick: "pepe".to_string(),
|
||||
amt: 1000.0,
|
||||
address: address1.clone(),
|
||||
},
|
||||
&Brc20RevealBuilder::new().inscription_number(1).build(),
|
||||
&block,
|
||||
0,
|
||||
&tx,
|
||||
&ctx,
|
||||
);
|
||||
cache.insert_token_transfer(
|
||||
&VerifiedBrc20BalanceData {
|
||||
tick: "pepe".to_string(),
|
||||
amt: 100.0,
|
||||
address: address1.clone(),
|
||||
},
|
||||
&Brc20RevealBuilder::new().inscription_number(2).build(),
|
||||
&block,
|
||||
1,
|
||||
&tx,
|
||||
&ctx,
|
||||
);
|
||||
// These mint+transfer from a 2nd address will delete the first address' entries from cache.
|
||||
cache.insert_token_mint(
|
||||
&VerifiedBrc20BalanceData {
|
||||
tick: "pepe".to_string(),
|
||||
amt: 1000.0,
|
||||
address: address2.clone(),
|
||||
},
|
||||
&Brc20RevealBuilder::new()
|
||||
.inscription_number(3)
|
||||
.inscriber_address(Some(address2.clone()))
|
||||
.build(),
|
||||
&block,
|
||||
2,
|
||||
&tx,
|
||||
&ctx,
|
||||
);
|
||||
cache.insert_token_transfer(
|
||||
&VerifiedBrc20BalanceData {
|
||||
tick: "pepe".to_string(),
|
||||
amt: 100.0,
|
||||
address: address2.clone(),
|
||||
},
|
||||
&Brc20RevealBuilder::new()
|
||||
.inscription_number(4)
|
||||
.inscriber_address(Some(address2.clone()))
|
||||
.build(),
|
||||
&block,
|
||||
3,
|
||||
&tx,
|
||||
&ctx,
|
||||
);
|
||||
// Validate another transfer from the first address. Should pass because we still have 900 avail balance.
|
||||
let result = verify_brc20_operation(
|
||||
&ParsedBrc20Operation::Transfer(ParsedBrc20BalanceData {
|
||||
tick: "pepe".to_string(),
|
||||
amt: "100".to_string(),
|
||||
}),
|
||||
&Brc20RevealBuilder::new()
|
||||
.inscription_number(5)
|
||||
.inscriber_address(Some(address1.clone()))
|
||||
.build(),
|
||||
&block,
|
||||
&BitcoinNetwork::Mainnet,
|
||||
&mut cache,
|
||||
&tx,
|
||||
&ctx,
|
||||
);
|
||||
assert!(
|
||||
result
|
||||
== Ok(VerifiedBrc20Operation::TokenTransfer(
|
||||
VerifiedBrc20BalanceData {
|
||||
tick: "pepe".to_string(),
|
||||
amt: 100.0,
|
||||
address: address1
|
||||
}
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
#[test_case(500.0 => Ok(Some(500.0)); "with transfer amt")]
|
||||
#[test_case(1000.0 => Ok(Some(0.0)); "with transfer to zero")]
|
||||
fn test_brc20_memory_cache_transfer_avail_balance(amt: f64) -> Result<Option<f64>, String> {
|
||||
let ctx = get_test_ctx();
|
||||
let mut conn = initialize_brc20_db(None, &ctx);
|
||||
let tx = conn.transaction().unwrap();
|
||||
let mut cache = Brc20MemoryCache::new(10);
|
||||
cache.insert_token_deploy(
|
||||
&VerifiedBrc20TokenDeployData {
|
||||
tick: "pepe".to_string(),
|
||||
max: 21000000.0,
|
||||
lim: 1000.0,
|
||||
dec: 18,
|
||||
address: "324A7GHA2azecbVBAFy4pzEhcPT1GjbUAp".to_string(),
|
||||
self_mint: false,
|
||||
},
|
||||
&Brc20RevealBuilder::new().inscription_number(0).build(),
|
||||
&BlockIdentifier {
|
||||
index: 800000,
|
||||
hash: "00000000000000000002d8ba402150b259ddb2b30a1d32ab4a881d4653bceb5b"
|
||||
.to_string(),
|
||||
},
|
||||
0,
|
||||
&tx,
|
||||
&ctx,
|
||||
);
|
||||
cache.insert_token_mint(
|
||||
&VerifiedBrc20BalanceData {
|
||||
tick: "pepe".to_string(),
|
||||
amt: 1000.0,
|
||||
address: "324A7GHA2azecbVBAFy4pzEhcPT1GjbUAp".to_string(),
|
||||
},
|
||||
&Brc20RevealBuilder::new().inscription_number(1).build(),
|
||||
&BlockIdentifier {
|
||||
index: 800001,
|
||||
hash: "00000000000000000002d8ba402150b259ddb2b30a1d32ab4a881d4653bceb5b"
|
||||
.to_string(),
|
||||
},
|
||||
0,
|
||||
&tx,
|
||||
&ctx,
|
||||
);
|
||||
assert!(
|
||||
cache.get_token_address_avail_balance(
|
||||
"pepe",
|
||||
"324A7GHA2azecbVBAFy4pzEhcPT1GjbUAp",
|
||||
&tx,
|
||||
&ctx,
|
||||
) == Some(1000.0)
|
||||
);
|
||||
cache.insert_token_transfer(
|
||||
&VerifiedBrc20BalanceData {
|
||||
tick: "pepe".to_string(),
|
||||
amt,
|
||||
address: "324A7GHA2azecbVBAFy4pzEhcPT1GjbUAp".to_string(),
|
||||
},
|
||||
&Brc20RevealBuilder::new().inscription_number(2).build(),
|
||||
&BlockIdentifier {
|
||||
index: 800002,
|
||||
hash: "00000000000000000002d8ba402150b259ddb2b30a1d32ab4a881d4653bceb5b"
|
||||
.to_string(),
|
||||
},
|
||||
0,
|
||||
&tx,
|
||||
&ctx,
|
||||
);
|
||||
Ok(cache.get_token_address_avail_balance(
|
||||
"pepe",
|
||||
"324A7GHA2azecbVBAFy4pzEhcPT1GjbUAp",
|
||||
&tx,
|
||||
&ctx,
|
||||
))
|
||||
}
|
||||
}
|
||||
429
components/ordhook-core/src/core/meta_protocols/brc20/db.rs
Normal file
429
components/ordhook-core/src/core/meta_protocols/brc20/db.rs
Normal file
@@ -0,0 +1,429 @@
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
use crate::db::{
|
||||
create_or_open_readwrite_db, format_inscription_id, open_existing_readonly_db,
|
||||
perform_query_one, perform_query_set,
|
||||
};
|
||||
use chainhook_sdk::{
|
||||
types::{
|
||||
BitcoinTransactionData, BlockIdentifier, Brc20BalanceData, Brc20Operation,
|
||||
Brc20TokenDeployData, Brc20TransferData,
|
||||
},
|
||||
utils::Context,
|
||||
};
|
||||
use rusqlite::{Connection, ToSql, Transaction};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Brc20DbTokenRow {
|
||||
pub inscription_id: String,
|
||||
pub inscription_number: u64,
|
||||
pub block_height: u64,
|
||||
pub tick: String,
|
||||
pub max: f64,
|
||||
pub lim: f64,
|
||||
pub dec: u64,
|
||||
pub address: String,
|
||||
pub self_mint: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Brc20DbLedgerRow {
|
||||
pub inscription_id: String,
|
||||
pub inscription_number: u64,
|
||||
pub ordinal_number: u64,
|
||||
pub block_height: u64,
|
||||
pub tx_index: u64,
|
||||
pub tick: String,
|
||||
pub address: String,
|
||||
pub avail_balance: f64,
|
||||
pub trans_balance: f64,
|
||||
pub operation: String,
|
||||
}
|
||||
|
||||
pub fn get_default_brc20_db_file_path(base_dir: &PathBuf) -> PathBuf {
|
||||
let mut destination_path = base_dir.clone();
|
||||
destination_path.push("brc20.sqlite");
|
||||
destination_path
|
||||
}
|
||||
|
||||
pub fn initialize_brc20_db(base_dir: Option<&PathBuf>, ctx: &Context) -> Connection {
|
||||
let db_path = base_dir.map(|dir| get_default_brc20_db_file_path(dir));
|
||||
let conn = create_or_open_readwrite_db(db_path.as_ref(), ctx);
|
||||
if let Err(e) = conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS tokens (
|
||||
inscription_id TEXT NOT NULL PRIMARY KEY,
|
||||
inscription_number INTEGER NOT NULL,
|
||||
block_height INTEGER NOT NULL,
|
||||
tick TEXT NOT NULL,
|
||||
max REAL NOT NULL,
|
||||
lim REAL NOT NULL,
|
||||
dec INTEGER NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
self_mint BOOL NOT NULL,
|
||||
UNIQUE (inscription_id),
|
||||
UNIQUE (inscription_number),
|
||||
UNIQUE (tick)
|
||||
)",
|
||||
[],
|
||||
) {
|
||||
ctx.try_log(|logger| warn!(logger, "Unable to create table tokens: {}", e.to_string()));
|
||||
} else {
|
||||
if let Err(e) = conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS index_tokens_on_block_height ON tokens(block_height);",
|
||||
[],
|
||||
) {
|
||||
ctx.try_log(|logger| warn!(logger, "unable to create brc20.sqlite: {}", e.to_string()));
|
||||
}
|
||||
}
|
||||
if let Err(e) = conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS ledger (
|
||||
inscription_id TEXT NOT NULL,
|
||||
inscription_number INTEGER NOT NULL,
|
||||
ordinal_number INTEGER NOT NULL,
|
||||
block_height INTEGER NOT NULL,
|
||||
tx_index INTEGER NOT NULL,
|
||||
tick TEXT NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
avail_balance REAL NOT NULL,
|
||||
trans_balance REAL NOT NULL,
|
||||
operation TEXT NOT NULL CHECK(operation IN ('deploy', 'mint', 'transfer', 'transfer_send', 'transfer_receive'))
|
||||
)",
|
||||
[],
|
||||
) {
|
||||
ctx.try_log(|logger| warn!(logger, "Unable to create table ledger: {}", e.to_string()));
|
||||
} else {
|
||||
if let Err(e) = conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS index_ledger_on_tick_address ON ledger(tick, address);",
|
||||
[],
|
||||
) {
|
||||
ctx.try_log(|logger| warn!(logger, "unable to create brc20.sqlite: {}", e.to_string()));
|
||||
}
|
||||
if let Err(e) = conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS index_ledger_on_ordinal_number_operation ON ledger(ordinal_number, operation);",
|
||||
[],
|
||||
) {
|
||||
ctx.try_log(|logger| warn!(logger, "unable to create brc20.sqlite: {}", e.to_string()));
|
||||
}
|
||||
if let Err(e) = conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS index_ledger_on_block_height_operation ON ledger(block_height, operation);",
|
||||
[],
|
||||
) {
|
||||
ctx.try_log(|logger| warn!(logger, "unable to create brc20.sqlite: {}", e.to_string()));
|
||||
}
|
||||
if let Err(e) = conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS index_ledger_on_inscription_id ON ledger(inscription_id);",
|
||||
[],
|
||||
) {
|
||||
ctx.try_log(|logger| warn!(logger, "unable to create brc20.sqlite: {}", e.to_string()));
|
||||
}
|
||||
if let Err(e) = conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS index_ledger_on_inscription_number ON ledger(inscription_number);",
|
||||
[],
|
||||
) {
|
||||
ctx.try_log(|logger| warn!(logger, "unable to create brc20.sqlite: {}", e.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
conn
|
||||
}
|
||||
|
||||
pub fn open_readwrite_brc20_db_conn(
|
||||
base_dir: &PathBuf,
|
||||
ctx: &Context,
|
||||
) -> Result<Connection, String> {
|
||||
let db_path = get_default_brc20_db_file_path(&base_dir);
|
||||
let conn = create_or_open_readwrite_db(Some(&db_path), ctx);
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
pub fn open_readonly_brc20_db_conn(
|
||||
base_dir: &PathBuf,
|
||||
ctx: &Context,
|
||||
) -> Result<Connection, String> {
|
||||
let db_path = get_default_brc20_db_file_path(&base_dir);
|
||||
let conn = open_existing_readonly_db(&db_path, ctx);
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
pub fn delete_activity_in_block_range(
|
||||
start_block: u32,
|
||||
end_block: u32,
|
||||
db_tx: &Connection,
|
||||
ctx: &Context,
|
||||
) {
|
||||
while let Err(e) = db_tx.execute(
|
||||
"DELETE FROM ledger WHERE block_height >= ?1 AND block_height <= ?2",
|
||||
rusqlite::params![&start_block, &end_block],
|
||||
) {
|
||||
ctx.try_log(|logger| warn!(logger, "unable to query brc20.sqlite: {}", e.to_string()));
|
||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||
}
|
||||
while let Err(e) = db_tx.execute(
|
||||
"DELETE FROM tokens WHERE block_height >= ?1 AND block_height <= ?2",
|
||||
rusqlite::params![&start_block, &end_block],
|
||||
) {
|
||||
ctx.try_log(|logger| warn!(logger, "unable to query brc20.sqlite: {}", e.to_string()));
|
||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_token(tick: &str, db_tx: &Connection, ctx: &Context) -> Option<Brc20DbTokenRow> {
|
||||
let args: &[&dyn ToSql] = &[&tick.to_sql().unwrap()];
|
||||
let query = "
|
||||
SELECT tick, max, lim, dec, address, inscription_id, inscription_number, block_height, self_mint
|
||||
FROM tokens
|
||||
WHERE tick = ?
|
||||
";
|
||||
perform_query_one(query, args, &db_tx, ctx, |row| Brc20DbTokenRow {
|
||||
tick: row.get(0).unwrap(),
|
||||
max: row.get(1).unwrap(),
|
||||
lim: row.get(2).unwrap(),
|
||||
dec: row.get(3).unwrap(),
|
||||
address: row.get(4).unwrap(),
|
||||
inscription_id: row.get(5).unwrap(),
|
||||
inscription_number: row.get(6).unwrap(),
|
||||
block_height: row.get(7).unwrap(),
|
||||
self_mint: row.get(8).unwrap(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_token_minted_supply(tick: &str, db_tx: &Transaction, ctx: &Context) -> Option<f64> {
|
||||
let args: &[&dyn ToSql] = &[&tick.to_sql().unwrap()];
|
||||
let query = "
|
||||
SELECT COALESCE(SUM(avail_balance + trans_balance), 0.0) AS minted
|
||||
FROM ledger
|
||||
WHERE tick = ?
|
||||
";
|
||||
perform_query_one(query, args, &db_tx, ctx, |row| row.get(0).unwrap()).unwrap_or(None)
|
||||
}
|
||||
|
||||
pub fn get_token_available_balance_for_address(
|
||||
tick: &str,
|
||||
address: &str,
|
||||
db_tx: &Transaction,
|
||||
ctx: &Context,
|
||||
) -> Option<f64> {
|
||||
let args: &[&dyn ToSql] = &[&tick.to_sql().unwrap(), &address.to_sql().unwrap()];
|
||||
let query = "
|
||||
SELECT SUM(avail_balance) AS avail_balance
|
||||
FROM ledger
|
||||
WHERE tick = ? AND address = ?
|
||||
";
|
||||
perform_query_one(query, args, &db_tx, ctx, |row| row.get(0).unwrap()).unwrap_or(None)
|
||||
}
|
||||
|
||||
pub fn get_unsent_token_transfer(
|
||||
ordinal_number: u64,
|
||||
db_tx: &Connection,
|
||||
ctx: &Context,
|
||||
) -> Option<Brc20DbLedgerRow> {
|
||||
let args: &[&dyn ToSql] = &[
|
||||
&ordinal_number.to_sql().unwrap(),
|
||||
&ordinal_number.to_sql().unwrap(),
|
||||
];
|
||||
let query = "
|
||||
SELECT inscription_id, inscription_number, ordinal_number, block_height, tx_index, tick, address, avail_balance, trans_balance, operation
|
||||
FROM ledger
|
||||
WHERE ordinal_number = ? AND operation = 'transfer'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM ledger WHERE ordinal_number = ? AND operation = 'transfer_send'
|
||||
)
|
||||
LIMIT 1
|
||||
";
|
||||
perform_query_one(query, args, &db_tx, ctx, |row| Brc20DbLedgerRow {
|
||||
inscription_id: row.get(0).unwrap(),
|
||||
inscription_number: row.get(1).unwrap(),
|
||||
ordinal_number: row.get(2).unwrap(),
|
||||
block_height: row.get(3).unwrap(),
|
||||
tx_index: row.get(4).unwrap(),
|
||||
tick: row.get(5).unwrap(),
|
||||
address: row.get(6).unwrap(),
|
||||
avail_balance: row.get(7).unwrap(),
|
||||
trans_balance: row.get(8).unwrap(),
|
||||
operation: row.get(9).unwrap(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_transfer_send_receiver_address(
|
||||
ordinal_number: u64,
|
||||
db_tx: &Connection,
|
||||
ctx: &Context,
|
||||
) -> Option<String> {
|
||||
let args: &[&dyn ToSql] = &[&ordinal_number.to_sql().unwrap()];
|
||||
let query = "
|
||||
SELECT address
|
||||
FROM ledger
|
||||
WHERE ordinal_number = ? AND operation = 'transfer_receive'
|
||||
LIMIT 1
|
||||
";
|
||||
perform_query_one(query, args, &db_tx, ctx, |row| row.get(0).unwrap())
|
||||
}
|
||||
|
||||
pub fn insert_ledger_rows(rows: &Vec<Brc20DbLedgerRow>, db_tx: &Transaction, ctx: &Context) {
|
||||
match db_tx.prepare_cached("INSERT INTO ledger
|
||||
(inscription_id, inscription_number, ordinal_number, block_height, tx_index, tick, address, avail_balance, trans_balance, operation)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") {
|
||||
Ok(mut stmt) => {
|
||||
for row in rows.iter() {
|
||||
while let Err(e) = stmt.execute(rusqlite::params![
|
||||
&row.inscription_id,
|
||||
&row.inscription_number,
|
||||
&row.ordinal_number,
|
||||
&row.block_height,
|
||||
&row.tx_index,
|
||||
&row.tick,
|
||||
&row.address,
|
||||
&row.avail_balance,
|
||||
&row.trans_balance,
|
||||
&row.operation
|
||||
]) {
|
||||
ctx.try_log(|logger| warn!(logger, "unable to insert into brc20.sqlite: {}", e.to_string()));
|
||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(error) => ctx.try_log(|logger| warn!(logger, "unable to prepare statement for brc20.sqlite: {}", error.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_token_rows(rows: &Vec<Brc20DbTokenRow>, db_tx: &Transaction, ctx: &Context) {
|
||||
match db_tx.prepare_cached(
|
||||
"INSERT INTO tokens
|
||||
(inscription_id, inscription_number, block_height, tick, max, lim, dec, address, self_mint)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
) {
|
||||
Ok(mut stmt) => {
|
||||
for row in rows.iter() {
|
||||
while let Err(e) = stmt.execute(rusqlite::params![
|
||||
&row.inscription_id,
|
||||
&row.inscription_number,
|
||||
&row.block_height,
|
||||
&row.tick,
|
||||
&row.max,
|
||||
&row.lim,
|
||||
&row.dec,
|
||||
&row.address,
|
||||
&row.self_mint,
|
||||
]) {
|
||||
ctx.try_log(|logger| {
|
||||
warn!(
|
||||
logger,
|
||||
"unable to insert into brc20.sqlite: {}",
|
||||
e.to_string()
|
||||
)
|
||||
});
|
||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => ctx.try_log(|logger| {
|
||||
warn!(
|
||||
logger,
|
||||
"unable to prepare statement for brc20.sqlite: {}",
|
||||
error.to_string()
|
||||
)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_brc20_operations_on_block(
|
||||
block_identifier: &BlockIdentifier,
|
||||
db_tx: &Connection,
|
||||
ctx: &Context,
|
||||
) -> HashMap<String, Brc20DbLedgerRow> {
|
||||
let args: &[&dyn ToSql] = &[&block_identifier.index.to_sql().unwrap()];
|
||||
let query = "
|
||||
SELECT
|
||||
inscription_id, inscription_number, ordinal_number, block_height, tx_index, tick, address, avail_balance, trans_balance, operation
|
||||
FROM ledger AS l
|
||||
WHERE block_height = ? AND operation <> 'transfer_receive'
|
||||
";
|
||||
let mut map = HashMap::new();
|
||||
let rows = perform_query_set(query, args, &db_tx, &ctx, |row| Brc20DbLedgerRow {
|
||||
inscription_id: row.get(0).unwrap(),
|
||||
inscription_number: row.get(1).unwrap(),
|
||||
ordinal_number: row.get(2).unwrap(),
|
||||
block_height: row.get(3).unwrap(),
|
||||
tx_index: row.get(4).unwrap(),
|
||||
tick: row.get(5).unwrap(),
|
||||
address: row.get(6).unwrap(),
|
||||
avail_balance: row.get(7).unwrap(),
|
||||
trans_balance: row.get(8).unwrap(),
|
||||
operation: row.get(9).unwrap(),
|
||||
});
|
||||
for row in rows.iter() {
|
||||
map.insert(row.inscription_id.clone(), row.clone());
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
pub fn augment_transaction_with_brc20_operation_data(
|
||||
tx: &mut BitcoinTransactionData,
|
||||
token_map: &mut HashMap<String, Brc20DbTokenRow>,
|
||||
block_ledger_map: &mut HashMap<String, Brc20DbLedgerRow>,
|
||||
db_conn: &Connection,
|
||||
ctx: &Context,
|
||||
) {
|
||||
let inscription_id = format_inscription_id(&tx.transaction_identifier, 0);
|
||||
let Some(entry) = block_ledger_map.remove(inscription_id.as_str()) else {
|
||||
return;
|
||||
};
|
||||
if token_map.get(&entry.tick) == None {
|
||||
let Some(row) = get_token(&entry.tick, &db_conn, &ctx) else {
|
||||
unreachable!("BRC-20 token not found when processing operation");
|
||||
};
|
||||
token_map.insert(entry.tick.clone(), row);
|
||||
}
|
||||
let token = token_map
|
||||
.get(&entry.tick)
|
||||
.expect("Token not present in map");
|
||||
let dec = token.dec as usize;
|
||||
match entry.operation.as_str() {
|
||||
"deploy" => {
|
||||
tx.metadata.brc20_operation = Some(Brc20Operation::Deploy(Brc20TokenDeployData {
|
||||
tick: token.tick.clone(),
|
||||
max: format!("{:.precision$}", token.max, precision = dec),
|
||||
lim: format!("{:.precision$}", token.lim, precision = dec),
|
||||
dec: token.dec.to_string(),
|
||||
address: token.address.clone(),
|
||||
inscription_id: token.inscription_id.clone(),
|
||||
self_mint: token.self_mint,
|
||||
}));
|
||||
}
|
||||
"mint" => {
|
||||
tx.metadata.brc20_operation = Some(Brc20Operation::Mint(Brc20BalanceData {
|
||||
tick: entry.tick.clone(),
|
||||
amt: format!("{:.precision$}", entry.avail_balance, precision = dec),
|
||||
address: entry.address.clone(),
|
||||
inscription_id: entry.inscription_id.clone(),
|
||||
}));
|
||||
}
|
||||
"transfer" => {
|
||||
tx.metadata.brc20_operation = Some(Brc20Operation::Transfer(Brc20BalanceData {
|
||||
tick: entry.tick.clone(),
|
||||
amt: format!("{:.precision$}", entry.trans_balance, precision = dec),
|
||||
address: entry.address.clone(),
|
||||
inscription_id: entry.inscription_id.clone(),
|
||||
}));
|
||||
}
|
||||
"transfer_send" => {
|
||||
let Some(receiver_address) =
|
||||
get_transfer_send_receiver_address(entry.ordinal_number, &db_conn, &ctx)
|
||||
else {
|
||||
unreachable!("Unable to fetch receiver address for transfer_send operation");
|
||||
};
|
||||
tx.metadata.brc20_operation = Some(Brc20Operation::TransferSend(Brc20TransferData {
|
||||
tick: entry.tick.clone(),
|
||||
amt: format!(
|
||||
"{:.precision$}",
|
||||
entry.trans_balance * -1.0,
|
||||
precision = dec
|
||||
),
|
||||
sender_address: entry.address.clone(),
|
||||
receiver_address,
|
||||
inscription_id: entry.inscription_id,
|
||||
}));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
25
components/ordhook-core/src/core/meta_protocols/brc20/mod.rs
Normal file
25
components/ordhook-core/src/core/meta_protocols/brc20/mod.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use chainhook_sdk::types::BitcoinNetwork;
|
||||
|
||||
pub mod db;
|
||||
pub mod parser;
|
||||
pub mod verifier;
|
||||
pub mod cache;
|
||||
pub mod test_utils;
|
||||
|
||||
pub fn brc20_activation_height(network: &BitcoinNetwork) -> u64 {
|
||||
match network {
|
||||
BitcoinNetwork::Mainnet => 779832,
|
||||
BitcoinNetwork::Regtest => todo!(),
|
||||
BitcoinNetwork::Testnet => todo!(),
|
||||
BitcoinNetwork::Signet => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn brc20_self_mint_activation_height(network: &BitcoinNetwork) -> u64 {
|
||||
match network {
|
||||
BitcoinNetwork::Mainnet => 837090,
|
||||
BitcoinNetwork::Regtest => todo!(),
|
||||
BitcoinNetwork::Testnet => todo!(),
|
||||
BitcoinNetwork::Signet => todo!(),
|
||||
}
|
||||
}
|
||||
651
components/ordhook-core/src/core/meta_protocols/brc20/parser.rs
Normal file
651
components/ordhook-core/src/core/meta_protocols/brc20/parser.rs
Normal file
@@ -0,0 +1,651 @@
|
||||
use regex::Regex;
|
||||
|
||||
use crate::ord::inscription::Inscription;
|
||||
use crate::ord::media::{Language, Media};
|
||||
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
pub struct ParsedBrc20TokenDeployData {
|
||||
pub tick: String,
|
||||
pub max: f64,
|
||||
pub lim: f64,
|
||||
pub dec: u64,
|
||||
pub self_mint: bool,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
pub struct ParsedBrc20BalanceData {
|
||||
pub tick: String,
|
||||
// Keep as `String` instead of `f64` so we can later decide if it was inscribed with a correct
|
||||
// number of decimals during verification, depending on the token's deployed definition.
|
||||
pub amt: String,
|
||||
}
|
||||
|
||||
impl ParsedBrc20BalanceData {
|
||||
pub fn float_amt(&self) -> f64 {
|
||||
self.amt.parse::<f64>().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
pub enum ParsedBrc20Operation {
|
||||
Deploy(ParsedBrc20TokenDeployData),
|
||||
Mint(ParsedBrc20BalanceData),
|
||||
Transfer(ParsedBrc20BalanceData),
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Brc20DeployJson {
|
||||
p: String,
|
||||
op: String,
|
||||
tick: String,
|
||||
max: String,
|
||||
lim: Option<String>,
|
||||
dec: Option<String>,
|
||||
self_mint: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Brc20MintOrTransferJson {
|
||||
p: String,
|
||||
op: String,
|
||||
tick: String,
|
||||
amt: String,
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref NUMERIC_FLOAT_REGEX: Regex =
|
||||
Regex::new(r#"^(([0-9]+)|([0-9]*\.?[0-9]+))$"#.into()).unwrap();
|
||||
pub static ref NUMERIC_INT_REGEX: Regex = Regex::new(r#"^([0-9]+)$"#.into()).unwrap();
|
||||
}
|
||||
|
||||
pub fn amt_has_valid_decimals(amt: &str, max_decimals: u64) -> bool {
|
||||
if amt.contains('.')
|
||||
&& amt.split('.').nth(1).map_or(0, |s| s.chars().count()) as u64 > max_decimals
|
||||
{
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn parse_float_numeric_value(n: &str, max_decimals: u64) -> Option<f64> {
|
||||
if NUMERIC_FLOAT_REGEX.is_match(&n) {
|
||||
if !amt_has_valid_decimals(n, max_decimals) {
|
||||
return None;
|
||||
}
|
||||
match n.parse::<f64>() {
|
||||
Ok(parsed) => {
|
||||
if parsed > u64::MAX as f64 {
|
||||
return None;
|
||||
}
|
||||
return Some(parsed);
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn parse_int_numeric_value(n: &str) -> Option<u64> {
|
||||
if NUMERIC_INT_REGEX.is_match(&n) {
|
||||
match n.parse::<u64>() {
|
||||
Ok(parsed) => {
|
||||
if parsed > u64::MAX {
|
||||
return None;
|
||||
}
|
||||
return Some(parsed);
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Attempts to parse an `Inscription` into a BRC20 operation by following the rules explained in
|
||||
/// https://layer1.gitbook.io/layer1-foundation/protocols/brc-20/indexing
|
||||
pub fn parse_brc20_operation(
|
||||
inscription: &Inscription,
|
||||
) -> Result<Option<ParsedBrc20Operation>, String> {
|
||||
match inscription.media() {
|
||||
Media::Code(Language::Json) | Media::Text => {}
|
||||
_ => return Ok(None),
|
||||
};
|
||||
let Some(inscription_body) = inscription.body() else {
|
||||
return Ok(None);
|
||||
};
|
||||
match serde_json::from_slice::<Brc20DeployJson>(inscription_body) {
|
||||
Ok(json) => {
|
||||
if json.p != "brc-20" || json.op != "deploy" {
|
||||
return Ok(None);
|
||||
}
|
||||
let mut deploy = ParsedBrc20TokenDeployData {
|
||||
tick: json.tick.to_lowercase(),
|
||||
max: 0.0,
|
||||
lim: 0.0,
|
||||
dec: 18,
|
||||
self_mint: false,
|
||||
};
|
||||
if json.self_mint == Some("true".to_string()) {
|
||||
if json.tick.len() != 5 {
|
||||
return Ok(None);
|
||||
}
|
||||
deploy.self_mint = true;
|
||||
} else if json.tick.len() != 4 {
|
||||
return Ok(None);
|
||||
}
|
||||
if let Some(dec) = json.dec {
|
||||
let Some(parsed_dec) = parse_int_numeric_value(&dec) else {
|
||||
return Ok(None);
|
||||
};
|
||||
if parsed_dec > 18 {
|
||||
return Ok(None);
|
||||
}
|
||||
deploy.dec = parsed_dec;
|
||||
}
|
||||
let Some(parsed_max) = parse_float_numeric_value(&json.max, deploy.dec) else {
|
||||
return Ok(None);
|
||||
};
|
||||
if parsed_max == 0.0 {
|
||||
if deploy.self_mint {
|
||||
deploy.max = u64::MAX as f64;
|
||||
} else {
|
||||
return Ok(None);
|
||||
}
|
||||
} else {
|
||||
deploy.max = parsed_max;
|
||||
}
|
||||
if let Some(lim) = json.lim {
|
||||
let Some(parsed_lim) = parse_float_numeric_value(&lim, deploy.dec) else {
|
||||
return Ok(None);
|
||||
};
|
||||
if parsed_lim == 0.0 {
|
||||
return Ok(None);
|
||||
}
|
||||
deploy.lim = parsed_lim;
|
||||
} else {
|
||||
deploy.lim = deploy.max;
|
||||
}
|
||||
return Ok(Some(ParsedBrc20Operation::Deploy(deploy)));
|
||||
}
|
||||
Err(_) => match serde_json::from_slice::<Brc20MintOrTransferJson>(inscription_body) {
|
||||
Ok(json) => {
|
||||
if json.p != "brc-20" || json.tick.len() < 4 || json.tick.len() > 5 {
|
||||
return Ok(None);
|
||||
}
|
||||
let op_str = json.op.as_str();
|
||||
match op_str {
|
||||
"mint" | "transfer" => {
|
||||
let Some(parsed_amt) = parse_float_numeric_value(&json.amt, 18) else {
|
||||
return Ok(None);
|
||||
};
|
||||
if parsed_amt == 0.0 {
|
||||
return Ok(None);
|
||||
}
|
||||
match op_str {
|
||||
"mint" => {
|
||||
return Ok(Some(ParsedBrc20Operation::Mint(
|
||||
ParsedBrc20BalanceData {
|
||||
tick: json.tick.to_lowercase(),
|
||||
amt: json.amt.clone(),
|
||||
},
|
||||
)));
|
||||
}
|
||||
"transfer" => {
|
||||
return Ok(Some(ParsedBrc20Operation::Transfer(
|
||||
ParsedBrc20BalanceData {
|
||||
tick: json.tick.to_lowercase(),
|
||||
amt: json.amt.clone(),
|
||||
},
|
||||
)));
|
||||
}
|
||||
_ => return Ok(None),
|
||||
}
|
||||
}
|
||||
_ => return Ok(None),
|
||||
}
|
||||
}
|
||||
Err(_) => return Ok(None),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{parse_brc20_operation, ParsedBrc20Operation};
|
||||
use crate::{
|
||||
core::meta_protocols::brc20::parser::{ParsedBrc20BalanceData, ParsedBrc20TokenDeployData},
|
||||
ord::inscription::Inscription,
|
||||
};
|
||||
use test_case::test_case;
|
||||
|
||||
struct InscriptionBuilder {
|
||||
body: Option<Vec<u8>>,
|
||||
content_encoding: Option<Vec<u8>>,
|
||||
content_type: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl InscriptionBuilder {
|
||||
fn new() -> Self {
|
||||
InscriptionBuilder {
|
||||
body: Some(r#"{"p":"brc-20", "op": "deploy", "tick": "pepe", "max": "21000000", "lim": "1000", "dec": "6"}"#.as_bytes().to_vec()),
|
||||
content_encoding: Some("utf-8".as_bytes().to_vec()),
|
||||
content_type: Some("text/plain".as_bytes().to_vec()),
|
||||
}
|
||||
}
|
||||
|
||||
fn body(mut self, val: &str) -> Self {
|
||||
self.body = Some(val.as_bytes().to_vec());
|
||||
self
|
||||
}
|
||||
|
||||
fn content_type(mut self, val: &str) -> Self {
|
||||
self.content_type = Some(val.as_bytes().to_vec());
|
||||
self
|
||||
}
|
||||
|
||||
fn build(self) -> Inscription {
|
||||
Inscription {
|
||||
body: self.body,
|
||||
content_encoding: self.content_encoding,
|
||||
content_type: self.content_type,
|
||||
duplicate_field: false,
|
||||
incomplete_field: false,
|
||||
metadata: None,
|
||||
metaprotocol: None,
|
||||
parent: None,
|
||||
pointer: None,
|
||||
unrecognized_even_field: false,
|
||||
delegate: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().build()
|
||||
=> Ok(Some(ParsedBrc20Operation::Deploy(ParsedBrc20TokenDeployData {
|
||||
tick: "pepe".to_string(),
|
||||
max: 21000000.0,
|
||||
lim: 1000.0,
|
||||
dec: 6,
|
||||
self_mint: false,
|
||||
}))); "with deploy"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(&String::from("{\"p\":\"brc-20\",\"op\":\"deploy\",\"tick\":\"X\0\0Z\",\"max\":\"21000000\",\"lim\":\"1000\",\"dec\":\"6\"}")).build()
|
||||
=> Ok(None); "with deploy null bytes"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "deploy", "tick": "PEPE", "max": "21000000", "lim": "1000", "dec": "6"}"#).build()
|
||||
=> Ok(Some(ParsedBrc20Operation::Deploy(ParsedBrc20TokenDeployData {
|
||||
tick: "pepe".to_string(),
|
||||
max: 21000000.0,
|
||||
lim: 1000.0,
|
||||
dec: 6,
|
||||
self_mint: false,
|
||||
}))); "with deploy uppercase"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "deploy", "tick": "pepe", "max": "21000000", "lim": "1000"}"#).build()
|
||||
=> Ok(Some(ParsedBrc20Operation::Deploy(ParsedBrc20TokenDeployData {
|
||||
tick: "pepe".to_string(),
|
||||
max: 21000000.0,
|
||||
lim: 1000.0,
|
||||
dec: 18,
|
||||
self_mint: false,
|
||||
}))); "with deploy without dec"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "deploy", "tick": "pepe", "max": "21000000"}"#).build()
|
||||
=> Ok(Some(ParsedBrc20Operation::Deploy(ParsedBrc20TokenDeployData {
|
||||
tick: "pepe".to_string(),
|
||||
max: 21000000.0,
|
||||
lim: 21000000.0,
|
||||
dec: 18,
|
||||
self_mint: false,
|
||||
}))); "with deploy without lim or dec"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "deploy", "tick": "pepe", "max": "21000000", "dec": "7"}"#).build()
|
||||
=> Ok(Some(ParsedBrc20Operation::Deploy(ParsedBrc20TokenDeployData {
|
||||
tick: "pepe".to_string(),
|
||||
max: 21000000.0,
|
||||
lim: 21000000.0,
|
||||
dec: 7,
|
||||
self_mint: false,
|
||||
}))); "with deploy without lim"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "deploy", "tick": "😉", "max": "21000000", "lim": "1000", "dec": "6"}"#).build()
|
||||
=> Ok(Some(ParsedBrc20Operation::Deploy(ParsedBrc20TokenDeployData {
|
||||
tick: "😉".to_string(),
|
||||
max: 21000000.0,
|
||||
lim: 1000.0,
|
||||
dec: 6,
|
||||
self_mint: false,
|
||||
}))); "with deploy 4-byte emoji tick"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "deploy", "tick": "a b", "max": "21000000", "lim": "1000", "dec": "6"}"#).build()
|
||||
=> Ok(Some(ParsedBrc20Operation::Deploy(ParsedBrc20TokenDeployData {
|
||||
tick: "a b".to_string(),
|
||||
max: 21000000.0,
|
||||
lim: 1000.0,
|
||||
dec: 6,
|
||||
self_mint: false,
|
||||
}))); "with deploy 4-byte space tick"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "deploy", "tick": "$pepe", "max": "21000000", "lim": "1000", "dec": "6", "self_mint": "true"}"#).build()
|
||||
=> Ok(Some(ParsedBrc20Operation::Deploy(ParsedBrc20TokenDeployData {
|
||||
tick: "$pepe".to_string(),
|
||||
max: 21000000.0,
|
||||
lim: 1000.0,
|
||||
dec: 6,
|
||||
self_mint: true,
|
||||
}))); "with deploy 5-byte self mint"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "deploy", "tick": "$pepe", "max": "0", "lim": "1000", "dec": "6", "self_mint": "true"}"#).build()
|
||||
=> Ok(Some(ParsedBrc20Operation::Deploy(ParsedBrc20TokenDeployData {
|
||||
tick: "$pepe".to_string(),
|
||||
max: u64::MAX as f64,
|
||||
lim: 1000.0,
|
||||
dec: 6,
|
||||
self_mint: true,
|
||||
}))); "with deploy self mint max 0"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "deploy", "tick": "$pepe", "max": "21000000", "lim": "1000", "dec": "6"}"#).build()
|
||||
=> Ok(None); "with deploy 5-byte no self mint"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "deploy", "tick": "pepe", "max": "21000000", "lim": "1000", "dec": "6", "foo": 99}"#).build()
|
||||
=> Ok(Some(ParsedBrc20Operation::Deploy(ParsedBrc20TokenDeployData {
|
||||
tick: "pepe".to_string(),
|
||||
max: 21000000.0,
|
||||
lim: 1000.0,
|
||||
dec: 6,
|
||||
self_mint: false,
|
||||
}))); "with deploy extra fields"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().content_type("text/html").build()
|
||||
=> Ok(None); "with invalid content_type"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p": "brc-20", "op": "deploy", "tick": "PEPE", "max": "21000000""#).build()
|
||||
=> Ok(None); "with invalid JSON"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc20", "op": "deploy", "tick": "pepe", "max": "21000000", "lim": "1000", "dec": "6",}"#).build()
|
||||
=> Ok(None); "with deploy JSON5"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"P":"brc20", "OP": "deploy", "TICK": "pepe", "MAX": "21000000", "LIM": "1000", "DEC": "6"}"#).build()
|
||||
=> Ok(None); "with deploy uppercase fields"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc20", "op": "deploy", "tick": "pepe", "max": "21000000", "lim": "1000", "dec": "6"}"#).build()
|
||||
=> Ok(None); "with deploy incorrect p field"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "deploi", "tick": "pepe", "max": "21000000", "lim": "1000", "dec": "6"}"#).build()
|
||||
=> Ok(None); "with deploy incorrect op field"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "deploy", "tick": "pep", "max": "21000000", "lim": "1000", "dec": "6"}"#).build()
|
||||
=> Ok(None); "with deploy short tick length"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "deploy", "tick": "pepepepe", "max": "21000000", "lim": "1000", "dec": "6"}"#).build()
|
||||
=> Ok(None); "with deploy long tick length"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "deploy", "tick": "pepe", "max": "21000000.", "lim": "1000", "dec": "6"}"#).build()
|
||||
=> Ok(None); "with deploy malformatted max"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "deploy", "tick": "pepe", "max": "21000000", "lim": " 1000 ", "dec": "6"}"#).build()
|
||||
=> Ok(None); "with deploy malformatted lim"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "deploy", "tick": "pepe", "max": "21000000", "lim": "1000.", "dec": "6.0"}"#).build()
|
||||
=> Ok(None); "with deploy malformatted dec"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "deploy", "tick": "pepe", "max": 21000000, "lim": "1000", "dec": "6"}"#).build()
|
||||
=> Ok(None); "with deploy int max"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "deploy", "tick": "pepe", "max": "21000000", "lim": 1000, "dec": "6"}"#).build()
|
||||
=> Ok(None); "with deploy int lim"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "deploy", "tick": "pepe", "max": "21000000", "lim": "1000", "dec": 6}"#).build()
|
||||
=> Ok(None); "with deploy int dec"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "deploy", "tick": "pepe", "max": "", "lim": "1000", "dec": "6"}"#).build()
|
||||
=> Ok(None); "with deploy empty max"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "deploy", "tick": "pepe", "max": "21000000", "lim": "", "dec": "6"}"#).build()
|
||||
=> Ok(None); "with deploy empty lim"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "deploy", "tick": "pepe", "max": "21000000", "lim": "1000", "dec": ""}"#).build()
|
||||
=> Ok(None); "with deploy empty dec"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "deploy", "tick": "pepe", "max": "99996744073709551615", "lim": "1000", "dec": "6"}"#).build()
|
||||
=> Ok(None); "with deploy large max"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "deploy", "tick": "pepe", "max": "21000000", "lim": "99996744073709551615", "dec": "6"}"#).build()
|
||||
=> Ok(None); "with deploy large lim"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "deploy", "tick": "pepe", "max": "21000000", "lim": "1000", "dec": "99996744073709551615"}"#).build()
|
||||
=> Ok(None); "with deploy large dec"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "deploy", "tick": "pepe", "max": "0", "lim": "1000", "dec": "6"}"#).build()
|
||||
=> Ok(None); "with deploy zero max"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "deploy", "tick": "pepe", "max": "21000000", "lim": "0", "dec": "6"}"#).build()
|
||||
=> Ok(None); "with deploy zero lim"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "deploy", "tick": "pepe", "max": "21000000", "lim": "1000", "dec": "0"}"#).build()
|
||||
=> Ok(Some(ParsedBrc20Operation::Deploy(ParsedBrc20TokenDeployData {
|
||||
tick: "pepe".to_string(),
|
||||
max: 21000000.0,
|
||||
lim: 1000.0,
|
||||
dec: 0,
|
||||
self_mint: false,
|
||||
}))); "with deploy zero dec"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "deploy", "tick": "pepe", "max": "21000000.000", "lim": "1000", "dec": "0"}"#).build()
|
||||
=> Ok(None); "with deploy extra max decimals"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "deploy", "tick": "pepe", "max": "21000000", "lim": "1000.000", "dec": "0"}"#).build()
|
||||
=> Ok(None); "with deploy extra lim decimals"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "mint", "tick": "pepe", "amt": "1000"}"#).build()
|
||||
=> Ok(Some(ParsedBrc20Operation::Mint(ParsedBrc20BalanceData {
|
||||
tick: "pepe".to_string(),
|
||||
amt: "1000".to_string()
|
||||
}))); "with mint"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "mint", "tick": "PEPE", "amt": "1000"}"#).build()
|
||||
=> Ok(Some(ParsedBrc20Operation::Mint(ParsedBrc20BalanceData {
|
||||
tick: "pepe".to_string(),
|
||||
amt: "1000".to_string()
|
||||
}))); "with mint uppercase"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "mint", "tick": "😉", "amt": "1000"}"#).build()
|
||||
=> Ok(Some(ParsedBrc20Operation::Mint(ParsedBrc20BalanceData {
|
||||
tick: "😉".to_string(),
|
||||
amt: "1000".to_string()
|
||||
}))); "with mint 4-byte emoji tick"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "mint", "tick": "$pepe", "amt": "1000"}"#).build()
|
||||
=> Ok(Some(ParsedBrc20Operation::Mint(ParsedBrc20BalanceData {
|
||||
tick: "$pepe".to_string(),
|
||||
amt: "1000".to_string()
|
||||
}))); "with mint 5-byte tick"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "mint", "tick": "a b", "amt": "1000"}"#).build()
|
||||
=> Ok(Some(ParsedBrc20Operation::Mint(ParsedBrc20BalanceData {
|
||||
tick: "a b".to_string(),
|
||||
amt: "1000".to_string()
|
||||
}))); "with mint 4-byte space tick"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "mint", "tick": "a b", "amt": "1000", "bar": "test"}"#).build()
|
||||
=> Ok(Some(ParsedBrc20Operation::Mint(ParsedBrc20BalanceData {
|
||||
tick: "a b".to_string(),
|
||||
amt: "1000".to_string()
|
||||
}))); "with mint extra fields"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "mint", "tick": "a b", "amt": "1000",}"#).build()
|
||||
=> Ok(None); "with mint JSON5"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"P":"brc-20", "OP": "mint", "TICK": "a b", "AMT": "1000"}"#).build()
|
||||
=> Ok(None); "with mint uppercase fields"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc20", "op": "mint", "tick": "pepe", "amt": "1000"}"#).build()
|
||||
=> Ok(None); "with mint incorrect p field"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "mintt", "tick": "pepe", "amt": "1000"}"#).build()
|
||||
=> Ok(None); "with mint incorrect op field"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "mint", "tick": "pepe"}"#).build()
|
||||
=> Ok(None); "with mint without amt"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "mint", "tick": "pep", "amt": "1000"}"#).build()
|
||||
=> Ok(None); "with mint short tick length"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "mint", "tick": "pepepepe", "amt": "1000"}"#).build()
|
||||
=> Ok(None); "with mint long tick length"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "mint", "tick": "pepe", "amt": 1000}"#).build()
|
||||
=> Ok(None); "with mint int amt"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "mint", "tick": "pepe", "amt": ""}"#).build()
|
||||
=> Ok(None); "with mint empty amt"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "mint", "tick": "pepe", "amt": "0"}"#).build()
|
||||
=> Ok(None); "with mint zero amt"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "mint", "tick": "pepe", "amt": "99996744073709551615"}"#).build()
|
||||
=> Ok(None); "with mint large amt"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "transfer", "tick": "pepe", "amt": "1000"}"#).build()
|
||||
=> Ok(Some(ParsedBrc20Operation::Transfer(ParsedBrc20BalanceData {
|
||||
tick: "pepe".to_string(),
|
||||
amt: "1000".to_string()
|
||||
}))); "with transfer"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "transfer", "tick": "PEPE", "amt": "1000"}"#).build()
|
||||
=> Ok(Some(ParsedBrc20Operation::Transfer(ParsedBrc20BalanceData {
|
||||
tick: "pepe".to_string(),
|
||||
amt: "1000".to_string()
|
||||
}))); "with transfer uppercase"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "transfer", "tick": "😉", "amt": "1000"}"#).build()
|
||||
=> Ok(Some(ParsedBrc20Operation::Transfer(ParsedBrc20BalanceData {
|
||||
tick: "😉".to_string(),
|
||||
amt: "1000".to_string()
|
||||
}))); "with transfer 4-byte emoji tick"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "transfer", "tick": "$pepe", "amt": "1000"}"#).build()
|
||||
=> Ok(Some(ParsedBrc20Operation::Transfer(ParsedBrc20BalanceData {
|
||||
tick: "$pepe".to_string(),
|
||||
amt: "1000".to_string()
|
||||
}))); "with transfer 5-byte tick"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "transfer", "tick": "a b", "amt": "1000"}"#).build()
|
||||
=> Ok(Some(ParsedBrc20Operation::Transfer(ParsedBrc20BalanceData {
|
||||
tick: "a b".to_string(),
|
||||
amt: "1000".to_string()
|
||||
}))); "with transfer 4-byte space tick"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "transfer", "tick": "a b", "amt": "1000", "bar": "test"}"#).build()
|
||||
=> Ok(Some(ParsedBrc20Operation::Transfer(ParsedBrc20BalanceData {
|
||||
tick: "a b".to_string(),
|
||||
amt: "1000".to_string()
|
||||
}))); "with transfer extra fields"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "transfer", "tick": "pepe", "amt": "1000",}"#).build()
|
||||
=> Ok(None); "with transfer JSON5"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"P":"brc-20", "OP": "transfer", "TICK": "a b", "AMT": "1000"}"#).build()
|
||||
=> Ok(None); "with transfer uppercase fields"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc20", "op": "transfer", "tick": "pepe", "amt": "1000"}"#).build()
|
||||
=> Ok(None); "with transfer incorrect p field"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "transferzz", "tick": "pepe", "amt": "1000"}"#).build()
|
||||
=> Ok(None); "with transfer incorrect op field"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "transfer", "tick": "pepe"}"#).build()
|
||||
=> Ok(None); "with transfer without amt"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "transfer", "tick": "pep", "amt": "1000"}"#).build()
|
||||
=> Ok(None); "with transfer short tick length"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "transfer", "tick": "pepepepe", "amt": "1000"}"#).build()
|
||||
=> Ok(None); "with transfer long tick length"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "transfer", "tick": "pepe", "amt": 1000}"#).build()
|
||||
=> Ok(None); "with transfer int amt"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "transfer", "tick": "pepe", "amt": ""}"#).build()
|
||||
=> Ok(None); "with transfer empty amt"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "transfer", "tick": "pepe", "amt": "0"}"#).build()
|
||||
=> Ok(None); "with transfer zero amt"
|
||||
)]
|
||||
#[test_case(
|
||||
InscriptionBuilder::new().body(r#"{"p":"brc-20", "op": "transfer", "tick": "pepe", "amt": "99996744073709551615"}"#).build()
|
||||
=> Ok(None); "with transfer large amt"
|
||||
)]
|
||||
fn test_brc20_parse(inscription: Inscription) -> Result<Option<ParsedBrc20Operation>, String> {
|
||||
parse_brc20_operation(&inscription)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
use chainhook_sdk::{types::{OrdinalInscriptionNumber, OrdinalInscriptionRevealData, OrdinalInscriptionTransferData, OrdinalInscriptionTransferDestination}, utils::Context};
|
||||
|
||||
pub fn get_test_ctx() -> Context {
|
||||
let logger = hiro_system_kit::log::setup_logger();
|
||||
let _guard = hiro_system_kit::log::setup_global_logger(logger.clone());
|
||||
Context {
|
||||
logger: Some(logger),
|
||||
tracer: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Brc20RevealBuilder {
|
||||
pub inscription_number: OrdinalInscriptionNumber,
|
||||
pub inscriber_address: Option<String>,
|
||||
pub inscription_id: String,
|
||||
pub ordinal_number: u64,
|
||||
pub parent: Option<String>,
|
||||
}
|
||||
|
||||
impl Brc20RevealBuilder {
|
||||
pub fn new() -> Self {
|
||||
Brc20RevealBuilder {
|
||||
inscription_number: OrdinalInscriptionNumber {
|
||||
classic: 0,
|
||||
jubilee: 0,
|
||||
},
|
||||
inscriber_address: Some("324A7GHA2azecbVBAFy4pzEhcPT1GjbUAp".to_string()),
|
||||
inscription_id:
|
||||
"9bb2314d666ae0b1db8161cb373fcc1381681f71445c4e0335aa80ea9c37fcddi0".to_string(),
|
||||
ordinal_number: 0,
|
||||
parent: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inscription_number(mut self, val: i64) -> Self {
|
||||
self.inscription_number = OrdinalInscriptionNumber {
|
||||
classic: val,
|
||||
jubilee: val,
|
||||
};
|
||||
self
|
||||
}
|
||||
|
||||
pub fn inscriber_address(mut self, val: Option<String>) -> Self {
|
||||
self.inscriber_address = val;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn inscription_id(mut self, val: &str) -> Self {
|
||||
self.inscription_id = val.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn ordinal_number(mut self, val: u64) -> Self {
|
||||
self.ordinal_number = val;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn parent(mut self, val: Option<String>) -> Self {
|
||||
self.parent = val;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> OrdinalInscriptionRevealData {
|
||||
OrdinalInscriptionRevealData {
|
||||
content_bytes: "".to_string(),
|
||||
content_type: "text/plain".to_string(),
|
||||
content_length: 10,
|
||||
inscription_number: self.inscription_number,
|
||||
inscription_fee: 100,
|
||||
inscription_output_value: 10000,
|
||||
inscription_id: self.inscription_id,
|
||||
inscription_input_index: 0,
|
||||
inscription_pointer: None,
|
||||
inscriber_address: self.inscriber_address,
|
||||
delegate: None,
|
||||
metaprotocol: None,
|
||||
metadata: None,
|
||||
parent: self.parent,
|
||||
ordinal_number: self.ordinal_number,
|
||||
ordinal_block_height: 767430,
|
||||
ordinal_offset: 0,
|
||||
tx_index: 0,
|
||||
transfers_pre_inscription: 0,
|
||||
satpoint_post_inscription:
|
||||
"9bb2314d666ae0b1db8161cb373fcc1381681f71445c4e0335aa80ea9c37fcdd:0:0"
|
||||
.to_string(),
|
||||
curse_type: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Brc20TransferBuilder {
|
||||
pub ordinal_number: u64,
|
||||
pub destination: OrdinalInscriptionTransferDestination,
|
||||
}
|
||||
|
||||
impl Brc20TransferBuilder {
|
||||
pub fn new() -> Self {
|
||||
Brc20TransferBuilder {
|
||||
ordinal_number: 0,
|
||||
destination: OrdinalInscriptionTransferDestination::Transferred(
|
||||
"bc1pls75sfwullhygkmqap344f5cqf97qz95lvle6fvddm0tpz2l5ffslgq3m0".to_string(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ordinal_number(mut self, val: u64) -> Self {
|
||||
self.ordinal_number = val;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn destination(mut self, val: OrdinalInscriptionTransferDestination) -> Self {
|
||||
self.destination = val;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> OrdinalInscriptionTransferData {
|
||||
OrdinalInscriptionTransferData {
|
||||
ordinal_number: self.ordinal_number,
|
||||
destination: self.destination,
|
||||
satpoint_pre_transfer: "".to_string(),
|
||||
satpoint_post_transfer: "".to_string(),
|
||||
post_transfer_output_value: Some(500),
|
||||
tx_index: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,996 @@
|
||||
use chainhook_sdk::types::{
|
||||
BitcoinNetwork, BlockIdentifier, OrdinalInscriptionRevealData, OrdinalInscriptionTransferData,
|
||||
OrdinalInscriptionTransferDestination,
|
||||
};
|
||||
use chainhook_sdk::utils::Context;
|
||||
use rusqlite::Transaction;
|
||||
|
||||
use super::brc20_self_mint_activation_height;
|
||||
use super::cache::Brc20MemoryCache;
|
||||
use super::parser::{amt_has_valid_decimals, ParsedBrc20Operation};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct VerifiedBrc20TokenDeployData {
|
||||
pub tick: String,
|
||||
pub max: f64,
|
||||
pub lim: f64,
|
||||
pub dec: u64,
|
||||
pub address: String,
|
||||
pub self_mint: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct VerifiedBrc20BalanceData {
|
||||
pub tick: String,
|
||||
pub amt: f64,
|
||||
pub address: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct VerifiedBrc20TransferData {
|
||||
pub tick: String,
|
||||
pub amt: f64,
|
||||
pub sender_address: String,
|
||||
pub receiver_address: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub enum VerifiedBrc20Operation {
|
||||
TokenDeploy(VerifiedBrc20TokenDeployData),
|
||||
TokenMint(VerifiedBrc20BalanceData),
|
||||
TokenTransfer(VerifiedBrc20BalanceData),
|
||||
TokenTransferSend(VerifiedBrc20TransferData),
|
||||
}
|
||||
|
||||
pub fn verify_brc20_operation(
|
||||
operation: &ParsedBrc20Operation,
|
||||
reveal: &OrdinalInscriptionRevealData,
|
||||
block_identifier: &BlockIdentifier,
|
||||
network: &BitcoinNetwork,
|
||||
cache: &mut Brc20MemoryCache,
|
||||
db_tx: &Transaction,
|
||||
ctx: &Context,
|
||||
) -> Result<VerifiedBrc20Operation, String> {
|
||||
let Some(inscriber_address) = reveal.inscriber_address.clone() else {
|
||||
return Err(format!("Invalid inscriber address"));
|
||||
};
|
||||
if inscriber_address.is_empty() {
|
||||
return Err(format!("Empty inscriber address"));
|
||||
}
|
||||
if reveal.inscription_number.classic < 0 {
|
||||
return Err(format!("Inscription is cursed"));
|
||||
}
|
||||
match operation {
|
||||
ParsedBrc20Operation::Deploy(data) => {
|
||||
if cache.get_token(&data.tick, db_tx, ctx).is_some() {
|
||||
return Err(format!("Token {} already exists", &data.tick));
|
||||
}
|
||||
if data.self_mint && block_identifier.index < brc20_self_mint_activation_height(network)
|
||||
{
|
||||
return Err(format!(
|
||||
"Self-minted token deploy {} prohibited before activation height",
|
||||
&data.tick
|
||||
));
|
||||
}
|
||||
return Ok(VerifiedBrc20Operation::TokenDeploy(
|
||||
VerifiedBrc20TokenDeployData {
|
||||
tick: data.tick.clone(),
|
||||
max: data.max,
|
||||
lim: data.lim,
|
||||
dec: data.dec,
|
||||
address: inscriber_address,
|
||||
self_mint: data.self_mint,
|
||||
},
|
||||
));
|
||||
}
|
||||
ParsedBrc20Operation::Mint(data) => {
|
||||
let Some(token) = cache.get_token(&data.tick, db_tx, ctx) else {
|
||||
return Err(format!(
|
||||
"Token {} does not exist on mint attempt",
|
||||
&data.tick
|
||||
));
|
||||
};
|
||||
if data.tick.len() == 5 {
|
||||
let Some(parent) = &reveal.parent else {
|
||||
return Err(format!(
|
||||
"Attempting to mint self-minted token {} without a parent ref",
|
||||
&data.tick
|
||||
));
|
||||
};
|
||||
if parent != &token.inscription_id {
|
||||
return Err(format!(
|
||||
"Mint attempt for self-minted token {} does not point to deploy as parent",
|
||||
&data.tick
|
||||
));
|
||||
}
|
||||
}
|
||||
if data.float_amt() > token.lim {
|
||||
return Err(format!(
|
||||
"Cannot mint more than {} tokens for {}, attempted to mint {}",
|
||||
token.lim, token.tick, data.amt
|
||||
));
|
||||
}
|
||||
if !amt_has_valid_decimals(&data.amt, token.dec) {
|
||||
return Err(format!(
|
||||
"Invalid decimals in amt field for {} mint, attempting to mint {}",
|
||||
token.tick, data.amt
|
||||
));
|
||||
}
|
||||
let Some(minted_supply) = cache.get_token_minted_supply(&data.tick, db_tx, ctx) else {
|
||||
unreachable!("BRC-20 token exists but does not have entries in the ledger");
|
||||
};
|
||||
let remaining_supply = token.max - minted_supply;
|
||||
if remaining_supply == 0.0 {
|
||||
return Err(format!(
|
||||
"No supply available for {} mint, attempted to mint {}, remaining {}",
|
||||
token.tick, data.amt, remaining_supply
|
||||
));
|
||||
}
|
||||
let real_mint_amt = data.float_amt().min(token.lim.min(remaining_supply));
|
||||
return Ok(VerifiedBrc20Operation::TokenMint(
|
||||
VerifiedBrc20BalanceData {
|
||||
tick: token.tick,
|
||||
amt: real_mint_amt,
|
||||
address: inscriber_address,
|
||||
},
|
||||
));
|
||||
}
|
||||
ParsedBrc20Operation::Transfer(data) => {
|
||||
let Some(token) = cache.get_token(&data.tick, db_tx, ctx) else {
|
||||
return Err(format!(
|
||||
"Token {} does not exist on transfer attempt",
|
||||
&data.tick
|
||||
));
|
||||
};
|
||||
if !amt_has_valid_decimals(&data.amt, token.dec) {
|
||||
return Err(format!(
|
||||
"Invalid decimals in amt field for {} transfer, attempting to transfer {}",
|
||||
token.tick, data.amt
|
||||
));
|
||||
}
|
||||
let Some(avail_balance) = cache.get_token_address_avail_balance(
|
||||
&token.tick,
|
||||
&inscriber_address,
|
||||
db_tx,
|
||||
ctx,
|
||||
) else {
|
||||
return Err(format!("Balance does not exist for {} transfer, attempting to transfer {}", token.tick, data.amt));
|
||||
};
|
||||
if avail_balance < data.float_amt() {
|
||||
return Err(format!("Insufficient balance for {} transfer, attempting to transfer {}, only {} available", token.tick, data.amt, avail_balance));
|
||||
}
|
||||
return Ok(VerifiedBrc20Operation::TokenTransfer(
|
||||
VerifiedBrc20BalanceData {
|
||||
tick: token.tick,
|
||||
amt: data.float_amt(),
|
||||
address: inscriber_address,
|
||||
},
|
||||
));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn verify_brc20_transfer(
|
||||
transfer: &OrdinalInscriptionTransferData,
|
||||
cache: &mut Brc20MemoryCache,
|
||||
db_tx: &Transaction,
|
||||
ctx: &Context,
|
||||
) -> Result<VerifiedBrc20TransferData, String> {
|
||||
let Some(transfer_row) = cache.get_unsent_token_transfer(transfer.ordinal_number, db_tx, ctx)
|
||||
else {
|
||||
return Err(format!(
|
||||
"No BRC-20 transfer in ordinal {} or transfer already sent",
|
||||
transfer.ordinal_number
|
||||
));
|
||||
};
|
||||
match &transfer.destination {
|
||||
OrdinalInscriptionTransferDestination::Transferred(receiver_address) => {
|
||||
return Ok(VerifiedBrc20TransferData {
|
||||
tick: transfer_row.tick.clone(),
|
||||
amt: transfer_row.trans_balance,
|
||||
sender_address: transfer_row.address.clone(),
|
||||
receiver_address: receiver_address.to_string(),
|
||||
});
|
||||
}
|
||||
OrdinalInscriptionTransferDestination::SpentInFees => {
|
||||
return Ok(VerifiedBrc20TransferData {
|
||||
tick: transfer_row.tick.clone(),
|
||||
amt: transfer_row.trans_balance,
|
||||
sender_address: transfer_row.address.clone(),
|
||||
receiver_address: transfer_row.address.clone(), // Return to sender
|
||||
});
|
||||
}
|
||||
OrdinalInscriptionTransferDestination::Burnt(_) => {
|
||||
return Ok(VerifiedBrc20TransferData {
|
||||
tick: transfer_row.tick.clone(),
|
||||
amt: transfer_row.trans_balance,
|
||||
sender_address: transfer_row.address.clone(),
|
||||
receiver_address: "".to_string(),
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use chainhook_sdk::types::{
|
||||
BitcoinNetwork, BlockIdentifier, OrdinalInscriptionRevealData,
|
||||
OrdinalInscriptionTransferData, OrdinalInscriptionTransferDestination,
|
||||
};
|
||||
use test_case::test_case;
|
||||
|
||||
use crate::core::meta_protocols::brc20::{
|
||||
cache::Brc20MemoryCache,
|
||||
db::initialize_brc20_db,
|
||||
parser::{ParsedBrc20BalanceData, ParsedBrc20Operation, ParsedBrc20TokenDeployData},
|
||||
test_utils::{get_test_ctx, Brc20RevealBuilder, Brc20TransferBuilder},
|
||||
verifier::{
|
||||
VerifiedBrc20BalanceData, VerifiedBrc20Operation, VerifiedBrc20TokenDeployData,
|
||||
},
|
||||
};
|
||||
|
||||
use super::{verify_brc20_operation, verify_brc20_transfer, VerifiedBrc20TransferData};
|
||||
|
||||
#[test_case(
|
||||
ParsedBrc20Operation::Deploy(ParsedBrc20TokenDeployData {
|
||||
tick: "pepe".to_string(),
|
||||
max: 21000000.0,
|
||||
lim: 1000.0,
|
||||
dec: 18,
|
||||
self_mint: false,
|
||||
}),
|
||||
(Brc20RevealBuilder::new().inscriber_address(None).build(), 830000)
|
||||
=> Err("Invalid inscriber address".to_string()); "with invalid address"
|
||||
)]
|
||||
#[test_case(
|
||||
ParsedBrc20Operation::Deploy(ParsedBrc20TokenDeployData {
|
||||
tick: "$pepe".to_string(),
|
||||
max: 21000000.0,
|
||||
lim: 1000.0,
|
||||
dec: 18,
|
||||
self_mint: true,
|
||||
}),
|
||||
(Brc20RevealBuilder::new().build(), 830000)
|
||||
=> Err("Self-minted token deploy $pepe prohibited before activation height".to_string());
|
||||
"with self mint before activation"
|
||||
)]
|
||||
#[test_case(
|
||||
ParsedBrc20Operation::Deploy(ParsedBrc20TokenDeployData {
|
||||
tick: "$pepe".to_string(),
|
||||
max: 21000000.0,
|
||||
lim: 1000.0,
|
||||
dec: 18,
|
||||
self_mint: true,
|
||||
}),
|
||||
(Brc20RevealBuilder::new().build(), 840000)
|
||||
=> Ok(VerifiedBrc20Operation::TokenDeploy(VerifiedBrc20TokenDeployData {
|
||||
tick: "$pepe".to_string(),
|
||||
max: 21000000.0,
|
||||
lim: 1000.0,
|
||||
dec: 18,
|
||||
address: "324A7GHA2azecbVBAFy4pzEhcPT1GjbUAp".to_string(),
|
||||
self_mint: true,
|
||||
}));
|
||||
"with valid self mint"
|
||||
)]
|
||||
#[test_case(
|
||||
ParsedBrc20Operation::Deploy(ParsedBrc20TokenDeployData {
|
||||
tick: "pepe".to_string(),
|
||||
max: 21000000.0,
|
||||
lim: 1000.0,
|
||||
dec: 18,
|
||||
self_mint: false,
|
||||
}),
|
||||
(Brc20RevealBuilder::new().inscriber_address(Some("".to_string())).build(), 830000)
|
||||
=> Err("Empty inscriber address".to_string()); "with empty address"
|
||||
)]
|
||||
#[test_case(
|
||||
ParsedBrc20Operation::Deploy(ParsedBrc20TokenDeployData {
|
||||
tick: "pepe".to_string(),
|
||||
max: 21000000.0,
|
||||
lim: 1000.0,
|
||||
dec: 18,
|
||||
self_mint: false,
|
||||
}),
|
||||
(Brc20RevealBuilder::new().inscription_number(-1).build(), 830000)
|
||||
=> Err("Inscription is cursed".to_string()); "with cursed inscription"
|
||||
)]
|
||||
#[test_case(
|
||||
ParsedBrc20Operation::Deploy(ParsedBrc20TokenDeployData {
|
||||
tick: "pepe".to_string(),
|
||||
max: 21000000.0,
|
||||
lim: 1000.0,
|
||||
dec: 18,
|
||||
self_mint: false,
|
||||
}),
|
||||
(Brc20RevealBuilder::new().build(), 830000)
|
||||
=> Ok(
|
||||
VerifiedBrc20Operation::TokenDeploy(VerifiedBrc20TokenDeployData {
|
||||
tick: "pepe".to_string(),
|
||||
max: 21000000.0,
|
||||
lim: 1000.0,
|
||||
dec: 18,
|
||||
address: "324A7GHA2azecbVBAFy4pzEhcPT1GjbUAp".to_string(),
|
||||
self_mint: false,
|
||||
})
|
||||
); "with deploy"
|
||||
)]
|
||||
#[test_case(
|
||||
ParsedBrc20Operation::Mint(ParsedBrc20BalanceData {
|
||||
tick: "pepe".to_string(),
|
||||
amt: "1000.0".to_string(),
|
||||
}),
|
||||
(Brc20RevealBuilder::new().build(), 830000)
|
||||
=> Err("Token pepe does not exist on mint attempt".to_string());
|
||||
"with mint non existing token"
|
||||
)]
|
||||
#[test_case(
|
||||
ParsedBrc20Operation::Transfer(ParsedBrc20BalanceData {
|
||||
tick: "pepe".to_string(),
|
||||
amt: "1000.0".to_string(),
|
||||
}),
|
||||
(Brc20RevealBuilder::new().build(), 830000)
|
||||
=> Err("Token pepe does not exist on transfer attempt".to_string());
|
||||
"with transfer non existing token"
|
||||
)]
|
||||
fn test_brc20_verify_for_empty_db(
|
||||
op: ParsedBrc20Operation,
|
||||
args: (OrdinalInscriptionRevealData, u64),
|
||||
) -> Result<VerifiedBrc20Operation, String> {
|
||||
let ctx = get_test_ctx();
|
||||
let mut conn = initialize_brc20_db(None, &ctx);
|
||||
let tx = conn.transaction().unwrap();
|
||||
verify_brc20_operation(
|
||||
&op,
|
||||
&args.0,
|
||||
&BlockIdentifier {
|
||||
index: args.1,
|
||||
hash: "00000000000000000002d8ba402150b259ddb2b30a1d32ab4a881d4653bceb5b"
|
||||
.to_string(),
|
||||
},
|
||||
&BitcoinNetwork::Mainnet,
|
||||
&mut Brc20MemoryCache::new(50),
|
||||
&tx,
|
||||
&ctx,
|
||||
)
|
||||
}
|
||||
|
||||
#[test_case(
|
||||
ParsedBrc20Operation::Deploy(ParsedBrc20TokenDeployData {
|
||||
tick: "pepe".to_string(),
|
||||
max: 21000000.0,
|
||||
lim: 1000.0,
|
||||
dec: 18,
|
||||
self_mint: false,
|
||||
}),
|
||||
Brc20RevealBuilder::new().inscription_number(1).build()
|
||||
=> Err("Token pepe already exists".to_string()); "with deploy existing token"
|
||||
)]
|
||||
#[test_case(
|
||||
ParsedBrc20Operation::Mint(ParsedBrc20BalanceData {
|
||||
tick: "pepe".to_string(),
|
||||
amt: "1000.0".to_string(),
|
||||
}),
|
||||
Brc20RevealBuilder::new().inscription_number(1).build()
|
||||
=> Ok(VerifiedBrc20Operation::TokenMint(VerifiedBrc20BalanceData {
|
||||
tick: "pepe".to_string(),
|
||||
amt: 1000.0,
|
||||
address: "324A7GHA2azecbVBAFy4pzEhcPT1GjbUAp".to_string(),
|
||||
})); "with mint"
|
||||
)]
|
||||
#[test_case(
|
||||
ParsedBrc20Operation::Mint(ParsedBrc20BalanceData {
|
||||
tick: "pepe".to_string(),
|
||||
amt: "10000.0".to_string(),
|
||||
}),
|
||||
Brc20RevealBuilder::new().inscription_number(1).build()
|
||||
=> Err("Cannot mint more than 1000 tokens for pepe, attempted to mint 10000.0".to_string());
|
||||
"with mint over lim"
|
||||
)]
|
||||
#[test_case(
|
||||
ParsedBrc20Operation::Mint(ParsedBrc20BalanceData {
|
||||
tick: "pepe".to_string(),
|
||||
amt: "100.000000000000000000000".to_string(),
|
||||
}),
|
||||
Brc20RevealBuilder::new().inscription_number(1).build()
|
||||
=> Err("Invalid decimals in amt field for pepe mint, attempting to mint 100.000000000000000000000".to_string());
|
||||
"with mint invalid decimals"
|
||||
)]
|
||||
#[test_case(
|
||||
ParsedBrc20Operation::Transfer(ParsedBrc20BalanceData {
|
||||
tick: "pepe".to_string(),
|
||||
amt: "100.0".to_string(),
|
||||
}),
|
||||
Brc20RevealBuilder::new().inscription_number(1).build()
|
||||
=> Err("Insufficient balance for pepe transfer, attempting to transfer 100.0, only 0 available".to_string());
|
||||
"with transfer on zero balance"
|
||||
)]
|
||||
#[test_case(
|
||||
ParsedBrc20Operation::Transfer(ParsedBrc20BalanceData {
|
||||
tick: "pepe".to_string(),
|
||||
amt: "100.000000000000000000000".to_string(),
|
||||
}),
|
||||
Brc20RevealBuilder::new().inscription_number(1).build()
|
||||
=> Err("Invalid decimals in amt field for pepe transfer, attempting to transfer 100.000000000000000000000".to_string());
|
||||
"with transfer invalid decimals"
|
||||
)]
|
||||
fn test_brc20_verify_for_existing_token(
|
||||
op: ParsedBrc20Operation,
|
||||
reveal: OrdinalInscriptionRevealData,
|
||||
) -> Result<VerifiedBrc20Operation, String> {
|
||||
let ctx = get_test_ctx();
|
||||
let mut conn = initialize_brc20_db(None, &ctx);
|
||||
let tx = conn.transaction().unwrap();
|
||||
let block = BlockIdentifier {
|
||||
index: 835727,
|
||||
hash: "00000000000000000002d8ba402150b259ddb2b30a1d32ab4a881d4653bceb5b".to_string(),
|
||||
};
|
||||
let mut cache = Brc20MemoryCache::new(10);
|
||||
cache.insert_token_deploy(
|
||||
&VerifiedBrc20TokenDeployData {
|
||||
tick: "pepe".to_string(),
|
||||
max: 21000000.0,
|
||||
lim: 1000.0,
|
||||
dec: 18,
|
||||
address: "324A7GHA2azecbVBAFy4pzEhcPT1GjbUAp".to_string(),
|
||||
self_mint: false,
|
||||
},
|
||||
&Brc20RevealBuilder::new().inscription_number(0).build(),
|
||||
&block,
|
||||
0,
|
||||
&tx,
|
||||
&ctx,
|
||||
);
|
||||
verify_brc20_operation(
|
||||
&op,
|
||||
&reveal,
|
||||
&block,
|
||||
&BitcoinNetwork::Mainnet,
|
||||
&mut cache,
|
||||
&tx,
|
||||
&ctx,
|
||||
)
|
||||
}
|
||||
|
||||
#[test_case(
|
||||
ParsedBrc20Operation::Mint(ParsedBrc20BalanceData {
|
||||
tick: "$pepe".to_string(),
|
||||
amt: "100.00".to_string(),
|
||||
}),
|
||||
Brc20RevealBuilder::new().inscription_number(1).build()
|
||||
=> Err("Attempting to mint self-minted token $pepe without a parent ref".to_string());
|
||||
"with mint without parent pointer"
|
||||
)]
|
||||
#[test_case(
|
||||
ParsedBrc20Operation::Mint(ParsedBrc20BalanceData {
|
||||
tick: "$pepe".to_string(),
|
||||
amt: "100.00".to_string(),
|
||||
}),
|
||||
Brc20RevealBuilder::new().inscription_number(1).parent(Some("test".to_string())).build()
|
||||
=> Err("Mint attempt for self-minted token $pepe does not point to deploy as parent".to_string());
|
||||
"with mint with wrong parent pointer"
|
||||
)]
|
||||
#[test_case(
|
||||
ParsedBrc20Operation::Mint(ParsedBrc20BalanceData {
|
||||
tick: "$pepe".to_string(),
|
||||
amt: "100.00".to_string(),
|
||||
}),
|
||||
Brc20RevealBuilder::new()
|
||||
.inscription_number(1)
|
||||
.parent(Some("9bb2314d666ae0b1db8161cb373fcc1381681f71445c4e0335aa80ea9c37fcddi0".to_string()))
|
||||
.build()
|
||||
=> Ok(VerifiedBrc20Operation::TokenMint(VerifiedBrc20BalanceData {
|
||||
tick: "$pepe".to_string(),
|
||||
amt: 100.0,
|
||||
address: "324A7GHA2azecbVBAFy4pzEhcPT1GjbUAp".to_string()
|
||||
}));
|
||||
"with mint with valid parent"
|
||||
)]
|
||||
fn test_brc20_verify_for_existing_self_mint_token(
|
||||
op: ParsedBrc20Operation,
|
||||
reveal: OrdinalInscriptionRevealData,
|
||||
) -> Result<VerifiedBrc20Operation, String> {
|
||||
let ctx = get_test_ctx();
|
||||
let mut conn = initialize_brc20_db(None, &ctx);
|
||||
let tx = conn.transaction().unwrap();
|
||||
let block = BlockIdentifier {
|
||||
index: 840000,
|
||||
hash: "00000000000000000002d8ba402150b259ddb2b30a1d32ab4a881d4653bceb5b".to_string(),
|
||||
};
|
||||
let mut cache = Brc20MemoryCache::new(10);
|
||||
cache.insert_token_deploy(
|
||||
&VerifiedBrc20TokenDeployData {
|
||||
tick: "$pepe".to_string(),
|
||||
max: 21000000.0,
|
||||
lim: 1000.0,
|
||||
dec: 18,
|
||||
address: "324A7GHA2azecbVBAFy4pzEhcPT1GjbUAp".to_string(),
|
||||
self_mint: true,
|
||||
},
|
||||
&Brc20RevealBuilder::new().inscription_number(0).build(),
|
||||
&block,
|
||||
0,
|
||||
&tx,
|
||||
&ctx,
|
||||
);
|
||||
verify_brc20_operation(
|
||||
&op,
|
||||
&reveal,
|
||||
&block,
|
||||
&BitcoinNetwork::Mainnet,
|
||||
&mut cache,
|
||||
&tx,
|
||||
&ctx,
|
||||
)
|
||||
}
|
||||
|
||||
#[test_case(
|
||||
ParsedBrc20Operation::Mint(ParsedBrc20BalanceData {
|
||||
tick: "pepe".to_string(),
|
||||
amt: "1000.0".to_string(),
|
||||
}),
|
||||
Brc20RevealBuilder::new().inscription_number(2).build()
|
||||
=> Err("No supply available for pepe mint, attempted to mint 1000.0, remaining 0".to_string());
|
||||
"with mint on no more supply"
|
||||
)]
|
||||
fn test_brc20_verify_for_minted_out_token(
|
||||
op: ParsedBrc20Operation,
|
||||
reveal: OrdinalInscriptionRevealData,
|
||||
) -> Result<VerifiedBrc20Operation, String> {
|
||||
let ctx = get_test_ctx();
|
||||
let mut conn = initialize_brc20_db(None, &ctx);
|
||||
let tx = conn.transaction().unwrap();
|
||||
let block = BlockIdentifier {
|
||||
index: 835727,
|
||||
hash: "00000000000000000002d8ba402150b259ddb2b30a1d32ab4a881d4653bceb5b".to_string(),
|
||||
};
|
||||
let mut cache = Brc20MemoryCache::new(10);
|
||||
cache.insert_token_deploy(
|
||||
&VerifiedBrc20TokenDeployData {
|
||||
tick: "pepe".to_string(),
|
||||
max: 21000000.0,
|
||||
lim: 1000.0,
|
||||
dec: 18,
|
||||
address: "324A7GHA2azecbVBAFy4pzEhcPT1GjbUAp".to_string(),
|
||||
self_mint: false,
|
||||
},
|
||||
&Brc20RevealBuilder::new().inscription_number(0).build(),
|
||||
&block,
|
||||
0,
|
||||
&tx,
|
||||
&ctx,
|
||||
);
|
||||
cache.insert_token_mint(
|
||||
&VerifiedBrc20BalanceData {
|
||||
tick: "pepe".to_string(),
|
||||
amt: 21000000.0, // For testing
|
||||
address: "324A7GHA2azecbVBAFy4pzEhcPT1GjbUAp".to_string(),
|
||||
},
|
||||
&Brc20RevealBuilder::new().inscription_number(1).build(),
|
||||
&block,
|
||||
1,
|
||||
&tx,
|
||||
&ctx,
|
||||
);
|
||||
verify_brc20_operation(
|
||||
&op,
|
||||
&reveal,
|
||||
&block,
|
||||
&BitcoinNetwork::Mainnet,
|
||||
&mut cache,
|
||||
&tx,
|
||||
&ctx,
|
||||
)
|
||||
}
|
||||
|
||||
#[test_case(
|
||||
ParsedBrc20Operation::Mint(ParsedBrc20BalanceData {
|
||||
tick: "pepe".to_string(),
|
||||
amt: "1000.0".to_string(),
|
||||
}),
|
||||
Brc20RevealBuilder::new().inscription_number(2).build()
|
||||
=> Ok(VerifiedBrc20Operation::TokenMint(VerifiedBrc20BalanceData {
|
||||
tick: "pepe".to_string(),
|
||||
amt: 500.0,
|
||||
address: "324A7GHA2azecbVBAFy4pzEhcPT1GjbUAp".to_string(),
|
||||
})); "with mint on low supply"
|
||||
)]
|
||||
fn test_brc20_verify_for_almost_minted_out_token(
|
||||
op: ParsedBrc20Operation,
|
||||
reveal: OrdinalInscriptionRevealData,
|
||||
) -> Result<VerifiedBrc20Operation, String> {
|
||||
let ctx = get_test_ctx();
|
||||
let mut conn = initialize_brc20_db(None, &ctx);
|
||||
let tx = conn.transaction().unwrap();
|
||||
let block = BlockIdentifier {
|
||||
index: 835727,
|
||||
hash: "00000000000000000002d8ba402150b259ddb2b30a1d32ab4a881d4653bceb5b".to_string(),
|
||||
};
|
||||
let mut cache = Brc20MemoryCache::new(10);
|
||||
cache.insert_token_deploy(
|
||||
&VerifiedBrc20TokenDeployData {
|
||||
tick: "pepe".to_string(),
|
||||
max: 21000000.0,
|
||||
lim: 1000.0,
|
||||
dec: 18,
|
||||
address: "324A7GHA2azecbVBAFy4pzEhcPT1GjbUAp".to_string(),
|
||||
self_mint: false,
|
||||
},
|
||||
&Brc20RevealBuilder::new().inscription_number(0).build(),
|
||||
&block,
|
||||
0,
|
||||
&tx,
|
||||
&ctx,
|
||||
);
|
||||
cache.insert_token_mint(
|
||||
&VerifiedBrc20BalanceData {
|
||||
tick: "pepe".to_string(),
|
||||
amt: 21000000.0 - 500.0, // For testing
|
||||
address: "324A7GHA2azecbVBAFy4pzEhcPT1GjbUAp".to_string(),
|
||||
},
|
||||
&Brc20RevealBuilder::new().inscription_number(1).build(),
|
||||
&block,
|
||||
1,
|
||||
&tx,
|
||||
&ctx,
|
||||
);
|
||||
verify_brc20_operation(
|
||||
&op,
|
||||
&reveal,
|
||||
&block,
|
||||
&BitcoinNetwork::Mainnet,
|
||||
&mut cache,
|
||||
&tx,
|
||||
&ctx,
|
||||
)
|
||||
}
|
||||
|
||||
#[test_case(
|
||||
ParsedBrc20Operation::Mint(ParsedBrc20BalanceData {
|
||||
tick: "pepe".to_string(),
|
||||
amt: "1000.0".to_string(),
|
||||
}),
|
||||
Brc20RevealBuilder::new()
|
||||
.inscription_number(3)
|
||||
.inscription_id("04b29b646f6389154e4fa0f0761472c27b9f13a482c715d9976edc474c258bc7i0")
|
||||
.build()
|
||||
=> Ok(VerifiedBrc20Operation::TokenMint(VerifiedBrc20BalanceData {
|
||||
tick: "pepe".to_string(),
|
||||
amt: 1000.0,
|
||||
address: "324A7GHA2azecbVBAFy4pzEhcPT1GjbUAp".to_string(),
|
||||
})); "with mint on existing balance address 1"
|
||||
)]
|
||||
#[test_case(
|
||||
ParsedBrc20Operation::Mint(ParsedBrc20BalanceData {
|
||||
tick: "pepe".to_string(),
|
||||
amt: "1000.0".to_string(),
|
||||
}),
|
||||
Brc20RevealBuilder::new()
|
||||
.inscription_number(3)
|
||||
.inscription_id("04b29b646f6389154e4fa0f0761472c27b9f13a482c715d9976edc474c258bc7i0")
|
||||
.inscriber_address(Some("19aeyQe8hGDoA1MHmmh2oM5Bbgrs9Jx7yZ".to_string()))
|
||||
.build()
|
||||
=> Ok(VerifiedBrc20Operation::TokenMint(VerifiedBrc20BalanceData {
|
||||
tick: "pepe".to_string(),
|
||||
amt: 1000.0,
|
||||
address: "19aeyQe8hGDoA1MHmmh2oM5Bbgrs9Jx7yZ".to_string(),
|
||||
})); "with mint on existing balance address 2"
|
||||
)]
|
||||
#[test_case(
|
||||
ParsedBrc20Operation::Transfer(ParsedBrc20BalanceData {
|
||||
tick: "pepe".to_string(),
|
||||
amt: "500.0".to_string(),
|
||||
}),
|
||||
Brc20RevealBuilder::new()
|
||||
.inscription_number(3)
|
||||
.inscription_id("04b29b646f6389154e4fa0f0761472c27b9f13a482c715d9976edc474c258bc7i0")
|
||||
.build()
|
||||
=> Ok(VerifiedBrc20Operation::TokenTransfer(VerifiedBrc20BalanceData {
|
||||
tick: "pepe".to_string(),
|
||||
amt: 500.0,
|
||||
address: "324A7GHA2azecbVBAFy4pzEhcPT1GjbUAp".to_string(),
|
||||
})); "with transfer"
|
||||
)]
|
||||
#[test_case(
|
||||
ParsedBrc20Operation::Transfer(ParsedBrc20BalanceData {
|
||||
tick: "pepe".to_string(),
|
||||
amt: "1000".to_string(),
|
||||
}),
|
||||
Brc20RevealBuilder::new()
|
||||
.inscription_number(3)
|
||||
.inscription_id("04b29b646f6389154e4fa0f0761472c27b9f13a482c715d9976edc474c258bc7i0")
|
||||
.build()
|
||||
=> Ok(VerifiedBrc20Operation::TokenTransfer(VerifiedBrc20BalanceData {
|
||||
tick: "pepe".to_string(),
|
||||
amt: 1000.0,
|
||||
address: "324A7GHA2azecbVBAFy4pzEhcPT1GjbUAp".to_string(),
|
||||
})); "with transfer full balance"
|
||||
)]
|
||||
#[test_case(
|
||||
ParsedBrc20Operation::Transfer(ParsedBrc20BalanceData {
|
||||
tick: "pepe".to_string(),
|
||||
amt: "5000.0".to_string(),
|
||||
}),
|
||||
Brc20RevealBuilder::new()
|
||||
.inscription_number(3)
|
||||
.inscription_id("04b29b646f6389154e4fa0f0761472c27b9f13a482c715d9976edc474c258bc7i0")
|
||||
.build()
|
||||
=> Err("Insufficient balance for pepe transfer, attempting to transfer 5000.0, only 1000 available".to_string());
|
||||
"with transfer insufficient balance"
|
||||
)]
|
||||
fn test_brc20_verify_for_token_with_mints(
|
||||
op: ParsedBrc20Operation,
|
||||
reveal: OrdinalInscriptionRevealData,
|
||||
) -> Result<VerifiedBrc20Operation, String> {
|
||||
let ctx = get_test_ctx();
|
||||
let mut conn = initialize_brc20_db(None, &ctx);
|
||||
let tx = conn.transaction().unwrap();
|
||||
let block = BlockIdentifier {
|
||||
index: 835727,
|
||||
hash: "00000000000000000002d8ba402150b259ddb2b30a1d32ab4a881d4653bceb5b".to_string(),
|
||||
};
|
||||
let mut cache = Brc20MemoryCache::new(10);
|
||||
cache.insert_token_deploy(
|
||||
&VerifiedBrc20TokenDeployData {
|
||||
tick: "pepe".to_string(),
|
||||
max: 21000000.0,
|
||||
lim: 1000.0,
|
||||
dec: 18,
|
||||
address: "324A7GHA2azecbVBAFy4pzEhcPT1GjbUAp".to_string(),
|
||||
self_mint: false,
|
||||
},
|
||||
&Brc20RevealBuilder::new()
|
||||
.inscription_number(0)
|
||||
.inscription_id(
|
||||
"e45957c419f130cd5c88cdac3eb1caf2d118aee20c17b15b74a611be395a065di0",
|
||||
)
|
||||
.build(),
|
||||
&block,
|
||||
0,
|
||||
&tx,
|
||||
&ctx,
|
||||
);
|
||||
// Mint from 2 addresses
|
||||
cache.insert_token_mint(
|
||||
&VerifiedBrc20BalanceData {
|
||||
tick: "pepe".to_string(),
|
||||
amt: 1000.0,
|
||||
address: "324A7GHA2azecbVBAFy4pzEhcPT1GjbUAp".to_string(),
|
||||
},
|
||||
&Brc20RevealBuilder::new()
|
||||
.inscription_number(1)
|
||||
.inscription_id(
|
||||
"269d46f148733ce86153e3ec0e0a3c78780e9b07e90a07e11753f0e934a60724i0",
|
||||
)
|
||||
.build(),
|
||||
&block,
|
||||
1,
|
||||
&tx,
|
||||
&ctx,
|
||||
);
|
||||
cache.insert_token_mint(
|
||||
&VerifiedBrc20BalanceData {
|
||||
tick: "pepe".to_string(),
|
||||
amt: 1000.0,
|
||||
address: "19aeyQe8hGDoA1MHmmh2oM5Bbgrs9Jx7yZ".to_string(),
|
||||
},
|
||||
&Brc20RevealBuilder::new()
|
||||
.inscription_number(2)
|
||||
.inscription_id(
|
||||
"704b85a939c34ec9dbbf79c0ffc69ba09566d732dbf1af2c04de65b0697aa1f8i0",
|
||||
)
|
||||
.build(),
|
||||
&block,
|
||||
2,
|
||||
&tx,
|
||||
&ctx,
|
||||
);
|
||||
verify_brc20_operation(
|
||||
&op,
|
||||
&reveal,
|
||||
&block,
|
||||
&BitcoinNetwork::Mainnet,
|
||||
&mut cache,
|
||||
&tx,
|
||||
&ctx,
|
||||
)
|
||||
}
|
||||
|
||||
#[test_case(
|
||||
Brc20TransferBuilder::new().ordinal_number(5000).build()
|
||||
=> Ok(VerifiedBrc20TransferData {
|
||||
tick: "pepe".to_string(),
|
||||
amt: 500.0,
|
||||
sender_address: "324A7GHA2azecbVBAFy4pzEhcPT1GjbUAp".to_string(),
|
||||
receiver_address: "bc1pls75sfwullhygkmqap344f5cqf97qz95lvle6fvddm0tpz2l5ffslgq3m0".to_string(),
|
||||
});
|
||||
"with transfer"
|
||||
)]
|
||||
#[test_case(
|
||||
Brc20TransferBuilder::new()
|
||||
.ordinal_number(5000)
|
||||
.destination(OrdinalInscriptionTransferDestination::SpentInFees)
|
||||
.build()
|
||||
=> Ok(VerifiedBrc20TransferData {
|
||||
tick: "pepe".to_string(),
|
||||
amt: 500.0,
|
||||
sender_address: "324A7GHA2azecbVBAFy4pzEhcPT1GjbUAp".to_string(),
|
||||
receiver_address: "324A7GHA2azecbVBAFy4pzEhcPT1GjbUAp".to_string()
|
||||
});
|
||||
"with transfer spent as fee"
|
||||
)]
|
||||
#[test_case(
|
||||
Brc20TransferBuilder::new()
|
||||
.ordinal_number(5000)
|
||||
.destination(OrdinalInscriptionTransferDestination::Burnt("test".to_string()))
|
||||
.build()
|
||||
=> Ok(VerifiedBrc20TransferData {
|
||||
tick: "pepe".to_string(),
|
||||
amt: 500.0,
|
||||
sender_address: "324A7GHA2azecbVBAFy4pzEhcPT1GjbUAp".to_string(),
|
||||
receiver_address: "".to_string()
|
||||
});
|
||||
"with transfer burnt"
|
||||
)]
|
||||
#[test_case(
|
||||
Brc20TransferBuilder::new().ordinal_number(200).build()
|
||||
=> Err("No BRC-20 transfer in ordinal 200 or transfer already sent".to_string());
|
||||
"with transfer non existent"
|
||||
)]
|
||||
fn test_brc20_verify_transfer_for_token_with_mint_and_transfer(
|
||||
transfer: OrdinalInscriptionTransferData,
|
||||
) -> Result<VerifiedBrc20TransferData, String> {
|
||||
let ctx = get_test_ctx();
|
||||
let mut conn = initialize_brc20_db(None, &ctx);
|
||||
let tx = conn.transaction().unwrap();
|
||||
let block = BlockIdentifier {
|
||||
index: 835727,
|
||||
hash: "00000000000000000002d8ba402150b259ddb2b30a1d32ab4a881d4653bceb5b".to_string(),
|
||||
};
|
||||
let mut cache = Brc20MemoryCache::new(10);
|
||||
cache.insert_token_deploy(
|
||||
&VerifiedBrc20TokenDeployData {
|
||||
tick: "pepe".to_string(),
|
||||
max: 21000000.0,
|
||||
lim: 1000.0,
|
||||
dec: 18,
|
||||
address: "324A7GHA2azecbVBAFy4pzEhcPT1GjbUAp".to_string(),
|
||||
self_mint: false,
|
||||
},
|
||||
&Brc20RevealBuilder::new()
|
||||
.inscription_number(0)
|
||||
.inscription_id(
|
||||
"e45957c419f130cd5c88cdac3eb1caf2d118aee20c17b15b74a611be395a065di0",
|
||||
)
|
||||
.build(),
|
||||
&block,
|
||||
0,
|
||||
&tx,
|
||||
&ctx,
|
||||
);
|
||||
cache.insert_token_mint(
|
||||
&VerifiedBrc20BalanceData {
|
||||
tick: "pepe".to_string(),
|
||||
amt: 1000.0,
|
||||
address: "324A7GHA2azecbVBAFy4pzEhcPT1GjbUAp".to_string(),
|
||||
},
|
||||
&Brc20RevealBuilder::new()
|
||||
.inscription_number(1)
|
||||
.inscription_id(
|
||||
"269d46f148733ce86153e3ec0e0a3c78780e9b07e90a07e11753f0e934a60724i0",
|
||||
)
|
||||
.build(),
|
||||
&block,
|
||||
1,
|
||||
&tx,
|
||||
&ctx,
|
||||
);
|
||||
cache.insert_token_transfer(
|
||||
&VerifiedBrc20BalanceData {
|
||||
tick: "pepe".to_string(),
|
||||
amt: 500.0,
|
||||
address: "324A7GHA2azecbVBAFy4pzEhcPT1GjbUAp".to_string(),
|
||||
},
|
||||
&Brc20RevealBuilder::new()
|
||||
.inscription_number(2)
|
||||
.ordinal_number(5000)
|
||||
.inscription_id(
|
||||
"704b85a939c34ec9dbbf79c0ffc69ba09566d732dbf1af2c04de65b0697aa1f8i0",
|
||||
)
|
||||
.build(),
|
||||
&block,
|
||||
2,
|
||||
&tx,
|
||||
&ctx,
|
||||
);
|
||||
verify_brc20_transfer(&transfer, &mut cache, &tx, &ctx)
|
||||
}
|
||||
|
||||
#[test_case(
|
||||
Brc20TransferBuilder::new().ordinal_number(5000).build()
|
||||
=> Err("No BRC-20 transfer in ordinal 5000 or transfer already sent".to_string());
|
||||
"with transfer already sent"
|
||||
)]
|
||||
fn test_brc20_verify_transfer_for_token_with_mint_transfer_and_send(
|
||||
transfer: OrdinalInscriptionTransferData,
|
||||
) -> Result<VerifiedBrc20TransferData, String> {
|
||||
let ctx = get_test_ctx();
|
||||
let mut conn = initialize_brc20_db(None, &ctx);
|
||||
let tx = conn.transaction().unwrap();
|
||||
let block = BlockIdentifier {
|
||||
index: 835727,
|
||||
hash: "00000000000000000002d8ba402150b259ddb2b30a1d32ab4a881d4653bceb5b".to_string(),
|
||||
};
|
||||
let mut cache = Brc20MemoryCache::new(10);
|
||||
cache.insert_token_deploy(
|
||||
&VerifiedBrc20TokenDeployData {
|
||||
tick: "pepe".to_string(),
|
||||
max: 21000000.0,
|
||||
lim: 1000.0,
|
||||
dec: 18,
|
||||
address: "324A7GHA2azecbVBAFy4pzEhcPT1GjbUAp".to_string(),
|
||||
self_mint: false,
|
||||
},
|
||||
&Brc20RevealBuilder::new()
|
||||
.inscription_number(0)
|
||||
.inscription_id(
|
||||
"e45957c419f130cd5c88cdac3eb1caf2d118aee20c17b15b74a611be395a065di0",
|
||||
)
|
||||
.build(),
|
||||
&block,
|
||||
0,
|
||||
&tx,
|
||||
&ctx,
|
||||
);
|
||||
cache.insert_token_mint(
|
||||
&VerifiedBrc20BalanceData {
|
||||
tick: "pepe".to_string(),
|
||||
amt: 1000.0,
|
||||
address: "324A7GHA2azecbVBAFy4pzEhcPT1GjbUAp".to_string(),
|
||||
},
|
||||
&Brc20RevealBuilder::new()
|
||||
.inscription_number(1)
|
||||
.inscription_id(
|
||||
"269d46f148733ce86153e3ec0e0a3c78780e9b07e90a07e11753f0e934a60724i0",
|
||||
)
|
||||
.build(),
|
||||
&block,
|
||||
1,
|
||||
&tx,
|
||||
&ctx,
|
||||
);
|
||||
cache.insert_token_transfer(
|
||||
&VerifiedBrc20BalanceData {
|
||||
tick: "pepe".to_string(),
|
||||
amt: 500.0,
|
||||
address: "324A7GHA2azecbVBAFy4pzEhcPT1GjbUAp".to_string(),
|
||||
},
|
||||
&Brc20RevealBuilder::new()
|
||||
.inscription_number(2)
|
||||
.ordinal_number(5000)
|
||||
.inscription_id(
|
||||
"704b85a939c34ec9dbbf79c0ffc69ba09566d732dbf1af2c04de65b0697aa1f8i0",
|
||||
)
|
||||
.build(),
|
||||
&block,
|
||||
2,
|
||||
&tx,
|
||||
&ctx,
|
||||
);
|
||||
cache.insert_token_transfer_send(
|
||||
&VerifiedBrc20TransferData {
|
||||
tick: "pepe".to_string(),
|
||||
amt: 500.0,
|
||||
sender_address: "324A7GHA2azecbVBAFy4pzEhcPT1GjbUAp".to_string(),
|
||||
receiver_address: "bc1pls75sfwullhygkmqap344f5cqf97qz95lvle6fvddm0tpz2l5ffslgq3m0"
|
||||
.to_string(),
|
||||
},
|
||||
&Brc20TransferBuilder::new().ordinal_number(5000).build(),
|
||||
&block,
|
||||
3,
|
||||
&tx,
|
||||
&ctx,
|
||||
);
|
||||
verify_brc20_transfer(&transfer, &mut cache, &tx, &ctx)
|
||||
}
|
||||
}
|
||||
1
components/ordhook-core/src/core/meta_protocols/mod.rs
Normal file
1
components/ordhook-core/src/core/meta_protocols/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod brc20;
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod meta_protocols;
|
||||
pub mod pipeline;
|
||||
pub mod protocol;
|
||||
|
||||
@@ -13,7 +14,7 @@ use chainhook_sdk::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
config::{Config, LogConfig, ResourcesConfig},
|
||||
config::{Config, LogConfig, MetaProtocolsConfig, ResourcesConfig},
|
||||
db::{find_pinned_block_bytes_at_block_height, open_ordhook_db_conn_rocks_db_loop},
|
||||
};
|
||||
|
||||
@@ -30,6 +31,7 @@ pub struct OrdhookConfig {
|
||||
pub db_path: PathBuf,
|
||||
pub first_inscription_height: u64,
|
||||
pub logs: LogConfig,
|
||||
pub meta_protocols: MetaProtocolsConfig,
|
||||
}
|
||||
|
||||
pub fn new_traversals_cache(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
collections::{BTreeMap, HashMap},
|
||||
sync::Arc,
|
||||
thread::{sleep, JoinHandle},
|
||||
time::Duration,
|
||||
@@ -19,10 +19,12 @@ use std::hash::BuildHasherDefault;
|
||||
|
||||
use crate::{
|
||||
core::{
|
||||
meta_protocols::brc20::{cache::Brc20MemoryCache, db::open_readwrite_brc20_db_conn},
|
||||
pipeline::processors::block_archiving::store_compacted_blocks,
|
||||
protocol::{
|
||||
inscription_parsing::{
|
||||
get_inscriptions_revealed_in_block, get_inscriptions_transferred_in_block,
|
||||
parse_inscriptions_in_standardized_block,
|
||||
},
|
||||
inscription_sequencing::{
|
||||
augment_block_with_ordinals_inscriptions_data_and_write_to_db_tx,
|
||||
@@ -37,6 +39,7 @@ use crate::{
|
||||
get_any_entry_in_ordinal_activities, open_ordhook_db_conn_rocks_db_loop,
|
||||
open_readonly_ordhook_db_conn,
|
||||
},
|
||||
service::write_brc20_block_operations,
|
||||
};
|
||||
|
||||
use crate::db::{TransactionBytesCursor, TraversalResult};
|
||||
@@ -177,9 +180,18 @@ pub fn process_blocks(
|
||||
|
||||
let mut updated_blocks = vec![];
|
||||
|
||||
let mut brc20_db_conn_rw = match open_readwrite_brc20_db_conn(&ordhook_config.db_path, &ctx) {
|
||||
Ok(dbs) => dbs,
|
||||
Err(e) => {
|
||||
panic!("Unable to open readwrite connection: {e}");
|
||||
}
|
||||
};
|
||||
let mut brc20_cache = Brc20MemoryCache::new(ordhook_config.resources.brc20_lru_cache_size);
|
||||
|
||||
for _cursor in 0..next_blocks.len() {
|
||||
let inscriptions_db_tx: rusqlite::Transaction<'_> =
|
||||
inscriptions_db_conn_rw.transaction().unwrap();
|
||||
let brc20_db_tx = brc20_db_conn_rw.transaction().unwrap();
|
||||
|
||||
let mut block = next_blocks.remove(0);
|
||||
|
||||
@@ -205,6 +217,8 @@ pub fn process_blocks(
|
||||
&mut cache_l1,
|
||||
cache_l2,
|
||||
&inscriptions_db_tx,
|
||||
Some(&brc20_db_tx),
|
||||
&mut brc20_cache,
|
||||
ordhook_config,
|
||||
ctx,
|
||||
);
|
||||
@@ -235,16 +249,21 @@ pub fn process_blocks(
|
||||
)
|
||||
});
|
||||
let _ = inscriptions_db_tx.rollback();
|
||||
let _ = brc20_db_tx.rollback();
|
||||
} else {
|
||||
match inscriptions_db_tx.commit() {
|
||||
Ok(_) => {
|
||||
// ctx.try_log(|logger| {
|
||||
// info!(
|
||||
// logger,
|
||||
// "Updates saved for block {}", block.block_identifier.index,
|
||||
// )
|
||||
// });
|
||||
}
|
||||
Ok(_) => match brc20_db_tx.commit() {
|
||||
Ok(_) => {}
|
||||
Err(_) => {
|
||||
// delete_data_in_ordhook_db(
|
||||
// block.block_identifier.index,
|
||||
// block.block_identifier.index,
|
||||
// ordhook_config,
|
||||
// ctx,
|
||||
// );
|
||||
todo!()
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
ctx.try_log(|logger| {
|
||||
error!(
|
||||
@@ -273,9 +292,15 @@ pub fn process_block(
|
||||
cache_l1: &mut BTreeMap<(TransactionIdentifier, usize, u64), TraversalResult>,
|
||||
cache_l2: &Arc<DashMap<(u32, [u8; 8]), TransactionBytesCursor, BuildHasherDefault<FxHasher>>>,
|
||||
inscriptions_db_tx: &Transaction,
|
||||
brc20_db_tx: Option<&Transaction>,
|
||||
brc20_cache: &mut Brc20MemoryCache,
|
||||
ordhook_config: &OrdhookConfig,
|
||||
ctx: &Context,
|
||||
) -> Result<(), String> {
|
||||
// Parsed BRC20 ops will be deposited here for this block.
|
||||
let mut brc20_operation_map = HashMap::new();
|
||||
parse_inscriptions_in_standardized_block(block, &mut brc20_operation_map, &ctx);
|
||||
|
||||
let any_processable_transactions = parallelize_inscription_data_computations(
|
||||
&block,
|
||||
&next_blocks,
|
||||
@@ -306,5 +331,15 @@ pub fn process_block(
|
||||
// Handle transfers
|
||||
let _ = augment_block_with_ordinals_transfer_data(block, inscriptions_db_tx, true, &inner_ctx);
|
||||
|
||||
if let Some(brc20_db_tx) = brc20_db_tx {
|
||||
write_brc20_block_operations(
|
||||
&block,
|
||||
&mut brc20_operation_map,
|
||||
brc20_cache,
|
||||
&brc20_db_tx,
|
||||
&ctx,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -74,6 +74,7 @@ pub fn start_transfers_recomputing_processor(
|
||||
block,
|
||||
&inscriptions_db_tx,
|
||||
false,
|
||||
None,
|
||||
&ctx,
|
||||
);
|
||||
|
||||
|
||||
@@ -2,15 +2,17 @@ use chainhook_sdk::bitcoincore_rpc_json::bitcoin::Txid;
|
||||
use chainhook_sdk::indexer::bitcoin::BitcoinTransactionFullBreakdown;
|
||||
use chainhook_sdk::indexer::bitcoin::{standardize_bitcoin_block, BitcoinBlockFullBreakdown};
|
||||
use chainhook_sdk::types::{
|
||||
BitcoinBlockData, BitcoinNetwork, BitcoinTransactionData, OrdinalInscriptionCurseType,
|
||||
OrdinalInscriptionNumber, OrdinalInscriptionRevealData, OrdinalInscriptionTransferData,
|
||||
OrdinalOperation,
|
||||
BitcoinBlockData, BitcoinNetwork, BitcoinTransactionData, BlockIdentifier,
|
||||
OrdinalInscriptionCurseType, OrdinalInscriptionNumber, OrdinalInscriptionRevealData,
|
||||
OrdinalInscriptionTransferData, OrdinalOperation,
|
||||
};
|
||||
use chainhook_sdk::utils::Context;
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::core::meta_protocols::brc20::brc20_activation_height;
|
||||
use crate::core::meta_protocols::brc20::parser::{parse_brc20_operation, ParsedBrc20Operation};
|
||||
use crate::ord::envelope::{Envelope, ParsedEnvelope, RawEnvelope};
|
||||
use crate::ord::inscription::Inscription;
|
||||
use crate::ord::inscription_id::InscriptionId;
|
||||
@@ -20,7 +22,7 @@ pub fn parse_inscriptions_from_witness(
|
||||
input_index: usize,
|
||||
witness_bytes: Vec<Vec<u8>>,
|
||||
txid: &str,
|
||||
) -> Option<Vec<OrdinalInscriptionRevealData>> {
|
||||
) -> Option<Vec<(OrdinalInscriptionRevealData, Inscription)>> {
|
||||
// Efficient debugging: Isolate one specific transaction
|
||||
// if !txid.eq("aa2ab56587c7d6609c95157e6dff37c5c3fa6531702f41229a289a5613887077") {
|
||||
// return None
|
||||
@@ -103,14 +105,17 @@ pub fn parse_inscriptions_from_witness(
|
||||
satpoint_post_inscription: format!(""),
|
||||
curse_type,
|
||||
};
|
||||
inscriptions.push(reveal_data);
|
||||
inscriptions.push((reveal_data, envelope.payload));
|
||||
}
|
||||
Some(inscriptions)
|
||||
}
|
||||
|
||||
pub fn parse_inscriptions_from_standardized_tx(
|
||||
tx: &BitcoinTransactionData,
|
||||
_ctx: &Context,
|
||||
tx: &mut BitcoinTransactionData,
|
||||
block_identifier: &BlockIdentifier,
|
||||
network: &BitcoinNetwork,
|
||||
brc20_operation_map: &mut HashMap<String, ParsedBrc20Operation>,
|
||||
ctx: &Context,
|
||||
) -> Vec<OrdinalOperation> {
|
||||
let mut operations = vec![];
|
||||
for (input_index, input) in tx.metadata.inputs.iter().enumerate() {
|
||||
@@ -125,8 +130,21 @@ pub fn parse_inscriptions_from_standardized_tx(
|
||||
witness_bytes,
|
||||
tx.transaction_identifier.get_hash_bytes_str(),
|
||||
) {
|
||||
for inscription in inscriptions.into_iter() {
|
||||
operations.push(OrdinalOperation::InscriptionRevealed(inscription));
|
||||
for (reveal, inscription) in inscriptions.into_iter() {
|
||||
if block_identifier.index >= brc20_activation_height(&network) {
|
||||
match parse_brc20_operation(&inscription) {
|
||||
Ok(Some(op)) => {
|
||||
brc20_operation_map.insert(reveal.inscription_id.clone(), op);
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(e) => {
|
||||
ctx.try_log(|logger| {
|
||||
warn!(logger, "Error parsing BRC-20 operation: {}", e)
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
operations.push(OrdinalOperation::InscriptionRevealed(reveal));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -148,8 +166,8 @@ pub fn parse_inscriptions_in_raw_tx(
|
||||
if let Some(inscriptions) =
|
||||
parse_inscriptions_from_witness(input_index, witness_bytes, &tx.txid)
|
||||
{
|
||||
for inscription in inscriptions.into_iter() {
|
||||
operations.push(OrdinalOperation::InscriptionRevealed(inscription));
|
||||
for (reveal, _inscription) in inscriptions.into_iter() {
|
||||
operations.push(OrdinalOperation::InscriptionRevealed(reveal));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -197,9 +215,19 @@ pub fn parse_inscriptions_and_standardize_block(
|
||||
Ok(block)
|
||||
}
|
||||
|
||||
pub fn parse_inscriptions_in_standardized_block(block: &mut BitcoinBlockData, ctx: &Context) {
|
||||
pub fn parse_inscriptions_in_standardized_block(
|
||||
block: &mut BitcoinBlockData,
|
||||
brc20_operation_map: &mut HashMap<String, ParsedBrc20Operation>,
|
||||
ctx: &Context,
|
||||
) {
|
||||
for tx in block.transactions.iter_mut() {
|
||||
tx.metadata.ordinal_operations = parse_inscriptions_from_standardized_tx(tx, ctx);
|
||||
tx.metadata.ordinal_operations = parse_inscriptions_from_standardized_tx(
|
||||
tx,
|
||||
&block.block_identifier,
|
||||
&block.metadata.network,
|
||||
brc20_operation_map,
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,12 @@ use fxhash::FxHasher;
|
||||
use rusqlite::{Connection, Transaction};
|
||||
|
||||
use crate::{
|
||||
core::{resolve_absolute_pointer, OrdhookConfig},
|
||||
core::{
|
||||
meta_protocols::brc20::db::{
|
||||
augment_transaction_with_brc20_operation_data, get_brc20_operations_on_block,
|
||||
},
|
||||
resolve_absolute_pointer, OrdhookConfig,
|
||||
},
|
||||
db::{
|
||||
find_blessed_inscription_with_ordinal_number, find_nth_classic_neg_number_at_block_height,
|
||||
find_nth_classic_pos_number_at_block_height, find_nth_jubilee_number_at_block_height,
|
||||
@@ -311,7 +316,6 @@ pub fn parallelize_inscription_data_computations(
|
||||
let _ = tx.send(None);
|
||||
}
|
||||
|
||||
let ctx_moved = inner_ctx.clone();
|
||||
let _ = hiro_system_kit::thread_named("Garbage collection").spawn(move || {
|
||||
for handle in thread_pool_handles.into_iter() {
|
||||
let _ = handle.join();
|
||||
@@ -921,6 +925,7 @@ pub fn consolidate_block_with_pre_computed_ordinals_data(
|
||||
block: &mut BitcoinBlockData,
|
||||
inscriptions_db_tx: &Transaction,
|
||||
include_transfers: bool,
|
||||
brc20_db_conn: Option<&Connection>,
|
||||
ctx: &Context,
|
||||
) {
|
||||
let network = get_bitcoin_network(&block.metadata.network);
|
||||
@@ -944,6 +949,11 @@ pub fn consolidate_block_with_pre_computed_ordinals_data(
|
||||
}
|
||||
break results;
|
||||
};
|
||||
let mut brc20_token_map = HashMap::new();
|
||||
let mut brc20_block_ledger_map = match brc20_db_conn {
|
||||
Some(conn) => get_brc20_operations_on_block(&block.block_identifier, &conn, &ctx),
|
||||
None => HashMap::new(),
|
||||
};
|
||||
for (tx_index, tx) in block.transactions.iter_mut().enumerate() {
|
||||
// Add inscriptions data
|
||||
consolidate_transaction_with_pre_computed_inscription_data(
|
||||
@@ -970,5 +980,14 @@ pub fn consolidate_block_with_pre_computed_ordinals_data(
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
if let Some(brc20_db_conn) = brc20_db_conn {
|
||||
augment_transaction_with_brc20_operation_data(
|
||||
tx,
|
||||
&mut brc20_token_map,
|
||||
&mut brc20_block_ledger_map,
|
||||
&brc20_db_conn,
|
||||
&ctx,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::collections::HashSet;
|
||||
use chainhook_sdk::{
|
||||
bitcoincore_rpc_json::bitcoin::{Address, Network, ScriptBuf},
|
||||
types::{
|
||||
BitcoinBlockData, BitcoinNetwork, BitcoinTransactionData, OrdinalInscriptionTransferData,
|
||||
BitcoinBlockData, BitcoinTransactionData, OrdinalInscriptionTransferData,
|
||||
OrdinalInscriptionTransferDestination, OrdinalOperation, TransactionIdentifier,
|
||||
},
|
||||
utils::Context,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet, HashMap},
|
||||
collections::{BTreeMap, HashMap},
|
||||
io::{Read, Write},
|
||||
path::PathBuf,
|
||||
thread::sleep,
|
||||
@@ -16,14 +16,18 @@ use chainhook_sdk::{
|
||||
indexer::bitcoin::BitcoinBlockFullBreakdown,
|
||||
types::{
|
||||
BitcoinBlockData, BlockIdentifier, OrdinalInscriptionNumber, OrdinalInscriptionRevealData,
|
||||
OrdinalInscriptionTransferData, TransactionIdentifier,
|
||||
TransactionIdentifier,
|
||||
},
|
||||
utils::Context,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
core::protocol::inscription_parsing::{
|
||||
get_inscriptions_revealed_in_block, get_inscriptions_transferred_in_block,
|
||||
config::Config,
|
||||
core::{
|
||||
meta_protocols::brc20::db::{delete_activity_in_block_range, open_readwrite_brc20_db_conn},
|
||||
protocol::inscription_parsing::{
|
||||
get_inscriptions_revealed_in_block, get_inscriptions_transferred_in_block,
|
||||
},
|
||||
},
|
||||
ord::sat::Sat,
|
||||
};
|
||||
@@ -48,13 +52,13 @@ pub fn open_readwrite_ordhook_db_conn(
|
||||
ctx: &Context,
|
||||
) -> Result<Connection, String> {
|
||||
let db_path = get_default_ordhook_db_file_path(&base_dir);
|
||||
let conn = create_or_open_readwrite_db(&db_path, ctx);
|
||||
let conn = create_or_open_readwrite_db(Some(&db_path), ctx);
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
pub fn initialize_ordhook_db(base_dir: &PathBuf, ctx: &Context) -> Connection {
|
||||
let db_path = get_default_ordhook_db_file_path(&base_dir);
|
||||
let conn = create_or_open_readwrite_db(&db_path, ctx);
|
||||
let conn = create_or_open_readwrite_db(Some(&db_path), ctx);
|
||||
// TODO: introduce initial output
|
||||
if let Err(e) = conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS inscriptions (
|
||||
@@ -172,29 +176,37 @@ pub fn initialize_ordhook_db(base_dir: &PathBuf, ctx: &Context) -> Connection {
|
||||
conn
|
||||
}
|
||||
|
||||
pub fn create_or_open_readwrite_db(db_path: &PathBuf, ctx: &Context) -> Connection {
|
||||
let open_flags = match std::fs::metadata(&db_path) {
|
||||
Err(e) => {
|
||||
if e.kind() == std::io::ErrorKind::NotFound {
|
||||
// need to create
|
||||
if let Some(dirp) = PathBuf::from(&db_path).parent() {
|
||||
std::fs::create_dir_all(dirp).unwrap_or_else(|e| {
|
||||
ctx.try_log(|logger| error!(logger, "{}", e.to_string()));
|
||||
});
|
||||
pub fn create_or_open_readwrite_db(db_path: Option<&PathBuf>, ctx: &Context) -> Connection {
|
||||
let open_flags = if let Some(db_path) = db_path {
|
||||
match std::fs::metadata(&db_path) {
|
||||
Err(e) => {
|
||||
if e.kind() == std::io::ErrorKind::NotFound {
|
||||
// need to create
|
||||
if let Some(dirp) = PathBuf::from(&db_path).parent() {
|
||||
std::fs::create_dir_all(dirp).unwrap_or_else(|e| {
|
||||
ctx.try_log(|logger| error!(logger, "{}", e.to_string()));
|
||||
});
|
||||
}
|
||||
OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE
|
||||
} else {
|
||||
panic!("FATAL: could not stat {}", db_path.display());
|
||||
}
|
||||
OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE
|
||||
} else {
|
||||
panic!("FATAL: could not stat {}", db_path.display());
|
||||
}
|
||||
Ok(_md) => {
|
||||
// can just open
|
||||
OpenFlags::SQLITE_OPEN_READ_WRITE
|
||||
}
|
||||
}
|
||||
Ok(_md) => {
|
||||
// can just open
|
||||
OpenFlags::SQLITE_OPEN_READ_WRITE
|
||||
}
|
||||
} else {
|
||||
OpenFlags::SQLITE_OPEN_READ_WRITE
|
||||
};
|
||||
|
||||
let path = match db_path {
|
||||
Some(path) => path.to_str().unwrap(),
|
||||
None => ":memory:",
|
||||
};
|
||||
let conn = loop {
|
||||
match Connection::open_with_flags(&db_path, open_flags) {
|
||||
match Connection::open_with_flags(&path, open_flags) {
|
||||
Ok(conn) => break conn,
|
||||
Err(e) => {
|
||||
ctx.try_log(|logger| error!(logger, "{}", e.to_string()));
|
||||
@@ -1275,17 +1287,25 @@ pub fn remove_entries_from_locations_at_block_height(
|
||||
pub fn delete_data_in_ordhook_db(
|
||||
start_block: u64,
|
||||
end_block: u64,
|
||||
blocks_db_rw: &DB,
|
||||
inscriptions_db_conn_rw: &Connection,
|
||||
config: &Config,
|
||||
ctx: &Context,
|
||||
) -> Result<(), String> {
|
||||
let blocks_db = open_ordhook_db_conn_rocks_db_loop(
|
||||
true,
|
||||
&config.expected_cache_path(),
|
||||
config.resources.ulimit,
|
||||
config.resources.memory_available,
|
||||
ctx,
|
||||
);
|
||||
let inscriptions_db_conn_rw =
|
||||
open_readwrite_ordhook_db_conn(&config.expected_cache_path(), ctx)?;
|
||||
ctx.try_log(|logger| {
|
||||
info!(
|
||||
logger,
|
||||
"Deleting entries from block #{start_block} to block #{end_block}"
|
||||
)
|
||||
});
|
||||
delete_blocks_in_block_range(start_block as u32, end_block as u32, blocks_db_rw, &ctx);
|
||||
delete_blocks_in_block_range(start_block as u32, end_block as u32, &blocks_db, &ctx);
|
||||
ctx.try_log(|logger| {
|
||||
info!(
|
||||
logger,
|
||||
@@ -1295,9 +1315,19 @@ pub fn delete_data_in_ordhook_db(
|
||||
delete_inscriptions_in_block_range(
|
||||
start_block as u32,
|
||||
end_block as u32,
|
||||
inscriptions_db_conn_rw,
|
||||
&inscriptions_db_conn_rw,
|
||||
&ctx,
|
||||
);
|
||||
if config.meta_protocols.brc20 {
|
||||
let conn = open_readwrite_brc20_db_conn(&config.expected_cache_path(), ctx)?;
|
||||
delete_activity_in_block_range(start_block as u32, end_block as u32, &conn, &ctx);
|
||||
ctx.try_log(|logger| {
|
||||
info!(
|
||||
logger,
|
||||
"Deleting BRC-20 activity from block #{start_block} to block #{end_block}"
|
||||
)
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,9 @@ extern crate hiro_system_kit;
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
|
||||
extern crate serde;
|
||||
|
||||
pub extern crate chainhook_sdk;
|
||||
@@ -20,3 +23,25 @@ pub mod ord;
|
||||
pub mod scan;
|
||||
pub mod service;
|
||||
pub mod utils;
|
||||
|
||||
use core::meta_protocols::brc20::db::initialize_brc20_db;
|
||||
|
||||
use chainhook_sdk::utils::Context;
|
||||
use config::Config;
|
||||
use db::initialize_ordhook_db;
|
||||
use rusqlite::Connection;
|
||||
|
||||
pub struct DbConnections {
|
||||
pub ordhook: Connection,
|
||||
pub brc20: Option<Connection>,
|
||||
}
|
||||
|
||||
pub fn initialize_db(config: &Config, ctx: &Context) -> DbConnections {
|
||||
DbConnections {
|
||||
ordhook: initialize_ordhook_db(&config.expected_cache_path(), ctx),
|
||||
brc20: match config.meta_protocols.brc20 {
|
||||
true => Some(initialize_brc20_db(Some(&config.expected_cache_path()), ctx)),
|
||||
false => None
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use crate::config::Config;
|
||||
use crate::core::meta_protocols::brc20::brc20_activation_height;
|
||||
use crate::core::meta_protocols::brc20::db::open_readonly_brc20_db_conn;
|
||||
use crate::core::protocol::inscription_parsing::{
|
||||
get_inscriptions_revealed_in_block, get_inscriptions_transferred_in_block,
|
||||
parse_inscriptions_and_standardize_block,
|
||||
@@ -15,7 +17,9 @@ use chainhook_sdk::chainhooks::bitcoin::{
|
||||
evaluate_bitcoin_chainhooks_on_chain_event, handle_bitcoin_hook_action,
|
||||
BitcoinChainhookOccurrence, BitcoinTriggerChainhook,
|
||||
};
|
||||
use chainhook_sdk::chainhooks::types::BitcoinChainhookSpecification;
|
||||
use chainhook_sdk::chainhooks::types::{
|
||||
BitcoinChainhookSpecification, BitcoinPredicateType, OrdinalOperations,
|
||||
};
|
||||
use chainhook_sdk::indexer::bitcoin::{
|
||||
build_http_client, download_and_parse_block_with_retry, retrieve_block_hash_with_retry,
|
||||
};
|
||||
@@ -47,7 +51,7 @@ pub async fn scan_bitcoin_chainstate_via_rpc_using_predicate(
|
||||
};
|
||||
let mut floating_end_block = false;
|
||||
|
||||
let mut block_heights_to_scan = if let Some(ref blocks) = predicate_spec.blocks {
|
||||
let block_heights_to_scan_res = if let Some(ref blocks) = predicate_spec.blocks {
|
||||
BlockHeights::Blocks(blocks.clone()).get_sorted_entries()
|
||||
} else {
|
||||
let start_block = match predicate_spec.start_block {
|
||||
@@ -75,6 +79,9 @@ pub async fn scan_bitcoin_chainstate_via_rpc_using_predicate(
|
||||
BlockHeights::BlockRange(start_block, end_block).get_sorted_entries()
|
||||
};
|
||||
|
||||
let mut block_heights_to_scan =
|
||||
block_heights_to_scan_res.map_err(|_e| format!("Block start / end block spec invalid"))?;
|
||||
|
||||
info!(
|
||||
ctx.expect_logger(),
|
||||
"Starting predicate evaluation on {} Bitcoin blocks",
|
||||
@@ -94,6 +101,21 @@ pub async fn scan_bitcoin_chainstate_via_rpc_using_predicate(
|
||||
while let Some(current_block_height) = block_heights_to_scan.pop_front() {
|
||||
let mut inscriptions_db_conn =
|
||||
open_readonly_ordhook_db_conn(&config.expected_cache_path(), ctx)?;
|
||||
let brc20_db_conn = match predicate_spec.predicate {
|
||||
BitcoinPredicateType::OrdinalsProtocol(OrdinalOperations::InscriptionFeed(
|
||||
ref feed_data,
|
||||
)) if feed_data.meta_protocols.is_some() => {
|
||||
if current_block_height >= brc20_activation_height(&bitcoin_config.network) {
|
||||
Some(open_readonly_brc20_db_conn(
|
||||
&config.expected_cache_path(),
|
||||
ctx,
|
||||
)?)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
number_of_blocks_scanned += 1;
|
||||
|
||||
@@ -133,6 +155,7 @@ pub async fn scan_bitcoin_chainstate_via_rpc_using_predicate(
|
||||
&mut block,
|
||||
&inscriptions_db_tx,
|
||||
true,
|
||||
brc20_db_conn.as_ref(),
|
||||
&Context::empty(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,13 @@ pub mod observers;
|
||||
mod runloops;
|
||||
|
||||
use crate::config::{Config, PredicatesApi};
|
||||
use crate::core::meta_protocols::brc20::cache::Brc20MemoryCache;
|
||||
use crate::core::meta_protocols::brc20::db::open_readwrite_brc20_db_conn;
|
||||
use crate::core::meta_protocols::brc20::parser::ParsedBrc20Operation;
|
||||
use crate::core::meta_protocols::brc20::verifier::{
|
||||
verify_brc20_operation, verify_brc20_transfer, VerifiedBrc20Operation,
|
||||
};
|
||||
use crate::core::meta_protocols::brc20::brc20_activation_height;
|
||||
use crate::core::pipeline::download_and_pipeline_blocks;
|
||||
use crate::core::pipeline::processors::block_archiving::start_block_archiving_processor;
|
||||
use crate::core::pipeline::processors::inscription_indexing::process_block;
|
||||
@@ -10,19 +17,19 @@ use crate::core::pipeline::processors::start_inscription_indexing_processor;
|
||||
use crate::core::pipeline::processors::transfers_recomputing::start_transfers_recomputing_processor;
|
||||
use crate::core::protocol::inscription_parsing::{
|
||||
get_inscriptions_revealed_in_block, get_inscriptions_transferred_in_block,
|
||||
parse_inscriptions_in_standardized_block,
|
||||
};
|
||||
use crate::core::protocol::inscription_sequencing::SequenceCursor;
|
||||
use crate::core::{new_traversals_lazy_cache, should_sync_ordhook_db, should_sync_rocks_db};
|
||||
use crate::db::{
|
||||
delete_data_in_ordhook_db, insert_entry_in_blocks, open_ordhook_db_conn_rocks_db_loop,
|
||||
open_readwrite_ordhook_db_conn, open_readwrite_ordhook_dbs, update_ordinals_db_with_block,
|
||||
BlockBytesCursor, TransactionBytesCursor,
|
||||
open_readwrite_ordhook_dbs, update_ordinals_db_with_block, BlockBytesCursor,
|
||||
TransactionBytesCursor,
|
||||
};
|
||||
use crate::db::{
|
||||
find_last_block_inserted, find_missing_blocks, run_compaction,
|
||||
update_sequence_metadata_with_block,
|
||||
};
|
||||
use crate::ord::inscription;
|
||||
use crate::scan::bitcoin::process_block_with_predicates;
|
||||
use crate::service::http_api::start_predicate_api_server;
|
||||
use crate::service::observers::{
|
||||
@@ -40,14 +47,15 @@ use chainhook_sdk::observer::{
|
||||
start_event_observer, BitcoinBlockDataCached, DataHandlerEvent, EventObserverConfig,
|
||||
HandleBlock, ObserverCommand, ObserverEvent, ObserverSidecar,
|
||||
};
|
||||
use chainhook_sdk::types::{BitcoinBlockData, BlockIdentifier};
|
||||
use chainhook_sdk::types::{BitcoinBlockData, BlockIdentifier, OrdinalOperation};
|
||||
use chainhook_sdk::utils::{BlockHeights, Context};
|
||||
use crossbeam_channel::unbounded;
|
||||
use crossbeam_channel::{select, Sender};
|
||||
use dashmap::DashMap;
|
||||
use fxhash::FxHasher;
|
||||
use rusqlite::Transaction;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::hash::BuildHasherDefault;
|
||||
use std::sync::mpsc::channel;
|
||||
use std::sync::Arc;
|
||||
@@ -135,6 +143,7 @@ impl Service {
|
||||
observer_command_rx,
|
||||
Some(observer_event_tx),
|
||||
Some(observer_sidecar),
|
||||
None,
|
||||
inner_ctx,
|
||||
);
|
||||
|
||||
@@ -188,6 +197,7 @@ impl Service {
|
||||
observer_command_rx,
|
||||
Some(observer_event_tx),
|
||||
Some(observer_sidecar),
|
||||
None,
|
||||
inner_ctx,
|
||||
);
|
||||
|
||||
@@ -336,7 +346,7 @@ impl Service {
|
||||
&self.ctx,
|
||||
);
|
||||
}
|
||||
ObserverEvent::PredicateDeregistered(spec) => {
|
||||
ObserverEvent::PredicateDeregistered(uuid) => {
|
||||
let observers_db_conn = match open_readwrite_observers_db_conn(
|
||||
&self.config.expected_cache_path(),
|
||||
&self.ctx,
|
||||
@@ -351,7 +361,7 @@ impl Service {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
remove_entry_from_observers(&spec.uuid(), &observers_db_conn, &self.ctx);
|
||||
remove_entry_from_observers(&uuid, &observers_db_conn, &self.ctx);
|
||||
}
|
||||
ObserverEvent::BitcoinPredicateTriggered(data) => {
|
||||
if let Some(ref tip) = data.apply.last() {
|
||||
@@ -511,27 +521,6 @@ impl Service {
|
||||
info!(self.ctx.expect_logger(), "Running database compaction",);
|
||||
run_compaction(&blocks_db_rw, tip);
|
||||
}
|
||||
|
||||
if rebuild_from_scratch {
|
||||
let blocks_db_rw = open_ordhook_db_conn_rocks_db_loop(
|
||||
false,
|
||||
&self.config.expected_cache_path(),
|
||||
self.config.resources.ulimit,
|
||||
self.config.resources.memory_available,
|
||||
&self.ctx,
|
||||
);
|
||||
|
||||
let inscriptions_db_conn_rw =
|
||||
open_readwrite_ordhook_db_conn(&self.config.expected_cache_path(), &self.ctx)?;
|
||||
|
||||
delete_data_in_ordhook_db(
|
||||
767430,
|
||||
820000,
|
||||
&blocks_db_rw,
|
||||
&inscriptions_db_conn_rw,
|
||||
&self.ctx,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
self.update_state(block_post_processor).await
|
||||
}
|
||||
@@ -560,7 +549,9 @@ impl Service {
|
||||
|
||||
let ordhook_config = self.config.get_ordhook_config();
|
||||
let first_inscription_height = ordhook_config.first_inscription_height;
|
||||
let blocks = BlockHeights::BlockRange(start_block, end_block).get_sorted_entries();
|
||||
let blocks = BlockHeights::BlockRange(start_block, end_block)
|
||||
.get_sorted_entries()
|
||||
.map_err(|_e| format!("Block start / end block spec invalid"))?;
|
||||
download_and_pipeline_blocks(
|
||||
&self.config,
|
||||
blocks.into(),
|
||||
@@ -595,7 +586,9 @@ impl Service {
|
||||
|
||||
let ordhook_config = self.config.get_ordhook_config();
|
||||
let first_inscription_height = ordhook_config.first_inscription_height;
|
||||
let blocks = BlockHeights::BlockRange(start_block, end_block).get_sorted_entries();
|
||||
let blocks = BlockHeights::BlockRange(start_block, end_block)
|
||||
.get_sorted_entries()
|
||||
.map_err(|_e| format!("Block start / end block spec invalid"))?;
|
||||
download_and_pipeline_blocks(
|
||||
&self.config,
|
||||
blocks.into(),
|
||||
@@ -662,8 +655,7 @@ fn chainhook_sidecar_mutate_ordhook_db(command: HandleBlock, config: &Config, ct
|
||||
if let Err(e) = delete_data_in_ordhook_db(
|
||||
block.block_identifier.index,
|
||||
block.block_identifier.index,
|
||||
&blocks_db_rw,
|
||||
&inscriptions_db_conn_rw,
|
||||
&config,
|
||||
&ctx,
|
||||
) {
|
||||
ctx.try_log(|logger| {
|
||||
@@ -757,19 +749,29 @@ pub fn chainhook_sidecar_mutate_blocks(
|
||||
) {
|
||||
Ok(dbs) => dbs,
|
||||
Err(e) => {
|
||||
ctx.try_log(|logger| error!(logger, "Unable to open readwtite connection: {e}",));
|
||||
ctx.try_log(|logger| error!(logger, "Unable to open readwrite connection: {e}",));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let inscriptions_db_tx = inscriptions_db_conn_rw.transaction().unwrap();
|
||||
|
||||
let mut brc20_db_conn_rw =
|
||||
match open_readwrite_brc20_db_conn(&config.expected_cache_path(), &ctx) {
|
||||
Ok(dbs) => dbs,
|
||||
Err(e) => {
|
||||
ctx.try_log(|logger| error!(logger, "Unable to open readwrite connection: {e}",));
|
||||
return;
|
||||
}
|
||||
};
|
||||
let mut brc20_cache = Brc20MemoryCache::new(config.resources.brc20_lru_cache_size);
|
||||
let brc20_db_tx = brc20_db_conn_rw.transaction().unwrap();
|
||||
|
||||
for block_id_to_rollback in blocks_ids_to_rollback.iter() {
|
||||
if let Err(e) = delete_data_in_ordhook_db(
|
||||
block_id_to_rollback.index,
|
||||
block_id_to_rollback.index,
|
||||
&blocks_db_rw,
|
||||
&inscriptions_db_tx,
|
||||
&config,
|
||||
&ctx,
|
||||
) {
|
||||
ctx.try_log(|logger| {
|
||||
@@ -814,8 +816,6 @@ pub fn chainhook_sidecar_mutate_blocks(
|
||||
} else {
|
||||
updated_blocks_ids.push(format!("{}", cache.block.block_identifier.index));
|
||||
|
||||
parse_inscriptions_in_standardized_block(&mut cache.block, &ctx);
|
||||
|
||||
let mut cache_l1 = BTreeMap::new();
|
||||
let mut sequence_cursor = SequenceCursor::new(&inscriptions_db_tx);
|
||||
|
||||
@@ -826,11 +826,13 @@ pub fn chainhook_sidecar_mutate_blocks(
|
||||
&mut cache_l1,
|
||||
&cache_l2,
|
||||
&inscriptions_db_tx,
|
||||
Some(&brc20_db_tx),
|
||||
&mut brc20_cache,
|
||||
&ordhook_config,
|
||||
&ctx,
|
||||
);
|
||||
|
||||
let inscriptions_revealed = get_inscriptions_revealed_in_block(&cache.block)
|
||||
let inscription_numbers = get_inscriptions_revealed_in_block(&cache.block)
|
||||
.iter()
|
||||
.map(|d| d.get_inscription_number().to_string())
|
||||
.collect::<Vec<String>>();
|
||||
@@ -843,12 +845,144 @@ pub fn chainhook_sidecar_mutate_blocks(
|
||||
logger,
|
||||
"Block #{} processed, mutated and revealed {} inscriptions [{}] and {inscriptions_transferred} transfers",
|
||||
cache.block.block_identifier.index,
|
||||
inscriptions_revealed.len(),
|
||||
inscriptions_revealed.join(", ")
|
||||
inscription_numbers.len(),
|
||||
inscription_numbers.join(", ")
|
||||
)
|
||||
});
|
||||
cache.processed_by_sidecar = true;
|
||||
}
|
||||
}
|
||||
let _ = inscriptions_db_tx.rollback();
|
||||
let _ = brc20_db_tx.rollback();
|
||||
}
|
||||
|
||||
pub fn write_brc20_block_operations(
|
||||
block: &BitcoinBlockData,
|
||||
brc20_operation_map: &mut HashMap<String, ParsedBrc20Operation>,
|
||||
brc20_cache: &mut Brc20MemoryCache,
|
||||
db_tx: &Transaction,
|
||||
ctx: &Context,
|
||||
) {
|
||||
if block.block_identifier.index < brc20_activation_height(&block.metadata.network) {
|
||||
return;
|
||||
}
|
||||
for (tx_index, tx) in block.transactions.iter().enumerate() {
|
||||
for op in tx.metadata.ordinal_operations.iter() {
|
||||
match op {
|
||||
OrdinalOperation::InscriptionRevealed(reveal) => {
|
||||
if let Some(parsed_brc20_operation) =
|
||||
brc20_operation_map.get(&reveal.inscription_id)
|
||||
{
|
||||
match verify_brc20_operation(
|
||||
parsed_brc20_operation,
|
||||
reveal,
|
||||
&block.block_identifier,
|
||||
&block.metadata.network,
|
||||
brc20_cache,
|
||||
&db_tx,
|
||||
&ctx,
|
||||
) {
|
||||
Ok(op) => {
|
||||
match op {
|
||||
VerifiedBrc20Operation::TokenDeploy(token) => {
|
||||
brc20_cache.insert_token_deploy(
|
||||
&token,
|
||||
reveal,
|
||||
&block.block_identifier,
|
||||
tx_index as u64,
|
||||
db_tx,
|
||||
ctx,
|
||||
);
|
||||
ctx.try_log(|logger| {
|
||||
info!(
|
||||
logger,
|
||||
"BRC-20 deploy {} ({}) at block {}",
|
||||
token.tick,
|
||||
token.address,
|
||||
block.block_identifier.index
|
||||
)
|
||||
});
|
||||
}
|
||||
VerifiedBrc20Operation::TokenMint(balance) => {
|
||||
brc20_cache.insert_token_mint(
|
||||
&balance,
|
||||
reveal,
|
||||
&block.block_identifier,
|
||||
tx_index as u64,
|
||||
db_tx,
|
||||
ctx,
|
||||
);
|
||||
ctx.try_log(|logger| {
|
||||
info!(
|
||||
logger,
|
||||
"BRC-20 mint {} {} ({}) at block {}",
|
||||
balance.tick, balance.amt, balance.address,
|
||||
block.block_identifier.index
|
||||
)
|
||||
});
|
||||
}
|
||||
VerifiedBrc20Operation::TokenTransfer(balance) => {
|
||||
brc20_cache.insert_token_transfer(
|
||||
&balance,
|
||||
reveal,
|
||||
&block.block_identifier,
|
||||
tx_index as u64,
|
||||
db_tx,
|
||||
ctx,
|
||||
);
|
||||
ctx.try_log(|logger| {
|
||||
info!(
|
||||
logger,
|
||||
"BRC-20 transfer {} {} ({}) at block {}",
|
||||
balance.tick, balance.amt, balance.address,
|
||||
block.block_identifier.index
|
||||
)
|
||||
});
|
||||
}
|
||||
VerifiedBrc20Operation::TokenTransferSend(_) => {
|
||||
unreachable!("BRC-20 token transfer send should never be generated on reveal")
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
ctx.try_log(|logger| {
|
||||
debug!(logger, "Error validating BRC-20 operation {}", e)
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
brc20_cache.ignore_inscription(reveal.ordinal_number);
|
||||
}
|
||||
}
|
||||
OrdinalOperation::InscriptionTransferred(transfer) => {
|
||||
match verify_brc20_transfer(transfer, brc20_cache, &db_tx, &ctx) {
|
||||
Ok(data) => {
|
||||
brc20_cache.insert_token_transfer_send(
|
||||
&data,
|
||||
&transfer,
|
||||
&block.block_identifier,
|
||||
tx_index as u64,
|
||||
db_tx,
|
||||
ctx,
|
||||
);
|
||||
ctx.try_log(|logger| {
|
||||
info!(
|
||||
logger,
|
||||
"BRC-20 transfer_send {} {} ({} -> {}) at block {}",
|
||||
data.tick, data.amt, data.sender_address, data.receiver_address,
|
||||
block.block_identifier.index
|
||||
)
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
ctx.try_log(|logger| {
|
||||
debug!(logger, "Error validating BRC-20 transfer {}", e)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
brc20_cache.db_cache.flush(db_tx, ctx);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ use chainhook_sdk::{
|
||||
chainhooks::types::{
|
||||
BitcoinChainhookFullSpecification, BitcoinChainhookNetworkSpecification,
|
||||
BitcoinChainhookSpecification, ChainhookConfig, ChainhookSpecification,
|
||||
InscriptionFeedData,
|
||||
},
|
||||
observer::EventObserverConfig,
|
||||
types::BitcoinBlockData,
|
||||
@@ -103,7 +104,7 @@ pub fn open_readwrite_observers_db_conn(
|
||||
ctx: &Context,
|
||||
) -> Result<Connection, String> {
|
||||
let db_path = get_default_observers_db_file_path(&base_dir);
|
||||
let conn = create_or_open_readwrite_db(&db_path, ctx);
|
||||
let conn = create_or_open_readwrite_db(Some(&db_path), ctx);
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
@@ -120,7 +121,7 @@ pub fn open_readwrite_observers_db_conn_or_panic(base_dir: &PathBuf, ctx: &Conte
|
||||
|
||||
pub fn initialize_observers_db(base_dir: &PathBuf, ctx: &Context) -> Connection {
|
||||
let db_path = get_default_observers_db_file_path(&base_dir);
|
||||
let conn = create_or_open_readwrite_db(&db_path, ctx);
|
||||
let conn = create_or_open_readwrite_db(Some(&db_path), ctx);
|
||||
// TODO: introduce initial output
|
||||
if let Err(e) = conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS observers (
|
||||
@@ -266,7 +267,11 @@ pub fn create_and_consolidate_chainhook_config_with_predicates(
|
||||
expired_at: None,
|
||||
expire_after_occurrence: None,
|
||||
predicate: chainhook_sdk::chainhooks::types::BitcoinPredicateType::OrdinalsProtocol(
|
||||
chainhook_sdk::chainhooks::types::OrdinalOperations::InscriptionFeed,
|
||||
chainhook_sdk::chainhooks::types::OrdinalOperations::InscriptionFeed(
|
||||
InscriptionFeedData {
|
||||
meta_protocols: None,
|
||||
},
|
||||
),
|
||||
),
|
||||
action: chainhook_sdk::chainhooks::types::HookAction::Noop,
|
||||
include_proof: false,
|
||||
|
||||
@@ -6,8 +6,7 @@ use napi::threadsafe_function::{
|
||||
};
|
||||
use ordhook::chainhook_sdk::chainhooks::bitcoin::BitcoinTransactionPayload;
|
||||
use ordhook::chainhook_sdk::chainhooks::types::{
|
||||
BitcoinChainhookFullSpecification, BitcoinChainhookNetworkSpecification, BitcoinPredicateType,
|
||||
HookAction, OrdinalOperations,
|
||||
BitcoinChainhookFullSpecification, BitcoinChainhookNetworkSpecification, BitcoinPredicateType, HookAction, InscriptionFeedData, OrdinalOperations
|
||||
};
|
||||
use ordhook::chainhook_sdk::observer::DataHandlerEvent;
|
||||
use ordhook::chainhook_sdk::utils::{BlockHeights, Context as OrdhookContext};
|
||||
@@ -184,7 +183,9 @@ impl OrdinalsIndexingRunloop {
|
||||
include_outputs: None,
|
||||
include_witness: None,
|
||||
predicate: BitcoinPredicateType::OrdinalsProtocol(
|
||||
OrdinalOperations::InscriptionFeed,
|
||||
OrdinalOperations::InscriptionFeed(InscriptionFeedData {
|
||||
meta_protocols: None
|
||||
}),
|
||||
),
|
||||
action: HookAction::Noop,
|
||||
},
|
||||
@@ -349,7 +350,7 @@ impl OrdinalsIndexer {
|
||||
#[napi]
|
||||
pub fn replay_block_range(&self, start_block: i64, end_block: i64) {
|
||||
let range = BlockHeights::BlockRange(start_block as u64, end_block as u64);
|
||||
let blocks = range.get_sorted_entries().into_iter().collect();
|
||||
let blocks = range.get_sorted_entries().unwrap().into_iter().collect();
|
||||
let _ = self
|
||||
.runloop
|
||||
.command_tx
|
||||
|
||||
@@ -8,7 +8,7 @@ WORKDIR /src
|
||||
|
||||
RUN apt-get update && apt-get install -y ca-certificates pkg-config libssl-dev libclang-11-dev libunwind-dev libunwind8 curl gnupg
|
||||
|
||||
RUN rustup update 1.72.0 && rustup default 1.72.0
|
||||
RUN rustup update 1.77.1 && rustup default 1.77.1
|
||||
|
||||
RUN mkdir /out
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "1.72.0"
|
||||
channel = "1.77.1"
|
||||
|
||||
Reference in New Issue
Block a user