use core::str;
use std::str::FromStr;
use crate::address::{Address, AddressTrait};
use crate::xdr;
use stellar_strkey::{Contract, Strkey};
#[derive(Clone, Debug)]
pub struct Contracts {
id: Vec<u8>,
}
pub trait ContractBehavior {
fn new(contract_id: &str) -> Result<Self, &'static str>
where
Self: Sized;
fn contract_id(&self) -> String;
fn to_string(&self) -> String;
fn address(&self) -> Address;
fn call(&self, method: &str, params: Option<Vec<xdr::ScVal>>) -> xdr::Operation;
fn get_footprint(&self) -> xdr::LedgerKey; }
impl ContractBehavior for Contracts {
fn new(contract_id: &str) -> std::result::Result<Contracts, &'static str> {
let contract_id = stellar_strkey::Contract::from_str(contract_id)
.map_err(|_| "Failed to decode contract ID")?;
Ok(Self {
id: contract_id.0.to_vec(),
})
}
fn call(&self, method: &str, params: Option<Vec<xdr::ScVal>>) -> xdr::Operation {
xdr::Operation {
source_account: None,
body: xdr::OperationBody::InvokeHostFunction(xdr::InvokeHostFunctionOp {
host_function: xdr::HostFunction::InvokeContract(xdr::InvokeContractArgs {
contract_address: xdr::ScAddress::Contract(xdr::ContractId(xdr::Hash(
self.get_id(),
))),
function_name: xdr::ScSymbol::from(xdr::StringM::from_str(method).unwrap()),
args: xdr::VecM::<xdr::ScVal>::try_from(params.unwrap_or_default()).unwrap(),
}),
auth: xdr::VecM::<xdr::SorobanAuthorizationEntry>::try_from(Vec::new()).unwrap(),
}),
}
}
fn contract_id(&self) -> String {
stellar_strkey::Contract(self.get_id()).to_string()
}
fn to_string(&self) -> String {
self.contract_id()
}
fn address(&self) -> Address {
Address::contract(&self.id).unwrap()
}
fn get_footprint(&self) -> xdr::LedgerKey {
xdr::LedgerKey::ContractData(xdr::LedgerKeyContractData {
contract: xdr::ScAddress::Contract(xdr::ContractId(xdr::Hash(self.get_id()))),
key: xdr::ScVal::LedgerKeyContractInstance,
durability: xdr::ContractDataDurability::Persistent,
})
}
}
impl Contracts {
fn get_id(&self) -> [u8; 32] {
*self
.id
.last_chunk::<32>()
.expect("Contract ID is less than 32 bytes")
}
}
#[cfg(test)]
mod tests {
use xdr::{Limits, OperationBody, WriteXdr};
use super::*;
const NULL_ADDRESS: &str = "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM";
#[test]
fn test_contract_constructor() {
let test_addresses = vec![
NULL_ADDRESS,
"CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE",
];
for cid in test_addresses {
let contract = Contracts::new(cid).expect("Failed to create contract");
assert_eq!(contract.contract_id(), cid);
}
}
#[test]
fn test_contract_obsolete_hex_id() {
let obsolete_hex_id = "0".repeat(63) + "1";
let result = Contracts::new(&obsolete_hex_id);
assert!(result.is_err(), "Expected an error for obsolete hex ID");
}
#[test]
fn test_contract_invalid_id() {
let invalid_id = "foobar";
let result = Contracts::new(invalid_id);
assert!(result.is_err(), "Expected an error for invalid contract ID");
}
#[test]
fn test_contract_address() {
let contract = Contracts::new(NULL_ADDRESS).expect("Failed to create contract");
let address_str = contract.address().to_string();
assert_eq!(
address_str, NULL_ADDRESS,
"Contract address should match the original contract ID"
);
}
#[test]
fn test_get_footprint_includes_correct_contract_ledger_keys() {
let contract = Contracts::new(NULL_ADDRESS).expect("Failed to create contract");
assert_eq!(contract.contract_id(), NULL_ADDRESS);
let actual_footprint = contract.get_footprint();
let expected_footprint = xdr::LedgerKey::ContractData(xdr::LedgerKeyContractData {
contract: xdr::ScAddress::Contract(xdr::ContractId(xdr::Hash(
stellar_strkey::Contract::from_string(NULL_ADDRESS)
.unwrap()
.0,
))),
key: xdr::ScVal::LedgerKeyContractInstance,
durability: xdr::ContractDataDurability::Persistent,
});
assert_eq!(actual_footprint, expected_footprint);
}
#[test]
fn test_call_method_with_arguments() {
let contract = Contracts::new(NULL_ADDRESS).expect("Failed to create contract");
let method = "method";
let arg1 = xdr::ScVal::Symbol(xdr::ScSymbol::from(xdr::StringM::from_str("arg!").unwrap()));
let arg2 = xdr::ScVal::I32(2);
let operation = contract.call(method, Some(vec![arg1.clone(), arg2.clone()]));
let expected_contract_address = xdr::ScAddress::Contract(xdr::ContractId(xdr::Hash(
stellar_strkey::Contract::from_string(NULL_ADDRESS)
.unwrap()
.0,
)));
if let OperationBody::InvokeHostFunction(host_function_op) = operation.body {
if let xdr::HostFunction::InvokeContract(args) = host_function_op.host_function {
assert_eq!(args.contract_address, expected_contract_address);
assert_eq!(
args.function_name,
xdr::ScSymbol::from(xdr::StringM::from_str(method).unwrap())
);
assert_eq!(args.args.len(), 2);
assert_eq!(args.args[0], arg1);
assert_eq!(args.args[1], arg2);
} else {
panic!("Expected InvokeContract host function");
}
} else {
panic!("Expected InvokeHostFunction operation body");
}
}
#[test]
fn test_call_with_no_parameters() {
let contract = Contracts::new(NULL_ADDRESS).expect("Failed to create contract");
let operation = contract.call("empty", None);
if let OperationBody::InvokeHostFunction(host_function_op) = operation.clone().body {
if let xdr::HostFunction::InvokeContract(args) = host_function_op.host_function {
assert_eq!(
args.function_name,
xdr::ScSymbol::from(xdr::StringM::from_str("empty").unwrap())
);
assert!(args.args.is_empty());
} else {
panic!("Expected InvokeContract host function");
}
} else {
panic!("Expected InvokeHostFunction operation body");
}
let xdr = operation.to_xdr(Limits::none()).unwrap();
assert!(
!xdr.is_empty(),
"XDR serialization should produce a non-empty result"
);
}
#[test]
fn test_call_builds_valid_xdr() {
let contract = Contracts::new(NULL_ADDRESS).expect("Failed to create contract");
let method = "method";
let arg1 = xdr::ScVal::Symbol(xdr::ScSymbol::from(xdr::StringM::from_str("arg!").unwrap()));
let arg2 = xdr::ScVal::I32(2);
let operation = contract.call(method, Some(vec![arg1, arg2]));
let xdr = operation.to_xdr(Limits::none()).unwrap();
assert!(
!xdr.is_empty(),
"XDR serialization should produce a non-empty result"
);
}
#[test]
fn test_contract_id_as_sc_address() {
let contract = Contracts::new(NULL_ADDRESS).expect("Failed to create contract");
let operation = contract.call("method", None);
if let OperationBody::InvokeHostFunction(host_function_op) = operation.body {
if let xdr::HostFunction::InvokeContract(args) = host_function_op.host_function {
let expected_address = xdr::ScAddress::Contract(xdr::ContractId(xdr::Hash(
stellar_strkey::Contract::from_string(NULL_ADDRESS)
.unwrap()
.0,
)));
assert_eq!(args.contract_address, expected_address);
} else {
panic!("Expected InvokeContract host function");
}
} else {
panic!("Expected InvokeHostFunction operation body");
}
}
#[test]
fn test_method_name_as_second_arg() {
let contract = Contracts::new(NULL_ADDRESS).expect("Failed to create contract");
let operation = contract.call("method", None);
if let OperationBody::InvokeHostFunction(host_function_op) = operation.body {
if let xdr::HostFunction::InvokeContract(args) = host_function_op.host_function {
assert_eq!(
args.function_name,
xdr::ScSymbol::from(xdr::StringM::from_str("method").unwrap())
);
} else {
panic!("Expected InvokeContract host function");
}
} else {
panic!("Expected InvokeHostFunction operation body");
}
}
#[test]
fn test_passes_all_params() {
let contract = Contracts::new(NULL_ADDRESS).expect("Failed to create contract");
let method = "method";
let arg1 = xdr::ScVal::Symbol(xdr::ScSymbol::from(xdr::StringM::from_str("arg!").unwrap()));
let arg2 = xdr::ScVal::I32(2);
let operation = contract.call(method, Some(vec![arg1.clone(), arg2.clone()]));
if let OperationBody::InvokeHostFunction(host_function_op) = operation.body {
if let xdr::HostFunction::InvokeContract(args) = host_function_op.host_function {
assert_eq!(args.args.len(), 2);
assert_eq!(args.args[0], arg1);
assert_eq!(args.args[1], arg2);
} else {
panic!("Expected InvokeContract host function");
}
} else {
panic!("Expected InvokeHostFunction operation body");
}
}
}