use std::fmt::Debug;
use dyn_clone::clone_trait_object;
use dyn_clone::DynClone;
use crate::clarity;
use crate::clarity::decode_clarity_type;
use crate::clarity::Clarity;
use crate::clarity::Codec;
use crate::clarity::FnArguments;
use crate::clarity::LengthPrefixedStr;
use crate::crypto::c32::Address;
use crate::crypto::Hash160;
pub(crate) const PAYLOAD_TYPE_TOKEN_TRANSFER: u8 = 0x00;
pub(crate) const PAYLOAD_TYPE_CONTRACT_CALL: u8 = 0x02;
pub trait Payload: Codec + DynClone + Debug {}
clone_trait_object!(Payload);
impl Codec for Box<dyn Payload> {
fn encode(&self) -> Result<Vec<u8>, clarity::Error> {
self.as_ref().encode()
}
fn decode(bytes: &[u8]) -> Result<Self, clarity::Error>
where
Self: Sized,
{
match bytes[0] {
PAYLOAD_TYPE_TOKEN_TRANSFER => {
let payload = TokenTransferPayload::decode(bytes)?;
Ok(Box::new(payload))
}
PAYLOAD_TYPE_CONTRACT_CALL => {
let payload = ContractCallPayload::decode(bytes)?;
Ok(Box::new(payload))
}
_ => Err(clarity::Error::UnexpectedType(bytes[0])),
}
}
}
#[derive(Debug, Clone)]
pub struct TokenTransferPayload {
pub address: Box<dyn Clarity>,
pub amount: u64,
pub memo: String,
}
impl TokenTransferPayload {
pub fn new<T, S>(address: T, amount: u64, memo: S) -> Self
where
T: Clarity,
S: Into<String>,
{
Self {
address: Box::new(address),
amount,
memo: memo.into(),
}
}
}
impl Codec for TokenTransferPayload {
fn encode(&self) -> Result<Vec<u8>, clarity::Error> {
let mut memo_bytes = vec![0; 34];
if self.memo.len() > 34 {
return Err(clarity::Error::BadStringLength(self.memo.len(), 34));
}
for (i, byte) in self.memo.as_bytes().iter().enumerate() {
memo_bytes[i] = *byte;
}
let mut buff = vec![PAYLOAD_TYPE_TOKEN_TRANSFER];
buff.extend_from_slice(&self.address.encode()?);
buff.extend_from_slice(&self.amount.to_be_bytes());
buff.extend_from_slice(&memo_bytes);
Ok(buff)
}
fn decode(bytes: &[u8]) -> Result<Self, clarity::Error>
where
Self: Sized,
{
if bytes[0] != PAYLOAD_TYPE_TOKEN_TRANSFER {
return Err(clarity::Error::UnexpectedType(bytes[0]));
}
let mut offset = 1;
let address = decode_clarity_type(&bytes[offset..])?;
let addr_len = address.len()?;
offset += addr_len;
let amount_bytes = &bytes[offset..offset + 8];
let amount = u64::from_be_bytes(amount_bytes.try_into()?);
offset += 8;
let memo_bytes = &bytes[offset..offset + 34];
let memo = String::from_utf8(memo_bytes.to_vec())?;
Ok(Self {
address,
amount,
memo,
})
}
}
impl Payload for TokenTransferPayload {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContractCallPayload {
pub address: Address,
pub contract: LengthPrefixedStr,
pub name: LengthPrefixedStr,
pub args: FnArguments,
}
impl ContractCallPayload {
pub fn new<T, K>(address: Address, contract: T, name: K, args: FnArguments) -> Self
where
T: Into<LengthPrefixedStr>,
K: Into<LengthPrefixedStr>,
{
Self {
address,
contract: contract.into(),
name: name.into(),
args,
}
}
}
impl Codec for ContractCallPayload {
fn encode(&self) -> Result<Vec<u8>, clarity::Error> {
let mut buff = vec![PAYLOAD_TYPE_CONTRACT_CALL];
buff.extend_from_slice(&[self.address.version]);
buff.extend_from_slice(self.address.hash.as_bytes());
buff.extend_from_slice(&self.contract.encode()?);
buff.extend_from_slice(&self.name.encode()?);
buff.extend_from_slice(&self.args.encode()?);
Ok(buff)
}
fn decode(bytes: &[u8]) -> Result<Self, clarity::Error>
where
Self: Sized,
{
if bytes[0] != PAYLOAD_TYPE_CONTRACT_CALL {
return Err(clarity::Error::UnexpectedType(bytes[0]));
}
let mut offset = 1;
let version = bytes[offset];
offset += 1;
let hash = Hash160::new(&bytes[offset..offset + 20]);
let address = Address::new(hash, version);
offset += 20;
let contract = LengthPrefixedStr::decode(&bytes[offset..])?;
let contract_len = contract.len()?;
let name = LengthPrefixedStr::decode(&bytes[offset + contract_len..])?;
let name_len = name.len()?;
let args = FnArguments::decode(&bytes[offset + contract_len + name_len..])?;
Ok(Self {
address,
contract,
name,
args,
})
}
}
impl Payload for ContractCallPayload {}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use super::*;
use crate::clarity::False;
use crate::clarity::Int;
use crate::clarity::PrincipalContract;
use crate::clarity::PrincipalStandard;
use crate::clarity::True;
use crate::clarity::UInt;
use crate::crypto::hex::bytes_to_hex;
#[test]
fn test_transaction_payload_token_transfer_encode() {
let std = get_test_standard_cv();
let con = get_test_contract_cv();
let std_payload = TokenTransferPayload::new(std, 100000, "Hello, world!");
let std_encoded = std_payload.encode().unwrap();
let std_decoded = TokenTransferPayload::decode(&std_encoded).unwrap();
assert_eq!(std_decoded.hex().unwrap(), std_payload.hex().unwrap());
let std_hex = bytes_to_hex(&std_encoded);
let std_expected = "00051a164247d6f2b425ac5771423ae6c80c754f7172b000000000000186a048656c6c6f2c20776f726c6421000000000000000000000000000000000000000000";
assert_eq!(std_hex, std_expected);
let con_payload = TokenTransferPayload::new(con, 100000, "Hello, world!");
let con_encoded = con_payload.encode().unwrap();
let con_hex = bytes_to_hex(&con_encoded);
let con_expected = "00061a164247d6f2b425ac5771423ae6c80c754f7172b00b6d792d636f6e747261637400000000000186a048656c6c6f2c20776f726c6421000000000000000000000000000000000000000000";
assert_eq!(con_hex, con_expected);
}
#[test]
fn test_transaction_payload_token_transfer_encode_empty() {
let std = get_test_standard_cv();
let payload = TokenTransferPayload::new(std, 100000, "");
let encoded = payload.encode().unwrap();
let decoded = TokenTransferPayload::decode(&encoded).unwrap();
assert_eq!(decoded.hex().unwrap(), payload.hex().unwrap());
let hex = bytes_to_hex(&encoded);
let expected = "00051a164247d6f2b425ac5771423ae6c80c754f7172b000000000000186a000000000000000000000000000000000000000000000000000000000000000000000";
assert_eq!(hex, expected);
}
#[test]
fn test_transaction_payload_contract_call_encode() {
let (address, contract, fn_name) = get_test_contract_fixtures();
let fn_args = clarity!(FnArguments, UInt::new(100), Int::new(-100));
let payload = ContractCallPayload::new(address, contract, fn_name, fn_args);
let encoded = payload.encode().unwrap();
let decoded = ContractCallPayload::decode(&encoded).unwrap();
assert_eq!(decoded, payload);
let hex = bytes_to_hex(&encoded);
let expected = "021a164247d6f2b425ac5771423ae6c80c754f7172b00b6d792d636f6e74726163740b6d792d66756e6374696f6e00000002010000000000000000000000000000006400ffffffffffffffffffffffffffffff9c";
assert_eq!(hex, expected);
}
#[test]
fn test_transaction_payload_contract_call_encode_empty() {
let (address, contract, fn_name) = get_test_contract_fixtures();
let fn_args = clarity!(FnArguments);
let payload = ContractCallPayload::new(address, contract, fn_name, fn_args);
let encoded = payload.encode().unwrap();
let decoded = ContractCallPayload::decode(&encoded).unwrap();
assert_eq!(decoded, payload);
let hex = bytes_to_hex(&encoded);
let expected =
"021a164247d6f2b425ac5771423ae6c80c754f7172b00b6d792d636f6e74726163740b6d792d66756e6374696f6e00000000";
assert_eq!(hex, expected);
}
#[test]
fn test_transaction_payload_contract_call_encode_complex() {
let (address, contract, fn_name) = get_test_contract_fixtures();
let fn_args = clarity!(
FnArguments,
clarity!(Tuple, ("a", UInt::new(100)), ("b", Int::new(-100))),
clarity!(
List,
clarity!(True),
clarity!(False),
clarity!(OptionalSome, UInt::new(100))
),
clarity!(Buffer, b"hello world".to_vec()),
True::new(),
UInt::new(100000),
False::new(),
Int::new(-100000)
);
let payload = ContractCallPayload::new(address, contract, fn_name, fn_args);
let encoded = payload.encode().unwrap();
let decoded = ContractCallPayload::decode(&encoded).unwrap();
assert_eq!(decoded, payload);
let hex = bytes_to_hex(&encoded);
let expected = "021a164247d6f2b425ac5771423ae6c80c754f7172b00b6d792d636f6e74726163740b6d792d66756e6374696f6e000000070c0000000201610100000000000000000000000000000064016200ffffffffffffffffffffffffffffff9c0b0000000303040a0100000000000000000000000000000064020000000b68656c6c6f20776f726c640301000000000000000000000000000186a00400fffffffffffffffffffffffffffe7960";
assert_eq!(hex, expected);
}
fn get_test_standard_cv() -> PrincipalStandard {
clarity!(
PrincipalStandard,
"STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6"
)
}
fn get_test_contract_cv() -> PrincipalContract {
clarity!(
PrincipalContract,
"STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6",
"my-contract"
)
}
fn get_test_contract_fixtures() -> (Address, String, String) {
let addr = Address::from_str("STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6").unwrap();
let contract_name = String::from("my-contract");
let fn_name = String::from("my-function");
(addr, contract_name.to_string(), fn_name.to_string())
}
}