mirror of
https://github.com/alexgo-io/bitcoin-indexer.git
synced 2026-01-12 16:52:57 +08:00
feat: import inscription parser
This commit is contained in:
@@ -141,7 +141,15 @@ pub fn serialize_bitcoin_payload_to_json<'a>(
|
||||
"transaction_identifier": transaction.transaction_identifier,
|
||||
"operations": transaction.operations,
|
||||
"metadata": json!({
|
||||
"inputs": transaction.metadata.inputs,
|
||||
"inputs": transaction.metadata.inputs.iter().map(|input| {
|
||||
json!({
|
||||
"previous_output": {
|
||||
"txid": format!("0x{}", input.previous_output.txid),
|
||||
"vout": input.previous_output.vout,
|
||||
},
|
||||
"sequence": input.sequence,
|
||||
})
|
||||
}).collect::<Vec<_>>(),
|
||||
"outputs": transaction.metadata.outputs,
|
||||
"stacks_operations": transaction.metadata.stacks_operations,
|
||||
"ordinal_operations": transaction.metadata.ordinal_operations,
|
||||
@@ -388,7 +396,7 @@ impl BitcoinChainhookSpecification {
|
||||
false
|
||||
}
|
||||
BitcoinPredicateType::Protocol(Protocols::Ordinal(
|
||||
OrdinalOperations::NewInscription,
|
||||
OrdinalOperations::InscriptionRevealed,
|
||||
)) => {
|
||||
for op in tx.metadata.ordinal_operations.iter() {
|
||||
if let OrdinalOperation::InscriptionReveal(_) = op {
|
||||
|
||||
@@ -337,7 +337,7 @@ pub enum StacksOperations {
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum OrdinalOperations {
|
||||
NewInscription,
|
||||
InscriptionRevealed,
|
||||
}
|
||||
|
||||
pub fn get_stacks_canonical_magic_bytes(network: &BitcoinNetwork) -> [u8; 2] {
|
||||
@@ -348,10 +348,6 @@ pub fn get_stacks_canonical_magic_bytes(network: &BitcoinNetwork) -> [u8; 2] {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_ordinal_canonical_magic_bytes() -> (usize, [u8; 3]) {
|
||||
return (37, *b"ord");
|
||||
}
|
||||
|
||||
pub struct PoxConfig {
|
||||
pub genesis_block_height: u64,
|
||||
pub prepare_phase_len: u64,
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
mod blocks_pool;
|
||||
mod ordinal;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::chainhooks::types::{
|
||||
get_canonical_pox_config, get_ordinal_canonical_magic_bytes, get_stacks_canonical_magic_bytes,
|
||||
PoxConfig, StacksOpcodes,
|
||||
get_canonical_pox_config, get_stacks_canonical_magic_bytes, PoxConfig, StacksOpcodes,
|
||||
};
|
||||
use crate::indexer::IndexerConfig;
|
||||
use crate::observer::BitcoinConfig;
|
||||
use crate::utils::Context;
|
||||
use bitcoincore_rpc::bitcoin;
|
||||
use bitcoincore_rpc::bitcoin::{self, Script};
|
||||
use bitcoincore_rpc_json::{GetRawTransactionResult, GetRawTransactionResultVout};
|
||||
pub use blocks_pool::BitcoinBlockPool;
|
||||
use chainhook_types::bitcoin::{OutPoint, TxIn, TxOut};
|
||||
@@ -23,6 +23,8 @@ use clarity_repl::clarity::util::hash::{hex_bytes, to_hex};
|
||||
use hiro_system_kit::slog;
|
||||
use rocket::serde::json::Value as JsonValue;
|
||||
|
||||
use self::ordinal::InscriptionParser;
|
||||
|
||||
#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Block {
|
||||
@@ -205,28 +207,26 @@ fn try_parse_ordinal_operation(
|
||||
block_height: u64,
|
||||
ctx: &Context,
|
||||
) -> Option<OrdinalOperation> {
|
||||
let (pos, magic_bytes) = get_ordinal_canonical_magic_bytes();
|
||||
let limit = pos + magic_bytes.len();
|
||||
|
||||
for input in tx.vin.iter() {
|
||||
if let Some(ref witnesses) = input.txinwitness {
|
||||
for witness in witnesses.iter() {
|
||||
if witness.len() > limit && witness[pos..limit] == magic_bytes {
|
||||
ctx.try_log(|logger| {
|
||||
slog::info!(
|
||||
logger,
|
||||
"Ordinal operation detected in transaction {}",
|
||||
tx.txid,
|
||||
)
|
||||
});
|
||||
return Some(OrdinalOperation::InscriptionReveal(
|
||||
OrdinalInscriptionRevealData {
|
||||
satoshi_point: "".into(),
|
||||
content_type: "".into(),
|
||||
content: vec![],
|
||||
},
|
||||
));
|
||||
}
|
||||
for bytes in witnesses.iter() {
|
||||
let script = Script::from(bytes.to_vec());
|
||||
let parser = InscriptionParser {
|
||||
instructions: script.instructions().peekable(),
|
||||
};
|
||||
|
||||
let inscription = match parser.parse_script() {
|
||||
Ok(inscription) => inscription,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
return Some(OrdinalOperation::InscriptionReveal(
|
||||
OrdinalInscriptionRevealData {
|
||||
satoshi_point: "".into(),
|
||||
content_type: inscription.content_type().unwrap_or("unknown").to_string(),
|
||||
content: format!("0x{}", to_hex(&inscription.body().unwrap_or(&vec![]))),
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::str::FromStr;
|
||||
use {
|
||||
bitcoincore_rpc::bitcoin::{
|
||||
blockdata::{
|
||||
opcodes,
|
||||
script::{self, Instruction, Instructions},
|
||||
},
|
||||
util::taproot::TAPROOT_ANNEX_PREFIX,
|
||||
Script, Witness,
|
||||
},
|
||||
std::{iter::Peekable, str},
|
||||
};
|
||||
|
||||
const PROTOCOL_ID: &[u8] = b"ord";
|
||||
|
||||
const BODY_TAG: &[u8] = &[];
|
||||
const CONTENT_TYPE_TAG: &[u8] = &[1];
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct Inscription {
|
||||
body: Option<Vec<u8>>,
|
||||
content_type: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl Inscription {
|
||||
fn append_reveal_script_to_builder(&self, mut builder: script::Builder) -> script::Builder {
|
||||
builder = builder
|
||||
.push_opcode(opcodes::OP_FALSE)
|
||||
.push_opcode(opcodes::all::OP_IF)
|
||||
.push_slice(PROTOCOL_ID);
|
||||
|
||||
if let Some(content_type) = &self.content_type {
|
||||
builder = builder
|
||||
.push_slice(CONTENT_TYPE_TAG)
|
||||
.push_slice(content_type);
|
||||
}
|
||||
|
||||
if let Some(body) = &self.body {
|
||||
builder = builder.push_slice(BODY_TAG);
|
||||
for chunk in body.chunks(520) {
|
||||
builder = builder.push_slice(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
builder.push_opcode(opcodes::all::OP_ENDIF)
|
||||
}
|
||||
|
||||
pub(crate) fn append_reveal_script(&self, builder: script::Builder) -> Script {
|
||||
self.append_reveal_script_to_builder(builder).into_script()
|
||||
}
|
||||
|
||||
pub(crate) fn media(&self) -> Media {
|
||||
if self.body.is_none() {
|
||||
return Media::Unknown;
|
||||
}
|
||||
|
||||
let content_type = match self.content_type() {
|
||||
Some(content_type) => content_type,
|
||||
None => return Media::Unknown,
|
||||
};
|
||||
|
||||
content_type.parse().unwrap_or(Media::Unknown)
|
||||
}
|
||||
|
||||
pub(crate) fn body(&self) -> Option<&[u8]> {
|
||||
Some(self.body.as_ref()?)
|
||||
}
|
||||
|
||||
pub(crate) fn into_body(self) -> Option<Vec<u8>> {
|
||||
self.body
|
||||
}
|
||||
|
||||
pub(crate) fn content_length(&self) -> Option<usize> {
|
||||
Some(self.body()?.len())
|
||||
}
|
||||
|
||||
pub(crate) fn content_type(&self) -> Option<&str> {
|
||||
str::from_utf8(self.content_type.as_ref()?).ok()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum InscriptionError {
|
||||
EmptyWitness,
|
||||
InvalidInscription,
|
||||
KeyPathSpend,
|
||||
NoInscription,
|
||||
Script(script::Error),
|
||||
UnrecognizedEvenField,
|
||||
}
|
||||
|
||||
type Result<T, E = InscriptionError> = std::result::Result<T, E>;
|
||||
|
||||
pub struct InscriptionParser<'a> {
|
||||
pub instructions: Peekable<Instructions<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> InscriptionParser<'a> {
|
||||
pub fn parse(witness: &Witness) -> Result<Inscription> {
|
||||
if witness.is_empty() {
|
||||
return Err(InscriptionError::EmptyWitness);
|
||||
}
|
||||
|
||||
if witness.len() == 1 {
|
||||
return Err(InscriptionError::KeyPathSpend);
|
||||
}
|
||||
|
||||
let annex = witness
|
||||
.last()
|
||||
.and_then(|element| element.first().map(|byte| *byte == TAPROOT_ANNEX_PREFIX))
|
||||
.unwrap_or(false);
|
||||
|
||||
if witness.len() == 2 && annex {
|
||||
return Err(InscriptionError::KeyPathSpend);
|
||||
}
|
||||
|
||||
let script = witness
|
||||
.iter()
|
||||
.nth(if annex {
|
||||
witness.len() - 1
|
||||
} else {
|
||||
witness.len() - 2
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
InscriptionParser {
|
||||
instructions: Script::from(Vec::from(script)).instructions().peekable(),
|
||||
}
|
||||
.parse_script()
|
||||
}
|
||||
|
||||
pub fn parse_script(mut self) -> Result<Inscription> {
|
||||
loop {
|
||||
let next = self.advance()?;
|
||||
|
||||
if next == Instruction::PushBytes(&[]) {
|
||||
if let Some(inscription) = self.parse_inscription()? {
|
||||
return Ok(inscription);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn advance(&mut self) -> Result<Instruction<'a>> {
|
||||
self.instructions
|
||||
.next()
|
||||
.ok_or(InscriptionError::NoInscription)?
|
||||
.map_err(InscriptionError::Script)
|
||||
}
|
||||
|
||||
fn parse_inscription(&mut self) -> Result<Option<Inscription>> {
|
||||
if self.advance()? == Instruction::Op(opcodes::all::OP_IF) {
|
||||
if !self.accept(Instruction::PushBytes(PROTOCOL_ID))? {
|
||||
return Err(InscriptionError::NoInscription);
|
||||
}
|
||||
|
||||
let mut fields = BTreeMap::new();
|
||||
|
||||
loop {
|
||||
match self.advance()? {
|
||||
Instruction::PushBytes(BODY_TAG) => {
|
||||
let mut body = Vec::new();
|
||||
while !self.accept(Instruction::Op(opcodes::all::OP_ENDIF))? {
|
||||
body.extend_from_slice(self.expect_push()?);
|
||||
}
|
||||
fields.insert(BODY_TAG, body);
|
||||
break;
|
||||
}
|
||||
Instruction::PushBytes(tag) => {
|
||||
if fields.contains_key(tag) {
|
||||
return Err(InscriptionError::InvalidInscription);
|
||||
}
|
||||
fields.insert(tag, self.expect_push()?.to_vec());
|
||||
}
|
||||
Instruction::Op(opcodes::all::OP_ENDIF) => break,
|
||||
_ => return Err(InscriptionError::InvalidInscription),
|
||||
}
|
||||
}
|
||||
|
||||
let body = fields.remove(BODY_TAG);
|
||||
let content_type = fields.remove(CONTENT_TYPE_TAG);
|
||||
|
||||
for tag in fields.keys() {
|
||||
if let Some(lsb) = tag.first() {
|
||||
if lsb % 2 == 0 {
|
||||
return Err(InscriptionError::UnrecognizedEvenField);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(Some(Inscription { body, content_type }));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn expect_push(&mut self) -> Result<&'a [u8]> {
|
||||
match self.advance()? {
|
||||
Instruction::PushBytes(bytes) => Ok(bytes),
|
||||
_ => Err(InscriptionError::InvalidInscription),
|
||||
}
|
||||
}
|
||||
|
||||
fn accept(&mut self, instruction: Instruction) -> Result<bool> {
|
||||
match self.instructions.peek() {
|
||||
Some(Ok(next)) => {
|
||||
if *next == instruction {
|
||||
self.advance()?;
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
Some(Err(err)) => Err(InscriptionError::Script(*err)),
|
||||
None => Ok(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Copy, Clone)]
|
||||
pub(crate) enum Media {
|
||||
Audio,
|
||||
Iframe,
|
||||
Image,
|
||||
Pdf,
|
||||
Text,
|
||||
Unknown,
|
||||
Video,
|
||||
}
|
||||
|
||||
impl Media {
|
||||
const TABLE: &'static [(&'static str, Media, &'static [&'static str])] = &[
|
||||
("application/json", Media::Text, &["json"]),
|
||||
("application/pdf", Media::Pdf, &["pdf"]),
|
||||
("application/pgp-signature", Media::Text, &["asc"]),
|
||||
("application/yaml", Media::Text, &["yaml", "yml"]),
|
||||
("audio/flac", Media::Audio, &["flac"]),
|
||||
("audio/mpeg", Media::Audio, &["mp3"]),
|
||||
("audio/wav", Media::Audio, &["wav"]),
|
||||
("image/apng", Media::Image, &["apng"]),
|
||||
("image/avif", Media::Image, &[]),
|
||||
("image/gif", Media::Image, &["gif"]),
|
||||
("image/jpeg", Media::Image, &["jpg", "jpeg"]),
|
||||
("image/png", Media::Image, &["png"]),
|
||||
("image/svg+xml", Media::Iframe, &["svg"]),
|
||||
("image/webp", Media::Image, &["webp"]),
|
||||
("model/stl", Media::Unknown, &["stl"]),
|
||||
("text/html;charset=utf-8", Media::Iframe, &["html"]),
|
||||
("text/plain;charset=utf-8", Media::Text, &["txt"]),
|
||||
("video/mp4", Media::Video, &["mp4"]),
|
||||
("video/webm", Media::Video, &["webm"]),
|
||||
];
|
||||
}
|
||||
|
||||
impl FromStr for Media {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
for entry in Self::TABLE {
|
||||
if entry.0 == s {
|
||||
return Ok(entry.1);
|
||||
}
|
||||
}
|
||||
|
||||
Err("unknown content type: {s}".to_string())
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
use bitcoincore_rpc::bitcoin::Script;
|
||||
|
||||
use crate::indexer::bitcoin::ordinal::InscriptionParser;
|
||||
|
||||
use super::super::tests::{helpers, process_bitcoin_blocks_and_check_expectations};
|
||||
|
||||
#[test]
|
||||
@@ -209,17 +213,17 @@ fn test_bitcoin_vector_040() {
|
||||
fn test_ordinal_inscription_parsing() {
|
||||
use clarity_repl::clarity::util::hash::hex_bytes;
|
||||
|
||||
let witness = hex_bytes("208737bc46923c3e64c7e6768c0346879468bf3aba795a5f5f56efca288f50ed2aac0063036f7264010118746578742f706c61696e3b636861727365743d7574662d38004c9948656c6c6f2030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030300a68").unwrap();
|
||||
let bytes = hex_bytes("208737bc46923c3e64c7e6768c0346879468bf3aba795a5f5f56efca288f50ed2aac0063036f7264010118746578742f706c61696e3b636861727365743d7574662d38004c9948656c6c6f2030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030300a68").unwrap();
|
||||
|
||||
let (pos, magic_bytes) = super::get_ordinal_canonical_magic_bytes();
|
||||
let limit = pos + magic_bytes.len();
|
||||
let script = Script::from(bytes);
|
||||
let parser = InscriptionParser {
|
||||
instructions: script.instructions().peekable(),
|
||||
};
|
||||
|
||||
println!("{:?}", &magic_bytes);
|
||||
let inscription = match parser.parse_script() {
|
||||
Ok(inscription) => inscription,
|
||||
Err(_) => panic!(),
|
||||
};
|
||||
|
||||
// println!("{:?}", &witness);
|
||||
println!("{:?}", &witness[pos..limit]);
|
||||
if witness.len() > limit && witness[pos..limit] == magic_bytes {
|
||||
} else {
|
||||
panic!();
|
||||
}
|
||||
println!("{:?}", inscription);
|
||||
}
|
||||
|
||||
@@ -273,6 +273,7 @@ pub struct BitcoinTransactionMetadata {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum OrdinalOperation {
|
||||
InscriptionCommit(OrdinalInscriptionCommitData),
|
||||
InscriptionReveal(OrdinalInscriptionRevealData),
|
||||
@@ -285,7 +286,7 @@ pub struct OrdinalInscriptionCommitData {}
|
||||
pub struct OrdinalInscriptionRevealData {
|
||||
pub satoshi_point: String,
|
||||
pub content_type: String,
|
||||
pub content: Vec<u8>,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
|
||||
Reference in New Issue
Block a user