use crate::chain::ergo_box::box_value::BoxValue;
use crate::chain::ergo_box::BoxId;
use crate::chain::ergo_box::BoxTokens;
use crate::chain::ergo_box::ErgoBox;
use crate::chain::ergo_box::ErgoBoxCandidate;
use crate::chain::ergo_box::NonMandatoryRegisters;
use crate::chain::ergo_box::RegisterValue;
use crate::chain::token::Token;
use crate::chain::tx_id::TxId;
use crate::ergo_tree::ErgoTree;
use crate::serialization::SigmaParsingError;
use crate::serialization::SigmaSerializationError;
use ergo_chain_types::Base16DecodedBytes;
use std::convert::TryFrom;
use std::convert::TryInto;
use std::str::FromStr;
extern crate derive_more;
use derive_more::From;
use serde::{Deserialize, Serialize};
use thiserror::Error;
mod box_value;
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
pub struct ErgoBoxJson {
#[serde(rename = "boxId", alias = "id")]
pub box_id: Option<BoxId>,
/// amount of money associated with the box
#[serde(rename = "value")]
pub value: BoxValue,
/// guarding script, which should be evaluated to true in order to open this box
#[serde(rename = "ergoTree", with = "super::ergo_tree")]
pub ergo_tree: ErgoTree,
/// secondary tokens the box contains
#[serde(rename = "assets")]
pub tokens: Vec<Token>,
/// additional registers the box can carry over
#[serde(rename = "additionalRegisters")]
pub additional_registers: NonMandatoryRegisters,
/// height when a transaction containing the box was created.
/// This height is declared by user and should not exceed height of the block,
/// containing the transaction with this box.
#[serde(rename = "creationHeight")]
pub creation_height: u32,
/// id of transaction which created the box
#[serde(rename = "transactionId", alias = "txId")]
pub transaction_id: TxId,
/// number of box (from 0 to total number of boxes the transaction with transactionId created - 1)
#[serde(rename = "index")]
pub index: u16,
}
impl TryFrom<ErgoBoxJson> for ErgoBox {
type Error = ErgoBoxFromJsonError;
fn try_from(box_json: ErgoBoxJson) -> Result<Self, Self::Error> {
let tokens = if box_json.tokens.is_empty() {
None
} else {
Some(box_json.tokens.try_into().map_err(|_| {
SigmaSerializationError::NotSupported(
"More than ErgoBox::MAX_TOKENS_COUNT tokens are not allowed in a box"
.to_string(),
)
})?)
};
let box_with_zero_id = ErgoBox {
box_id: BoxId::zero(),
value: box_json.value,
ergo_tree: box_json.ergo_tree,
tokens,
additional_registers: box_json.additional_registers,
creation_height: box_json.creation_height,
transaction_id: box_json.transaction_id,
index: box_json.index,
};
let box_id = box_with_zero_id.calc_box_id()?;
let ergo_box = ErgoBox {
box_id,
..box_with_zero_id
};
match box_json.box_id {
Some(box_id) => {
if ergo_box.box_id() == box_id {
Ok(ergo_box)
} else {
//dbg!(&ergo_box);
Err(ErgoBoxFromJsonError::InvalidBoxId {
json: box_id,
actual: ergo_box.box_id(),
})
}
}
None => Ok(ergo_box),
}
}
}
impl From<ErgoBox> for ErgoBoxJson {
fn from(ergo_box: ErgoBox) -> ErgoBoxJson {
let tokens = ergo_box
.tokens
.as_ref()
.map(BoxTokens::as_vec)
.cloned()
.unwrap_or_default(); // JSON serialization for assets requires that tokens be [] instead of null
ErgoBoxJson {
box_id: Some(ergo_box.box_id),
value: ergo_box.value,
ergo_tree: ergo_box.ergo_tree,
tokens,
additional_registers: ergo_box.additional_registers,
creation_height: ergo_box.creation_height,
transaction_id: ergo_box.transaction_id,
index: ergo_box.index,
}
}
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
pub struct ErgoBoxCandidateJson {
/// amount of money associated with the box
#[serde(rename = "value")]
pub value: BoxValue,
/// guarding script, which should be evaluated to true in order to open this box
#[serde(rename = "ergoTree", with = "super::ergo_tree")]
pub ergo_tree: ErgoTree,
/// secondary tokens the box contains
#[serde(rename = "assets")]
pub tokens: Vec<Token>,
/// additional registers the box can carry over
#[serde(rename = "additionalRegisters")]
pub additional_registers: NonMandatoryRegisters,
/// height when a transaction containing the box was created.
/// This height is declared by user and should not exceed height of the block,
/// containing the transaction with this box.
#[serde(rename = "creationHeight")]
pub creation_height: u32,
}
impl From<ErgoBoxCandidate> for ErgoBoxCandidateJson {
fn from(ergo_box_candidate: ErgoBoxCandidate) -> Self {
let tokens = ergo_box_candidate
.tokens
.as_ref()
.map(BoxTokens::as_vec)
.cloned()
.unwrap_or_default(); // JSON serialization for assets requires that tokens be [] instead of null
ErgoBoxCandidateJson {
value: ergo_box_candidate.value,
ergo_tree: ergo_box_candidate.ergo_tree,
tokens,
additional_registers: ergo_box_candidate.additional_registers,
creation_height: ergo_box_candidate.creation_height,
}
}
}
impl TryFrom<ErgoBoxCandidateJson> for ErgoBoxCandidate {
type Error = ErgoBoxFromJsonError;
fn try_from(box_json: ErgoBoxCandidateJson) -> Result<Self, Self::Error> {
let tokens = if box_json.tokens.is_empty() {
None
} else {
Some(box_json.tokens.try_into().map_err(|_| {
SigmaSerializationError::NotSupported(
"More than ErgoBox::MAX_TOKENS_COUNT tokens are not allowed in a box"
.to_string(),
)
})?)
};
Ok(ErgoBoxCandidate {
value: box_json.value,
ergo_tree: box_json.ergo_tree,
tokens,
additional_registers: box_json.additional_registers,
creation_height: box_json.creation_height,
})
}
}
/// Errors on parsing ErgoBox from JSON
#[derive(Error, PartialEq, Eq, Debug, Clone)]
pub enum ErgoBoxFromJsonError {
/// Box id parsed from JSON differs from calculated from box serialized bytes
#[error(
"Box id parsed from JSON {json} differs from calculated from box serialized bytes {actual}"
)]
InvalidBoxId { json: BoxId, actual: BoxId },
/// Box serialization failed (id calculation)
#[error("Box serialization failed (id calculation): {0}")]
SerializationError(#[from] SigmaSerializationError),
}
#[derive(Deserialize, PartialEq, Eq, Debug, Clone)]
pub struct ConstantHolder(#[serde(deserialize_with = "super::t_as_string_or_struct")] RichConstant);
impl From<ConstantHolder> for RegisterValue {
fn from(ch: ConstantHolder) -> Self {
RegisterValue::sigma_parse_bytes(ch.0.raw_value.0.as_slice())
}
}
#[derive(Deserialize, PartialEq, Eq, Debug, Clone)]
struct RichConstant {
#[serde(rename = "rawValue", alias = "serializedValue")]
raw_value: ConstantWrapper,
}
#[derive(Deserialize, PartialEq, Eq, Debug, Clone)]
#[serde(try_from = "Base16DecodedBytes")]
struct ConstantWrapper(Vec<u8>);
#[derive(Error, PartialEq, Eq, Debug, Clone, From)]
pub enum ConstantParsingError {
#[error("Base16 decoding error: {0}")]
DecodeError(base16::DecodeError),
#[error("Deserialization error: {0}")]
DeserializationError(SigmaParsingError),
}
impl TryFrom<Base16DecodedBytes> for ConstantWrapper {
type Error = ConstantParsingError;
fn try_from(Base16DecodedBytes(bytes): Base16DecodedBytes) -> Result<Self, Self::Error> {
Ok(ConstantWrapper(bytes))
}
}
impl FromStr for RichConstant {
type Err = ConstantParsingError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let Base16DecodedBytes(bytes) = Base16DecodedBytes::try_from(s)?;
Ok(RichConstant {
raw_value: ConstantWrapper(bytes),
})
}
}
#[allow(clippy::panic)]
#[allow(clippy::unwrap_used)]
#[cfg(test)]
mod tests {
use std::convert::TryInto;
use crate::chain::ergo_box::ErgoBox;
use crate::chain::ergo_box::NonMandatoryRegisterId;
use crate::chain::ergo_box::NonMandatoryRegisters;
use crate::chain::token::Token;
use pretty_assertions::assert_eq;
use proptest::prelude::*;
proptest! {
#[test]
fn ergo_box_roundtrip(b in any::<ErgoBox>()) {
let j = serde_json::to_string(&b)?;
// eprintln!("{}", j);
let b_parsed: ErgoBox = serde_json::from_str(&j)?;
prop_assert_eq![b, b_parsed];
}
}
#[test]
fn parse_registers() {
let json = r#"
{"R4":"05b0b5cad8e6dbaef44a","R5":"048ce5d4e505"}
"#;
let regs: NonMandatoryRegisters = serde_json::from_str(json).unwrap();
assert_eq!(regs.len(), 2)
}
#[test]
fn parse_registers_explorer_api_v1() {
let json = r#"
{"R4":{"serializedValue":"0500","sigmaType":"SLong","renderedValue":"0"}}
"#;
let regs: NonMandatoryRegisters = serde_json::from_str(json).unwrap();
assert!(regs.get_constant(NonMandatoryRegisterId::R4).is_ok());
}
#[test]
fn parse_registers_explorer_api_v2() {
let json = r#"
{
"R4": {
"decodedValue": "Coll(-89,30,-127,32,-20,-100,-42,0,-25,-9,-25,107,-100,27,10,-97,127,127,-93,109,-48,70,51,-111,27,85,107,-116,97,102,87,45)",
"valueType": "Coll[Byte]",
"rawValue": "0e20a71e8120ec9cd600e7f7e76b9c1b0a9f7f7fa36dd04633911b556b8c6166572d"
}
}
"#;
let regs: NonMandatoryRegisters = serde_json::from_str(json).unwrap();
assert!(regs.get_constant(NonMandatoryRegisterId::R4).is_ok());
}
#[test]
fn parse_registers_error() {
// base16 decoding error
let json = r#"
{"R4":"0"}
"#;
let regs: Result<NonMandatoryRegisters, _> = serde_json::from_str(json);
assert!(regs.is_err());
}
#[test]
fn parse_registers_unit() {
let json = r#" {"R4":"62"} "#;
let regs: NonMandatoryRegisters = serde_json::from_str(json).unwrap();
assert_eq!(regs.len(), 1)
}
#[test]
fn parse_ergo_box() {
let box_json = r#"{
"boxId": "e56847ed19b3dc6b72828fcfb992fdf7310828cf291221269b7ffc72fd66706e",
"value": 67500000000,
"ergoTree": "100204a00b08cd021dde34603426402615658f1d970cfa7c7bd92ac81a8b16eeebff264d59ce4604ea02d192a39a8cc7a70173007301",
"assets": [],
"creationHeight": 284761,
"additionalRegisters": {},
"transactionId": "9148408c04c2e38a6402a7950d6157730fa7d49e9ab3b9cadec481d7769918e9",
"index": 1
}"#;
let b: ErgoBox = serde_json::from_str(box_json).unwrap();
assert_eq!(b.value, 67500000000u64.try_into().unwrap());
}
#[test]
fn parse_ergo_box_without_id() {
let box_json = r#"{
"value": 67500000000,
"ergoTree": "100204a00b08cd021dde34603426402615658f1d970cfa7c7bd92ac81a8b16eeebff264d59ce4604ea02d192a39a8cc7a70173007301",
"assets": [],
"creationHeight": 284761,
"additionalRegisters": {},
"transactionId": "9148408c04c2e38a6402a7950d6157730fa7d49e9ab3b9cadec481d7769918e9",
"index": 1
}"#;
let b: ErgoBox = serde_json::from_str(box_json).unwrap();
let id: String = b.box_id().into();
assert_eq!(
id,
"e56847ed19b3dc6b72828fcfb992fdf7310828cf291221269b7ffc72fd66706e"
);
}
#[test]
fn parse_ergo_box_alternative_box_id_field_name() {
// check that using "id" field name instead of "boxId" also works
// used in explorer API
let box_json = r#"{
"id": "e56847ed19b3dc6b72828fcfb992fdf7310828cf291221269b7ffc72fd66706e",
"value": 67500000000,
"ergoTree": "100204a00b08cd021dde34603426402615658f1d970cfa7c7bd92ac81a8b16eeebff264d59ce4604ea02d192a39a8cc7a70173007301",
"assets": [],
"creationHeight": 284761,
"additionalRegisters": {},
"transactionId": "9148408c04c2e38a6402a7950d6157730fa7d49e9ab3b9cadec481d7769918e9",
"index": 1
}"#;
let b: ErgoBox = serde_json::from_str(box_json).unwrap();
assert_eq!(b.value, 67500000000u64.try_into().unwrap());
}
#[test]
fn parse_ergo_box_alternative_transaction_id_field_name() {
// check that using "txId" field name instead of "transactionId" also works
// used in explorer API
let box_json = r#"{
"id": "e56847ed19b3dc6b72828fcfb992fdf7310828cf291221269b7ffc72fd66706e",
"value": 67500000000,
"ergoTree": "100204a00b08cd021dde34603426402615658f1d970cfa7c7bd92ac81a8b16eeebff264d59ce4604ea02d192a39a8cc7a70173007301",
"assets": [],
"creationHeight": 284761,
"additionalRegisters": {},
"txId": "9148408c04c2e38a6402a7950d6157730fa7d49e9ab3b9cadec481d7769918e9",
"index": 1
}"#;
let b: ErgoBox = serde_json::from_str(box_json).unwrap();
assert_eq!(b.value, 67500000000u64.try_into().unwrap());
}
#[test]
fn parse_ergo_box_from_explorer() {
let box_json = r#"
{
"id": "3e762407d99b006d53b6583adcca08ef690b42fb0b2ed7abf63179eb6b9033b2",
"txId": "93d344aa527e18e5a221db060ea1a868f46b61e4537e6e5f69ecc40334c15e38",
"value": 2875858910,
"index": 0,
"creationHeight": 352126,
"ergoTree": "101f0400040004020402040004000402050005000580dac4090580dac409050005c00c05c80104000e20b662db51cf2dc39f110a021c2a31c74f0a1a18ffffbf73e8a051a7b8c0f09ebc0580dac40904040404050005feffffffffffffffff01050005e807050005e807050005a0060101050005c00c05a006d81ed601b2db6501fe730000d602b2a5730100d603c17202d604db6308a7d605b27204730200d6068c720502d607db63087202d608b27207730300d6098c720802d60a9472067209d60bb27204730400d60c8c720b02d60db27207730500d60e8c720d02d60f94720c720ed610e4c6a70505d611e4c672020505d612e4c6a70405d613e4c672020405d614b2a5730600d615e4c672140405d61695720a73077215d61795720a72157308d61899c1a77309d619e4c672140505d61a997203730ad61be4c672010405d61ca172189c7212721bd61d9c7213721bd61e9593721d730b730c9d9c721a730d721dd1ededed938cb2db63087201730e0001730fedededed9272037310edec720a720fefed720a720fed939a720672109a72097211939a720c72129a720e7213eded939a721272167213939a721072177211939a72187219721aeded938c720d018c720b01938c7208018c720501938cb27207731100018cb272047312000193721995720f9ca1721b95937212731373149d721c72127216d801d61f997218721c9c9593721f7315731695937210731773189d721f7210721795720f95917216731992721e731a731b95917217731c90721e731d92721e731e",
"address": "9aFbqNsmDwSxCdcLDKmSxVTL58ms2A39Rpn2zodVzkBN5MzB8zvW5PFX551W1A5vUdFJ3yxwvwgYTTS4JrPQcb5qxBbRDJkGNikuqHRXhnbniK4ajumEj7ot2o7DbcNFaM674fWufQzSGS1KtgMw95ZojyqhswUNbKpYDV1PhKw62bEMdJL9vAvzea4KwKXGUTdYYkcPdQKFWXfrdo2nTS3ucFNxqyTRB3VtZk7AWE3eeNHFcXZ1kLkfrX1ZBjpQ7qrBemHk4KZgS8fzmm6hPSZThiVVtBfQ2CZhJQdAZjRwGrw5TDcZ4BBDAZxg9h13vZ7tQSPsdAtjMFQT1DxbqAruKxX38ZwaQ3UfWmbBpbJEThAQaS4gsCBBSjswrv8BvupxaHZ4oQmA2LZiz4nYaPr8MJtR4fbM9LErwV4yDVMb873bRE5TBF59NipUyHAir7ysajPjbGc8aRLqsMVjntFSCFYx7822RBrj7RRX11CpiGK6vdfKHe3k14EH6YaNXvGSq8DrfNHEK4SgreknTqCgjL6i3EMZKPCW8Lao3Q5tbJFnFjEyntpUDf5zfGgFURxzobeEY4USqFaxyppHkgLjQuFQtDWbYVu3ztQL6hdWHjZXMK4VVvEDeLd1woebD1CyqS5kJHpGa78wQZ4iKygw4ijYrodZpqqEwTXdqwEB6xaLfkxZCBPrYPST3xz67GGTBUFy6zkXP5vwVVM5gWQJFdWCZniAAzBpzHeVq1yzaBp5GTJgr9bfrrAmuX8ra1m125yfeT9sTWroVu",
"assets": [
{
"tokenId": "2d554219a80c011cc51509e34fa4950965bb8e01de4d012536e766c9ca08bc2c",
"index": 0,
"amount": 99999999998
},
{
"tokenId": "bcd5db3a2872f279ef89edaa51a9344a6095ea1f03396874b695b5ba95ff602e",
"index": 1,
"amount": 99995619990
},
{
"tokenId": "9f90c012e03bf99397e363fb1571b7999941e0862a217307e3467ee80cf53af7",
"index": 2,
"amount": 1
}
],
"additionalRegisters": {
"R4": "0504",
"R5": "05d4d59604"
},
"spentTransactionId": null,
"mainChain": true
}
"#;
let b: ErgoBox = serde_json::from_str(box_json).unwrap();
assert_eq!(b.value, 2875858910u64.try_into().unwrap());
}
#[test]
fn parse_token_amount_as_num() {
let token_json = r#"
{
"tokenId": "2d554219a80c011cc51509e34fa4950965bb8e01de4d012536e766c9ca08bc2c",
"amount": 99999999998
}"#;
let t: Token = serde_json::from_str(token_json).unwrap();
assert_eq!(t.amount, 99999999998u64.try_into().unwrap());
}
#[test]
fn parse_token_amount_as_str() {
let token_json = r#"
{
"tokenId": "2d554219a80c011cc51509e34fa4950965bb8e01de4d012536e766c9ca08bc2c",
"amount": "99999999998"
}"#;
let t: Token = serde_json::from_str(token_json).unwrap();
assert_eq!(t.amount, 99999999998u64.try_into().unwrap());
}
#[test]
fn encode_token_amount_as_num() {
let token_json = "{\n \"tokenId\": \"2d554219a80c011cc51509e34fa4950965bb8e01de4d012536e766c9ca08bc2c\",\n \"amount\": 99999999998\n}";
let t: Token = serde_json::from_str(token_json).unwrap();
assert_eq!(t.amount, 99999999998u64.try_into().unwrap());
let to_json = serde_json::to_string_pretty(&t).unwrap();
assert_eq!(to_json, token_json);
}
}