Skip to content

Commit

Permalink
Decode transactions from Bitcoin Core with ord decode --txid (#2907)
Browse files Browse the repository at this point in the history
  • Loading branch information
casey authored Dec 25, 2023
1 parent c6a6bfb commit 3bf8964
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 36 deletions.
14 changes: 7 additions & 7 deletions src/envelope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ type Result<T> = std::result::Result<T, script::Error>;
type RawEnvelope = Envelope<Vec<Vec<u8>>>;
pub(crate) type ParsedEnvelope = Envelope<Inscription>;

#[derive(Debug, Default, PartialEq, Clone)]
pub(crate) struct Envelope<T> {
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<T> {
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<Vec<u8>> {
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ use {
};

pub use self::{
envelope::Envelope,
fee_rate::FeeRate,
inscription::Inscription,
object::Object,
Expand Down
2 changes: 1 addition & 1 deletion src/subcommand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
96 changes: 84 additions & 12 deletions src/subcommand/decode.rs
Original file line number Diff line number Diff line change
@@ -1,30 +1,102 @@
use super::*;

#[derive(Serialize, Eq, PartialEq, Deserialize, Debug)]
pub struct Output {
pub inscriptions: Vec<Inscription>,
pub struct CompactOutput {
pub inscriptions: Vec<CompactInscription>,
}

#[derive(Serialize, Eq, PartialEq, Deserialize, Debug)]
pub struct RawOutput {
pub inscriptions: Vec<ParsedEnvelope>,
}

#[derive(Serialize, Eq, PartialEq, Deserialize, Debug)]
pub struct CompactInscription {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub body: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content_encoding: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content_type: Option<String>,
#[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<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metaprotocol: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent: Option<InscriptionId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pointer: Option<u64>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub unrecognized_even_field: bool,
}

impl TryFrom<Inscription> for CompactInscription {
type Error = Error;

fn try_from(inscription: Inscription) -> Result<Self> {
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<PathBuf>,
#[arg(
long,
conflicts_with = "file",
help = "Fetch transaction with <TXID> from Bitcoin Core."
)]
txid: Option<Txid>,
#[arg(long, conflicts_with = "txid", help = "Load transaction from <FILE>.")]
file: Option<PathBuf>,
#[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::<Result<Vec<CompactInscription>>>()?,
}))
} else {
Ok(Box::new(RawOutput { inscriptions }))
}
}
}
98 changes: 82 additions & 16 deletions tests/decode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u8> {
Expand Down Expand Up @@ -46,16 +49,22 @@ fn transaction() -> Vec<u8> {
#[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>(),
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>(),
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,
}],
}
},
);
}

Expand All @@ -64,13 +73,70 @@ fn from_stdin() {
assert_eq!(
CommandBuilder::new("decode")
.stdin(transaction())
.run_and_deserialize_output::<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>(),
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>(),
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>(),
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,
}],
}
},
);
}

0 comments on commit 3bf8964

Please sign in to comment.