From 7c006cb6c6807bd08acb5158d811fe6896cfb9b4 Mon Sep 17 00:00:00 2001 From: edsonalcala Date: Fri, 4 Oct 2024 23:15:14 +0100 Subject: [PATCH 1/8] wip: added bitcoin from json --- Cargo.toml | 2 +- src/bitcoin/bitcoin_transaction.rs | 80 +++++++++++++- src/bitcoin/types/lock_time/lock_time.rs | 89 +++++++++++++++- src/bitcoin/types/script_buf.rs | 92 +++++++++++++++- src/bitcoin/types/tx_in/hash.rs | 2 +- src/bitcoin/types/tx_in/outpoint.rs | 128 +++++++++++++++++++++-- src/bitcoin/types/tx_in/witness.rs | 99 +++++++++++++++++- src/bitcoin/types/version.rs | 81 +++++++++++++- tests/bitcoin_integration_test.rs | 2 - 9 files changed, 545 insertions(+), 30 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d772f3b..af9844b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ near-sdk = { version = "5.3.0" } serde-big-array = "0.5.1" bs58 = "0.5.1" serde = "1.0" +serde_json = "1.0" sha2 = { version = "0.10.8", optional = true } [dev-dependencies] @@ -68,4 +69,3 @@ tokio = { version = "1.38", features = ["full"] } # misc eyre = "0.6" -serde_json = "1.0" diff --git a/src/bitcoin/bitcoin_transaction.rs b/src/bitcoin/bitcoin_transaction.rs index b02fa66..ae020d8 100644 --- a/src/bitcoin/bitcoin_transaction.rs +++ b/src/bitcoin/bitcoin_transaction.rs @@ -1,8 +1,7 @@ -use std::io::{BufRead, Write}; - use borsh::{BorshDeserialize, BorshSerialize}; use near_sdk::serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; +use std::io::{BufRead, Write}; use super::{ constants::{SEGWIT_FLAG, SEGWIT_MARKER}, @@ -93,7 +92,7 @@ impl BitcoinTransaction { let mut buffer = Vec::new(); - self.encode_for_sighash_for_segwig(&mut buffer, input_index, script_code, value); + self.encode_for_sighash_for_segwit(&mut buffer, input_index, script_code, value); // Sighash type buffer.extend_from_slice(&(sighash_type as u32).to_le_bytes()); @@ -125,7 +124,7 @@ impl BitcoinTransaction { buffer } - fn encode_for_sighash_for_segwig( + fn encode_for_sighash_for_segwit( &self, buffer: &mut Vec, input_index: usize, @@ -194,6 +193,11 @@ impl BitcoinTransaction { // To avoid serialization ambiguity, no inputs means we use BIP141 serialization self.input.is_empty() } + + pub fn from_json(json: &str) -> Result { + let tx: Self = near_sdk::serde_json::from_str(json)?; + Ok(tx) + } } impl Encodable for Vec { @@ -493,4 +497,72 @@ mod tests { assert_eq!(buffer.len(), serialized.len()); assert_eq!(buffer, serialized); } + + #[test] + fn test_from_json_bitcoin_transaction() { + let json = r#" + { + "version": "1", + "lock_time": "0", + "input": [ + { + "previous_output": { + "txid": "bc25cc0dddd0a202c21e66521a692c0586330a9a9dcc38ccd9b4d2093037f31a", + "vout": 0 + }, + "script_sig": "", + "sequence": 4294967295, + "witness": [] + } + ], + "output": [ + { + "value": 1, + "script_pubkey": "76a9148356ecd5f1761e60c144dc2f4de6bf7d8be7690688ad" + }, + { + "value": 2649, + "script_pubkey": "76a9148356ecd5f1761e60c144dc2f4de6bf7d8be7690688ac" + } + ] + } + "#; + + let tx = OmniBitcoinTransaction::from_json(json).unwrap(); + println!("tx: {:?}", tx); + } + + #[test] + fn test_from_json_bitcoin_transaction_2() { + let json = r#" + { + "version": "1", + "lock_time": "0", + "input": [ + { + "previous_output": { + "txid": "bc25cc0dddd0a202c21e66521a692c0586330a9a9dcc38ccd9b4d2093037f31a", + "vout": 0 + }, + "script_sig": [], + "sequence": 4294967295, + "witness": [] + } + ], + "output": [ + { + "value": 1, + "script_pubkey": "76a9148356ecd5f1761e60c144dc2f4de6bf7d8be7690688ad" + }, + { + "value": 2649, + "script_pubkey": "76a9148356ecd5f1761e60c144dc2f4de6bf7d8be7690688ac" + } + ] + } + "#; + + let tx = OmniBitcoinTransaction::from_json(json).unwrap(); + println!("tx: {:?}", tx); + } } diff --git a/src/bitcoin/types/lock_time/lock_time.rs b/src/bitcoin/types/lock_time/lock_time.rs index 282447a..9bf60fd 100644 --- a/src/bitcoin/types/lock_time/lock_time.rs +++ b/src/bitcoin/types/lock_time/lock_time.rs @@ -1,10 +1,17 @@ -use crate::bitcoin::encoding::{Decodable, Encodable}; +use crate::bitcoin::{ + encoding::{Decodable, Encodable}, + types::lock_time::constants::LOCK_TIME_THRESHOLD, +}; use super::{height::Height, time::Time}; -use std::io::{BufRead, Write}; +use std::{ + fmt, + io::{BufRead, Write}, +}; use borsh::{BorshDeserialize, BorshSerialize}; use near_sdk::serde::{Deserialize, Serialize}; +use serde::Deserializer; /// Locktime itself is an unsigned 4-byte integer which can be parsed two ways: /// @@ -16,9 +23,7 @@ use near_sdk::serde::{Deserialize, Serialize}; /// The transaction can be added to any block whose block time is greater than the locktime. /// /// [Bitcoin Devguide]: https://developer.bitcoin.org/devguide/transactions.html#locktime-and-sequence-number -#[derive( - Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize, -)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, BorshSerialize, BorshDeserialize)] pub struct LockTime(u32); impl LockTime { @@ -69,6 +74,64 @@ impl Decodable for LockTime { } } +impl<'de> Deserialize<'de> for LockTime { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct StringOrNumberVisitor; + + impl<'de> serde::de::Visitor<'de> for StringOrNumberVisitor { + type Value = LockTime; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a string or a number") + } + + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + if let Ok(value_parsed) = value.parse::() { + if value_parsed < LOCK_TIME_THRESHOLD { + return Ok(LockTime::from_height(value_parsed).unwrap()); + } else { + return Ok(LockTime::from_time(value_parsed).unwrap()); + } + } + // if the string is not a valid number, we return an error + Err(serde::de::Error::custom( + "Invalid lock time: expected a number", + )) + } + + fn visit_u32(self, value: u32) -> Result + where + E: serde::de::Error, + { + if value < LOCK_TIME_THRESHOLD { + Ok(LockTime::from_height(value).unwrap()) + } else { + Ok(LockTime::from_time(value).unwrap()) + } + } + + fn visit_u64(self, value: u64) -> Result + where + E: serde::de::Error, + { + if value < LOCK_TIME_THRESHOLD as u64 { + Ok(LockTime::from_height(value as u32).unwrap()) + } else { + Ok(LockTime::from_time(value as u32).unwrap()) + } + } + } + + deserializer.deserialize_any(StringOrNumberVisitor) + } +} + #[cfg(test)] mod tests { use super::*; @@ -154,4 +217,20 @@ mod tests { let decoded = LockTime::decode(&mut &buffer[..]).unwrap(); assert_eq!(locktime, decoded); } + + #[test] + fn test_from_json_locktime() { + let json = r#"0"#; + + let locktime: LockTime = serde_json::from_str(json).unwrap(); + assert_eq!(locktime, LockTime::from_height(0).unwrap()); + } + + #[test] + fn test_serde_json_locktime_with_number_as_string() { + let json = r#""0""#; + + let locktime: LockTime = serde_json::from_str(json).unwrap(); + assert_eq!(locktime, LockTime::from_height(0).unwrap()); + } } diff --git a/src/bitcoin/types/script_buf.rs b/src/bitcoin/types/script_buf.rs index d91c3ed..be39ce8 100644 --- a/src/bitcoin/types/script_buf.rs +++ b/src/bitcoin/types/script_buf.rs @@ -2,13 +2,10 @@ use core::fmt; use std::io::{BufRead, Write}; use borsh::{BorshDeserialize, BorshSerialize}; -use serde::{Deserialize, Serialize}; use crate::bitcoin::encoding::{encode::Encodable, Decodable}; -#[derive( - Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize, -)] +#[derive(Debug, Default, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] pub struct ScriptBuf(pub Vec); impl ScriptBuf { @@ -53,3 +50,90 @@ impl Decodable for ScriptBuf { Ok(Self(Decodable::decode_from_finite_reader(r)?)) } } + +impl serde::Serialize for ScriptBuf { + /// User-facing serialization for `Script`. + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_bytes(&self.0) + } +} + +impl<'de> serde::Deserialize<'de> for ScriptBuf { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use core::fmt::Formatter; + + if deserializer.is_human_readable() { + struct Visitor; + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = ScriptBuf; + + fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { + println!("expecting"); + formatter.write_str("a script hex") + } + + // fn visit_str(self, v: &str) -> Result + // where + // E: serde::de::Error, + // { + // Ok(ScriptBuf::from_hex(v).unwrap()) + // } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + if v.is_empty() { + Ok(ScriptBuf(vec![])) + } else { + ScriptBuf::from_hex(v).map_err(E::custom) + } + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let mut vec = Vec::new(); + while let Some(byte) = seq.next_element()? { + vec.push(byte); + } + Ok(ScriptBuf(vec)) + } + } + // deserializer.deserialize_str(Visitor) + deserializer.deserialize_any(Visitor) + } else { + struct BytesVisitor; + + impl<'de> serde::de::Visitor<'de> for BytesVisitor { + type Value = ScriptBuf; + + fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { + formatter.write_str("a script Vec") + } + + fn visit_bytes(self, v: &[u8]) -> Result + where + E: serde::de::Error, + { + Ok(ScriptBuf::from_bytes(v.to_vec())) + } + + fn visit_byte_buf(self, v: Vec) -> Result + where + E: serde::de::Error, + { + Ok(ScriptBuf::from_bytes(v)) + } + } + deserializer.deserialize_byte_buf(BytesVisitor) + } + } +} diff --git a/src/bitcoin/types/tx_in/hash.rs b/src/bitcoin/types/tx_in/hash.rs index a1816cc..619f5fe 100644 --- a/src/bitcoin/types/tx_in/hash.rs +++ b/src/bitcoin/types/tx_in/hash.rs @@ -9,7 +9,7 @@ use crate::bitcoin::encoding::{encode::Encodable, extensions::WriteExt, Decodabl #[derive( Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, BorshSerialize, BorshDeserialize, )] -pub struct Hash([u8; 32]); +pub struct Hash(pub [u8; 32]); impl Hash { pub const fn as_byte_array(&self) -> [u8; 32] { diff --git a/src/bitcoin/types/tx_in/outpoint.rs b/src/bitcoin/types/tx_in/outpoint.rs index 16983b1..c67b371 100644 --- a/src/bitcoin/types/tx_in/outpoint.rs +++ b/src/bitcoin/types/tx_in/outpoint.rs @@ -1,20 +1,22 @@ -use std::io::{BufRead, Write}; +use std::{ + fmt, + io::{BufRead, Write}, +}; use borsh::{BorshDeserialize, BorshSerialize}; -use serde::{Deserialize, Serialize}; - -use crate::bitcoin::encoding::{Decodable, Encodable}; +use serde::{de::MapAccess, Deserialize, Deserializer, Serialize}; +use super::hash::Hash; use super::tx_id::Txid; +use crate::bitcoin::encoding::{Decodable, Encodable}; + /// A reference to a transaction output. /// /// ### Bitcoin Core References /// /// * [COutPoint definition](https://github.com/bitcoin/bitcoin/blob/345457b542b6a980ccfbc868af0970a6f91d1b82/src/primitives/transaction.h#L26) -#[derive( - Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, BorshSerialize, BorshDeserialize, -)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, BorshSerialize, BorshDeserialize)] pub struct OutPoint { /// The referenced transaction's txid. pub txid: Txid, @@ -69,6 +71,72 @@ impl Decodable for OutPoint { } } +impl<'de> Deserialize<'de> for OutPoint { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_map(OutPointVisitor) + } +} + +struct OutPointVisitor; + +impl<'de> serde::de::Visitor<'de> for OutPointVisitor { + type Value = OutPoint; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a map with txid as a hex string and vout as a number or string") + } + + fn visit_map(self, mut map: V) -> Result + where + V: MapAccess<'de>, + { + let mut txid = None; + let mut vout = None; + + while let Some(key) = map.next_key::()? { + match key.as_str() { + "txid" => { + let txid_str: String = map.next_value()?; + let txid_bytes = hex::decode(&txid_str).map_err(serde::de::Error::custom)?; + if txid_bytes.len() != 32 { + return Err(serde::de::Error::custom("Invalid txid length")); + } + let mut hash_bytes = [0u8; 32]; + hash_bytes.copy_from_slice(&txid_bytes); + txid = Some(Txid(Hash(hash_bytes))); + } + "vout" => { + println!("vout"); + vout = Some( + map.next_value::() + .and_then(|vout_value| match vout_value { + serde_json::Value::Number(num) => num + .as_u64() + .map(|n| n as u32) + .ok_or_else(|| serde::de::Error::custom("Invalid vout number")), + serde_json::Value::String(s) => s + .parse::() + .map_err(|_| serde::de::Error::custom("Invalid vout string")), + _ => Err(serde::de::Error::custom("Invalid vout type")), + })?, + ); + } + _ => { + return Err(serde::de::Error::custom(format!("Unexpected key: {}", key))); + } + } + } + + let txid = txid.ok_or_else(|| serde::de::Error::missing_field("txid"))?; + let vout = vout.ok_or_else(|| serde::de::Error::missing_field("vout"))?; + + Ok(OutPoint { txid, vout }) + } +} + #[cfg(test)] mod tests { use super::*; @@ -87,4 +155,50 @@ mod tests { let decoded_outpoint = OutPoint::decode_from_finite_reader(&mut buf.as_slice()).unwrap(); assert_eq!(decoded_outpoint, outpoint); } + + #[test] + fn test_serde_json_outpoint() { + let json_string = r#"{ + "txid":"bc25cc0dddd0a202c21e66521a692c0586330a9a9dcc38ccd9b4d2093037f31a", + "vout":0 + }"#; + + let outpoint: OutPoint = serde_json::from_str(json_string).unwrap(); + println!("outpoint = {:?}", outpoint); + assert_eq!( + outpoint, + OutPoint { + txid: Txid( + Hash::from_hex( + "bc25cc0dddd0a202c21e66521a692c0586330a9a9dcc38ccd9b4d2093037f31a" + ) + .unwrap() + ), + vout: 0 + } + ); + } + + #[test] + fn test_serde_json_outpoint_with_string_vout() { + let json_string = r#"{ + "txid":"bc25cc0dddd0a202c21e66521a692c0586330a9a9dcc38ccd9b4d2093037f31a", + "vout":"0" + }"#; + + let outpoint: OutPoint = serde_json::from_str(json_string).unwrap(); + println!("outpoint = {:?}", outpoint); + assert_eq!( + outpoint, + OutPoint { + txid: Txid( + Hash::from_hex( + "bc25cc0dddd0a202c21e66521a692c0586330a9a9dcc38ccd9b4d2093037f31a" + ) + .unwrap() + ), + vout: 0 + } + ); + } } diff --git a/src/bitcoin/types/tx_in/witness.rs b/src/bitcoin/types/tx_in/witness.rs index 07c0c5a..43eeef1 100644 --- a/src/bitcoin/types/tx_in/witness.rs +++ b/src/bitcoin/types/tx_in/witness.rs @@ -1,7 +1,6 @@ use std::io::{BufRead, Write}; use borsh::{BorshDeserialize, BorshSerialize}; -use serde::{Deserialize, Serialize}; use crate::bitcoin::encoding::{ decode::MAX_VEC_SIZE, extensions::WriteExt, utils::VarInt, Decodable, Encodable, @@ -17,7 +16,7 @@ use crate::bitcoin::encoding::{ /// saving some allocations. /// /// [segwit upgrade]: -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] pub struct Witness { /// Contains the witness `Vec>` serialization. /// @@ -259,3 +258,99 @@ fn resize_if_needed(vec: &mut Vec, required_len: usize) { vec.resize(new_len, 0); } } + +pub(crate) struct SerializeBytesAsHex<'a>(pub(crate) &'a [u8]); + +impl<'a> serde::Serialize for SerializeBytesAsHex<'a> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use hex::ToHex; + + serializer.collect_str(&format_args!("{}", self.0.encode_hex::())) + } +} + +// Serde keep backward compatibility with old Vec> format +impl serde::Serialize for Witness { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeSeq; + + let human_readable = serializer.is_human_readable(); + let mut seq = serializer.serialize_seq(Some(self.witness_elements))?; + + // Note that the `Iter` strips the varints out when iterating. + for elem in self.iter() { + if human_readable { + seq.serialize_element(&SerializeBytesAsHex(elem))?; + } else { + seq.serialize_element(&elem)?; + } + } + seq.end() + } +} + +impl<'de> serde::Deserialize<'de> for Witness { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct Visitor; // Human-readable visitor. + + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = Witness; + + fn expecting(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "a sequence of hex arrays") + } + + fn visit_seq>( + self, + mut a: A, + ) -> Result { + use serde::de::{self, Unexpected}; + let mut ret = match a.size_hint() { + Some(len) => Vec::with_capacity(len), + None => Vec::new(), + }; + + while let Some(elem) = a.next_element::()? { + let vec = hex::decode(&elem).map_err(|e| match e { + hex::FromHexError::InvalidHexCharacter { c, .. } => { + match core::char::from_u32(c.into()) { + Some(c) => de::Error::invalid_value( + Unexpected::Char(c), + &"a valid hex character", + ), + None => de::Error::invalid_value( + Unexpected::Other("invalid hex character"), + &"a valid hex character", + ), + } + } + hex::FromHexError::OddLength => { + de::Error::invalid_length(0, &"an even length string") + } + hex::FromHexError::InvalidStringLength => { + de::Error::invalid_length(0, &"an even length string") + } + })?; + ret.push(vec); + } + Ok(Witness::from_slice(&ret)) + } + } + + if deserializer.is_human_readable() { + deserializer.deserialize_seq(Visitor) + } else { + let vec: Vec> = serde::Deserialize::deserialize(deserializer)?; + Ok(Witness::from_slice(&vec)) + } + } +} diff --git a/src/bitcoin/types/version.rs b/src/bitcoin/types/version.rs index c4e4df3..bef2df3 100644 --- a/src/bitcoin/types/version.rs +++ b/src/bitcoin/types/version.rs @@ -1,7 +1,11 @@ -use std::io::{self, BufRead, Write}; +use std::{ + fmt, + io::{self, BufRead, Write}, +}; use borsh::{BorshDeserialize, BorshSerialize}; use near_sdk::serde::{Deserialize, Serialize}; +use serde::Deserializer; use crate::bitcoin::encoding::{Decodable, Encodable}; @@ -10,9 +14,7 @@ use crate::bitcoin::encoding::{Decodable, Encodable}; /// Currently, as specified by [BIP-68], only version 1 and 2 are considered standard. /// /// [BIP-68]: https://github.com/bitcoin/bips/blob/master/bip-0068.mediawiki -#[derive( - Debug, Copy, PartialEq, Eq, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize, -)] +#[derive(Debug, Copy, PartialEq, Eq, Clone, Serialize, BorshSerialize, BorshDeserialize)] #[borsh(use_discriminant = true)] pub enum Version { /// The original Bitcoin transaction version (pre-BIP-68) @@ -58,6 +60,63 @@ impl Decodable for Version { } } +impl<'de> Deserialize<'de> for Version { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct StringOrNumberVisitor; + + impl<'de> serde::de::Visitor<'de> for StringOrNumberVisitor { + type Value = Version; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a string or a number") + } + + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + let value_parsed = value + .trim() + .parse::() + .map_err(serde::de::Error::custom)?; + + match value_parsed { + 1 => Ok(Version::One), + 2 => Ok(Version::Two), + _ => Err(serde::de::Error::custom("Invalid version number")), + } + } + + fn visit_u32(self, value: u32) -> Result + where + E: serde::de::Error, + { + match value { + 1 => Ok(Version::One), + 2 => Ok(Version::Two), + _ => Err(serde::de::Error::custom("Invalid version number")), + } + } + + fn visit_u64(self, value: u64) -> Result + where + E: serde::de::Error, + { + match value { + 1 => Ok(Version::One), + 2 => Ok(Version::Two), + _ => Err(serde::de::Error::custom("Invalid version number")), + } + } + } + + deserializer.deserialize_any(StringOrNumberVisitor) + } +} + #[cfg(test)] mod tests { use super::*; @@ -135,4 +194,18 @@ mod tests { assert_eq!(version, deserialized); } + + #[test] + fn test_version_serde_deserialization() { + let json = r#"1"#; + let version: Version = serde_json::from_str(json).unwrap(); + assert_eq!(version, Version::One); + } + + #[test] + fn test_version_serde_deserialization_2() { + let json = r#"2"#; + let version: Version = serde_json::from_str(json).unwrap(); + assert_eq!(version, Version::Two); + } } diff --git a/tests/bitcoin_integration_test.rs b/tests/bitcoin_integration_test.rs index 53ce6f1..440b4dc 100644 --- a/tests/bitcoin_integration_test.rs +++ b/tests/bitcoin_integration_test.rs @@ -62,8 +62,6 @@ async fn test_send_p2pkh_using_rust_bitcoin_and_omni_library() -> Result<()> { let blockchain_info = client.get_blockchain_info().unwrap(); assert_eq!(0, blockchain_info.blocks); - // let bitcoind = bitcoind::BitcoinD::new().unwrap(); - // Setup testing environment let mut btc_test_context = BTCTestContext::new(client).unwrap(); From e48519e2e3601c713e46a4ee8c35920248819029 Mon Sep 17 00:00:00 2001 From: edsonalcala Date: Fri, 4 Oct 2024 23:39:16 +0100 Subject: [PATCH 2/8] wip: extended tests --- src/bitcoin/bitcoin_transaction.rs | 98 ++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/src/bitcoin/bitcoin_transaction.rs b/src/bitcoin/bitcoin_transaction.rs index ae020d8..08a59f0 100644 --- a/src/bitcoin/bitcoin_transaction.rs +++ b/src/bitcoin/bitcoin_transaction.rs @@ -564,5 +564,103 @@ mod tests { let tx = OmniBitcoinTransaction::from_json(json).unwrap(); println!("tx: {:?}", tx); + + assert_eq!(tx.version, Version::One); + assert_eq!(tx.lock_time, LockTime::from_height(0).unwrap()); + // input + assert_eq!(tx.input[0].script_sig, OmniScriptBuf::default()); + assert_eq!(tx.input[0].witness, OmniWitness::default()); + assert_eq!(tx.input[0].sequence, OmniSequence(4294967295)); + assert_eq!( + tx.input[0].previous_output, + OmniOutPoint { + txid: OmniTxid( + OmniHash::from_hex( + "bc25cc0dddd0a202c21e66521a692c0586330a9a9dcc38ccd9b4d2093037f31a" + ) + .unwrap() + ), + vout: 0 + } + ); + assert_eq!(tx.input.len(), 1); + // output + assert_eq!( + tx.output[0].script_pubkey, + OmniScriptBuf::from_hex("76a9148356ecd5f1761e60c144dc2f4de6bf7d8be7690688ad").unwrap() + ); + assert_eq!( + tx.output[1].script_pubkey, + OmniScriptBuf::from_hex("76a9148356ecd5f1761e60c144dc2f4de6bf7d8be7690688ac").unwrap() + ); + assert_eq!(tx.output[0].value, OmniAmount::from_sat(1)); + assert_eq!(tx.output[1].value, OmniAmount::from_sat(2649)); + assert_eq!(tx.output.len(), 2); + } + + #[test] + fn test_from_json_bitcoin_transaction_3() { + let json = r#" + { + "version": "2", + "lock_time": "0", + "input": [ + { + "previous_output": { + "txid": "bc25cc0dddd0a202c21e66521a692c0586330a9a9dcc38ccd9b4d2093037f31a", + "vout": 0 + }, + "script_sig": [], + "sequence": 4294967295, + "witness": [] + } + ], + "output": [ + { + "value": 1, + "script_pubkey": "76a9148356ecd5f1761e60c144dc2f4de6bf7d8be7690688ad" + }, + { + "value": 2649, + "script_pubkey": "76a9148356ecd5f1761e60c144dc2f4de6bf7d8be7690688ac" + } + ] + } + "#; + + let tx = OmniBitcoinTransaction::from_json(json).unwrap(); + println!("tx: {:?}", tx); + + assert_eq!(tx.version, Version::Two); + assert_eq!(tx.lock_time, LockTime::from_height(0).unwrap()); + // input + assert_eq!(tx.input[0].script_sig, OmniScriptBuf::default()); + assert_eq!(tx.input[0].witness, OmniWitness::default()); + assert_eq!(tx.input[0].sequence, OmniSequence(4294967295)); + assert_eq!( + tx.input[0].previous_output, + OmniOutPoint { + txid: OmniTxid( + OmniHash::from_hex( + "bc25cc0dddd0a202c21e66521a692c0586330a9a9dcc38ccd9b4d2093037f31a" + ) + .unwrap() + ), + vout: 0 + } + ); + assert_eq!(tx.input.len(), 1); + // output + assert_eq!( + tx.output[0].script_pubkey, + OmniScriptBuf::from_hex("76a9148356ecd5f1761e60c144dc2f4de6bf7d8be7690688ad").unwrap() + ); + assert_eq!( + tx.output[1].script_pubkey, + OmniScriptBuf::from_hex("76a9148356ecd5f1761e60c144dc2f4de6bf7d8be7690688ac").unwrap() + ); + assert_eq!(tx.output[0].value, OmniAmount::from_sat(1)); + assert_eq!(tx.output[1].value, OmniAmount::from_sat(2649)); + assert_eq!(tx.output.len(), 2); } } From b9a7c4b1683c6a88d34fff7a598bb881d6ac68f9 Mon Sep 17 00:00:00 2001 From: edsonalcala Date: Mon, 7 Oct 2024 16:31:04 +0100 Subject: [PATCH 3/8] fix: added near serde annotation --- src/bitcoin/bitcoin_transaction.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/bitcoin/bitcoin_transaction.rs b/src/bitcoin/bitcoin_transaction.rs index 08a59f0..5c06bad 100644 --- a/src/bitcoin/bitcoin_transaction.rs +++ b/src/bitcoin/bitcoin_transaction.rs @@ -12,6 +12,7 @@ use super::{ }; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(crate = "near_sdk::serde")] pub struct BitcoinTransaction { /// The protocol version, is currently expected to be 1 or 2 (BIP 68). pub version: Version, From af50dbc5c6f08db0d58a6b2a11789f5391572594 Mon Sep 17 00:00:00 2001 From: edsonalcala Date: Mon, 7 Oct 2024 17:29:32 +0100 Subject: [PATCH 4/8] fix lint --- src/bitcoin/types/tx_in/witness.rs | 38 +++++++++++++++--------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/bitcoin/types/tx_in/witness.rs b/src/bitcoin/types/tx_in/witness.rs index 43eeef1..32528dd 100644 --- a/src/bitcoin/types/tx_in/witness.rs +++ b/src/bitcoin/types/tx_in/witness.rs @@ -259,7 +259,7 @@ fn resize_if_needed(vec: &mut Vec, required_len: usize) { } } -pub(crate) struct SerializeBytesAsHex<'a>(pub(crate) &'a [u8]); +pub struct SerializeBytesAsHex<'a>(pub(crate) &'a [u8]); impl<'a> serde::Serialize for SerializeBytesAsHex<'a> { fn serialize(&self, serializer: S) -> Result @@ -313,31 +313,31 @@ impl<'de> serde::Deserialize<'de> for Witness { self, mut a: A, ) -> Result { - use serde::de::{self, Unexpected}; - let mut ret = match a.size_hint() { - Some(len) => Vec::with_capacity(len), - None => Vec::new(), - }; + let mut ret = a.size_hint().map_or_else(Vec::new, Vec::with_capacity); while let Some(elem) = a.next_element::()? { let vec = hex::decode(&elem).map_err(|e| match e { hex::FromHexError::InvalidHexCharacter { c, .. } => { - match core::char::from_u32(c.into()) { - Some(c) => de::Error::invalid_value( - Unexpected::Char(c), - &"a valid hex character", - ), - None => de::Error::invalid_value( - Unexpected::Other("invalid hex character"), - &"a valid hex character", - ), - } + core::char::from_u32(c.into()).map_or_else( + || { + serde::de::Error::invalid_value( + serde::de::Unexpected::Other("invalid hex character"), + &"a valid hex character", + ) + }, + |c| { + serde::de::Error::invalid_value( + serde::de::Unexpected::Char(c), + &"a valid hex character", + ) + }, + ) } hex::FromHexError::OddLength => { - de::Error::invalid_length(0, &"an even length string") + serde::de::Error::invalid_length(0, &"an even length string") } hex::FromHexError::InvalidStringLength => { - de::Error::invalid_length(0, &"an even length string") + serde::de::Error::invalid_length(0, &"an even length string") } })?; ret.push(vec); @@ -350,7 +350,7 @@ impl<'de> serde::Deserialize<'de> for Witness { deserializer.deserialize_seq(Visitor) } else { let vec: Vec> = serde::Deserialize::deserialize(deserializer)?; - Ok(Witness::from_slice(&vec)) + Ok(Self::from_slice(&vec)) } } } From a3bc91fb51db018dee059bae4821d2d9b01f2a3f Mon Sep 17 00:00:00 2001 From: edsonalcala Date: Mon, 7 Oct 2024 18:47:27 +0100 Subject: [PATCH 5/8] fix: serialize version json --- src/bitcoin/types/version.rs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/bitcoin/types/version.rs b/src/bitcoin/types/version.rs index bef2df3..8a0d5ab 100644 --- a/src/bitcoin/types/version.rs +++ b/src/bitcoin/types/version.rs @@ -14,7 +14,7 @@ use crate::bitcoin::encoding::{Decodable, Encodable}; /// Currently, as specified by [BIP-68], only version 1 and 2 are considered standard. /// /// [BIP-68]: https://github.com/bitcoin/bips/blob/master/bip-0068.mediawiki -#[derive(Debug, Copy, PartialEq, Eq, Clone, Serialize, BorshSerialize, BorshDeserialize)] +#[derive(Debug, Copy, PartialEq, Eq, Clone, BorshSerialize, BorshDeserialize)] #[borsh(use_discriminant = true)] pub enum Version { /// The original Bitcoin transaction version (pre-BIP-68) @@ -60,6 +60,19 @@ impl Decodable for Version { } } +impl Serialize for Version { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let version_number = match self { + Version::One => 1, + Version::Two => 2, + }; + serializer.serialize_i32(version_number) + } +} + impl<'de> Deserialize<'de> for Version { fn deserialize(deserializer: D) -> Result where @@ -117,6 +130,12 @@ impl<'de> Deserialize<'de> for Version { } } +impl fmt::Display for Version { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.to_string(), f) + } +} + #[cfg(test)] mod tests { use super::*; @@ -180,6 +199,7 @@ mod tests { fn test_version_serde_serialization() { let version = Version::One; let serialized = serde_json::to_string(&version).unwrap(); + let deserialized: Version = serde_json::from_str(&serialized).unwrap(); // Check that the version is the same after serde serialization and deserialization From ec52d442332c686a504a1368ceda30141f7670f8 Mon Sep 17 00:00:00 2001 From: edsonalcala Date: Mon, 7 Oct 2024 20:12:50 +0100 Subject: [PATCH 6/8] fix: added json schema --- Cargo.toml | 3 ++- src/bitcoin/bitcoin_transaction.rs | 13 ++++++++++++- src/bitcoin/types/lock_time/lock_time.rs | 5 ++++- src/bitcoin/types/script_buf.rs | 3 ++- src/bitcoin/types/tx_in/hash.rs | 13 ++++++++++++- src/bitcoin/types/tx_in/outpoint.rs | 6 +++++- src/bitcoin/types/tx_in/sequence.rs | 13 ++++++++++++- src/bitcoin/types/tx_in/tx_id.rs | 13 ++++++++++++- src/bitcoin/types/tx_in/tx_in.rs | 19 +++++++++++++++---- src/bitcoin/types/tx_in/witness.rs | 16 +++++++++------- src/bitcoin/types/tx_out/amount.rs | 13 ++++++++++++- src/bitcoin/types/tx_out/tx_out.rs | 14 +++++++++++++- src/bitcoin/types/version.rs | 3 ++- 13 files changed, 112 insertions(+), 22 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index af9844b..bb12d20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ overflow-checks = true [features] default = ["all"] all = ["near", "bitcoin", "evm"] -bitcoin = ["sha2"] +bitcoin = ["sha2", "schemars"] evm = [] near = [] @@ -34,6 +34,7 @@ bs58 = "0.5.1" serde = "1.0" serde_json = "1.0" sha2 = { version = "0.10.8", optional = true } +schemars = { version = "0.8.11", optional = true } [dev-dependencies] # ethereum diff --git a/src/bitcoin/bitcoin_transaction.rs b/src/bitcoin/bitcoin_transaction.rs index 5c06bad..7db6aa3 100644 --- a/src/bitcoin/bitcoin_transaction.rs +++ b/src/bitcoin/bitcoin_transaction.rs @@ -1,5 +1,6 @@ use borsh::{BorshDeserialize, BorshSerialize}; use near_sdk::serde::{Deserialize, Serialize}; +use schemars::JsonSchema; use sha2::{Digest, Sha256}; use std::io::{BufRead, Write}; @@ -11,7 +12,17 @@ use super::{ }, }; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive( + Debug, + Clone, + PartialEq, + Eq, + Serialize, + Deserialize, + BorshSerialize, + BorshDeserialize, + JsonSchema, +)] #[serde(crate = "near_sdk::serde")] pub struct BitcoinTransaction { /// The protocol version, is currently expected to be 1 or 2 (BIP 68). diff --git a/src/bitcoin/types/lock_time/lock_time.rs b/src/bitcoin/types/lock_time/lock_time.rs index 9bf60fd..e9286cf 100644 --- a/src/bitcoin/types/lock_time/lock_time.rs +++ b/src/bitcoin/types/lock_time/lock_time.rs @@ -11,6 +11,7 @@ use std::{ use borsh::{BorshDeserialize, BorshSerialize}; use near_sdk::serde::{Deserialize, Serialize}; +use schemars::JsonSchema; use serde::Deserializer; /// Locktime itself is an unsigned 4-byte integer which can be parsed two ways: @@ -23,7 +24,9 @@ use serde::Deserializer; /// The transaction can be added to any block whose block time is greater than the locktime. /// /// [Bitcoin Devguide]: https://developer.bitcoin.org/devguide/transactions.html#locktime-and-sequence-number -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, BorshSerialize, BorshDeserialize)] +#[derive( + Debug, Clone, Copy, PartialEq, Eq, Serialize, BorshSerialize, BorshDeserialize, JsonSchema, +)] pub struct LockTime(u32); impl LockTime { diff --git a/src/bitcoin/types/script_buf.rs b/src/bitcoin/types/script_buf.rs index be39ce8..3693241 100644 --- a/src/bitcoin/types/script_buf.rs +++ b/src/bitcoin/types/script_buf.rs @@ -2,10 +2,11 @@ use core::fmt; use std::io::{BufRead, Write}; use borsh::{BorshDeserialize, BorshSerialize}; +use schemars::JsonSchema; use crate::bitcoin::encoding::{encode::Encodable, Decodable}; -#[derive(Debug, Default, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] +#[derive(Debug, Default, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize, JsonSchema)] pub struct ScriptBuf(pub Vec); impl ScriptBuf { diff --git a/src/bitcoin/types/tx_in/hash.rs b/src/bitcoin/types/tx_in/hash.rs index 619f5fe..860be17 100644 --- a/src/bitcoin/types/tx_in/hash.rs +++ b/src/bitcoin/types/tx_in/hash.rs @@ -2,13 +2,24 @@ use core::fmt; use std::{io::BufRead, str::FromStr}; use borsh::{BorshDeserialize, BorshSerialize}; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use crate::bitcoin::encoding::{encode::Encodable, extensions::WriteExt, Decodable}; #[derive( - Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, BorshSerialize, BorshDeserialize, + Debug, + Copy, + Clone, + Eq, + PartialEq, + Serialize, + Deserialize, + BorshSerialize, + BorshDeserialize, + JsonSchema, )] +#[serde(crate = "near_sdk::serde")] pub struct Hash(pub [u8; 32]); impl Hash { diff --git a/src/bitcoin/types/tx_in/outpoint.rs b/src/bitcoin/types/tx_in/outpoint.rs index c67b371..89d8f22 100644 --- a/src/bitcoin/types/tx_in/outpoint.rs +++ b/src/bitcoin/types/tx_in/outpoint.rs @@ -4,6 +4,7 @@ use std::{ }; use borsh::{BorshDeserialize, BorshSerialize}; +use schemars::JsonSchema; use serde::{de::MapAccess, Deserialize, Deserializer, Serialize}; use super::hash::Hash; @@ -16,7 +17,10 @@ use crate::bitcoin::encoding::{Decodable, Encodable}; /// ### Bitcoin Core References /// /// * [COutPoint definition](https://github.com/bitcoin/bitcoin/blob/345457b542b6a980ccfbc868af0970a6f91d1b82/src/primitives/transaction.h#L26) -#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, BorshSerialize, BorshDeserialize)] +#[derive( + Debug, Copy, Clone, Eq, PartialEq, Serialize, BorshSerialize, BorshDeserialize, JsonSchema, +)] +#[serde(crate = "near_sdk::serde")] pub struct OutPoint { /// The referenced transaction's txid. pub txid: Txid, diff --git a/src/bitcoin/types/tx_in/sequence.rs b/src/bitcoin/types/tx_in/sequence.rs index 56f6882..dc0eed1 100644 --- a/src/bitcoin/types/tx_in/sequence.rs +++ b/src/bitcoin/types/tx_in/sequence.rs @@ -1,14 +1,25 @@ use std::io::{BufRead, Write}; use borsh::{BorshDeserialize, BorshSerialize}; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use crate::bitcoin::encoding::{Decodable, Encodable}; /// Bitcoin transaction input sequence number. #[derive( - Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize, + Debug, + Copy, + Clone, + PartialEq, + Eq, + Serialize, + Deserialize, + BorshSerialize, + BorshDeserialize, + JsonSchema, )] +#[serde(crate = "near_sdk::serde")] pub struct Sequence(pub u32); impl Sequence { diff --git a/src/bitcoin/types/tx_in/tx_id.rs b/src/bitcoin/types/tx_in/tx_id.rs index c051d90..3bb749f 100644 --- a/src/bitcoin/types/tx_in/tx_id.rs +++ b/src/bitcoin/types/tx_in/tx_id.rs @@ -5,11 +5,22 @@ use crate::bitcoin::encoding::{Decodable, Encodable}; use super::hash::Hash; use borsh::{BorshDeserialize, BorshSerialize}; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; #[derive( - Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, BorshSerialize, BorshDeserialize, + Debug, + Copy, + Clone, + Eq, + PartialEq, + Serialize, + Deserialize, + BorshSerialize, + BorshDeserialize, + JsonSchema, )] +#[serde(crate = "near_sdk::serde")] pub struct Txid(pub Hash); impl Txid { diff --git a/src/bitcoin/types/tx_in/tx_in.rs b/src/bitcoin/types/tx_in/tx_in.rs index 91d2cc2..7ce2680 100644 --- a/src/bitcoin/types/tx_in/tx_in.rs +++ b/src/bitcoin/types/tx_in/tx_in.rs @@ -1,10 +1,10 @@ use std::io::{self, BufRead, Write}; -use borsh::{BorshDeserialize, BorshSerialize}; -use serde::{Deserialize, Serialize}; - use crate::bitcoin::encoding::{Decodable, Encodable}; use crate::bitcoin::types::script_buf::ScriptBuf; +use borsh::{BorshDeserialize, BorshSerialize}; +use near_sdk::serde::{Deserialize, Serialize}; +use schemars::JsonSchema; use super::{outpoint::OutPoint, sequence::Sequence, witness::Witness}; @@ -17,7 +17,18 @@ use super::{outpoint::OutPoint, sequence::Sequence, witness::Witness}; /// ### Bitcoin Core References /// /// * [CTxIn definition](https://github.com/bitcoin/bitcoin/blob/345457b542b6a980ccfbc868af0970a6f91d1b82/src/primitives/transaction.h#L65) -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive( + Debug, + Clone, + PartialEq, + Eq, + Serialize, + Deserialize, + BorshSerialize, + BorshDeserialize, + JsonSchema, +)] +#[serde(crate = "near_sdk::serde")] pub struct TxIn { /// The reference to the previous output that is being used as an input. pub previous_output: OutPoint, diff --git a/src/bitcoin/types/tx_in/witness.rs b/src/bitcoin/types/tx_in/witness.rs index 32528dd..45dae92 100644 --- a/src/bitcoin/types/tx_in/witness.rs +++ b/src/bitcoin/types/tx_in/witness.rs @@ -1,6 +1,8 @@ use std::io::{BufRead, Write}; use borsh::{BorshDeserialize, BorshSerialize}; +use near_sdk::serde::{Deserialize, Deserializer, Serialize, Serializer}; +use schemars::JsonSchema; use crate::bitcoin::encoding::{ decode::MAX_VEC_SIZE, extensions::WriteExt, utils::VarInt, Decodable, Encodable, @@ -16,7 +18,7 @@ use crate::bitcoin::encoding::{ /// saving some allocations. /// /// [segwit upgrade]: -#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] +#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize, JsonSchema)] pub struct Witness { /// Contains the witness `Vec>` serialization. /// @@ -261,10 +263,10 @@ fn resize_if_needed(vec: &mut Vec, required_len: usize) { pub struct SerializeBytesAsHex<'a>(pub(crate) &'a [u8]); -impl<'a> serde::Serialize for SerializeBytesAsHex<'a> { +impl<'a> Serialize for SerializeBytesAsHex<'a> { fn serialize(&self, serializer: S) -> Result where - S: serde::Serializer, + S: Serializer, { use hex::ToHex; @@ -273,10 +275,10 @@ impl<'a> serde::Serialize for SerializeBytesAsHex<'a> { } // Serde keep backward compatibility with old Vec> format -impl serde::Serialize for Witness { +impl Serialize for Witness { fn serialize(&self, serializer: S) -> Result where - S: serde::Serializer, + S: Serializer, { use serde::ser::SerializeSeq; @@ -295,10 +297,10 @@ impl serde::Serialize for Witness { } } -impl<'de> serde::Deserialize<'de> for Witness { +impl<'de> Deserialize<'de> for Witness { fn deserialize(deserializer: D) -> Result where - D: serde::Deserializer<'de>, + D: Deserializer<'de>, { struct Visitor; // Human-readable visitor. diff --git a/src/bitcoin/types/tx_out/amount.rs b/src/bitcoin/types/tx_out/amount.rs index e34ca55..e0d414b 100644 --- a/src/bitcoin/types/tx_out/amount.rs +++ b/src/bitcoin/types/tx_out/amount.rs @@ -4,6 +4,7 @@ use std::{ }; use borsh::{BorshDeserialize, BorshSerialize}; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use crate::bitcoin::encoding::{Decodable, Encodable}; @@ -13,8 +14,18 @@ use crate::bitcoin::encoding::{Decodable, Encodable}; /// The [`Amount`] type can be used to express Bitcoin amounts that support /// arithmetic and conversion to various denominations. #[derive( - Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize, + Debug, + Copy, + Clone, + PartialEq, + Eq, + Serialize, + Deserialize, + BorshSerialize, + BorshDeserialize, + JsonSchema, )] +#[serde(crate = "near_sdk::serde")] pub struct Amount(u64); impl Amount { diff --git a/src/bitcoin/types/tx_out/tx_out.rs b/src/bitcoin/types/tx_out/tx_out.rs index 8c39eb0..ac3f1d4 100644 --- a/src/bitcoin/types/tx_out/tx_out.rs +++ b/src/bitcoin/types/tx_out/tx_out.rs @@ -1,6 +1,7 @@ use std::io::{BufRead, Write}; use borsh::{BorshDeserialize, BorshSerialize}; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use crate::bitcoin::{ @@ -21,7 +22,18 @@ use super::amount::Amount; /// ### Bitcoin Core References /// /// * [CTxOut definition](https://github.com/bitcoin/bitcoin/blob/345457b542b6a980ccfbc868af0970a6f91d1b82/src/primitives/transaction.h#L148) -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive( + Debug, + Clone, + PartialEq, + Eq, + Serialize, + Deserialize, + BorshSerialize, + BorshDeserialize, + JsonSchema, +)] +#[serde(crate = "near_sdk::serde")] pub struct TxOut { /// The value of the output, in satoshis. pub value: Amount, diff --git a/src/bitcoin/types/version.rs b/src/bitcoin/types/version.rs index 8a0d5ab..5acc106 100644 --- a/src/bitcoin/types/version.rs +++ b/src/bitcoin/types/version.rs @@ -5,6 +5,7 @@ use std::{ use borsh::{BorshDeserialize, BorshSerialize}; use near_sdk::serde::{Deserialize, Serialize}; +use schemars::JsonSchema; use serde::Deserializer; use crate::bitcoin::encoding::{Decodable, Encodable}; @@ -14,7 +15,7 @@ use crate::bitcoin::encoding::{Decodable, Encodable}; /// Currently, as specified by [BIP-68], only version 1 and 2 are considered standard. /// /// [BIP-68]: https://github.com/bitcoin/bips/blob/master/bip-0068.mediawiki -#[derive(Debug, Copy, PartialEq, Eq, Clone, BorshSerialize, BorshDeserialize)] +#[derive(Debug, Copy, PartialEq, Eq, Clone, BorshSerialize, BorshDeserialize, JsonSchema)] #[borsh(use_discriminant = true)] pub enum Version { /// The original Bitcoin transaction version (pre-BIP-68) From f997fff9e570507b6ed8289a1c029bfe84277782 Mon Sep 17 00:00:00 2001 From: edsonalcala Date: Mon, 7 Oct 2024 23:21:11 +0100 Subject: [PATCH 7/8] fix: serialization issues solved --- src/bitcoin/bitcoin_transaction.rs | 71 +++++++++++++++++++++++++++++ src/bitcoin/types/tx_in/outpoint.rs | 43 ++++++++++++++++- 2 files changed, 112 insertions(+), 2 deletions(-) diff --git a/src/bitcoin/bitcoin_transaction.rs b/src/bitcoin/bitcoin_transaction.rs index 7db6aa3..63234b0 100644 --- a/src/bitcoin/bitcoin_transaction.rs +++ b/src/bitcoin/bitcoin_transaction.rs @@ -675,4 +675,75 @@ mod tests { assert_eq!(tx.output[1].value, OmniAmount::from_sat(2649)); assert_eq!(tx.output.len(), 2); } + + #[test] + fn test_from_json_bitcoin_transaction_4() { + let json = r#" + { + "version": "2", + "lock_time": "0", + "input": [ + { + "previous_output": { + "txid": "bc25cc0dddd0a202c21e66521a692c0586330a9a9dcc38ccd9b4d2093037f31a", + "vout": 0 + }, + "script_sig": [], + "sequence": 4294967295, + "witness": [] + } + ], + "output": [ + { + "value": 1, + "script_pubkey": "76a9148356ecd5f1761e60c144dc2f4de6bf7d8be7690688ad" + }, + { + "value": 2649, + "script_pubkey": "76a9148356ecd5f1761e60c144dc2f4de6bf7d8be7690688ac" + } + ] + } + "#; + + let tx = OmniBitcoinTransaction::from_json(json).unwrap(); + println!("tx: {:?}", tx); + + assert_eq!(tx.version, Version::Two); + assert_eq!(tx.lock_time, LockTime::from_height(0).unwrap()); + } + + #[test] + fn test_from_json_bitcoin_transaction_5() { + let json_data = r#" + { + "version": 1, + "lock_time": 1, + "input": [ + { + "previous_output": { + "txid": [59, 103, 22, 67, 189, 12, 138, 114, 42, 90, 207, 173, 211, 254, 197, 194, 92, 65, 224, 168, 146, 169, 213, 217, 184, 81, 123, 217, 19, 81, 69, 71], + "vout": 0 + }, + "script_sig": [], + "sequence": 4294967295, + "witness": [] + } + ], + "output": [ + { + "value": 500000000, + "script_pubkey": [118, 169, 20, 136, 240, 168, 35, 147, 140, 88, 207, 91, 23, 200, 235, 147, 198, 130, 128, 99, 91, 115, 78, 136, 172] + }, + { + "value": 4499999000, + "script_pubkey": [118, 169, 20, 197, 64, 140, 145, 44, 231, 221, 181, 123, 174, 124, 22, 79, 148, 247, 47, 225, 189, 178, 180, 136, 172] + } + ] + } + "#; + + let result: Result = serde_json::from_str(json_data); + assert!(result.is_ok(), "Failed to deserialize: {:?}", result.err()); + } } diff --git a/src/bitcoin/types/tx_in/outpoint.rs b/src/bitcoin/types/tx_in/outpoint.rs index 89d8f22..1746538 100644 --- a/src/bitcoin/types/tx_in/outpoint.rs +++ b/src/bitcoin/types/tx_in/outpoint.rs @@ -103,8 +103,27 @@ impl<'de> serde::de::Visitor<'de> for OutPointVisitor { while let Some(key) = map.next_key::()? { match key.as_str() { "txid" => { - let txid_str: String = map.next_value()?; - let txid_bytes = hex::decode(&txid_str).map_err(serde::de::Error::custom)?; + let txid_value: serde_json::Value = map.next_value()?; + let txid_bytes = match txid_value { + serde_json::Value::String(txid_str) => { + hex::decode(&txid_str).map_err(serde::de::Error::custom)? + } + serde_json::Value::Array(txid_array) => txid_array + .into_iter() + .map(|x| { + if let serde_json::Value::Number(num) = x { + if let Some(i) = num.as_u64() { + if i <= u8::MAX as u64 { + return Ok(i as u8); + } + } + } + Err(serde::de::Error::custom("Invalid number for u8")) + }) + .collect::, _>>()?, + _ => return Err(serde::de::Error::custom("Invalid format for txid")), + }; + if txid_bytes.len() != 32 { return Err(serde::de::Error::custom("Invalid txid length")); } @@ -205,4 +224,24 @@ mod tests { } ); } + + #[test] + fn test_serde_json_outpoint_with_arrays() { + let json_string = r#"{ + "txid": [59, 103, 22, 67, 189, 12, 138, 114, 42, 90, 207, 173, 211, 254, 197, 194, 92, 65, 224, 168, 146, 169, 213, 217, 184, 81, 123, 217, 19, 81, 69, 71], + "vout":"0" + }"#; + + let outpoint: OutPoint = serde_json::from_str(json_string).unwrap(); + assert_eq!( + outpoint, + OutPoint { + txid: Txid(Hash([ + 59, 103, 22, 67, 189, 12, 138, 114, 42, 90, 207, 173, 211, 254, 197, 194, 92, + 65, 224, 168, 146, 169, 213, 217, 184, 81, 123, 217, 19, 81, 69, 71 + ])), + vout: 0 + } + ); + } } From 95783f45d5200ee7412c6b8656c259c0973a1191 Mon Sep 17 00:00:00 2001 From: edsonalcala Date: Tue, 15 Oct 2024 16:22:03 +0100 Subject: [PATCH 8/8] fixed lint --- src/bitcoin/types/version.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/bitcoin/types/version.rs b/src/bitcoin/types/version.rs index 5acc106..80465a5 100644 --- a/src/bitcoin/types/version.rs +++ b/src/bitcoin/types/version.rs @@ -67,8 +67,8 @@ impl Serialize for Version { S: serde::Serializer, { let version_number = match self { - Version::One => 1, - Version::Two => 2, + Self::One => 1, + Self::Two => 2, }; serializer.serialize_i32(version_number) } @@ -133,7 +133,11 @@ impl<'de> Deserialize<'de> for Version { impl fmt::Display for Version { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Display::fmt(&self.to_string(), f) + let version_number = match self { + Self::One => "1", + Self::Two => "2", + }; + write!(f, "{}", version_number) } }