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:
Rafael Cárdenas
2024-05-09 11:45:22 -06:00
committed by GitHub
parent 4a9e336065
commit 729affb699
26 changed files with 4545 additions and 861 deletions

1996
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -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())),

View File

@@ -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)]

View File

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

View File

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

View 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,
))
}
}

View 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,
}));
}
_ => {}
}
}

View 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!(),
}
}

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

View File

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

View File

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

View File

@@ -0,0 +1 @@
pub mod brc20;

View File

@@ -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(

View File

@@ -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(())
}

View File

@@ -74,6 +74,7 @@ pub fn start_transfers_recomputing_processor(
block,
&inscriptions_db_tx,
false,
None,
&ctx,
);

View File

@@ -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,
);
}
}

View File

@@ -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,
);
}
}
}

View File

@@ -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,

View File

@@ -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(())
}

View File

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

View File

@@ -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(),
);
}

View File

@@ -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);
}

View File

@@ -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,

View File

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

View File

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

View File

@@ -1,2 +1,2 @@
[toolchain]
channel = "1.72.0"
channel = "1.77.1"