From 3bf89647f8a41ed1bfa6c571258abaff0b216285 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Tue, 26 Dec 2023 03:47:18 +0800 Subject: [PATCH] Decode transactions from Bitcoin Core with `ord decode --txid` (#2907) --- src/envelope.rs | 14 +++--- src/lib.rs | 1 + src/subcommand.rs | 2 +- src/subcommand/decode.rs | 96 ++++++++++++++++++++++++++++++++++----- tests/decode.rs | 98 +++++++++++++++++++++++++++++++++------- 5 files changed, 175 insertions(+), 36 deletions(-) diff --git a/src/envelope.rs b/src/envelope.rs index 427757dba7..65fc93b049 100644 --- a/src/envelope.rs +++ b/src/envelope.rs @@ -25,13 +25,13 @@ type Result = std::result::Result; type RawEnvelope = Envelope>>; pub(crate) type ParsedEnvelope = Envelope; -#[derive(Debug, Default, PartialEq, Clone)] -pub(crate) struct Envelope { - pub(crate) input: u32, - pub(crate) offset: u32, - pub(crate) payload: T, - pub(crate) pushnum: bool, - pub(crate) stutter: bool, +#[derive(Default, PartialEq, Clone, Serialize, Deserialize, Debug, Eq)] +pub struct Envelope { + pub input: u32, + pub offset: u32, + pub payload: T, + pub pushnum: bool, + pub stutter: bool, } fn remove_field(fields: &mut BTreeMap<&[u8], Vec<&[u8]>>, field: &[u8]) -> Option> { diff --git a/src/lib.rs b/src/lib.rs index c18bf58866..0d41e615ee 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -85,6 +85,7 @@ use { }; pub use self::{ + envelope::Envelope, fee_rate::FeeRate, inscription::Inscription, object::Object, diff --git a/src/subcommand.rs b/src/subcommand.rs index 16dc0910f5..efd9280af5 100644 --- a/src/subcommand.rs +++ b/src/subcommand.rs @@ -54,7 +54,7 @@ impl Subcommand { pub(crate) fn run(self, options: Options) -> SubcommandResult { match self { Self::Balances => balances::run(options), - Self::Decode(decode) => decode.run(), + Self::Decode(decode) => decode.run(options), Self::Epochs => epochs::run(), Self::Find(find) => find.run(options), Self::Index(index) => index.run(options), diff --git a/src/subcommand/decode.rs b/src/subcommand/decode.rs index 61258778c5..0f32ffb6ea 100644 --- a/src/subcommand/decode.rs +++ b/src/subcommand/decode.rs @@ -1,30 +1,102 @@ use super::*; #[derive(Serialize, Eq, PartialEq, Deserialize, Debug)] -pub struct Output { - pub inscriptions: Vec, +pub struct CompactOutput { + pub inscriptions: Vec, +} + +#[derive(Serialize, Eq, PartialEq, Deserialize, Debug)] +pub struct RawOutput { + pub inscriptions: Vec, +} + +#[derive(Serialize, Eq, PartialEq, Deserialize, Debug)] +pub struct CompactInscription { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub body: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub content_encoding: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub content_type: Option, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub duplicate_field: bool, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub incomplete_field: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub metadata: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub metaprotocol: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub parent: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub pointer: Option, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub unrecognized_even_field: bool, +} + +impl TryFrom for CompactInscription { + type Error = Error; + + fn try_from(inscription: Inscription) -> Result { + Ok(Self { + content_encoding: inscription + .content_encoding() + .map(|header_value| header_value.to_str().map(str::to_string)) + .transpose()?, + content_type: inscription.content_type().map(str::to_string), + metaprotocol: inscription.metaprotocol().map(str::to_string), + parent: inscription.parent(), + pointer: inscription.pointer(), + body: inscription.body.map(hex::encode), + duplicate_field: inscription.duplicate_field, + incomplete_field: inscription.incomplete_field, + metadata: inscription.metadata.map(hex::encode), + unrecognized_even_field: inscription.unrecognized_even_field, + }) + } } #[derive(Debug, Parser)] pub(crate) struct Decode { - transaction: Option, + #[arg( + long, + conflicts_with = "file", + help = "Fetch transaction with from Bitcoin Core." + )] + txid: Option, + #[arg(long, conflicts_with = "txid", help = "Load transaction from .")] + file: Option, + #[arg( + long, + help = "Serialize inscriptions in a compact, human-readable format." + )] + compact: bool, } impl Decode { - pub(crate) fn run(self) -> SubcommandResult { - let transaction = if let Some(path) = self.transaction { - Transaction::consensus_decode(&mut File::open(path)?)? + pub(crate) fn run(self, options: Options) -> SubcommandResult { + let transaction = if let Some(txid) = self.txid { + options + .bitcoin_rpc_client()? + .get_raw_transaction(&txid, None)? + } else if let Some(file) = self.file { + Transaction::consensus_decode(&mut File::open(file)?)? } else { Transaction::consensus_decode(&mut io::stdin())? }; let inscriptions = ParsedEnvelope::from_transaction(&transaction); - Ok(Box::new(Output { - inscriptions: inscriptions - .into_iter() - .map(|inscription| inscription.payload) - .collect(), - })) + if self.compact { + Ok(Box::new(CompactOutput { + inscriptions: inscriptions + .clone() + .into_iter() + .map(|inscription| inscription.payload.try_into()) + .collect::>>()?, + })) + } else { + Ok(Box::new(RawOutput { inscriptions })) + } } } diff --git a/tests/decode.rs b/tests/decode.rs index 47b09cc34b..48b400d694 100644 --- a/tests/decode.rs +++ b/tests/decode.rs @@ -4,7 +4,10 @@ use { absolute::LockTime, consensus::Encodable, opcodes, script, ScriptBuf, Sequence, Transaction, TxIn, Witness, }, - ord::{subcommand::decode::Output, Inscription}, + ord::{ + subcommand::decode::{CompactInscription, CompactOutput, RawOutput}, + Envelope, Inscription, + }, }; fn transaction() -> Vec { @@ -46,16 +49,22 @@ fn transaction() -> Vec { #[test] fn from_file() { assert_eq!( - CommandBuilder::new("decode transaction.bin") + CommandBuilder::new("decode --file transaction.bin") .write("transaction.bin", transaction()) - .run_and_deserialize_output::(), - Output { - inscriptions: vec![Inscription { - body: Some(vec![0, 1, 2, 3]), - content_type: Some(b"text/plain;charset=utf-8".to_vec()), - ..Default::default() + .run_and_deserialize_output::(), + RawOutput { + inscriptions: vec![Envelope { + payload: Inscription { + body: Some(vec![0, 1, 2, 3]), + content_type: Some(b"text/plain;charset=utf-8".into()), + ..Default::default() + }, + input: 0, + offset: 0, + pushnum: false, + stutter: false, }], - } + }, ); } @@ -64,13 +73,70 @@ fn from_stdin() { assert_eq!( CommandBuilder::new("decode") .stdin(transaction()) - .run_and_deserialize_output::(), - Output { - inscriptions: vec![Inscription { - body: Some(vec![0, 1, 2, 3]), - content_type: Some(b"text/plain;charset=utf-8".to_vec()), - ..Default::default() + .run_and_deserialize_output::(), + RawOutput { + inscriptions: vec![Envelope { + payload: Inscription { + body: Some(vec![0, 1, 2, 3]), + content_type: Some(b"text/plain;charset=utf-8".into()), + ..Default::default() + }, + input: 0, + offset: 0, + pushnum: false, + stutter: false, + }], + }, + ); +} + +#[test] +fn from_core() { + let rpc_server = test_bitcoincore_rpc::spawn(); + create_wallet(&rpc_server); + rpc_server.mine_blocks(1); + + let (_inscription, reveal) = inscribe(&rpc_server); + + assert_eq!( + CommandBuilder::new(format!("decode --txid {reveal}")) + .rpc_server(&rpc_server) + .run_and_deserialize_output::(), + RawOutput { + inscriptions: vec![Envelope { + payload: Inscription { + body: Some(b"FOO".into()), + content_type: Some(b"text/plain;charset=utf-8".into()), + ..Default::default() + }, + input: 0, + offset: 0, + pushnum: false, + stutter: false, + }], + }, + ); +} + +#[test] +fn compact() { + assert_eq!( + CommandBuilder::new("decode --compact --file transaction.bin") + .write("transaction.bin", transaction()) + .run_and_deserialize_output::(), + CompactOutput { + inscriptions: vec![CompactInscription { + body: Some("00010203".into()), + content_encoding: None, + content_type: Some("text/plain;charset=utf-8".into()), + duplicate_field: false, + incomplete_field: false, + metadata: None, + metaprotocol: None, + parent: None, + pointer: None, + unrecognized_even_field: false, }], - } + }, ); }