use std::{cell::RefCell, convert::TryFrom, rc::Rc};
use thiserror::Error;
use casper_types::{
account::AccountHash,
bytesrepr::FromBytes,
system::{mint, mint::Error as MintError},
AccessRights, CLType, CLTyped, CLValue, CLValueError, Key, ProtocolVersion, RuntimeArgs,
RuntimeFootprint, StoredValue, StoredValueTypeMismatch, URef, U512,
};
use crate::{
global_state::{error::Error as GlobalStateError, state::StateReader},
tracking_copy::{TrackingCopy, TrackingCopyEntityExt, TrackingCopyError, TrackingCopyExt},
};
#[derive(Clone, Error, Debug)]
pub enum TransferError {
#[error("Invalid key {0}")]
UnexpectedKeyVariant(Key),
#[error("{}", _0)]
TypeMismatch(StoredValueTypeMismatch),
#[error("Forged reference: {}", _0)]
ForgedReference(URef),
#[error("Invalid access rights: {}", required)]
InvalidAccess {
required: AccessRights,
},
#[error("{0}")]
CLValue(CLValueError),
#[error("Invalid purse")]
InvalidPurse,
#[error("Invalid argument")]
InvalidArgument,
#[error("Missing argument")]
MissingArgument,
#[error("Attempt to transfer amount 0")]
AttemptToTransferZero,
#[error("Invalid operation")]
InvalidOperation,
#[error("Either the source or the target must be an admin (private chain).")]
RestrictedTransferAttempted,
#[error("Unable to determine if the target of a transfer is an admin")]
UnableToVerifyTargetIsAdmin,
#[error("{0}")]
TrackingCopy(TrackingCopyError),
#[error("{0}")]
Mint(MintError),
}
impl From<GlobalStateError> for TransferError {
fn from(gse: GlobalStateError) -> Self {
TransferError::TrackingCopy(TrackingCopyError::Storage(gse))
}
}
impl From<TrackingCopyError> for TransferError {
fn from(tce: TrackingCopyError) -> Self {
TransferError::TrackingCopy(tce)
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum TransferTargetMode {
ExistingAccount {
target_account_hash: AccountHash,
main_purse: URef,
},
PurseExists {
target_account_hash: Option<AccountHash>,
purse_uref: URef,
},
CreateAccount(AccountHash),
}
impl TransferTargetMode {
pub fn target_account_hash(&self) -> Option<AccountHash> {
match self {
TransferTargetMode::PurseExists {
target_account_hash,
..
} => *target_account_hash,
TransferTargetMode::ExistingAccount {
target_account_hash,
..
} => Some(*target_account_hash),
TransferTargetMode::CreateAccount(target_account_hash) => Some(*target_account_hash),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TransferArgs {
to: Option<AccountHash>,
source: URef,
target: URef,
amount: U512,
arg_id: Option<u64>,
}
impl TransferArgs {
pub fn new(
to: Option<AccountHash>,
source: URef,
target: URef,
amount: U512,
arg_id: Option<u64>,
) -> Self {
Self {
to,
source,
target,
amount,
arg_id,
}
}
pub fn to(&self) -> Option<AccountHash> {
self.to
}
pub fn source(&self) -> URef {
self.source
}
pub fn target(&self) -> URef {
self.target
}
pub fn amount(&self) -> U512 {
self.amount
}
pub fn arg_id(&self) -> Option<u64> {
self.arg_id
}
}
impl TryFrom<TransferArgs> for RuntimeArgs {
type Error = CLValueError;
fn try_from(transfer_args: TransferArgs) -> Result<Self, Self::Error> {
let mut runtime_args = RuntimeArgs::new();
runtime_args.insert(mint::ARG_TO, transfer_args.to)?;
runtime_args.insert(mint::ARG_SOURCE, transfer_args.source)?;
runtime_args.insert(mint::ARG_TARGET, transfer_args.target)?;
runtime_args.insert(mint::ARG_AMOUNT, transfer_args.amount)?;
runtime_args.insert(mint::ARG_ID, transfer_args.arg_id)?;
Ok(runtime_args)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TransferRuntimeArgsBuilder {
inner: RuntimeArgs,
}
impl TransferRuntimeArgsBuilder {
pub fn new(imputed_runtime_args: RuntimeArgs) -> TransferRuntimeArgsBuilder {
TransferRuntimeArgsBuilder {
inner: imputed_runtime_args,
}
}
fn purse_exists<R>(&self, uref: URef, tracking_copy: Rc<RefCell<TrackingCopy<R>>>) -> bool
where
R: StateReader<Key, StoredValue, Error = GlobalStateError>,
{
let key = match tracking_copy
.borrow_mut()
.get_purse_balance_key(uref.into())
{
Ok(key) => key,
Err(_) => return false,
};
tracking_copy
.borrow_mut()
.get_available_balance(key)
.is_ok()
}
fn resolve_source_uref<R>(
&self,
account: &RuntimeFootprint,
tracking_copy: Rc<RefCell<TrackingCopy<R>>>,
) -> Result<URef, TransferError>
where
R: StateReader<Key, StoredValue, Error = GlobalStateError>,
{
let imputed_runtime_args = &self.inner;
let arg_name = mint::ARG_SOURCE;
let uref = match imputed_runtime_args.get(arg_name) {
Some(cl_value) if *cl_value.cl_type() == CLType::URef => {
self.map_cl_value::<URef>(cl_value)?
}
Some(cl_value) if *cl_value.cl_type() == CLType::Option(CLType::URef.into()) => {
let Some(uref): Option<URef> = self.map_cl_value(cl_value)? else {
return account.main_purse().ok_or(TransferError::InvalidOperation);
};
uref
}
Some(_) => return Err(TransferError::InvalidArgument),
None => return account.main_purse().ok_or(TransferError::InvalidOperation),
};
if account
.main_purse()
.ok_or(TransferError::InvalidOperation)?
.addr()
== uref.addr()
{
return Ok(uref);
}
let normalized_uref = Key::URef(uref).normalize();
let maybe_named_key = account
.named_keys()
.keys()
.find(|&named_key| named_key.normalize() == normalized_uref);
match maybe_named_key {
Some(Key::URef(found_uref)) => {
if found_uref.is_writeable() {
if !self.purse_exists(found_uref.to_owned(), tracking_copy) {
return Err(TransferError::InvalidPurse);
}
Ok(uref)
} else {
Err(TransferError::InvalidAccess {
required: AccessRights::WRITE,
})
}
}
Some(key) => Err(TransferError::TypeMismatch(StoredValueTypeMismatch::new(
"Key::URef".to_string(),
key.type_string(),
))),
None => Err(TransferError::ForgedReference(uref)),
}
}
pub fn resolve_transfer_target_mode<R>(
&mut self,
protocol_version: ProtocolVersion,
tracking_copy: Rc<RefCell<TrackingCopy<R>>>,
) -> Result<TransferTargetMode, TransferError>
where
R: StateReader<Key, StoredValue, Error = GlobalStateError>,
{
let imputed_runtime_args = &self.inner;
let to_name = mint::ARG_TO;
let target_account_hash = match imputed_runtime_args.get(to_name) {
Some(cl_value)
if *cl_value.cl_type() == CLType::Option(Box::new(CLType::ByteArray(32))) =>
{
let to: Option<AccountHash> = self.map_cl_value(cl_value)?;
to
}
Some(_) | None => None,
};
let target_name = mint::ARG_TARGET;
let account_hash = match imputed_runtime_args.get(target_name) {
Some(cl_value) if *cl_value.cl_type() == CLType::URef => {
let purse_uref = self.map_cl_value(cl_value)?;
if !self.purse_exists(purse_uref, tracking_copy) {
return Err(TransferError::InvalidPurse);
}
return Ok(TransferTargetMode::PurseExists {
purse_uref,
target_account_hash,
});
}
Some(cl_value) if *cl_value.cl_type() == CLType::ByteArray(32) => {
self.map_cl_value(cl_value)?
}
Some(cl_value) if *cl_value.cl_type() == CLType::Key => {
let account_key: Key = self.map_cl_value(cl_value)?;
let account_hash: AccountHash = account_key
.into_account()
.ok_or(TransferError::UnexpectedKeyVariant(account_key))?;
account_hash
}
Some(cl_value) if *cl_value.cl_type() == CLType::PublicKey => {
let public_key = self.map_cl_value(cl_value)?;
AccountHash::from(&public_key)
}
Some(_) => return Err(TransferError::InvalidArgument),
None => return Err(TransferError::MissingArgument),
};
match tracking_copy
.borrow_mut()
.runtime_footprint_by_account_hash(protocol_version, account_hash)
{
Ok((_, entity)) => {
let main_purse_addable = entity
.main_purse()
.ok_or(TransferError::InvalidPurse)?
.with_access_rights(AccessRights::ADD);
Ok(TransferTargetMode::ExistingAccount {
target_account_hash: account_hash,
main_purse: main_purse_addable,
})
}
Err(_) => Ok(TransferTargetMode::CreateAccount(account_hash)),
}
}
fn resolve_amount(&self) -> Result<U512, TransferError> {
let imputed_runtime_args = &self.inner;
let amount = match imputed_runtime_args.get(mint::ARG_AMOUNT) {
Some(amount_value) if *amount_value.cl_type() == CLType::U512 => {
self.map_cl_value(amount_value)?
}
Some(amount_value) if *amount_value.cl_type() == CLType::U64 => {
let amount: u64 = self.map_cl_value(amount_value)?;
U512::from(amount)
}
Some(_) => return Err(TransferError::InvalidArgument),
None => return Err(TransferError::MissingArgument),
};
if amount.is_zero() {
return Err(TransferError::AttemptToTransferZero);
}
Ok(amount)
}
fn resolve_id(&self) -> Result<Option<u64>, TransferError> {
let id: Option<u64> = if let Some(id_value) = self.inner.get(mint::ARG_ID) {
self.map_cl_value(id_value)?
} else {
None
};
Ok(id)
}
pub fn build<R>(
mut self,
from: &RuntimeFootprint,
protocol_version: ProtocolVersion,
tracking_copy: Rc<RefCell<TrackingCopy<R>>>,
) -> Result<TransferArgs, TransferError>
where
R: StateReader<Key, StoredValue, Error = GlobalStateError>,
{
let (to, target) = match self
.resolve_transfer_target_mode(protocol_version, Rc::clone(&tracking_copy))?
{
TransferTargetMode::ExistingAccount {
main_purse: purse_uref,
target_account_hash: target_account,
} => (Some(target_account), purse_uref),
TransferTargetMode::PurseExists {
target_account_hash,
purse_uref,
} => (target_account_hash, purse_uref),
TransferTargetMode::CreateAccount(_) => {
return Err(TransferError::InvalidOperation);
}
};
let source = self.resolve_source_uref(from, Rc::clone(&tracking_copy))?;
if source.addr() == target.addr() {
return Err(TransferError::InvalidPurse);
}
let amount = self.resolve_amount()?;
let arg_id = self.resolve_id()?;
Ok(TransferArgs {
to,
source,
target,
amount,
arg_id,
})
}
fn map_cl_value<T: CLTyped + FromBytes>(&self, cl_value: &CLValue) -> Result<T, TransferError> {
cl_value.clone().into_t().map_err(TransferError::CLValue)
}
}