use crate::core::account_control::{AccountBlockError, AccountBlockHandle};
use crate::core::account_groups::{AccountGroupError, AccountGroupsHandle};
use crate::param::{AccountGroupId, AccountId, Asset, DEFAULT_ACCOUNT_GROUP};
use crate::pretrade::{AccountBlock, RejectCode};
use crate::storage::{self, Storage, StorageBuilder};
pub(crate) struct AccountCurrencies<StorageFactory>
where
StorageFactory: storage::LockingPolicyFactory + storage::CreateStorageFor<AccountId> + 'static,
{
accounts: Storage<AccountId, Asset, StorageFactory::Policy>,
groups: Storage<AccountGroupId, Asset, StorageFactory::Policy>,
}
impl<StorageFactory> AccountCurrencies<StorageFactory>
where
StorageFactory: storage::LockingPolicyFactory + storage::CreateStorageFor<AccountId> + 'static,
{
pub(crate) fn new(builder: &StorageBuilder<StorageFactory>) -> Self {
Self {
accounts: builder.create_for_bound_key(),
groups: builder.create_for_any_key(),
}
}
fn set_account_currency(&self, account: AccountId, currency: Asset) {
let initial_currency = currency.clone();
self.accounts
.with_mut(account, || initial_currency, |slot, _| *slot = currency);
}
fn clear_account_currency(&self, account: AccountId) {
self.accounts.remove(&account);
}
fn account_currency(&self, account: AccountId) -> Option<Asset> {
self.accounts.with(&account, |currency| currency.clone())
}
fn set_group_currency(&self, group: AccountGroupId, currency: Asset) {
let initial_currency = currency.clone();
self.groups
.with_mut(group, || initial_currency, |slot, _| *slot = currency);
}
fn clear_group_currency(&self, group: AccountGroupId) {
self.groups.remove(&group);
}
fn group_currency(&self, group: AccountGroupId) -> Option<Asset> {
self.groups.with(&group, |currency| currency.clone())
}
}
pub struct Accounts<StorageFactory>
where
StorageFactory: storage::LockingPolicyFactory + storage::CreateStorageFor<AccountId> + 'static,
{
handle: AccountGroupsHandle<StorageFactory>,
block_handle: AccountBlockHandle<StorageFactory>,
currencies: StorageFactory::Shared<AccountCurrencies<StorageFactory>>,
}
impl<StorageFactory> Clone for Accounts<StorageFactory>
where
StorageFactory: storage::LockingPolicyFactory + storage::CreateStorageFor<AccountId> + 'static,
{
fn clone(&self) -> Self {
Self {
handle: self.handle.clone(),
block_handle: self.block_handle.clone(),
currencies: self.currencies.clone(),
}
}
}
impl<StorageFactory> Accounts<StorageFactory>
where
StorageFactory: storage::LockingPolicyFactory + storage::CreateStorageFor<AccountId> + 'static,
{
pub(crate) fn new(
handle: AccountGroupsHandle<StorageFactory>,
block_handle: AccountBlockHandle<StorageFactory>,
currencies: StorageFactory::Shared<AccountCurrencies<StorageFactory>>,
) -> Self {
Self {
handle,
block_handle,
currencies,
}
}
pub fn register_group(
&self,
accounts: &[AccountId],
group: AccountGroupId,
) -> Result<(), AccountGroupError> {
self.handle.register_group(accounts, group)
}
pub fn unregister_group(
&self,
accounts: &[AccountId],
group: AccountGroupId,
) -> Result<(), AccountGroupError> {
self.handle.unregister_group(accounts, group)
}
pub fn group_of(&self, account: AccountId) -> Option<AccountGroupId> {
self.handle.group_of(account)
}
pub fn set_currency(&self, account: AccountId, currency: Asset) {
self.currencies.set_account_currency(account, currency);
}
pub fn clear_currency(&self, account: AccountId) {
self.currencies.clear_account_currency(account);
}
pub fn set_group_currency(&self, group: AccountGroupId, currency: Asset) {
self.currencies.set_group_currency(group, currency);
}
pub fn clear_group_currency(&self, group: AccountGroupId) {
self.currencies.clear_group_currency(group);
}
pub(crate) fn currency_of(&self, account: AccountId) -> Option<Asset> {
self.currencies
.account_currency(account)
.or_else(|| {
self.group_of(account)
.and_then(|group| self.currencies.group_currency(group))
})
.or_else(|| self.currencies.group_currency(DEFAULT_ACCOUNT_GROUP))
}
pub fn block(&self, account: AccountId, reason: String) {
self.block_handle.record(account, engine_block(reason));
}
pub fn unblock(&self, account: AccountId) {
self.block_handle.unblock_account(account);
}
pub fn replace_block_reason(
&self,
account: AccountId,
reason: String,
) -> Result<(), AccountBlockError> {
self.block_handle
.replace_reason(account, engine_block(reason))
}
pub fn block_group(
&self,
group: AccountGroupId,
reason: String,
) -> Result<(), AccountBlockError> {
self.block_handle.block_group(group, engine_block(reason))
}
pub fn unblock_group(&self, group: AccountGroupId) -> Result<(), AccountBlockError> {
self.block_handle.unblock_group(group)
}
pub fn replace_group_block_reason(
&self,
group: AccountGroupId,
reason: String,
) -> Result<(), AccountBlockError> {
self.block_handle
.replace_group_reason(group, engine_block(reason))
}
}
fn engine_block(reason: String) -> AccountBlock {
AccountBlock::new("Engine", RejectCode::AccountBlocked, reason.clone(), reason)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::account_control::BlockedAccounts;
use crate::core::account_groups::AccountGroups;
use crate::core::HasAccountId;
use crate::pretrade::RejectScope;
use crate::storage::{LockingPolicyFactory, NoLocking, StorageBuilder};
use crate::RequestFieldAccessError;
fn account(id: u64) -> AccountId {
AccountId::from_u64(id)
}
fn group(id: u32) -> AccountGroupId {
AccountGroupId::from_u32(id).expect("account group id must be valid")
}
fn asset(value: &str) -> Asset {
Asset::new(value).expect("asset must be valid")
}
struct AccountOrder(AccountId);
impl HasAccountId for AccountOrder {
fn account_id(&self) -> Result<AccountId, RequestFieldAccessError> {
Ok(self.0)
}
}
type TestAccountsHandles = (
Accounts<NoLocking>,
<NoLocking as LockingPolicyFactory>::Shared<BlockedAccounts<NoLocking>>,
<NoLocking as LockingPolicyFactory>::Shared<AccountGroups<NoLocking>>,
);
fn new_accounts() -> TestAccountsHandles {
let builder = StorageBuilder::new(NoLocking);
let blocked = NoLocking::new_shared(BlockedAccounts::new(&builder));
let registry = NoLocking::new_shared(AccountGroups::new(&builder));
let currencies = NoLocking::new_shared(AccountCurrencies::new(&builder));
let accounts = Accounts::new(
AccountGroupsHandle::from_inner(registry.clone()),
AccountBlockHandle::from_inner(blocked.clone()),
currencies,
);
(accounts, blocked, registry)
}
#[test]
fn accounts_block_then_check_rejects_and_unblock_clears() {
let (accounts, blocked, registry) = new_accounts();
accounts.block(account(1), "manual review".to_owned());
assert!(blocked
.check(®istry, &AccountOrder(account(1)), RejectScope::Order)
.is_some());
accounts.unblock(account(1));
assert!(blocked
.check(®istry, &AccountOrder(account(1)), RejectScope::Order)
.is_none());
}
#[test]
fn accounts_unblock_of_non_blocked_account_is_noop() {
let (accounts, blocked, registry) = new_accounts();
accounts.unblock(account(1));
assert!(blocked
.check(®istry, &AccountOrder(account(1)), RejectScope::Order)
.is_none());
}
#[test]
fn accounts_replace_block_reason_updates_cause_and_errors_when_absent() {
let (accounts, blocked, registry) = new_accounts();
assert_eq!(
accounts.replace_block_reason(account(1), "x".to_owned()),
Err(AccountBlockError::AccountNotBlocked {
account: account(1)
})
);
accounts.block(account(1), "first".to_owned());
accounts
.replace_block_reason(account(1), "second".to_owned())
.expect("replacing reason on a blocked account must succeed");
let rejects = blocked
.check(®istry, &AccountOrder(account(1)), RejectScope::Order)
.expect("blocked account must return rejects");
assert_eq!(rejects[0].code, RejectCode::AccountBlocked);
assert_eq!(rejects[0].reason, "second");
}
#[test]
fn accounts_block_group_tracks_membership_live() {
let (accounts, blocked, registry) = new_accounts();
accounts
.block_group(group(7), "group halt".to_owned())
.expect("group block must succeed");
registry
.register_group(&[account(1)], group(7))
.expect("registration must succeed");
assert!(blocked
.check(®istry, &AccountOrder(account(1)), RejectScope::Order)
.is_some());
registry
.unregister_group(&[account(1)], group(7))
.expect("unregistration must succeed");
assert!(blocked
.check(®istry, &AccountOrder(account(1)), RejectScope::Order)
.is_none());
}
#[test]
fn accounts_unblock_group_and_replace_group_reason() {
let (accounts, blocked, registry) = new_accounts();
registry
.register_group(&[account(1)], group(7))
.expect("registration must succeed");
accounts
.block_group(group(7), "first".to_owned())
.expect("group block must succeed");
accounts
.replace_group_block_reason(group(7), "second".to_owned())
.expect("replacing group reason must succeed");
let rejects = blocked
.check(®istry, &AccountOrder(account(1)), RejectScope::Order)
.expect("member of blocked group must be rejected");
assert_eq!(rejects[0].reason, "second");
accounts
.unblock_group(group(7))
.expect("group unblock must succeed");
assert!(blocked
.check(®istry, &AccountOrder(account(1)), RejectScope::Order)
.is_none());
}
#[test]
fn accounts_group_operations_reject_reserved_default_group() {
let (accounts, _blocked, _registry) = new_accounts();
assert_eq!(
accounts.block_group(DEFAULT_ACCOUNT_GROUP, "x".to_owned()),
Err(AccountBlockError::ReservedGroup)
);
assert_eq!(
accounts.unblock_group(DEFAULT_ACCOUNT_GROUP),
Err(AccountBlockError::ReservedGroup)
);
assert_eq!(
accounts.replace_group_block_reason(DEFAULT_ACCOUNT_GROUP, "x".to_owned()),
Err(AccountBlockError::ReservedGroup)
);
}
#[test]
fn accounts_replace_group_block_reason_errors_when_group_not_blocked() {
let (accounts, _blocked, _registry) = new_accounts();
assert_eq!(
accounts.replace_group_block_reason(group(7), "x".to_owned()),
Err(AccountBlockError::GroupNotBlocked { group: group(7) })
);
}
#[test]
fn currency_of_prefers_account_override() {
let (accounts, _blocked, _registry) = new_accounts();
accounts
.register_group(&[account(1)], group(7))
.expect("registration must succeed");
accounts.set_group_currency(group(7), asset("USD"));
accounts.set_currency(account(1), asset("EUR"));
assert_eq!(accounts.currency_of(account(1)), Some(asset("EUR")));
}
#[test]
fn currency_of_uses_group_fallback() {
let (accounts, _blocked, _registry) = new_accounts();
accounts
.register_group(&[account(1)], group(7))
.expect("registration must succeed");
accounts.set_group_currency(group(7), asset("USD"));
assert_eq!(accounts.currency_of(account(1)), Some(asset("USD")));
}
#[test]
fn currency_of_uses_default_fallback() {
let (accounts, _blocked, _registry) = new_accounts();
accounts
.register_group(&[account(1)], group(7))
.expect("registration must succeed");
accounts.set_group_currency(DEFAULT_ACCOUNT_GROUP, asset("USD"));
assert_eq!(accounts.currency_of(account(1)), Some(asset("USD")));
assert_eq!(accounts.currency_of(account(2)), Some(asset("USD")));
}
#[test]
fn currency_clear_reveals_lower_priority_tiers() {
let (accounts, _blocked, _registry) = new_accounts();
accounts
.register_group(&[account(1)], group(7))
.expect("registration must succeed");
accounts.set_group_currency(DEFAULT_ACCOUNT_GROUP, asset("USD"));
accounts.set_group_currency(group(7), asset("EUR"));
accounts.set_currency(account(1), asset("GBP"));
accounts.clear_currency(account(1));
assert_eq!(accounts.currency_of(account(1)), Some(asset("EUR")));
accounts.clear_group_currency(group(7));
assert_eq!(accounts.currency_of(account(1)), Some(asset("USD")));
accounts.clear_group_currency(DEFAULT_ACCOUNT_GROUP);
assert_eq!(accounts.currency_of(account(1)), None);
}
#[test]
fn currency_of_unset_returns_none() {
let (accounts, _blocked, _registry) = new_accounts();
assert_eq!(accounts.currency_of(account(1)), None);
}
#[test]
fn default_account_group_currency_is_allowed() {
let (accounts, _blocked, _registry) = new_accounts();
accounts.set_group_currency(DEFAULT_ACCOUNT_GROUP, asset("USD"));
assert_eq!(accounts.currency_of(account(1)), Some(asset("USD")));
accounts.clear_group_currency(DEFAULT_ACCOUNT_GROUP);
assert_eq!(accounts.currency_of(account(1)), None);
}
}