diff --git a/.gitignore b/.gitignore index 3a6feaa..77557fe 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ Cargo.lock neardev res/ref_exchange_local.wasm +res/ref_escrow_local.wasm + diff --git a/Cargo.toml b/Cargo.toml index 706ccd0..95cb9f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,8 @@ members = [ "./ref-exchange", "./test-token", - "./ref-farming" + "./ref-farming", + "./ref-escrow" ] diff --git a/README.md b/README.md index 918ff7d..b4eb9b7 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,9 @@ This mono repo contains the source code for the smart contracts of Ref Finance o | - | - | - | | [test-token](test-token/src/lib.rs) | - | Test token contract | | [ref-exchange](ref-exchange/src/lib.rs) | [docs](https://ref-finance.gitbook.io/ref-finance/smart-contracts/ref-exchange) | Main exchange contract, that allows to deposit and withdraw tokens, exchange them via various pools | +| ref-farm | TODO | TODO | +| [ref-escrow](ref-escrow/src/lib.rs) | TODO | Escrow contract for OTC and limit orders | + ## Development diff --git a/ref-escrow/Cargo.toml b/ref-escrow/Cargo.toml new file mode 100644 index 0000000..bbc1b14 --- /dev/null +++ b/ref-escrow/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "ref-escrow" +version = "0.1.0" +authors = ["referencedev "] +edition = "2018" +publish = false + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +near-sdk = { git = "https://github.com/near/near-sdk-rs", rev = "9d99077" } +near-contract-standards = { git = "https://github.com/near/near-sdk-rs", rev = "9d99077" } + +[dev-dependencies] +near-sdk-sim = { git = "https://github.com/near/near-sdk-rs", rev = "9d99077" } +test-token = { path = "../test-token" } diff --git a/ref-escrow/build_docker.sh b/ref-escrow/build_docker.sh new file mode 100755 index 0000000..e247d8b --- /dev/null +++ b/ref-escrow/build_docker.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +# Exit script as soon as a command fails. +set -e + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + +NAME="build_ref_escrow" + +if docker ps -a --format '{{.Names}}' | grep -Eq "^${NAME}\$"; then + echo "Container exists" +else +docker create \ + --mount type=bind,source=$DIR/..,target=/host \ + --cap-add=SYS_PTRACE --security-opt seccomp=unconfined \ + --name=$NAME \ + -w /host/ref-escrow \ + -e RUSTFLAGS='-C link-arg=-s' \ + -it \ + nearprotocol/contract-builder \ + /bin/bash +fi + +docker start $NAME +docker exec -it $NAME /bin/bash -c "rustup target add wasm32-unknown-unknown; cargo build --target wasm32-unknown-unknown --release" + +mkdir -p res +cp $DIR/../target/wasm32-unknown-unknown/release/ref_escrow.wasm $DIR/../res/ref_escrow_release.wasm + diff --git a/ref-escrow/build_local.sh b/ref-escrow/build_local.sh new file mode 100755 index 0000000..6beda76 --- /dev/null +++ b/ref-escrow/build_local.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -e + +RUSTFLAGS='-C link-arg=-s' cargo +stable build --target wasm32-unknown-unknown --release +cd .. +cp target/wasm32-unknown-unknown/release/ref_escrow.wasm ./res/ref_escrow_local.wasm diff --git a/ref-escrow/src/account.rs b/ref-escrow/src/account.rs new file mode 100644 index 0000000..03503a9 --- /dev/null +++ b/ref-escrow/src/account.rs @@ -0,0 +1,243 @@ +use std::collections::HashMap; + +use near_contract_standards::storage_management::{StorageBalance, StorageBalanceBounds}; +use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; +use near_sdk::collections::LookupMap; +use near_sdk::json_types::{ValidAccountId, U128}; +use near_sdk::serde::{Deserialize, Serialize}; +use near_sdk::{assert_one_yocto, env, log, AccountId, Balance, Promise, StorageUsage}; +use std::convert::TryInto; + +/// Max account length is 64 + 4 bytes for serialization. +const MAX_ACCOUNT_LENGTH: u64 = 68; + +#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)] +#[serde(crate = "near_sdk::serde")] +pub struct Account { + /// Amount of NEAR for storage only. + pub near_amount: U128, + /// Number of active offers. + pub num_offers: u32, + /// Amounts for different tokens. + pub amounts: HashMap, +} + +impl Account { + pub fn add_offer(&mut self) { + self.num_offers += 1; + self.assert_storage(); + } + + pub fn remove_offer(&mut self) { + assert!(self.num_offers > 0, "ERR_INTERNAL"); + self.num_offers -= 1; + self.assert_storage(); + } + + pub fn deposit(&mut self, token_id: &AccountId, amount: Balance) { + (*self.amounts.entry(token_id.clone()).or_insert(U128(0))).0 += amount; + self.assert_storage(); + } + + pub fn withdraw(&mut self, token_id: &AccountId, amount: Balance) { + let current_amount = (*self.amounts.get(token_id).expect("ERR_NOT_ENOUGH_FUNDS")).0; + assert!(current_amount > amount, "ERR_NOT_ENOUGH_FUNDS"); + if current_amount == amount { + self.amounts.remove(token_id); + } else { + self.amounts + .insert(token_id.clone(), U128(current_amount - amount)); + } + } + + fn storage_used(&self) -> StorageUsage { + // Single Offer is up to 320 bytes. + (self.amounts.len() as u64) * (MAX_ACCOUNT_LENGTH + 16) + + 16 + + 4 + + (self.num_offers as u64) * 320 + } + + pub fn assert_storage(&self) { + assert!( + (self.storage_used() as u128) * env::storage_byte_cost() < self.near_amount.0, + "ERR_NO_STORAGE" + ); + } +} + +impl AccountStorage for Account { + fn new(near_amount: Balance) -> Self { + Self { + near_amount: U128(near_amount), + num_offers: 0, + amounts: HashMap::new(), + } + } + + fn storage_total(&self) -> Balance { + self.near_amount.0 + } + + fn storage_available(&self) -> Balance { + self.near_amount.0 - self.storage_used() as u128 * env::storage_byte_cost() + } + + fn min_storage_usage() -> Balance { + 16 + 4 + } + + fn remove(&self, _force: bool) { + // TODO: currently doesn't reassign. + } +} + +/// Trait for account to manage it's internal storage. +pub trait AccountStorage: BorshSerialize + BorshDeserialize { + fn new(near_amount: Balance) -> Self; + fn storage_total(&self) -> Balance; + fn storage_available(&self) -> Balance; + fn min_storage_usage() -> Balance; + + /// Should handle removing account. + /// If not `force` can fail if account is not ready to be removed. + /// If `force` should re-assign any resources to owner or alternative and remove the account. + fn remove(&self, force: bool); +} + +/// Manages user accounts in the contract. +#[derive(BorshSerialize, BorshDeserialize)] +pub struct AccountManager +where + Account: AccountStorage, +{ + accounts: LookupMap, +} + +/// Generic account manager that handles storage and updates of accounts. +impl AccountManager +where + Account: AccountStorage, +{ + pub fn new() -> Self { + Self { + accounts: LookupMap::new(b"a".to_vec()), + } + } + + /// Get account from the storage. + pub fn get_account(&self, account_id: &AccountId) -> Option { + self.accounts.get(account_id) + } + + /// Set account to the storage. + pub fn set_account(&mut self, account_id: &AccountId, account: &Account) { + self.accounts.insert(account_id, account); + } + + /// Should handle removing account from storage. + /// If not `force` can fail if account is not ready to be removed. + /// If `force` should re-assign any resources to owner or alternative and remove the account. + pub fn remove_account(&mut self, account_id: &AccountId, force: bool) { + let account = self.get_account_or(account_id); + account.remove(force); + self.accounts.remove(account_id); + } + + pub fn get_account_or(&self, account_id: &AccountId) -> Account { + self.get_account(account_id).expect("ERR_MISSING_ACCOUNT") + } + + pub fn update_account(&mut self, account_id: &AccountId, f: F) + where + F: Fn(&mut Account), + { + let mut account = self.get_account_or(account_id); + f(&mut account); + self.set_account(&account_id, &account); + } + + pub fn internal_register_account(&mut self, account_id: &AccountId, near_amount: Balance) { + let account = Account::new(near_amount); + self.set_account(account_id, &account); + } + + pub fn internal_storage_deposit( + &mut self, + account_id: Option, + registration_only: Option, + ) -> StorageBalance { + let amount = env::attached_deposit(); + let account_id = account_id + .map(|a| a.into()) + .unwrap_or_else(|| env::predecessor_account_id()); + let registration_only = registration_only.unwrap_or(false); + let min_balance = self.internal_storage_balance_bounds().min.0; + let already_registered = self.get_account(&account_id).is_some(); + if amount < min_balance && !already_registered { + env::panic(b"ERR_DEPOSIT_LESS_THAN_MIN_STORAGE"); + } + if registration_only { + // Registration only setups the account but doesn't leave space for tokens. + if already_registered { + log!("ERR_ACC_REGISTERED"); + if amount > 0 { + Promise::new(env::predecessor_account_id()).transfer(amount); + } + } else { + self.internal_register_account(&account_id, min_balance); + let refund = amount - min_balance; + if refund > 0 { + Promise::new(env::predecessor_account_id()).transfer(refund); + } + } + } else { + self.internal_register_account(&account_id, amount); + } + self.internal_storage_balance_of(account_id.try_into().unwrap()) + .unwrap() + } + + pub fn internal_storage_withdraw(&mut self, amount: Option) -> StorageBalance { + assert_one_yocto(); + let account_id = env::predecessor_account_id(); + let account = self.get_account_or(&account_id); + let available = account.storage_available(); + let amount = amount.map(|a| a.0).unwrap_or(available); + assert!(amount <= available, "ERR_STORAGE_WITHDRAW_TOO_MUCH"); + Promise::new(account_id.clone()).transfer(amount); + self.internal_storage_balance_of(account_id.try_into().unwrap()) + .unwrap() + } + + /// Unregisters the account. + pub fn internal_storage_unregister(&mut self, force: Option) -> bool { + assert_one_yocto(); + let account_id = env::predecessor_account_id(); + if let Some(account) = self.get_account(&account_id) { + self.remove_account(&account_id, force.unwrap_or(false)); + Promise::new(account_id.clone()).transfer(account.storage_total()); + true + } else { + false + } + } + + pub fn internal_storage_balance_bounds(&self) -> StorageBalanceBounds { + StorageBalanceBounds { + min: Account::min_storage_usage().into(), + max: None, + } + } + + pub fn internal_storage_balance_of( + &self, + account_id: ValidAccountId, + ) -> Option { + self.get_account(account_id.as_ref()) + .map(|account| StorageBalance { + total: U128(account.storage_total()), + available: U128(account.storage_available()), + }) + } +} diff --git a/ref-escrow/src/lib.rs b/ref-escrow/src/lib.rs new file mode 100644 index 0000000..38fa30d --- /dev/null +++ b/ref-escrow/src/lib.rs @@ -0,0 +1,290 @@ +use near_contract_standards::fungible_token::core_impl::ext_fungible_token; +use near_contract_standards::fungible_token::receiver::FungibleTokenReceiver; +use near_contract_standards::storage_management::{ + StorageBalance, StorageBalanceBounds, StorageManagement, +}; +use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; +use near_sdk::collections::LookupMap; +use near_sdk::json_types::{ValidAccountId, WrappedDuration, WrappedTimestamp, U128}; +use near_sdk::serde::{Deserialize, Serialize}; +use near_sdk::{ + assert_one_yocto, env, ext_contract, near_bindgen, serde_json, AccountId, Gas, PanicOnDefault, + Promise, PromiseOrValue, PromiseResult, +}; + +pub use crate::account::{Account, AccountManager}; + +mod account; + +near_sdk::setup_alloc!(); + +/// Amount of gas for fungible token transfers. +pub const GAS_FOR_FT_TRANSFER: Gas = 10_000_000_000_000; + +#[derive(Serialize, Deserialize)] +#[serde(crate = "near_sdk::serde")] +#[serde(untagged)] +pub enum ReceiverMessage { + Offer { + taker: Option, + take_token_id: ValidAccountId, + take_min_amount: U128, + min_offer_time: WrappedDuration, + max_offer_time: WrappedDuration, + }, + Take { + offer_id: u32, + }, +} + +#[ext_contract(ext_self)] +pub trait RefEscrow { + fn exchange_callback_post_withdraw( + &mut self, + token_id: AccountId, + sender_id: AccountId, + amount: U128, + ); +} + +#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)] +#[serde(crate = "near_sdk::serde")] +pub struct Offer { + pub offerer: AccountId, + /// Optionally only a single taker can take this offer. + pub taker: Option, + pub offer_token_id: AccountId, + pub offer_amount: U128, + pub take_token_id: AccountId, + pub take_min_amount: U128, + pub offer_min_expiry: WrappedTimestamp, + pub offer_max_expiry: WrappedTimestamp, +} + +#[near_bindgen] +#[derive(BorshSerialize, BorshDeserialize, PanicOnDefault)] +pub struct Contract { + last_offer_id: u32, + offers: LookupMap, + account_manager: AccountManager, +} + +#[near_bindgen] +impl Contract { + #[init] + pub fn new() -> Self { + Self { + last_offer_id: 0, + offers: LookupMap::new(b"o".to_vec()), + account_manager: AccountManager::new(), + } + } + + /// Withdraw funds from the account. + #[payable] + pub fn withdraw(&mut self, token_id: ValidAccountId, amount: U128) -> Promise { + assert_one_yocto(); + let sender_id = env::predecessor_account_id(); + self.account_manager.update_account(&sender_id, |account| { + account.withdraw(token_id.as_ref(), amount.0); + }); + ext_fungible_token::ft_transfer( + sender_id.clone(), + amount, + None, + token_id.as_ref(), + 1, + GAS_FOR_FT_TRANSFER, + ) + .then(ext_self::exchange_callback_post_withdraw( + token_id.as_ref().clone(), + sender_id.clone(), + amount, + &env::current_account_id(), + 0, + GAS_FOR_FT_TRANSFER, + )) + } + + #[private] + pub fn exchange_callback_post_withdraw( + &mut self, + token_id: AccountId, + sender_id: AccountId, + amount: U128, + ) { + assert_eq!( + env::promise_results_count(), + 1, + "ERR_CALLBACK_POST_WITHDRAW_INVALID", + ); + match env::promise_result(0) { + PromiseResult::NotReady => unreachable!(), + PromiseResult::Successful(_) => {} + PromiseResult::Failed => { + // This reverts the changes from withdraw function. If account doesn't exit, deposits to the owner's account. + if let Some(mut account) = self.account_manager.get_account(&sender_id) { + account.deposit(&token_id, amount.0); + self.account_manager.set_account(&sender_id, &account); + } else { + // TODO: figure out where to send money in this case? + env::log( + format!( + "Account {} is not registered or not enough storage. Money are stuck in this contract.", + sender_id + ) + .as_bytes(), + ); + } + } + }; + } + + /// Close offer. Only offerer can call this. + /// Offer minimum expiry should pass to close it. + /// Deposits money into the account for withdrawal. + pub fn close_offer(&mut self, offer_id: u32) { + let sender_id = env::predecessor_account_id(); + let offer = self.offers.get(&offer_id).expect("ERR_MISSING_OFFER"); + assert_eq!(offer.offerer, sender_id, "ERR_NOT_OFFERER"); + assert!( + env::block_timestamp() >= offer.offer_min_expiry.0, + "ERR_CAN_NOT_CLOSE_OFFER_YET" + ); + self.offers.remove(&offer_id); + self.account_manager.update_account(&sender_id, |account| { + account.remove_offer(); + account.deposit(&offer.offer_token_id, offer.offer_amount.0); + }); + } + + pub fn get_offer(&self, offer_id: u32) -> Offer { + self.offers.get(&offer_id).expect("ERR_MISSING_OFFER") + } + + pub fn get_last_offer_id(&self) -> u32 { + self.last_offer_id + } + + pub fn get_offers(&self, from_index: u32, limit: u32) -> Vec { + (from_index..std::cmp::min(from_index + limit, self.last_offer_id)) + .map(|index| self.get_offer(index)) + .collect() + } + + pub fn get_account(&self, account_id: ValidAccountId) -> Account { + self.account_manager.get_account_or(account_id.as_ref()) + } +} + +#[near_bindgen] +impl FungibleTokenReceiver for Contract { + /// Callback on receiving tokens by this contract. + /// `msg` format is JSON serialized `ReceiverMessage`. + fn ft_on_transfer( + &mut self, + sender_id: ValidAccountId, + amount: U128, + msg: String, + ) -> PromiseOrValue { + let token_id = env::predecessor_account_id(); + let message = serde_json::from_str::(&msg).expect("ERR_MSG_WRONG_FORMAT"); + match message { + ReceiverMessage::Offer { + taker, + take_token_id, + take_min_amount, + min_offer_time, + max_offer_time, + } => { + // Account must be registered and have enough space for an extra offer. + self.account_manager + .update_account(&sender_id.as_ref(), move |account| { + account.add_offer(); + }); + self.offers.insert( + &self.last_offer_id, + &Offer { + offerer: sender_id.as_ref().clone(), + taker: taker.map(|a| a.as_ref().clone()), + offer_token_id: token_id.clone(), + offer_amount: amount, + take_token_id: take_token_id.as_ref().clone(), + take_min_amount, + offer_min_expiry: (env::block_timestamp() + min_offer_time.0).into(), + offer_max_expiry: (env::block_timestamp() + max_offer_time.0).into(), + }, + ); + env::log( + format!( + "Offer {}: offering {} {} for {} {}", + self.last_offer_id, amount.0, token_id, take_min_amount.0, take_token_id + ) + .as_bytes(), + ); + self.last_offer_id += 1; + PromiseOrValue::Value(U128(0)) + } + ReceiverMessage::Take { offer_id } => { + let offer = self.offers.get(&offer_id).expect("ERR_MISSING_OFFER"); + assert!( + env::block_timestamp() < offer.offer_max_expiry.0, + "ERR_OFFER_EXPIRED" + ); + assert_ne!( + &offer.offerer, + sender_id.as_ref(), + "ERR_OFFER_CAN_NOT_SELF_TAKE" + ); + let mut offerer_account = self.account_manager.get_account_or(&offer.offerer); + let mut taker_account = self.account_manager.get_account_or(sender_id.as_ref()); + assert_eq!(offer.take_token_id, token_id, "ERR_WRONG_TAKE_TOKEN"); + assert!(amount.0 >= offer.take_min_amount.0, "ERR_NOT_ENOUGH_AMOUNT"); + if let Some(taker) = offer.taker { + assert_eq!(&taker, sender_id.as_ref(), "ERR_INCORRECT_TAKER"); + } + self.offers.remove(&offer_id); + offerer_account.remove_offer(); + offerer_account.deposit(&offer.take_token_id, amount.0); + taker_account.deposit(&offer.offer_token_id, offer.offer_amount.0); + self.account_manager + .set_account(&offer.offerer, &offerer_account); + self.account_manager + .set_account(sender_id.as_ref(), &taker_account); + + PromiseOrValue::Value(U128(0)) + } + } + } +} + +#[near_bindgen] +impl StorageManagement for Contract { + #[payable] + fn storage_deposit( + &mut self, + account_id: Option, + registration_only: Option, + ) -> StorageBalance { + self.account_manager + .internal_storage_deposit(account_id, registration_only) + } + + #[payable] + fn storage_withdraw(&mut self, amount: Option) -> StorageBalance { + self.account_manager.internal_storage_withdraw(amount) + } + + #[payable] + fn storage_unregister(&mut self, force: Option) -> bool { + self.account_manager.internal_storage_unregister(force) + } + + fn storage_balance_bounds(&self) -> StorageBalanceBounds { + self.account_manager.internal_storage_balance_bounds() + } + + fn storage_balance_of(&self, account_id: ValidAccountId) -> Option { + self.account_manager.internal_storage_balance_of(account_id) + } +} diff --git a/ref-escrow/tests/test_escrow.rs b/ref-escrow/tests/test_escrow.rs new file mode 100644 index 0000000..3351d38 --- /dev/null +++ b/ref-escrow/tests/test_escrow.rs @@ -0,0 +1,150 @@ +use std::convert::TryFrom; + +use near_sdk::json_types::{ValidAccountId, U128}; +use near_sdk::serde_json; +use near_sdk::AccountId; +use near_sdk_sim::{ + call, deploy, init_simulator, to_yocto, view, ContractAccount, ExecutionResult, UserAccount, +}; + +use ref_escrow::{Account, ContractContract as Escrow, Offer, ReceiverMessage}; +use test_token::ContractContract as TestToken; + +near_sdk_sim::lazy_static_include::lazy_static_include_bytes! { + TEST_TOKEN_WASM_BYTES => "../res/test_token.wasm", + ESCROW_WASM_BYTES => "../res/ref_escrow_local.wasm", +} + +fn to_va(a: AccountId) -> ValidAccountId { + ValidAccountId::try_from(a).unwrap() +} + +pub fn show_promises(r: ExecutionResult) { + for promise in r.promise_results() { + println!("{:?}", promise); + } +} + +fn test_token( + root: &UserAccount, + token_id: AccountId, + accounts_to_register: Vec, +) -> ContractAccount { + let t = deploy!( + contract: TestToken, + contract_id: token_id, + bytes: &TEST_TOKEN_WASM_BYTES, + signer_account: root + ); + call!(root, t.new()).assert_success(); + call!( + root, + t.mint(to_va(root.account_id.clone()), to_yocto("1000").into()) + ) + .assert_success(); + for account_id in accounts_to_register { + call!( + root, + t.storage_deposit(Some(to_va(account_id)), None), + deposit = to_yocto("1") + ) + .assert_success(); + } + t +} + +fn balance_of(token: &ContractAccount, account_id: &AccountId) -> u128 { + view!(token.ft_balance_of(to_va(account_id.clone()))) + .unwrap_json::() + .0 +} + +fn accounts(i: usize) -> AccountId { + vec![ + "escrow".to_string(), + "dai".to_string(), + "usdt".to_string(), + "user1".to_string(), + "user2".to_string(), + ][i] + .clone() +} + +#[test] +fn test_basics() { + let root = init_simulator(None); + let escrow = deploy!( + contract: Escrow, + contract_id: accounts(0), + bytes: &ESCROW_WASM_BYTES, + signer_account: root, + init_method: new() + ); + let user1 = root.create_user(accounts(3), to_yocto("1000")); + let user2 = root.create_user(accounts(4), to_yocto("1000")); + let token1 = test_token(&root, accounts(1), vec![accounts(0), accounts(4)]); + call!( + user1, + token1.mint(to_va(accounts(3)), U128(to_yocto("1000"))) + ) + .assert_success(); + let token2 = test_token(&root, accounts(2), vec![accounts(0), accounts(3)]); + call!( + user2, + token2.mint(to_va(accounts(4)), U128(to_yocto("1000"))) + ) + .assert_success(); + call!( + user1, + escrow.storage_deposit(None, None), + deposit = to_yocto("1") + ) + .assert_success(); + call!( + user2, + escrow.storage_deposit(None, None), + deposit = to_yocto("1") + ) + .assert_success(); + call!( + user1, + token1.ft_transfer_call( + to_va(accounts(0)), + U128(to_yocto("500")), + None, + serde_json::to_string(&ReceiverMessage::Offer { + taker: None, + take_token_id: to_va(accounts(2)), + take_min_amount: U128(to_yocto("50")), + min_offer_time: 100.into(), + max_offer_time: 1000000000000000.into() + }) + .unwrap() + ), + deposit = 1 + ) + .assert_success(); + let offer: Offer = view!(escrow.get_offer(0)).unwrap_json(); + assert_eq!(offer.offerer, accounts(3)); + call!( + user2, + token2.ft_transfer_call( + to_va(accounts(0)), + U128(to_yocto("50")), + None, + serde_json::to_string(&ReceiverMessage::Take { offer_id: 0 }).unwrap() + ), + deposit = 1 + ) + .assert_success(); + let account1: Account = view!(escrow.get_account(to_va(accounts(3)))).unwrap_json(); + let account2: Account = view!(escrow.get_account(to_va(accounts(4)))).unwrap_json(); + assert_eq!( + account1.amounts.into_iter().collect::>(), + vec![(accounts(2), U128(to_yocto("50")))] + ); + assert_eq!( + account2.amounts.into_iter().collect::>(), + vec![(accounts(1), U128(to_yocto("500")))] + ); +} diff --git a/test-token/src/lib.rs b/test-token/src/lib.rs index 5ea935e..71f57e6 100644 --- a/test-token/src/lib.rs +++ b/test-token/src/lib.rs @@ -24,7 +24,9 @@ impl Contract { } pub fn mint(&mut self, account_id: ValidAccountId, amount: U128) { - self.token.internal_register_account(account_id.as_ref()); + if !self.token.accounts.contains_key(account_id.as_ref()) { + self.token.internal_register_account(account_id.as_ref()); + } self.token .internal_deposit(account_id.as_ref(), amount.into()); }