wormhole/cosmwasm/packages/accounting/src/contract.rs

1191 lines
38 KiB
Rust

use std::marker::PhantomData;
use anyhow::{bail, ensure, Context};
use cosmwasm_std::{CustomQuery, Deps, DepsMut, Event, Order, StdResult};
use cw_storage_plus::Bound;
use thiserror::Error as ThisError;
use crate::{
msg::Instantiate,
state::{
account::{self, Balance},
transfer, Account, Kind, Modification, Transfer, ACCOUNTS, MODIFICATIONS, TRANSFERS,
},
};
/// Instantiate the on-chain state for accounting. Unlike other methods in this crate,
/// `instantiate` does not perform any validation of the data in `init`.
pub fn instantiate<C: CustomQuery>(deps: DepsMut<C>, init: Instantiate) -> anyhow::Result<Event> {
let num_accounts = init.accounts.len();
let num_transfers = init.transfers.len();
let num_modifications = init.modifications.len();
for a in init.accounts {
ACCOUNTS
.save(deps.storage, a.key, &a.balance)
.context("failed to save `Account`")?;
}
for t in init.transfers {
TRANSFERS
.save(deps.storage, t.key, &t.data)
.context("failed to save `Transfer`")?;
}
for m in init.modifications {
MODIFICATIONS
.save(deps.storage, m.sequence, &m)
.context("failed to save `Modification`")?;
}
Ok(Event::new("InstantiateAccounting")
.add_attribute("num_accounts", num_accounts.to_string())
.add_attribute("num_transfers", num_transfers.to_string())
.add_attribute("num_modifications", num_modifications.to_string()))
}
#[derive(ThisError, Debug)]
pub enum TransferError {
#[error("transfer already committed")]
DuplicateTransfer,
#[error("insufficient balance in destination account")]
InsufficientDestinationBalance,
#[error("insufficient balance in source account")]
InsufficientSourceBalance,
#[error("cannot unlock native tokens without an existing native account")]
MissingNativeAccount,
#[error("cannot burn wrapped tokens without an existing wrapped account")]
MissingWrappedAccount,
}
/// Validates a transfer without committing it to the on-chain state. If an error occurs that is
/// not due to the underlying cosmwasm framework, the returned error will be downcastable to
/// `TransferError`.
pub fn validate_transfer<C: CustomQuery>(deps: Deps<C>, t: &Transfer) -> anyhow::Result<()> {
transfer(deps, t).map(drop)
}
/// Commits a transfer to the on-chain state. If an error occurs that is not due to the underlying
/// cosmwasm framework, the returned error will be downcastable to `TransferError`.
///
/// # Examples
///
/// ```
/// # fn example() -> anyhow::Result<()> {
/// # use accounting::{
/// # commit_transfer,
/// # state::{transfer, Transfer},
/// # TransferError,
/// # };
/// # use cosmwasm_std::{testing::mock_dependencies, Uint256};
/// #
/// # let mut deps = mock_dependencies();
/// # let tx = Transfer {
/// # key: transfer::Key::new(3, [1u8; 32].into(), 5),
/// # data: transfer::Data {
/// # amount: Uint256::from(400u128),
/// # token_chain: 3,
/// # token_address: [3u8; 32].into(),
/// # recipient_chain: 9,
/// # },
/// # };
/// #
/// commit_transfer(deps.as_mut(), tx.clone())?;
///
/// // Repeating the transfer should return an error.
/// let err = commit_transfer(deps.as_mut(), tx)
/// .expect_err("successfully committed duplicate transfer");
/// if let Some(e) = err.downcast_ref::<TransferError>() {
/// assert!(matches!(e, TransferError::DuplicateTransfer));
/// } else {
/// println!("framework error: {err:#}");
/// }
/// #
/// # Ok(())
/// # }
/// #
/// # example().unwrap();
/// ```
pub fn commit_transfer<C: CustomQuery>(deps: DepsMut<C>, t: Transfer) -> anyhow::Result<Event> {
let (src, dst) = transfer(deps.as_ref(), &t)?;
ACCOUNTS
.save(deps.storage, src.key, &src.balance)
.context("failed to save updated source account")?;
ACCOUNTS
.save(deps.storage, dst.key, &dst.balance)
.context("failed to save updated destination account")?;
let evt = cw_transcode::to_event(&t).context("failed to transcode `Transfer` to `Event`")?;
TRANSFERS
.save(deps.storage, t.key, &t.data)
.context("failed to save `transfer::Data`")?;
Ok(evt)
}
// Carries out the transfer described by `t` and returns the updated source and destination
// accounts.
fn transfer<C: CustomQuery>(deps: Deps<C>, t: &Transfer) -> anyhow::Result<(Account, Account)> {
if TRANSFERS.has(deps.storage, t.key.clone()) {
bail!(TransferError::DuplicateTransfer);
}
let mut src = {
let key = account::Key::new(
t.key.emitter_chain(),
t.data.token_chain,
t.data.token_address,
);
let balance = match ACCOUNTS
.may_load(deps.storage, key.clone())
.context("failed to load source account")?
{
Some(s) => s,
None => {
ensure!(
key.chain_id() == key.token_chain(),
TransferError::MissingWrappedAccount
);
Balance::zero()
}
};
Account { key, balance }
};
let mut dst = {
let key = account::Key::new(
t.data.recipient_chain,
t.data.token_chain,
t.data.token_address,
);
let balance = match ACCOUNTS
.may_load(deps.storage, key.clone())
.context("failed to load destination account")?
{
Some(b) => b,
None => {
ensure!(
key.chain_id() != key.token_chain(),
TransferError::MissingNativeAccount,
);
Balance::zero()
}
};
Account { key, balance }
};
src.lock_or_burn(t.data.amount)
.context(TransferError::InsufficientSourceBalance)?;
dst.unlock_or_mint(t.data.amount)
.context(TransferError::InsufficientDestinationBalance)?;
Ok((src, dst))
}
#[derive(ThisError, Debug)]
pub enum ModifyBalanceError {
#[error("modification already processed")]
DuplicateModification,
#[error("insufficient balance in account")]
InsufficientBalance,
}
/// Modifies the on-chain balance of a single account. If an error occurs that is not due to the
/// underlying cosmwasm framework, the returned error will be downcastable to `ModifyBalanceError`.
///
/// # Examples
///
/// ```
/// # fn example() {
/// # use accounting::{
/// # modify_balance,
/// # state::{Kind, Modification},
/// # ModifyBalanceError,
/// # };
/// # use cosmwasm_std::{testing::mock_dependencies, Uint256};
/// # let mut deps = mock_dependencies();
/// #
/// /// Subtract the balance from an account that doesn't exist.
/// let m = Modification {
/// sequence: 0,
/// chain_id: 1,
/// token_chain: 2,
/// token_address: [3u8; 32].into(),
/// kind: Kind::Sub,
/// amount: Uint256::from(4u128),
/// reason: "test".into(),
/// };
///
/// let err = modify_balance(deps.as_mut(), m)
/// .expect_err("successfully modified account with insufficient balance");
/// if let Some(e) = err.downcast_ref::<ModifyBalanceError>() {
/// assert!(matches!(e, ModifyBalanceError::InsufficientBalance));
/// } else {
/// println!("framework error: {err:#}");
/// }
/// # }
/// #
/// # example();
/// ```
pub fn modify_balance<C: CustomQuery>(
deps: DepsMut<C>,
msg: Modification,
) -> anyhow::Result<Event> {
if MODIFICATIONS.has(deps.storage, msg.sequence) {
bail!(ModifyBalanceError::DuplicateModification);
}
let key = ACCOUNTS.key(account::Key::new(
msg.chain_id,
msg.token_chain,
msg.token_address,
));
let balance = key
.may_load(deps.storage)
.context("failed to load account")?
.unwrap_or(Balance::zero());
let new_balance = match msg.kind {
Kind::Add => balance.checked_add(msg.amount),
Kind::Sub => balance.checked_sub(msg.amount),
}
.map(Balance::from)
.context(ModifyBalanceError::InsufficientBalance)?;
key.save(deps.storage, &new_balance)
.context("failed to save account")?;
MODIFICATIONS
.save(deps.storage, msg.sequence, &msg)
.context("failed to store `Modification`")?;
cw_transcode::to_event(&msg).context("failed to transcode `Modification` to `Event`")
}
/// Query the balance for the account associated with `key`.
pub fn query_balance<C: CustomQuery>(deps: Deps<C>, key: account::Key) -> StdResult<Balance> {
ACCOUNTS.load(deps.storage, key)
}
/// Query information for all accounts.
pub fn query_all_accounts<C: CustomQuery>(
deps: Deps<C>,
start_after: Option<account::Key>,
) -> impl Iterator<Item = StdResult<Account>> + '_ {
let start = start_after.map(|key| Bound::Exclusive((key, PhantomData)));
ACCOUNTS
.range(deps.storage, start, None, Order::Ascending)
.map(|item| item.map(|(key, balance)| Account { key, balance }))
}
/// Query the data associated with a transfer.
pub fn query_transfer<C: CustomQuery>(
deps: Deps<C>,
key: transfer::Key,
) -> StdResult<transfer::Data> {
TRANSFERS.load(deps.storage, key)
}
/// Check if a transfer with the associated key exists.
pub fn has_transfer<C: CustomQuery>(deps: Deps<C>, key: transfer::Key) -> bool {
TRANSFERS.has(deps.storage, key)
}
/// Query information for all transfers.
pub fn query_all_transfers<C: CustomQuery>(
deps: Deps<C>,
start_after: Option<transfer::Key>,
) -> impl Iterator<Item = StdResult<Transfer>> + '_ {
let start = start_after.map(|key| Bound::Exclusive((key, PhantomData)));
TRANSFERS
.range(deps.storage, start, None, Order::Ascending)
.map(|item| item.map(|(key, data)| Transfer { key, data }))
}
/// Query the data associated with a modification.
pub fn query_modification<C: CustomQuery>(deps: Deps<C>, sequence: u64) -> StdResult<Modification> {
MODIFICATIONS.load(deps.storage, sequence)
}
/// Query information for all modifications.
pub fn query_all_modifications<C: CustomQuery>(
deps: Deps<C>,
start_after: Option<u64>,
) -> impl Iterator<Item = StdResult<Modification>> + '_ {
let start = start_after.map(|seq| Bound::Exclusive((seq, PhantomData)));
MODIFICATIONS
.range(deps.storage, start, None, Order::Ascending)
.map(|item| item.map(|(_, v)| v))
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use cosmwasm_std::{testing::mock_dependencies, StdError, Uint256};
use super::*;
fn create_accounts(count: usize) -> Vec<Account> {
let mut out = Vec::with_capacity(count * count);
for i in 0..count {
for j in 0..count {
let key = account::Key::new(i as u16, j as u16, [i as u8; 32].into());
let balance = Uint256::from(j as u128).into();
out.push(Account { key, balance });
}
}
out
}
fn save_accounts(deps: DepsMut, accounts: &[Account]) {
for a in accounts {
ACCOUNTS
.save(deps.storage, a.key.clone(), &a.balance)
.unwrap();
}
}
fn create_transfers(count: usize) -> Vec<Transfer> {
let mut out = Vec::with_capacity(count);
for i in 0..count {
let key = transfer::Key::new(i as u16, [i as u8; 32].into(), i as u64);
let data = transfer::Data {
amount: Uint256::from(i as u128),
token_chain: i as u16,
token_address: [i as u8; 32].into(),
recipient_chain: i as u16,
};
out.push(Transfer { key, data });
}
out
}
fn save_transfers(deps: DepsMut, transfers: &[Transfer]) {
for t in transfers {
TRANSFERS
.save(deps.storage, t.key.clone(), &t.data)
.unwrap();
}
}
fn create_modifications(count: usize) -> Vec<Modification> {
let mut out = Vec::with_capacity(count);
for i in 0..count {
let m = Modification {
sequence: i as u64,
chain_id: i as u16,
token_chain: i as u16,
token_address: [i as u8; 32].into(),
kind: if i % 2 == 0 { Kind::Add } else { Kind::Sub },
amount: Uint256::from(i as u128),
reason: format!("{i}"),
};
out.push(m);
}
out
}
fn save_modifications(deps: DepsMut, modifications: &[Modification]) {
for m in modifications {
MODIFICATIONS.save(deps.storage, m.sequence, m).unwrap();
}
}
#[test]
fn instantiate_accounting() {
let mut deps = mock_dependencies();
let count = 3;
let msg = Instantiate {
accounts: create_accounts(count),
transfers: create_transfers(count),
modifications: create_modifications(count),
};
instantiate(deps.as_mut(), msg.clone()).unwrap();
for a in msg.accounts {
assert_eq!(a.balance, query_balance(deps.as_ref(), a.key).unwrap());
}
for t in msg.transfers {
assert_eq!(t.data, query_transfer(deps.as_ref(), t.key).unwrap());
}
for m in msg.modifications {
assert_eq!(m, query_modification(deps.as_ref(), m.sequence).unwrap());
}
}
#[test]
fn simple_transfer() {
let mut deps = mock_dependencies();
let tx = Transfer {
key: transfer::Key::new(3, [1u8; 32].into(), 5),
data: transfer::Data {
amount: Uint256::from(400u128),
token_chain: 3,
token_address: [3u8; 32].into(),
recipient_chain: 9,
},
};
validate_transfer(deps.as_ref(), &tx).unwrap();
let evt = commit_transfer(deps.as_mut(), tx.clone()).unwrap();
let src = account::Key::new(
tx.key.emitter_chain(),
tx.data.token_chain,
tx.data.token_address,
);
assert_eq!(tx.data.amount, *query_balance(deps.as_ref(), src).unwrap());
let dst = account::Key::new(
tx.data.recipient_chain,
tx.data.token_chain,
tx.data.token_address,
);
assert_eq!(tx.data.amount, *query_balance(deps.as_ref(), dst).unwrap());
let expected = Event::new("Transfer")
.add_attribute("key", serde_json_wasm::to_string(&tx.key).unwrap())
.add_attribute("data", serde_json_wasm::to_string(&tx.data).unwrap());
assert_eq!(expected, evt);
assert_eq!(tx.data, query_transfer(deps.as_ref(), tx.key).unwrap());
}
#[test]
fn duplicate_transfer() {
let mut deps = mock_dependencies();
let tx = Transfer {
key: transfer::Key::new(3, [1u8; 32].into(), 5),
data: transfer::Data {
amount: Uint256::from(400u128),
token_chain: 3,
token_address: [3u8; 32].into(),
recipient_chain: 9,
},
};
validate_transfer(deps.as_ref(), &tx).unwrap();
commit_transfer(deps.as_mut(), tx.clone()).unwrap();
// Repeating the transfer should return an error and should not change the balances.
let e = validate_transfer(deps.as_ref(), &tx).unwrap_err();
assert!(matches!(
e.downcast().unwrap(),
TransferError::DuplicateTransfer
));
let e = commit_transfer(deps.as_mut(), tx.clone())
.expect_err("successfully committed duplicate transfer");
assert!(matches!(
e.downcast().unwrap(),
TransferError::DuplicateTransfer
));
let src = account::Key::new(
tx.key.emitter_chain(),
tx.data.token_chain,
tx.data.token_address,
);
assert_eq!(tx.data.amount, *query_balance(deps.as_ref(), src).unwrap());
let dst = account::Key::new(
tx.data.recipient_chain,
tx.data.token_chain,
tx.data.token_address,
);
assert_eq!(tx.data.amount, *query_balance(deps.as_ref(), dst).unwrap());
assert_eq!(tx.data, query_transfer(deps.as_ref(), tx.key).unwrap());
}
#[test]
fn round_trip_transfer() {
let mut deps = mock_dependencies();
let tx = Transfer {
key: transfer::Key::new(3, [1u8; 32].into(), 5),
data: transfer::Data {
amount: Uint256::from(400u128),
token_chain: 3,
token_address: [3u8; 32].into(),
recipient_chain: 9,
},
};
validate_transfer(deps.as_ref(), &tx).unwrap();
commit_transfer(deps.as_mut(), tx.clone()).unwrap();
let rx = Transfer {
key: transfer::Key::new(tx.data.recipient_chain, [6u8; 32].into(), 2),
data: transfer::Data {
amount: tx.data.amount,
token_chain: tx.data.token_chain,
token_address: tx.data.token_address,
recipient_chain: tx.key.emitter_chain(),
},
};
validate_transfer(deps.as_ref(), &rx).unwrap();
commit_transfer(deps.as_mut(), rx.clone()).unwrap();
let src = account::Key::new(
tx.key.emitter_chain(),
tx.data.token_chain,
tx.data.token_address,
);
assert_eq!(Balance::zero(), query_balance(deps.as_ref(), src).unwrap());
let dst = account::Key::new(
tx.data.recipient_chain,
tx.data.token_chain,
tx.data.token_address,
);
assert_eq!(Balance::zero(), query_balance(deps.as_ref(), dst).unwrap());
assert_eq!(tx.data, query_transfer(deps.as_ref(), tx.key).unwrap());
assert_eq!(rx.data, query_transfer(deps.as_ref(), rx.key).unwrap());
}
#[test]
fn missing_wrapped_account() {
let mut deps = mock_dependencies();
let tx = Transfer {
key: transfer::Key::new(9, [1u8; 32].into(), 5),
data: transfer::Data {
amount: Uint256::from(400u128),
token_chain: 3,
token_address: [3u8; 32].into(),
recipient_chain: 3,
},
};
let e = validate_transfer(deps.as_ref(), &tx).unwrap_err();
assert!(matches!(
e.downcast().unwrap(),
TransferError::MissingWrappedAccount
));
let e = commit_transfer(deps.as_mut(), tx)
.expect_err("successfully committed transfer with missing wrapped account");
assert!(matches!(
e.downcast().unwrap(),
TransferError::MissingWrappedAccount
));
}
#[test]
fn missing_native_account() {
let mut deps = mock_dependencies();
let tx = Transfer {
key: transfer::Key::new(9, [1u8; 32].into(), 5),
data: transfer::Data {
amount: Uint256::from(400u128),
token_chain: 3,
token_address: [3u8; 32].into(),
recipient_chain: 3,
},
};
// Set up a fake wrapped account so the check for the wrapped account succeeds.
ACCOUNTS
.save(
&mut deps.storage,
account::Key::new(
tx.key.emitter_chain(),
tx.data.token_chain,
tx.data.token_address,
),
&tx.data.amount.into(),
)
.unwrap();
let e = validate_transfer(deps.as_ref(), &tx).unwrap_err();
assert!(matches!(
e.downcast().unwrap(),
TransferError::MissingNativeAccount
));
let e = commit_transfer(deps.as_mut(), tx)
.expect_err("successfully committed transfer with missing native account");
assert!(matches!(
e.downcast().unwrap(),
TransferError::MissingNativeAccount
));
}
#[test]
fn repeated_transfer() {
const ITERATIONS: usize = 10;
let mut deps = mock_dependencies();
let emitter_chain = 3;
let emitter_address = [3u8; 32].into();
let data = transfer::Data {
amount: Uint256::from(400u128),
token_chain: 3,
token_address: [3u8; 32].into(),
recipient_chain: 9,
};
for i in 0..ITERATIONS {
let tx = Transfer {
key: transfer::Key::new(emitter_chain, emitter_address, i as u64),
data: data.clone(),
};
validate_transfer(deps.as_ref(), &tx).unwrap();
commit_transfer(deps.as_mut(), tx).unwrap();
}
let src = account::Key::new(emitter_chain, data.token_chain, data.token_address);
assert_eq!(
data.amount * Uint256::from(ITERATIONS as u128),
*query_balance(deps.as_ref(), src).unwrap()
);
let dst = account::Key::new(data.recipient_chain, data.token_chain, data.token_address);
assert_eq!(
data.amount * Uint256::from(ITERATIONS as u128),
*query_balance(deps.as_ref(), dst).unwrap()
);
}
#[test]
fn wrapped_transfer() {
let mut deps = mock_dependencies();
// Do an initial simple transfer.
let tx = Transfer {
key: transfer::Key::new(3, [1u8; 32].into(), 5),
data: transfer::Data {
amount: Uint256::from(400u128),
token_chain: 3,
token_address: [3u8; 32].into(),
recipient_chain: 9,
},
};
validate_transfer(deps.as_ref(), &tx).unwrap();
commit_transfer(deps.as_mut(), tx.clone()).unwrap();
// Now transfer some of the wrapped tokens to a new chain.
let wrapped = Transfer {
key: transfer::Key::new(tx.data.recipient_chain, [2u8; 32].into(), 9),
data: transfer::Data {
amount: Uint256::from(200u128),
token_chain: tx.data.token_chain,
token_address: tx.data.token_address,
recipient_chain: 11,
},
};
validate_transfer(deps.as_ref(), &wrapped).unwrap();
commit_transfer(deps.as_mut(), wrapped.clone()).unwrap();
// The balance on the original chain should not have changed.
let src = account::Key::new(
tx.key.emitter_chain(),
tx.data.token_chain,
tx.data.token_address,
);
assert_eq!(tx.data.amount, *query_balance(deps.as_ref(), src).unwrap());
// The destination chain should have the difference between the two transfers.
let dst = account::Key::new(
tx.data.recipient_chain,
tx.data.token_chain,
tx.data.token_address,
);
assert_eq!(
tx.data.amount - wrapped.data.amount,
*query_balance(deps.as_ref(), dst).unwrap()
);
// The third chain should only have the wrapped amount.
let w = account::Key::new(
wrapped.data.recipient_chain,
tx.data.token_chain,
tx.data.token_address,
);
assert_eq!(
wrapped.data.amount,
*query_balance(deps.as_ref(), w).unwrap()
);
}
#[test]
fn insufficient_wrapped_balance() {
let mut deps = mock_dependencies();
let tx = Transfer {
key: transfer::Key::new(3, [1u8; 32].into(), 5),
data: transfer::Data {
amount: Uint256::from(400u128),
token_chain: 3,
token_address: [3u8; 32].into(),
recipient_chain: 9,
},
};
validate_transfer(deps.as_ref(), &tx).unwrap();
commit_transfer(deps.as_mut(), tx.clone()).unwrap();
// Now try to transfer back more tokens than were originally sent.
let rx = Transfer {
key: transfer::Key::new(tx.data.recipient_chain, [6u8; 32].into(), 2),
data: transfer::Data {
amount: tx.data.amount * Uint256::from(2u128),
token_chain: tx.data.token_chain,
token_address: tx.data.token_address,
recipient_chain: tx.key.emitter_chain(),
},
};
let e = validate_transfer(deps.as_ref(), &rx).unwrap_err();
assert!(matches!(
e.downcast().unwrap(),
TransferError::InsufficientSourceBalance
));
let e = commit_transfer(deps.as_mut(), rx)
.expect_err("successfully transferred more tokens than available");
assert!(matches!(
e.downcast().unwrap(),
TransferError::InsufficientSourceBalance
));
}
#[test]
fn insufficient_native_balance() {
let mut deps = mock_dependencies();
let tx = Transfer {
key: transfer::Key::new(3, [1u8; 32].into(), 5),
data: transfer::Data {
amount: Uint256::from(400u128),
token_chain: 3,
token_address: [3u8; 32].into(),
recipient_chain: 9,
},
};
validate_transfer(deps.as_ref(), &tx).unwrap();
commit_transfer(deps.as_mut(), tx.clone()).unwrap();
// Artificially increase the wrapped balance so that the check for wrapped tokens passes.
ACCOUNTS
.update(
&mut deps.storage,
account::Key::new(
tx.data.recipient_chain,
tx.data.token_chain,
tx.data.token_address,
),
|b| {
b.unwrap()
.checked_mul(Uint256::from(2u128))
.map(From::from)
.map_err(|source| StdError::Overflow { source })
},
)
.unwrap();
// Now try to transfer back more tokens than were originally sent.
let rx = Transfer {
key: transfer::Key::new(tx.data.recipient_chain, [6u8; 32].into(), 2),
data: transfer::Data {
amount: tx.data.amount * Uint256::from(2u128),
token_chain: tx.data.token_chain,
token_address: tx.data.token_address,
recipient_chain: tx.key.emitter_chain(),
},
};
let e = validate_transfer(deps.as_ref(), &rx).unwrap_err();
assert!(matches!(
e.downcast().unwrap(),
TransferError::InsufficientDestinationBalance
));
let e = commit_transfer(deps.as_mut(), rx)
.expect_err("successfully transferred more tokens than available");
assert!(matches!(
e.downcast().unwrap(),
TransferError::InsufficientDestinationBalance
));
}
#[test]
fn simple_modify() {
let mut deps = mock_dependencies();
let m = Modification {
sequence: 0,
chain_id: 1,
token_chain: 2,
token_address: [3u8; 32].into(),
kind: Kind::Add,
amount: Uint256::from(4u128),
reason: "test".into(),
};
let evt = modify_balance(deps.as_mut(), m.clone()).unwrap();
let acc = account::Key::new(m.chain_id, m.token_chain, m.token_address);
assert_eq!(m.amount, *query_balance(deps.as_ref(), acc).unwrap());
assert_eq!(m, query_modification(deps.as_ref(), m.sequence).unwrap());
let expected = Event::new("Modification")
.add_attribute("sequence", serde_json_wasm::to_string(&m.sequence).unwrap())
.add_attribute("chain_id", serde_json_wasm::to_string(&m.chain_id).unwrap())
.add_attribute(
"token_chain",
serde_json_wasm::to_string(&m.token_chain).unwrap(),
)
.add_attribute(
"token_address",
serde_json_wasm::to_string(&m.token_address).unwrap(),
)
.add_attribute("kind", serde_json_wasm::to_string(&m.kind).unwrap())
.add_attribute("amount", serde_json_wasm::to_string(&m.amount).unwrap())
.add_attribute("reason", serde_json_wasm::to_string(&m.reason).unwrap());
assert_eq!(expected, evt);
}
#[test]
fn duplicate_modify() {
let mut deps = mock_dependencies();
let m = Modification {
sequence: 0,
chain_id: 1,
token_chain: 2,
token_address: [3u8; 32].into(),
kind: Kind::Add,
amount: Uint256::from(4u128),
reason: "test".into(),
};
modify_balance(deps.as_mut(), m.clone()).unwrap();
// Trying the same modification again should fail.
let e = modify_balance(deps.as_mut(), m).expect_err("successfully modified balance twice");
assert!(matches!(
e.downcast().unwrap(),
ModifyBalanceError::DuplicateModification
));
}
#[test]
fn round_trip_modify() {
let mut deps = mock_dependencies();
let mut m = Modification {
sequence: 0,
chain_id: 1,
token_chain: 2,
token_address: [3u8; 32].into(),
kind: Kind::Add,
amount: Uint256::from(4u128),
reason: "test".into(),
};
modify_balance(deps.as_mut(), m.clone()).unwrap();
m.sequence += 1;
m.kind = Kind::Sub;
modify_balance(deps.as_mut(), m.clone()).unwrap();
let acc = account::Key::new(m.chain_id, m.token_chain, m.token_address);
assert_eq!(Balance::zero(), query_balance(deps.as_ref(), acc).unwrap());
}
#[test]
fn repeated_modify() {
const ITERATIONS: u64 = 10;
let mut deps = mock_dependencies();
let mut m = Modification {
sequence: 0,
chain_id: 1,
token_chain: 2,
token_address: [3u8; 32].into(),
kind: Kind::Add,
amount: Uint256::from(4u128),
reason: "test".into(),
};
for i in 0..ITERATIONS {
m.sequence = i;
modify_balance(deps.as_mut(), m.clone()).unwrap();
}
let acc = account::Key::new(m.chain_id, m.token_chain, m.token_address);
assert_eq!(
m.amount * Uint256::from(ITERATIONS),
*query_balance(deps.as_ref(), acc).unwrap()
);
}
#[test]
fn modify_insufficient_balance() {
let mut deps = mock_dependencies();
let m = Modification {
sequence: 0,
chain_id: 1,
token_chain: 2,
token_address: [3u8; 32].into(),
kind: Kind::Sub,
amount: Uint256::from(4u128),
reason: "test".into(),
};
let e = modify_balance(deps.as_mut(), m)
.expect_err("successfully modified account with insufficient balance");
assert!(matches!(
e.downcast().unwrap(),
ModifyBalanceError::InsufficientBalance
));
}
#[test]
fn query_account_balance() {
let mut deps = mock_dependencies();
let count = 2;
save_accounts(deps.as_mut(), &create_accounts(count));
for i in 0..count {
for j in 0..count {
let key = account::Key::new(i as u16, j as u16, [i as u8; 32].into());
let balance = query_balance(deps.as_ref(), key).unwrap();
assert_eq!(balance, Balance::new(Uint256::from(j as u128)))
}
}
}
#[test]
fn query_missing_account() {
let mut deps = mock_dependencies();
let count = 2;
save_accounts(deps.as_mut(), &create_accounts(count));
let missing = account::Key::new(
(count + 1) as u16,
(count + 2) as u16,
[(count + 3) as u8; 32].into(),
);
query_balance(deps.as_ref(), missing)
.expect_err("successfully queried missing account key");
}
#[test]
fn query_all_balances() {
let mut deps = mock_dependencies();
let count = 3;
save_accounts(deps.as_mut(), &create_accounts(count));
let found = query_all_accounts(deps.as_ref(), None)
.map(|item| item.map(|acc| (acc.key, acc.balance)))
.collect::<StdResult<BTreeMap<_, _>>>()
.unwrap();
assert_eq!(found.len(), count * count);
for i in 0..count {
for j in 0..count {
let key = account::Key::new(i as u16, j as u16, [i as u8; 32].into());
assert!(found.contains_key(&key));
}
}
}
#[test]
fn query_all_balances_start_after() {
let mut deps = mock_dependencies();
let count = 3;
save_accounts(deps.as_mut(), &create_accounts(count));
for i in 0..count {
for j in 0..count {
let start_after = Some(account::Key::new(i as u16, j as u16, [i as u8; 32].into()));
let found = query_all_accounts(deps.as_ref(), start_after)
.map(|item| item.map(|acc| (acc.key, acc.balance)))
.collect::<StdResult<BTreeMap<_, _>>>()
.unwrap();
assert_eq!(found.len(), (count - i - 1) * count + (count - j - 1),);
for y in j + 1..count {
let key = account::Key::new(i as u16, y as u16, [i as u8; 32].into());
assert!(found.contains_key(&key));
}
for x in i + 1..count {
for y in 0..count {
let key = account::Key::new(x as u16, y as u16, [x as u8; 32].into());
assert!(found.contains_key(&key));
}
}
}
}
}
#[test]
fn query_transfer_data() {
let mut deps = mock_dependencies();
let count = 2;
save_transfers(deps.as_mut(), &create_transfers(count));
for i in 0..count {
let expected = transfer::Data {
amount: Uint256::from(i as u128),
token_chain: i as u16,
token_address: [i as u8; 32].into(),
recipient_chain: i as u16,
};
let key = transfer::Key::new(i as u16, [i as u8; 32].into(), i as u64);
let actual = query_transfer(deps.as_ref(), key).unwrap();
assert_eq!(expected, actual);
}
}
#[test]
fn query_missing_transfer() {
let mut deps = mock_dependencies();
let count = 2;
save_transfers(deps.as_mut(), &create_transfers(count));
let missing = transfer::Key::new(
(count + 1) as u16,
[(count + 2) as u8; 32].into(),
(count + 3) as u64,
);
query_transfer(deps.as_ref(), missing)
.expect_err("successfully queried missing transfer key");
}
#[test]
fn query_all_transfer_data() {
let mut deps = mock_dependencies();
let count = 3;
save_transfers(deps.as_mut(), &create_transfers(count));
let found = query_all_transfers(deps.as_ref(), None)
.map(|item| item.map(|acc| (acc.key, acc.data)))
.collect::<StdResult<BTreeMap<_, _>>>()
.unwrap();
assert_eq!(found.len(), count);
for i in 0..count {
let key = transfer::Key::new(i as u16, [i as u8; 32].into(), i as u64);
assert!(found.contains_key(&key));
}
}
#[test]
fn query_all_transfer_data_start_after() {
let mut deps = mock_dependencies();
let count = 3;
save_transfers(deps.as_mut(), &create_transfers(count));
for i in 0..count {
let start_after = Some(transfer::Key::new(i as u16, [i as u8; 32].into(), i as u64));
let found = query_all_transfers(deps.as_ref(), start_after)
.map(|item| item.map(|acc| (acc.key, acc.data)))
.collect::<StdResult<BTreeMap<_, _>>>()
.unwrap();
assert_eq!(found.len(), count - i - 1);
for x in i + 1..count {
let key = transfer::Key::new(x as u16, [x as u8; 32].into(), x as u64);
assert!(found.contains_key(&key));
}
}
}
#[test]
fn query_modification_data() {
let mut deps = mock_dependencies();
let count = 2;
save_modifications(deps.as_mut(), &create_modifications(count));
for i in 0..count {
let expected = Modification {
sequence: i as u64,
chain_id: i as u16,
token_chain: i as u16,
token_address: [i as u8; 32].into(),
kind: if i % 2 == 0 { Kind::Add } else { Kind::Sub },
amount: Uint256::from(i as u128),
reason: format!("{i}"),
};
let key = i as u64;
let actual = query_modification(deps.as_ref(), key).unwrap();
assert_eq!(expected, actual);
}
}
#[test]
fn query_missing_modification() {
let mut deps = mock_dependencies();
let count = 2;
save_modifications(deps.as_mut(), &create_modifications(count));
let missing = (count + 1) as u64;
query_modification(deps.as_ref(), missing)
.expect_err("successfully queried missing modification key");
}
#[test]
fn query_all_modification_data() {
let mut deps = mock_dependencies();
let count = 3;
save_modifications(deps.as_mut(), &create_modifications(count));
let found = query_all_modifications(deps.as_ref(), None)
.map(|item| item.map(|m| (m.sequence, m)))
.collect::<StdResult<BTreeMap<_, _>>>()
.unwrap();
assert_eq!(found.len(), count);
for i in 0..count {
let key = i as u64;
assert!(found.contains_key(&key));
}
}
#[test]
fn query_all_modification_data_start_after() {
let mut deps = mock_dependencies();
let count = 3;
save_modifications(deps.as_mut(), &create_modifications(count));
for i in 0..count {
let start_after = Some(i as u64);
let found = query_all_modifications(deps.as_ref(), start_after)
.map(|item| item.map(|m| (m.sequence, m)))
.collect::<StdResult<BTreeMap<_, _>>>()
.unwrap();
assert_eq!(found.len(), count - i - 1);
for x in i + 1..count {
let key = x as u64;
assert!(found.contains_key(&key));
}
}
}
}