use cfg_traits::{
liquidity_pools::{
LpMessageBatch, LpMessageForwarded, LpMessageHash, LpMessageProof, LpMessageRecovery,
LpMessageSerializer, MessageHash,
},
Seconds,
};
use cfg_types::domain_address::Domain;
use frame_support::{pallet_prelude::RuntimeDebug, BoundedVec};
use parity_scale_codec::{Decode, Encode, MaxEncodedLen};
use scale_info::{prelude::string::ToString, TypeInfo};
use serde::{
de::{Deserializer, Error as _, SeqAccess, Visitor},
ser::{Error as _, SerializeTuple},
Deserialize, Serialize, Serializer,
};
use sp_core::H160;
use sp_io::hashing::keccak_256;
use sp_runtime::{traits::ConstU32, DispatchError, DispatchResult};
use sp_std::{boxed::Box, vec, vec::Vec};
use crate::gmpf; type Address = [u8; 32];
type TrancheId = [u8; 16];
pub const TOKEN_NAME_SIZE: usize = 128;
pub const TOKEN_SYMBOL_SIZE: usize = 32;
const MAX_BATCH_MESSAGES: u32 = 16;
#[derive(
Encode,
Decode,
Serialize,
Deserialize,
Clone,
PartialEq,
Eq,
RuntimeDebug,
TypeInfo,
MaxEncodedLen,
)]
pub struct SerializableDomain(u8, u64);
impl From<Domain> for SerializableDomain {
fn from(domain: Domain) -> Self {
match domain {
Domain::Centrifuge => Self(0, 0),
Domain::Evm(chain_id) => Self(1, chain_id),
}
}
}
impl TryInto<Domain> for SerializableDomain {
type Error = DispatchError;
fn try_into(self) -> Result<Domain, DispatchError> {
match self.0 {
0 => Ok(Domain::Centrifuge),
1 => Ok(Domain::Evm(self.1)),
_ => Err(DispatchError::Other("Unknown domain")),
}
}
}
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)]
pub struct NonBatchMessage(Message);
impl TryFrom<Message> for NonBatchMessage {
type Error = DispatchError;
fn try_from(message: Message) -> Result<Self, DispatchError> {
match message {
Message::Batch { .. } => Err(DispatchError::Other("A submessage can not be a batch")),
_ => Ok(Self(message)),
}
}
}
impl MaxEncodedLen for NonBatchMessage {
fn max_encoded_len() -> usize {
Message::<(), ()>::max_encoded_len()
}
}
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen, Default)]
pub struct BatchMessages(BoundedVec<NonBatchMessage, ConstU32<MAX_BATCH_MESSAGES>>);
impl Serialize for BatchMessages {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut tuple = serializer.serialize_tuple(self.0.len())?;
for msg in self.0.iter() {
let encoded = gmpf::to_vec(&msg.0).map_err(|e| S::Error::custom(e.to_string()))?;
tuple.serialize_element(&encoded)?;
}
tuple.end()
}
}
impl<'de> Deserialize<'de> for BatchMessages {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct MsgVisitor;
impl<'de> Visitor<'de> for MsgVisitor {
type Value = BatchMessages;
fn expecting(&self, formatter: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result {
formatter.write_str("A sequence of pairs size-submessage")
}
fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
let mut batch = BatchMessages::default();
while seq.next_element::<u16>().unwrap_or(None).is_some() {
let msg = seq
.next_element()?
.ok_or(A::Error::custom("expected submessage"))?;
batch
.try_add(msg)
.map_err(|e| A::Error::custom::<&'static str>(e.into()))?;
}
Ok(batch)
}
}
let limit = MAX_BATCH_MESSAGES as usize * 2; deserializer.deserialize_tuple(limit, MsgVisitor)
}
}
impl TryFrom<Vec<Message>> for BatchMessages {
type Error = DispatchError;
fn try_from(messages: Vec<Message>) -> Result<Self, DispatchError> {
Ok(Self(
messages
.into_iter()
.map(TryFrom::try_from)
.collect::<Result<Vec<_>, _>>()?
.try_into()
.map_err(|_| DispatchError::Other("Batch limit reached"))?,
))
}
}
impl IntoIterator for BatchMessages {
type IntoIter = sp_std::vec::IntoIter<Self::Item>;
type Item = Message;
fn into_iter(self) -> Self::IntoIter {
let messages: Vec<_> = self.0.into_iter().map(|msg| msg.0).collect();
messages.into_iter()
}
}
impl BatchMessages {
pub fn try_add(&mut self, message: Message) -> DispatchResult {
self.0
.try_push(message.try_into()?)
.map_err(|_| DispatchError::Other("Batch limit reached"))?;
Ok(())
}
pub fn len(&self) -> usize {
self.0.len()
}
}
#[derive(Encode, Decode, Serialize, Deserialize, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)]
pub struct NonForwardMessage(Box<Message>);
impl TryFrom<Message> for NonForwardMessage {
type Error = DispatchError;
fn try_from(message: Message) -> Result<Self, DispatchError> {
match message {
Message::Forwarded { .. } => Err(DispatchError::Other(
"The inner forwarded message can not be a forwarded one",
)),
_ => Ok(Self(message.into())),
}
}
}
impl From<NonForwardMessage> for Message {
fn from(value: NonForwardMessage) -> Self {
*value.0
}
}
impl MaxEncodedLen for NonForwardMessage {
fn max_encoded_len() -> usize {
Message::<BatchMessages, ()>::max_encoded_len()
}
}
#[derive(
Encode,
Decode,
Serialize,
Deserialize,
Clone,
PartialEq,
Eq,
RuntimeDebug,
TypeInfo,
MaxEncodedLen,
Default,
)]
pub enum Message<BatchContent = BatchMessages, ForwardContent = NonForwardMessage> {
#[default]
Invalid,
MessageProof {
hash: [u8; 32],
},
InitiateMessageRecovery {
hash: [u8; 32],
router: [u8; 32],
},
DisputeMessageRecovery {
hash: [u8; 32],
router: [u8; 32],
},
Batch(BatchContent),
ScheduleUpgrade {
contract: [u8; 20],
},
CancelUpgrade {
contract: [u8; 20],
},
RecoverAssets {
contract: Address,
asset: Address,
recipient: Address,
amount: [u8; 32],
},
UpdateCentrifugeGasPrice {
price: u128,
},
AddAsset {
currency: u128,
evm_address: [u8; 20],
},
AddPool { pool_id: u64 },
AddTranche {
pool_id: u64,
tranche_id: TrancheId,
#[serde(with = "serde_big_array::BigArray")]
token_name: [u8; TOKEN_NAME_SIZE],
token_symbol: [u8; TOKEN_SYMBOL_SIZE],
decimals: u8,
hook: Address,
},
AllowAsset { pool_id: u64, currency: u128 },
DisallowAsset { pool_id: u64, currency: u128 },
UpdateTranchePrice {
pool_id: u64,
tranche_id: TrancheId,
currency: u128,
price: u128,
computed_at: Seconds,
},
UpdateTrancheMetadata {
pool_id: u64,
tranche_id: TrancheId,
#[serde(with = "serde_big_array::BigArray")]
token_name: [u8; TOKEN_NAME_SIZE],
token_symbol: [u8; TOKEN_SYMBOL_SIZE],
},
UpdateTrancheHook {
pool_id: u64,
tranche_id: TrancheId,
hook: Address,
},
TransferAssets {
currency: u128,
receiver: Address,
amount: u128,
},
TransferTrancheTokens {
pool_id: u64,
tranche_id: TrancheId,
domain: SerializableDomain,
receiver: Address,
amount: u128,
},
UpdateRestriction {
pool_id: u64,
tranche_id: TrancheId,
update: UpdateRestrictionMessage,
},
DepositRequest {
pool_id: u64,
tranche_id: TrancheId,
investor: Address,
currency: u128,
amount: u128,
},
RedeemRequest {
pool_id: u64,
tranche_id: TrancheId,
investor: Address,
currency: u128,
amount: u128,
},
FulfilledDepositRequest {
pool_id: u64,
tranche_id: TrancheId,
investor: Address,
currency: u128,
currency_payout: u128,
tranche_tokens_payout: u128,
},
FulfilledRedeemRequest {
pool_id: u64,
tranche_id: TrancheId,
investor: Address,
currency: u128,
currency_payout: u128,
tranche_tokens_payout: u128,
},
CancelDepositRequest {
pool_id: u64,
tranche_id: TrancheId,
investor: Address,
currency: u128,
},
CancelRedeemRequest {
pool_id: u64,
tranche_id: TrancheId,
investor: Address,
currency: u128,
},
FulfilledCancelDepositRequest {
pool_id: u64,
tranche_id: TrancheId,
investor: Address,
currency: u128,
currency_payout: u128,
fulfilled_invest_amount: u128,
},
FulfilledCancelRedeemRequest {
pool_id: u64,
tranche_id: TrancheId,
investor: Address,
currency: u128,
tranche_tokens_payout: u128,
},
TriggerRedeemRequest {
pool_id: u64,
tranche_id: TrancheId,
investor: Address,
currency: u128,
amount: u128,
},
Forwarded {
source_domain: SerializableDomain,
forwarding_contract: H160,
message: ForwardContent,
},
}
impl LpMessageSerializer for Message {
fn serialize(&self) -> Vec<u8> {
gmpf::to_vec(self).unwrap_or_default()
}
fn deserialize(data: &[u8]) -> Result<Self, DispatchError> {
gmpf::from_slice(data).map_err(|_| DispatchError::Other("LP Deserialization issue"))
}
}
impl LpMessageBatch for Message {
fn pack_with(&mut self, other: Self) -> Result<(), DispatchError> {
match self {
Message::Batch(content) => content.try_add(other),
this => {
*this = Message::Batch(BatchMessages::try_from(vec![this.clone(), other])?);
Ok(())
}
}
}
fn submessages(&self) -> Vec<Self> {
match self {
Message::Batch(content) => content.clone().into_iter().collect(),
message => vec![message.clone()],
}
}
fn empty() -> Message {
Message::Batch(BatchMessages::default())
}
}
impl LpMessageHash for Message {
fn get_message_hash(&self) -> MessageHash {
keccak_256(&LpMessageSerializer::serialize(self))
}
}
impl LpMessageProof for Message {
fn is_proof_message(&self) -> bool {
matches!(self, Message::MessageProof { .. })
}
fn to_proof_message(&self) -> Self {
Message::MessageProof {
hash: self.get_message_hash(),
}
}
}
impl LpMessageRecovery for Message {
fn initiate_recovery_message(hash: MessageHash, router: [u8; 32]) -> Self {
Message::InitiateMessageRecovery { hash, router }
}
fn dispute_recovery_message(hash: MessageHash, router: [u8; 32]) -> Self {
Message::DisputeMessageRecovery { hash, router }
}
}
impl LpMessageForwarded for Message {
type Domain = Domain;
fn is_forwarded(&self) -> bool {
matches!(self, Message::Forwarded { .. })
}
fn unwrap_forwarded(self) -> Option<(Domain, H160, Self)> {
match self {
Self::Forwarded {
source_domain,
forwarding_contract,
message,
} => source_domain
.try_into()
.ok()
.map(|domain| (domain, forwarding_contract, message.into())),
_ => None,
}
}
fn try_wrap_forward(
source_domain: Domain,
forwarding_contract: H160,
message: Self,
) -> Result<Self, DispatchError> {
Ok(Self::Forwarded {
source_domain: source_domain.into(),
forwarding_contract,
message: message.try_into()?,
})
}
}
#[derive(
Encode,
Decode,
Serialize,
Deserialize,
Clone,
PartialEq,
Eq,
RuntimeDebug,
TypeInfo,
MaxEncodedLen,
)]
pub enum UpdateRestrictionMessage {
Invalid,
UpdateMember {
member: Address,
valid_until: Seconds,
},
Freeze {
address: Address,
},
Unfreeze {
address: Address,
},
}
#[cfg(test)]
mod tests {
use cfg_primitives::{PoolId, TrancheId};
use cfg_types::fixed_point::Ratio;
use cfg_utils::vec_to_fixed_array;
use frame_support::assert_err;
use hex::FromHex;
use sp_runtime::{traits::One, FixedPointNumber};
use super::*;
use crate::{Domain, DomainAddress};
const AMOUNT: u128 = 100000000000000000000000000;
const POOL_ID: PoolId = 12378532;
const TOKEN_ID: u128 = 246803579;
#[test]
fn ensure_non_recursive_max_encoded_len_computation() {
Message::<BatchMessages, NonForwardMessage>::max_encoded_len();
}
#[test]
fn invalid() {
let msg: Message<BatchMessages> = Message::Invalid;
assert_eq!(gmpf::to_vec(&msg).unwrap(), vec![0]);
}
#[test]
fn encoding_domain() {
assert_eq!(
hex::encode(gmpf::to_vec(&SerializableDomain::from(Domain::Centrifuge)).unwrap()),
"000000000000000000"
);
assert_eq!(
hex::encode(gmpf::to_vec(&SerializableDomain::from(Domain::Evm(1))).unwrap()),
"010000000000000001"
);
assert_eq!(
hex::encode(gmpf::to_vec(&SerializableDomain::from(Domain::Evm(1284))).unwrap()),
"010000000000000504"
);
assert_eq!(
hex::encode(gmpf::to_vec(&SerializableDomain::from(Domain::Evm(43114))).unwrap()),
"01000000000000a86a"
);
}
#[test]
fn add_currency() {
test_encode_decode_identity(
Message::AddAsset {
currency: TOKEN_ID,
evm_address: default_address_20(),
},
"090000000000000000000000000eb5ec7b1231231231231231231231231231231231231231",
)
}
#[test]
fn add_pool_long() {
test_encode_decode_identity(Message::AddPool { pool_id: POOL_ID }, "0a0000000000bce1a4")
}
#[test]
fn allow_asset() {
test_encode_decode_identity(
Message::AllowAsset {
currency: TOKEN_ID,
pool_id: POOL_ID,
},
"0c0000000000bce1a40000000000000000000000000eb5ec7b",
)
}
#[test]
fn add_tranche() {
test_encode_decode_identity(
Message::AddTranche {
pool_id: 1,
tranche_id: default_tranche_id(),
token_name: vec_to_fixed_array(b"Some Name"),
token_symbol: vec_to_fixed_array( b"SYMBOL"),
decimals: 15,
hook: default_address_32(),
},
"0b0000000000000001811acd5b3f17c06841c7e41e9e04cb1b536f6d65204e616d65000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000053594d424f4c00000000000000000000000000000000000000000000000000000f4564564564564564564564564564564564564564564564564564564564564564",
)
}
#[test]
fn update_tranche_token_price() {
test_encode_decode_identity(
Message::UpdateTranchePrice {
pool_id: 1,
tranche_id: default_tranche_id(),
currency: TOKEN_ID,
price: Ratio::one().into_inner(),
computed_at: 1698131924,
},
"0e0000000000000001811acd5b3f17c06841c7e41e9e04cb1b0000000000000000000000000eb5ec7b00000000000000000de0b6b3a76400000000000065376fd4",
)
}
#[test]
fn update_member() {
test_encode_decode_identity(
Message::UpdateRestriction{
pool_id: 2,
tranche_id: default_tranche_id(),
update: UpdateRestrictionMessage::UpdateMember {
member: default_address_32(),
valid_until: 1706260138,
}
},
"130000000000000002811acd5b3f17c06841c7e41e9e04cb1b0145645645645645645645645645645645645645645645645645645645645645640000000065b376aa",
)
}
#[test]
fn transfer_to_evm_address() {
test_encode_decode_identity(
Message::TransferAssets {
currency: TOKEN_ID,
receiver: vec_to_fixed_array(default_address_20()),
amount: AMOUNT,
},
"110000000000000000000000000eb5ec7b1231231231231231231231231231231231231231000000000000000000000000000000000052b7d2dcc80cd2e4000000"
);
}
#[test]
fn transfer_to_centrifuge() {
test_encode_decode_identity(
Message::TransferAssets {
currency: TOKEN_ID,
receiver: default_address_32(),
amount: AMOUNT,
},
"110000000000000000000000000eb5ec7b4564564564564564564564564564564564564564564564564564564564564564000000000052b7d2dcc80cd2e4000000"
);
}
#[test]
fn transfer_tranche_tokens_to_chain() {
let domain_address = DomainAddress::Evm(1284, default_address_20().into());
test_encode_decode_identity(
Message::TransferTrancheTokens {
pool_id: 1,
tranche_id: default_tranche_id(),
domain: domain_address.domain().into(),
receiver: domain_address.bytes(),
amount: AMOUNT,
},
"120000000000000001811acd5b3f17c06841c7e41e9e04cb1b0100000000000005041231231231231231231231231231231231231231000000000000050445564d00000000000052b7d2dcc80cd2e4000000"
);
}
#[test]
fn transfer_tranche_tokens_to_centrifuge() {
test_encode_decode_identity(
Message::TransferTrancheTokens {
pool_id: 1,
tranche_id: default_tranche_id(),
domain: Domain::Centrifuge.into(),
receiver: default_address_32(),
amount: AMOUNT,
},
"120000000000000001811acd5b3f17c06841c7e41e9e04cb1b0000000000000000004564564564564564564564564564564564564564564564564564564564564564000000000052b7d2dcc80cd2e4000000"
)
}
#[test]
fn deposit_request() {
test_encode_decode_identity(
Message::DepositRequest {
pool_id: 1,
tranche_id: default_tranche_id(),
investor: default_address_32(),
currency: TOKEN_ID,
amount: AMOUNT,
},
"140000000000000001811acd5b3f17c06841c7e41e9e04cb1b45645645645645645645645645645645645645645645645645645645645645640000000000000000000000000eb5ec7b000000000052b7d2dcc80cd2e4000000",
)
}
#[test]
fn cancel_deposit_request() {
test_encode_decode_identity(
Message::CancelDepositRequest {
pool_id: 1,
tranche_id: default_tranche_id(),
investor: default_address_32(),
currency: TOKEN_ID,
},
"180000000000000001811acd5b3f17c06841c7e41e9e04cb1b45645645645645645645645645645645645645645645645645645645645645640000000000000000000000000eb5ec7b",
)
}
#[test]
fn redeem_request() {
test_encode_decode_identity(
Message::RedeemRequest {
pool_id: 1,
tranche_id: default_tranche_id(),
investor: default_address_32(),
currency: TOKEN_ID,
amount: AMOUNT,
},
"150000000000000001811acd5b3f17c06841c7e41e9e04cb1b45645645645645645645645645645645645645645645645645645645645645640000000000000000000000000eb5ec7b000000000052b7d2dcc80cd2e4000000",
)
}
#[test]
fn cancel_redeem_request() {
test_encode_decode_identity(
Message::CancelRedeemRequest {
pool_id: 1,
tranche_id: default_tranche_id(),
investor: default_address_32(),
currency: TOKEN_ID,
},
"190000000000000001811acd5b3f17c06841c7e41e9e04cb1b45645645645645645645645645645645645645645645645645645645645645640000000000000000000000000eb5ec7b",
)
}
#[test]
fn fulfilled_cancel_deposit_request() {
test_encode_decode_identity(
Message::FulfilledCancelDepositRequest {
pool_id: POOL_ID,
tranche_id: default_tranche_id(),
investor: vec_to_fixed_array(default_address_20()),
currency: TOKEN_ID,
currency_payout: AMOUNT / 2,
fulfilled_invest_amount: AMOUNT / 4,
},
"1a0000000000bce1a4811acd5b3f17c06841c7e41e9e04cb1b12312312312312312312312312312312312312310000000000000000000000000000000000000000000000000eb5ec7b0000000000295be96e64066972000000000000000014adf4b7320334b9000000",
)
}
#[test]
fn fulfilled_cancel_redeem_request() {
test_encode_decode_identity(
Message::FulfilledCancelRedeemRequest {
pool_id: POOL_ID,
tranche_id: default_tranche_id(),
investor: vec_to_fixed_array(default_address_20()),
currency: TOKEN_ID,
tranche_tokens_payout: AMOUNT / 2,
},
"1b0000000000bce1a4811acd5b3f17c06841c7e41e9e04cb1b12312312312312312312312312312312312312310000000000000000000000000000000000000000000000000eb5ec7b0000000000295be96e64066972000000",
)
}
#[test]
fn fulfilled_deposit_request() {
test_encode_decode_identity(
Message::FulfilledDepositRequest {
pool_id: POOL_ID,
tranche_id: default_tranche_id(),
investor: vec_to_fixed_array(default_address_20()),
currency: TOKEN_ID,
currency_payout: AMOUNT,
tranche_tokens_payout: AMOUNT / 2,
},
"160000000000bce1a4811acd5b3f17c06841c7e41e9e04cb1b12312312312312312312312312312312312312310000000000000000000000000000000000000000000000000eb5ec7b000000000052b7d2dcc80cd2e40000000000000000295be96e64066972000000",
)
}
#[test]
fn fulfilled_redeem_request() {
test_encode_decode_identity(
Message::FulfilledRedeemRequest {
pool_id: POOL_ID,
tranche_id: default_tranche_id(),
investor: vec_to_fixed_array(default_address_20()),
currency: TOKEN_ID,
currency_payout: AMOUNT,
tranche_tokens_payout: AMOUNT / 2,
},
"170000000000bce1a4811acd5b3f17c06841c7e41e9e04cb1b12312312312312312312312312312312312312310000000000000000000000000000000000000000000000000eb5ec7b000000000052b7d2dcc80cd2e40000000000000000295be96e64066972000000",
)
}
#[test]
fn schedule_upgrade() {
test_encode_decode_identity(
Message::ScheduleUpgrade {
contract: default_address_20(),
},
"051231231231231231231231231231231231231231",
)
}
#[test]
fn cancel_upgrade() {
test_encode_decode_identity(
Message::CancelUpgrade {
contract: default_address_20(),
},
"061231231231231231231231231231231231231231",
)
}
#[test]
fn recover_assets() {
let msg = Message::RecoverAssets {
contract: [2u8; 32],
asset: [1u8; 32],
recipient: [3u8; 32],
amount: (sp_core::U256::MAX - 1).into(),
};
test_encode_decode_identity(
msg,
concat!(
"07",
"0202020202020202020202020202020202020202020202020202020202020202",
"0101010101010101010101010101010101010101010101010101010101010101",
"0303030303030303030303030303030303030303030303030303030303030303",
"fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe",
),
);
}
#[test]
fn update_tranche_token_metadata() {
test_encode_decode_identity(
Message::UpdateTrancheMetadata {
pool_id: 1,
tranche_id: default_tranche_id(),
token_name: vec_to_fixed_array(b"Some Name"),
token_symbol: vec_to_fixed_array(b"SYMBOL"),
},
"0f0000000000000001811acd5b3f17c06841c7e41e9e04cb1b536f6d65204e616d65000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000053594d424f4c0000000000000000000000000000000000000000000000000000",
)
}
#[test]
fn disallow_asset() {
test_encode_decode_identity(
Message::DisallowAsset {
pool_id: POOL_ID,
currency: TOKEN_ID,
},
"0d0000000000bce1a40000000000000000000000000eb5ec7b",
)
}
#[test]
fn disallow_asset_zero() {
test_encode_decode_identity(
Message::DisallowAsset {
pool_id: 0,
currency: 0,
},
"0d000000000000000000000000000000000000000000000000",
)
}
#[test]
fn batch_empty() {
test_encode_decode_identity(Message::Batch(BatchMessages::default()), concat!("04"))
}
#[test]
fn batch_messages() {
test_encode_decode_identity(
Message::Batch(
BatchMessages::try_from(vec![
Message::AddPool { pool_id: 0 },
Message::AllowAsset {
currency: TOKEN_ID,
pool_id: POOL_ID,
},
])
.unwrap(),
),
concat!(
"04", "0009", "0a0000000000000000", "0019", "0c0000000000bce1a40000000000000000000000000eb5ec7b", ),
)
}
#[test]
fn batch_of_batches_deserialization() {
let encoded = concat!(
"04", "000c", "04", "0009", "0a0000000000000000", );
assert_err!(
gmpf::from_slice::<Message>(&hex::decode(encoded).unwrap()),
gmpf::Error::Message("A submessage can not be a batch".into()),
);
}
fn test_encode_decode_identity(msg: Message, expected_hex: &str) {
let encoded = gmpf::to_vec(&msg).unwrap();
assert_eq!(hex::encode(encoded.clone()), expected_hex);
let decoded: Message =
gmpf::from_slice(&hex::decode(expected_hex).expect("Decode should work"))
.expect("Deserialization should work");
assert_eq!(decoded, msg);
}
fn default_address_20() -> [u8; 20] {
<[u8; 20]>::from_hex("1231231231231231231231231231231231231231")
.expect("Should be valid 20 bytes")
}
fn default_address_32() -> [u8; 32] {
<[u8; 32]>::from_hex("4564564564564564564564564564564564564564564564564564564564564564")
.expect("Should be valid 32 bytes")
}
fn default_tranche_id() -> TrancheId {
<[u8; 16]>::from_hex("811acd5b3f17c06841c7e41e9e04cb1b")
.expect("Should be valid tranche id")
}
}