#[cfg(feature = "abi")]
use std::collections::BTreeMap;
use std::{
cell::RefCell,
collections::VecDeque,
io::{Error, Write},
mem,
num::NonZeroU128,
rc::Rc,
};
#[cfg(feature = "abi")]
use borsh::BorshSchema;
use near_sdk_macros::near;
use crate::{
AccountId, CryptoHash, Gas, GasWeight, NearToken, PromiseIndex, PublicKey,
env::migrate_to_allowance,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Allowance {
Unlimited,
Limited(NonZeroU128),
}
impl Allowance {
pub fn unlimited() -> Allowance {
Allowance::Unlimited
}
pub fn limited(balance: NearToken) -> Option<Allowance> {
NonZeroU128::new(balance.as_yoctonear()).map(Allowance::Limited)
}
}
enum PromiseAction {
CreateAccount,
DeployContract {
code: Vec<u8>,
},
FunctionCall {
function_name: String,
arguments: Vec<u8>,
amount: NearToken,
gas: Gas,
},
FunctionCallWeight {
function_name: String,
arguments: Vec<u8>,
amount: NearToken,
gas: Gas,
weight: GasWeight,
},
Transfer {
amount: NearToken,
},
Stake {
amount: NearToken,
public_key: PublicKey,
},
AddFullAccessKey {
public_key: PublicKey,
nonce: u64,
},
AddAccessKey {
public_key: PublicKey,
allowance: Allowance,
receiver_id: AccountId,
function_names: String,
nonce: u64,
},
DeleteKey {
public_key: PublicKey,
},
DeleteAccount {
beneficiary_id: AccountId,
},
DeployGlobalContract {
code: Vec<u8>,
},
DeployGlobalContractByAccountId {
code: Vec<u8>,
},
UseGlobalContract {
code_hash: CryptoHash,
},
UseGlobalContractByAccountId {
account_id: AccountId,
},
#[cfg(feature = "deterministic-account-ids")]
DeterministicStateInit {
state_init: crate::state_init::StateInit,
deposit: NearToken,
},
}
impl PromiseAction {
pub fn add(self, promise_index: PromiseIndex) {
use PromiseAction::*;
match self {
CreateAccount => crate::env::promise_batch_action_create_account(promise_index),
DeployContract { code } => {
crate::env::promise_batch_action_deploy_contract(promise_index, &code)
}
FunctionCall { function_name, arguments, amount, gas } => {
crate::env::promise_batch_action_function_call(
promise_index,
&function_name,
&arguments,
amount,
gas,
)
}
FunctionCallWeight { function_name, arguments, amount, gas, weight } => {
crate::env::promise_batch_action_function_call_weight(
promise_index,
&function_name,
&arguments,
amount,
gas,
GasWeight(weight.0),
)
}
Transfer { amount } => crate::env::promise_batch_action_transfer(promise_index, amount),
Stake { amount, public_key } => {
crate::env::promise_batch_action_stake(promise_index, amount, &public_key)
}
AddFullAccessKey { public_key, nonce } => {
crate::env::promise_batch_action_add_key_with_full_access(
promise_index,
&public_key,
nonce,
)
}
AddAccessKey { public_key, allowance, receiver_id, function_names, nonce } => {
crate::env::promise_batch_action_add_key_allowance_with_function_call(
promise_index,
&public_key,
nonce,
allowance,
&receiver_id,
&function_names,
)
}
DeleteKey { public_key } => {
crate::env::promise_batch_action_delete_key(promise_index, &public_key)
}
DeleteAccount { beneficiary_id } => {
crate::env::promise_batch_action_delete_account(promise_index, &beneficiary_id)
}
DeployGlobalContract { code } => {
crate::env::promise_batch_action_deploy_global_contract(promise_index, &code)
}
DeployGlobalContractByAccountId { code } => {
crate::env::promise_batch_action_deploy_global_contract_by_account_id(
promise_index,
&code,
)
}
UseGlobalContract { code_hash } => {
crate::env::promise_batch_action_use_global_contract(promise_index, &code_hash)
}
UseGlobalContractByAccountId { account_id } => {
crate::env::promise_batch_action_use_global_contract_by_account_id(
promise_index,
account_id,
)
}
#[cfg(feature = "deterministic-account-ids")]
DeterministicStateInit {
state_init: crate::state_init::StateInit::V1(state_init),
deposit,
} => {
use crate::GlobalContractId;
let action_index = match &state_init.code {
GlobalContractId::CodeHash(code_hash) => {
crate::env::promise_batch_action_state_init(
promise_index,
code_hash.into(),
deposit,
)
}
GlobalContractId::AccountId(account_id) => {
crate::env::promise_batch_action_state_init_by_account_id(
promise_index,
account_id,
deposit,
)
}
};
for (key, value) in &state_init.data {
crate::env::set_state_init_data_entry(promise_index, action_index, key, value);
}
}
}
}
}
enum PromiseSingleSubtype {
Regular {
account_id: AccountId,
after: Option<Rc<RefCell<Promise>>>,
promise_index: Option<PromiseIndex>,
},
Yielded(PromiseIndex),
}
struct PromiseSingle {
pub subtype: PromiseSingleSubtype,
#[cfg(feature = "deterministic-account-ids")]
pub refund_to: Option<AccountId>,
pub actions: Vec<PromiseAction>,
}
impl PromiseSingle {
pub const fn new(subtype: PromiseSingleSubtype) -> Self {
Self {
subtype,
#[cfg(feature = "deterministic-account-ids")]
refund_to: None,
actions: Vec::new(),
}
}
pub fn construct_recursively(&mut self) -> PromiseIndex {
let promise_index = match &mut self.subtype {
PromiseSingleSubtype::Regular { account_id, after, promise_index } => *promise_index
.get_or_insert_with(|| {
if let Some(after) =
after.as_mut().and_then(|p| p.borrow_mut().construct_recursively())
{
crate::env::promise_batch_then(after, account_id)
} else {
crate::env::promise_batch_create(account_id)
}
}),
PromiseSingleSubtype::Yielded(promise_index) => *promise_index,
};
#[cfg(feature = "deterministic-account-ids")]
if let Some(refund_to) = self.refund_to.take() {
crate::env::promise_set_refund_to(promise_index, &refund_to);
}
for action in mem::take(&mut self.actions) {
action.add(promise_index);
}
promise_index
}
}
pub struct PromiseJoint {
pub promises: VecDeque<Promise>,
pub promise_index: Option<PromiseIndex>,
}
impl PromiseJoint {
pub fn construct_recursively(&mut self) -> Option<PromiseIndex> {
if self.promise_index.is_none() {
let mut promises = mem::take(&mut self.promises);
if promises.is_empty() {
return None;
}
self.promise_index = Some(crate::env::promise_and(
&promises.iter_mut().filter_map(Promise::construct_recursively).collect::<Vec<_>>(),
));
}
self.promise_index
}
}
#[must_use = "return or detach explicitly via `.detach()`"]
pub struct Promise {
subtype: PromiseSubtype,
should_return: RefCell<bool>,
}
#[cfg(feature = "abi")]
impl BorshSchema for Promise {
fn add_definitions_recursively(
definitions: &mut BTreeMap<borsh::schema::Declaration, borsh::schema::Definition>,
) {
<()>::add_definitions_recursively(definitions);
}
fn declaration() -> borsh::schema::Declaration {
<()>::declaration()
}
}
enum PromiseSubtype {
Single(PromiseSingle),
Joint(PromiseJoint),
}
impl Promise {
pub fn new(account_id: AccountId) -> Self {
Self::new_with_subtype(PromiseSubtype::Single(PromiseSingle::new(
PromiseSingleSubtype::Regular { account_id, after: None, promise_index: None },
)))
}
const fn new_with_subtype(subtype: PromiseSubtype) -> Self {
Self { subtype, should_return: RefCell::new(false) }
}
pub fn new_yield(
function_name: &str,
arguments: impl AsRef<[u8]>,
gas: Gas,
weight: GasWeight,
) -> (Self, YieldId) {
let (promise_index, yield_id) =
crate::env::promise_yield_create_id(function_name, arguments, gas, weight);
let promise = Self::new_with_subtype(PromiseSubtype::Single(PromiseSingle::new(
PromiseSingleSubtype::Yielded(promise_index),
)));
(promise, yield_id)
}
#[cfg(feature = "deterministic-account-ids")]
pub fn refund_to(mut self, account_id: impl Into<AccountId>) -> Self {
let PromiseSubtype::Single(promise) = &mut self.subtype else {
crate::env::panic_str("Cannot set refund account for a joint promise.");
};
promise.refund_to = Some(account_id.into());
self
}
fn add_action(mut self, action: PromiseAction) -> Self {
match &mut self.subtype {
PromiseSubtype::Single(x) => x.actions.push(action),
PromiseSubtype::Joint(_) => {
crate::env::panic_str("Cannot add action to a joint promise.")
}
}
self
}
pub fn create_account(self) -> Self {
self.add_action(PromiseAction::CreateAccount)
}
pub fn deploy_contract(self, code: impl Into<Vec<u8>>) -> Self {
self.add_action(PromiseAction::DeployContract { code: code.into() })
}
pub fn deploy_global_contract(self, code: impl Into<Vec<u8>>) -> Self {
self.add_action(PromiseAction::DeployGlobalContract { code: code.into() })
}
pub fn deploy_global_contract_by_account_id(self, code: impl Into<Vec<u8>>) -> Self {
self.add_action(PromiseAction::DeployGlobalContractByAccountId { code: code.into() })
}
pub fn use_global_contract(self, code_hash: impl Into<CryptoHash>) -> Self {
self.add_action(PromiseAction::UseGlobalContract { code_hash: code_hash.into() })
}
pub fn use_global_contract_by_account_id(self, account_id: AccountId) -> Self {
self.add_action(PromiseAction::UseGlobalContractByAccountId { account_id })
}
#[cfg(feature = "deterministic-account-ids")]
pub fn state_init(self, state_init: crate::state_init::StateInit, deposit: NearToken) -> Self {
self.add_action(PromiseAction::DeterministicStateInit { state_init, deposit })
}
pub fn function_call(
self,
function_name: impl Into<String>,
arguments: impl Into<Vec<u8>>,
amount: NearToken,
gas: Gas,
) -> Self {
self.add_action(PromiseAction::FunctionCall {
function_name: function_name.into(),
arguments: arguments.into(),
amount,
gas,
})
}
pub fn function_call_weight(
self,
function_name: impl Into<String>,
arguments: impl Into<Vec<u8>>,
amount: NearToken,
gas: Gas,
weight: GasWeight,
) -> Self {
self.add_action(PromiseAction::FunctionCallWeight {
function_name: function_name.into(),
arguments: arguments.into(),
amount,
gas,
weight,
})
}
pub fn transfer(self, amount: NearToken) -> Self {
self.add_action(PromiseAction::Transfer { amount })
}
pub fn stake(self, amount: NearToken, public_key: PublicKey) -> Self {
self.add_action(PromiseAction::Stake { amount, public_key })
}
pub fn add_full_access_key(self, public_key: PublicKey) -> Self {
self.add_full_access_key_with_nonce(public_key, 0)
}
pub fn add_full_access_key_with_nonce(self, public_key: PublicKey, nonce: u64) -> Self {
self.add_action(PromiseAction::AddFullAccessKey { public_key, nonce })
}
pub fn add_access_key_allowance(
self,
public_key: PublicKey,
allowance: Allowance,
receiver_id: AccountId,
function_names: impl Into<String>,
) -> Self {
self.add_access_key_allowance_with_nonce(
public_key,
allowance,
receiver_id,
function_names,
0,
)
}
#[deprecated(since = "5.0.0", note = "Use add_access_key_allowance instead")]
pub fn add_access_key(
self,
public_key: PublicKey,
allowance: NearToken,
receiver_id: AccountId,
function_names: impl Into<String>,
) -> Self {
let allowance = migrate_to_allowance(allowance);
self.add_access_key_allowance(public_key, allowance, receiver_id, function_names)
}
pub fn add_access_key_allowance_with_nonce(
self,
public_key: PublicKey,
allowance: Allowance,
receiver_id: AccountId,
function_names: impl Into<String>,
nonce: u64,
) -> Self {
self.add_action(PromiseAction::AddAccessKey {
public_key,
allowance,
receiver_id,
function_names: function_names.into(),
nonce,
})
}
#[deprecated(since = "5.0.0", note = "Use add_access_key_allowance_with_nonce instead")]
pub fn add_access_key_with_nonce(
self,
public_key: PublicKey,
allowance: NearToken,
receiver_id: AccountId,
function_names: impl Into<String>,
nonce: u64,
) -> Self {
let allowance = migrate_to_allowance(allowance);
self.add_access_key_allowance_with_nonce(
public_key,
allowance,
receiver_id,
function_names,
nonce,
)
}
pub fn delete_key(self, public_key: PublicKey) -> Self {
self.add_action(PromiseAction::DeleteKey { public_key })
}
pub fn delete_account(self, beneficiary_id: AccountId) -> Self {
self.add_action(PromiseAction::DeleteAccount { beneficiary_id })
}
pub fn and(mut self, mut other: Promise) -> Promise {
match (&mut self.subtype, &mut other.subtype) {
(PromiseSubtype::Joint(x), PromiseSubtype::Joint(o)) => {
x.promises.append(&mut o.promises);
self
}
(PromiseSubtype::Joint(x), _) => {
x.promises.push_back(other);
self
}
(_, PromiseSubtype::Joint(o)) => {
o.promises.push_front(self);
other
}
_ => Promise {
subtype: PromiseSubtype::Joint(PromiseJoint {
promises: [self, other].into(),
promise_index: None,
}),
should_return: RefCell::new(false),
},
}
}
pub fn then(self, other: Promise) -> Promise {
Promise::then_impl(Rc::new(RefCell::new(self)), other)
}
fn then_impl(this: Rc<RefCell<Self>>, mut other: Promise) -> Promise {
match &mut other.subtype {
PromiseSubtype::Single(x) => match &mut x.subtype {
PromiseSingleSubtype::Regular { after, .. } => {
if after.replace(this).is_some() {
crate::env::panic_str(
"Cannot callback promise which is already scheduled after another",
);
}
}
PromiseSingleSubtype::Yielded(_) => {
crate::env::panic_str("Cannot callback yielded promise.")
}
},
PromiseSubtype::Joint(_) => crate::env::panic_str("Cannot callback joint promise."),
}
other
}
pub fn then_concurrent(
self,
promises: impl IntoIterator<Item = Promise>,
) -> ConcurrentPromises {
let this = Rc::new(RefCell::new(self));
let mapped_promises =
promises.into_iter().map(|other| Promise::then_impl(Rc::clone(&this), other)).collect();
ConcurrentPromises { promises: mapped_promises }
}
#[allow(clippy::wrong_self_convention)]
pub fn as_return(self) -> Self {
*self.should_return.borrow_mut() = true;
self
}
fn construct_recursively(&mut self) -> Option<PromiseIndex> {
let res = match &mut self.subtype {
PromiseSubtype::Single(x) => x.construct_recursively(),
PromiseSubtype::Joint(x) => x.construct_recursively()?,
};
if *self.should_return.borrow() {
crate::env::promise_return(res);
}
Some(res)
}
#[inline]
pub fn detach(self) {}
}
impl Drop for Promise {
fn drop(&mut self) {
self.construct_recursively();
}
}
impl serde::Serialize for Promise {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
*self.should_return.borrow_mut() = true;
serializer.serialize_unit()
}
}
impl borsh::BorshSerialize for Promise {
fn serialize<W: Write>(&self, _writer: &mut W) -> Result<(), Error> {
*self.should_return.borrow_mut() = true;
Ok(())
}
}
#[cfg(feature = "abi")]
impl schemars::JsonSchema for Promise {
fn schema_name() -> String {
"Promise".to_string()
}
fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
schemars::schema::Schema::Bool(true)
}
}
#[near(inside_nearsdk, serializers = [json, borsh])]
#[derive(Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Hash)]
pub struct YieldId(
#[serde_as(as = "::serde_with::base64::Base64")]
#[cfg_attr(feature = "abi", schemars(with = "String"))]
pub(crate) CryptoHash,
);
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub struct ResumeError;
impl std::fmt::Display for ResumeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "failed to resume yielded promise: not found or already resumed")
}
}
impl std::error::Error for ResumeError {}
impl YieldId {
pub fn resume(self, data: impl AsRef<[u8]>) -> Result<(), ResumeError> {
if crate::env::promise_yield_resume(&self.0, data) { Ok(()) } else { Err(ResumeError) }
}
}
#[must_use = "return or detach explicitly via `.detach()`"]
#[derive(serde::Serialize)]
#[serde(untagged)]
pub enum PromiseOrValue<T> {
Promise(Promise),
Value(T),
}
impl<T> PromiseOrValue<T> {
#[inline]
pub fn detach(self) {}
}
#[cfg(feature = "abi")]
impl<T> BorshSchema for PromiseOrValue<T>
where
T: BorshSchema,
{
fn add_definitions_recursively(
definitions: &mut BTreeMap<borsh::schema::Declaration, borsh::schema::Definition>,
) {
T::add_definitions_recursively(definitions);
}
fn declaration() -> borsh::schema::Declaration {
T::declaration()
}
}
impl<T> From<Promise> for PromiseOrValue<T> {
fn from(promise: Promise) -> Self {
PromiseOrValue::Promise(promise)
}
}
impl<T: borsh::BorshSerialize> borsh::BorshSerialize for PromiseOrValue<T> {
fn serialize<W: Write>(&self, writer: &mut W) -> Result<(), Error> {
match self {
PromiseOrValue::Value(x) => x.serialize(writer),
PromiseOrValue::Promise(p) => p.serialize(writer),
}
}
}
#[cfg(feature = "abi")]
impl<T: schemars::JsonSchema> schemars::JsonSchema for PromiseOrValue<T> {
fn schema_name() -> String {
format!("PromiseOrValue{}", T::schema_name())
}
fn json_schema(r#gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
T::json_schema(r#gen)
}
}
#[must_use = "return or detach explicitly via `.detach()`"]
pub struct ConcurrentPromises {
promises: Vec<Promise>,
}
impl ConcurrentPromises {
pub fn join(self) -> Promise {
self.promises
.into_iter()
.reduce(|left, right| left.and(right))
.expect("cannot join empty concurrent promises")
}
pub fn split_off(&mut self, at: usize) -> ConcurrentPromises {
let right_side = self.promises.split_off(at);
ConcurrentPromises { promises: right_side }
}
#[inline]
pub fn detach(self) {}
}
#[cfg(not(target_arch = "wasm32"))]
#[cfg(test)]
mod tests {
use crate::mock::MockAction;
use crate::test_utils::get_created_receipts;
use crate::test_utils::test_env::{alice, bob};
use crate::{
AccountId, Allowance, NearToken, Promise, PublicKey, test_utils::VMContextBuilder,
testing_env,
};
fn pk() -> PublicKey {
"ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp".parse().unwrap()
}
fn get_actions() -> std::vec::IntoIter<MockAction> {
let receipts = get_created_receipts();
let first_receipt = receipts.into_iter().next().unwrap();
first_receipt.actions.into_iter()
}
fn has_add_key_with_full_access(public_key: PublicKey, nonce: Option<u64>) -> bool {
let public_key = near_crypto::PublicKey::try_from(public_key).unwrap();
get_actions().any(|el| {
matches!(
el,
MockAction::AddKeyWithFullAccess { public_key: p, nonce: n, receipt_index: _, }
if p == public_key
&& (nonce.is_none() || Some(n) == nonce)
)
})
}
fn has_add_key_with_function_call(
public_key: PublicKey,
allowance: u128,
receiver_id: AccountId,
function_names: String,
nonce: Option<u64>,
) -> bool {
let public_key = near_crypto::PublicKey::try_from(public_key).unwrap();
get_actions().any(|el| {
matches!(
el,
MockAction::AddKeyWithFunctionCall {
public_key: p,
allowance: a,
receiver_id: r,
method_names,
nonce: n,
receipt_index: _,
}
if p == public_key
&& a.unwrap() == NearToken::from_yoctonear(allowance)
&& r == receiver_id
&& method_names.clone() == function_names.split(',').collect::<Vec<_>>()
&& (nonce.is_none() || Some(n) == nonce)
)
})
}
#[test]
fn test_add_full_access_key() {
testing_env!(VMContextBuilder::new().signer_account_id(alice()).build());
let public_key: PublicKey = pk();
{
Promise::new(alice()).create_account().add_full_access_key(public_key.clone()).detach();
}
assert!(has_add_key_with_full_access(public_key, None));
}
#[test]
fn test_add_full_access_key_with_nonce() {
testing_env!(VMContextBuilder::new().signer_account_id(alice()).build());
let public_key: PublicKey = pk();
let nonce = 42;
{
Promise::new(alice())
.create_account()
.add_full_access_key_with_nonce(public_key.clone(), nonce)
.detach();
}
assert!(has_add_key_with_full_access(public_key, Some(nonce)));
}
#[test]
fn test_add_access_key_allowance() {
testing_env!(VMContextBuilder::new().signer_account_id(alice()).build());
let public_key: PublicKey = pk();
let allowance = 100;
let receiver_id = bob();
let function_names = "method_a,method_b".to_string();
{
Promise::new(alice())
.create_account()
.add_access_key_allowance(
public_key.clone(),
Allowance::Limited(allowance.try_into().unwrap()),
receiver_id.clone(),
function_names.clone(),
)
.detach();
}
assert!(has_add_key_with_function_call(
public_key,
allowance,
receiver_id,
function_names,
None
));
}
#[test]
fn test_add_access_key() {
testing_env!(VMContextBuilder::new().signer_account_id(alice()).build());
let public_key: PublicKey = pk();
let allowance = NearToken::from_yoctonear(100);
let receiver_id = bob();
let function_names = "method_a,method_b".to_string();
{
#[allow(deprecated)]
Promise::new(alice())
.create_account()
.add_access_key(
public_key.clone(),
allowance,
receiver_id.clone(),
function_names.clone(),
)
.detach();
}
assert!(has_add_key_with_function_call(
public_key,
allowance.as_yoctonear(),
receiver_id,
function_names,
None
));
}
#[test]
fn test_add_access_key_allowance_with_nonce() {
testing_env!(VMContextBuilder::new().signer_account_id(alice()).build());
let public_key: PublicKey = pk();
let allowance = 100;
let receiver_id = bob();
let function_names = "method_a,method_b".to_string();
let nonce = 42;
{
Promise::new(alice())
.create_account()
.add_access_key_allowance_with_nonce(
public_key.clone(),
Allowance::Limited(allowance.try_into().unwrap()),
receiver_id.clone(),
function_names.clone(),
nonce,
)
.detach();
}
assert!(has_add_key_with_function_call(
public_key,
allowance,
receiver_id,
function_names,
Some(nonce)
));
}
#[test]
fn test_add_access_key_with_nonce() {
testing_env!(VMContextBuilder::new().signer_account_id(alice()).build());
let public_key: PublicKey = pk();
let allowance = NearToken::from_yoctonear(100);
let receiver_id = bob();
let function_names = "method_a,method_b".to_string();
let nonce = 42;
{
#[allow(deprecated)]
Promise::new(alice())
.create_account()
.add_access_key_with_nonce(
public_key.clone(),
allowance,
receiver_id.clone(),
function_names.clone(),
nonce,
)
.detach();
}
assert!(has_add_key_with_function_call(
public_key,
allowance.as_yoctonear(),
receiver_id,
function_names,
Some(nonce)
));
}
#[test]
fn test_delete_key() {
testing_env!(VMContextBuilder::new().signer_account_id(alice()).build());
let public_key: PublicKey = pk();
{
Promise::new(alice())
.create_account()
.add_full_access_key(public_key.clone())
.delete_key(public_key.clone())
.detach();
}
let public_key = near_crypto::PublicKey::try_from(public_key).unwrap();
let has_action = get_actions().any(|el| {
matches!(
el,
MockAction::DeleteKey { public_key: p , receipt_index: _, } if p == public_key
)
});
assert!(has_action);
}
#[test]
fn test_deploy_global_contract() {
testing_env!(VMContextBuilder::new().signer_account_id(alice()).build());
let code = vec![1, 2, 3, 4];
{
Promise::new(alice()).create_account().deploy_global_contract(code.clone()).detach();
}
let has_action = get_actions().any(|el| {
matches!(
el,
MockAction::DeployGlobalContract { code: c, receipt_index: _, mode: _ } if c == code
)
});
assert!(has_action);
}
#[test]
fn test_deploy_global_contract_by_account_id() {
testing_env!(VMContextBuilder::new().signer_account_id(alice()).build());
let code = vec![5, 6, 7, 8];
{
Promise::new(alice())
.create_account()
.deploy_global_contract_by_account_id(code.clone())
.detach();
}
let has_action = get_actions().any(|el| {
matches!(
el,
MockAction::DeployGlobalContract { code: c, receipt_index: _, mode: _ } if c == code
)
});
assert!(has_action);
}
#[test]
fn test_use_global_contract() {
testing_env!(VMContextBuilder::new().signer_account_id(alice()).build());
let code_hash = [0u8; 32];
{
Promise::new(alice()).create_account().use_global_contract(code_hash).detach();
}
let has_action = get_actions().any(|el| matches!(el, MockAction::UseGlobalContract { .. }));
assert!(has_action);
}
#[test]
fn test_use_global_contract_by_account_id() {
testing_env!(VMContextBuilder::new().signer_account_id(alice()).build());
let deployer = bob();
{
Promise::new(alice())
.create_account()
.use_global_contract_by_account_id(deployer.clone())
.detach();
}
let has_action = get_actions().any(|el| {
matches!(
el,
MockAction::UseGlobalContract {
contract_id: near_primitives::action::GlobalContractIdentifier::AccountId(contract_id),
receipt_index: _
}
if contract_id == deployer
)
});
assert!(has_action);
}
#[test]
fn test_allowance_debug() {
let unlimited = Allowance::Unlimited;
assert_eq!(format!("{:?}", unlimited), "Unlimited");
let limited = Allowance::Limited(100.try_into().unwrap());
assert_eq!(format!("{:?}", limited), "Limited(100)");
}
#[test]
fn test_allowance_eq() {
assert_eq!(Allowance::Unlimited, Allowance::Unlimited);
assert_eq!(
Allowance::Limited(100.try_into().unwrap()),
Allowance::Limited(100.try_into().unwrap())
);
assert_ne!(Allowance::Unlimited, Allowance::Limited(100.try_into().unwrap()));
assert_ne!(
Allowance::Limited(100.try_into().unwrap()),
Allowance::Limited(200.try_into().unwrap())
);
}
#[test]
fn test_allowance_copy() {
let a = Allowance::Unlimited;
let b = a; assert_eq!(a, b);
let c = Allowance::Limited(500.try_into().unwrap());
let d = c; assert_eq!(c, d);
}
#[test]
fn test_allowance_hash() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(Allowance::Unlimited);
set.insert(Allowance::Limited(100.try_into().unwrap()));
set.insert(Allowance::Unlimited); assert_eq!(set.len(), 2);
}
#[test]
fn test_allowance_limited_zero_returns_none() {
assert!(Allowance::limited(NearToken::from_yoctonear(0)).is_none());
}
#[test]
fn test_allowance_limited_nonzero() {
let allowance = Allowance::limited(NearToken::from_yoctonear(100));
assert_eq!(allowance, Some(Allowance::Limited(100.try_into().unwrap())));
}
#[test]
fn test_then() {
testing_env!(VMContextBuilder::new().signer_account_id(alice()).build());
let sub_account_1: AccountId = "sub1.alice.near".parse().unwrap();
{
Promise::new(alice())
.create_account()
.then(Promise::new(sub_account_1).create_account())
.detach();
}
let receipts = get_created_receipts();
let main_account_creation = &receipts[0];
let sub_creation = &receipts[1];
assert!(
main_account_creation.receipt_indices.is_empty(),
"first receipt must not have dependencies"
);
assert_eq!(
&sub_creation.receipt_indices,
&[0],
"then_concurrent() must create dependency on receipt 0"
);
}
#[test]
fn test_then_concurrent() {
testing_env!(VMContextBuilder::new().signer_account_id(alice()).build());
let sub_account_1: AccountId = "sub1.alice.near".parse().unwrap();
let sub_account_2: AccountId = "sub2.alice.near".parse().unwrap();
{
let p1 = Promise::new(sub_account_1.clone()).create_account();
let p2 = Promise::new(sub_account_2.clone()).create_account();
Promise::new(alice()).create_account().then_concurrent(vec![p1, p2]).detach();
}
let receipts = get_created_receipts();
let main_account_creation = &receipts[0];
let sub1_creation = &receipts[1];
let sub2_creation = &receipts[2];
assert_eq!(sub1_creation.receiver_id, sub_account_1);
assert_eq!(sub2_creation.receiver_id, sub_account_2);
assert!(
main_account_creation.receipt_indices.is_empty(),
"first receipt must not have dependencies"
);
assert_eq!(
&sub1_creation.receipt_indices,
&[0],
"then_concurrent() must create dependency on receipt 0"
);
assert_eq!(
&sub2_creation.receipt_indices,
&[0],
"then_concurrent() must create dependency on receipt 0"
);
}
#[test]
fn test_then_concurrent_split_off_then() {
testing_env!(VMContextBuilder::new().signer_account_id(alice()).build());
let sub_account_1: AccountId = "sub1.alice.near".parse().unwrap();
let sub_account_2: AccountId = "sub2.alice.near".parse().unwrap();
let sub_account_3: AccountId = "sub3.sub2.alice.near".parse().unwrap();
{
let p1 = Promise::new(sub_account_1.clone()).create_account();
let p2 = Promise::new(sub_account_2.clone()).create_account();
let p3 = Promise::new(sub_account_3.clone()).create_account();
Promise::new(alice())
.create_account()
.then_concurrent(vec![p1, p2])
.split_off(1)
.join()
.then(p3)
.detach();
}
let receipts = get_created_receipts();
let main_account_creation = &receipts[0];
let sub1_creation = &receipts[3];
let sub2_creation = &receipts[1];
let sub3_creation = &receipts[2];
assert_eq!(sub1_creation.receiver_id, sub_account_1);
assert_eq!(sub2_creation.receiver_id, sub_account_2);
assert_eq!(sub3_creation.receiver_id, sub_account_3);
let sub2_creation_index = sub2_creation.actions[0].receipt_index().unwrap();
assert!(
main_account_creation.receipt_indices.is_empty(),
"first receipt must not have dependencies"
);
assert_eq!(
&sub1_creation.receipt_indices,
&[0],
"then_concurrent() must create dependency on receipt 0"
);
assert_eq!(
&sub2_creation.receipt_indices,
&[0],
"then_concurrent() must create dependency on receipt 0"
);
assert_eq!(
&sub3_creation.receipt_indices,
&[sub2_creation_index],
"then() must create dependency on sub2_creation"
);
}
#[test]
fn test_then_concurrent_twice() {
testing_env!(VMContextBuilder::new().signer_account_id(alice()).build());
let sub_account_1: AccountId = "sub1.alice.near".parse().unwrap();
let sub_account_2: AccountId = "sub2.alice.near".parse().unwrap();
let sub_account_3: AccountId = "sub3.sub2.alice.near".parse().unwrap();
let sub_account_4: AccountId = "sub4.sub2.alice.near".parse().unwrap();
{
let p1 = Promise::new(sub_account_1.clone()).create_account();
let p2 = Promise::new(sub_account_2.clone()).create_account();
let p3 = Promise::new(sub_account_3.clone()).create_account();
let p4 = Promise::new(sub_account_4.clone()).create_account();
Promise::new(alice())
.create_account()
.then_concurrent(vec![p1, p2])
.join()
.then_concurrent(vec![p3, p4])
.detach();
}
let receipts = get_created_receipts();
let main_account_creation = &receipts[0];
let sub1_creation = &receipts[1];
let sub2_creation = &receipts[2];
let sub3_creation = &receipts[3];
let sub4_creation = &receipts[4];
assert_eq!(sub1_creation.receiver_id, sub_account_1);
assert_eq!(sub2_creation.receiver_id, sub_account_2);
assert_eq!(sub3_creation.receiver_id, sub_account_3);
assert_eq!(sub4_creation.receiver_id, sub_account_4);
let sub1_creation_index = sub1_creation.actions[0].receipt_index().unwrap();
let sub2_creation_index = sub2_creation.actions[0].receipt_index().unwrap();
assert!(
main_account_creation.receipt_indices.is_empty(),
"first receipt must not have dependencies"
);
assert_eq!(
&sub1_creation.receipt_indices,
&[0],
"then_concurrent() must create dependency on receipt 0"
);
assert_eq!(
&sub2_creation.receipt_indices,
&[0],
"then_concurrent() must create dependency on receipt 0"
);
assert_eq!(
&sub3_creation.receipt_indices,
&[sub1_creation_index, sub2_creation_index],
"then_concurrent() must create dependency on sub1_creation_index + sub2_creation"
);
assert_eq!(
&sub4_creation.receipt_indices,
&[sub1_creation_index, sub2_creation_index],
"then_concurrent() must create dependency on sub1_creation_index + sub2_creation"
);
}
#[test]
#[should_panic(expected = "Cannot callback yielded promise.")]
fn test_yielded_promise_cannot_be_continuation() {
use crate::{Gas, GasWeight};
testing_env!(VMContextBuilder::new().signer_account_id(alice()).build());
let (yielded, _yield_id) =
Promise::new_yield("callback", vec![], Gas::from_tgas(5), GasWeight(1));
let regular = Promise::new(alice()).create_account();
regular.then(yielded).detach();
}
#[test]
fn test_new_yield_creates_promise_and_yield_id() {
use crate::{Gas, GasWeight};
testing_env!(VMContextBuilder::new().signer_account_id(alice()).build());
let (_promise, yield_id) =
Promise::new_yield("on_callback", b"test_args", Gas::from_tgas(10), GasWeight(1));
let yield_id_bytes: [u8; 32] = yield_id.0;
assert!(yield_id_bytes.iter().any(|&b| b != 0), "YieldId should not be all zeros");
}
#[test]
fn test_yield_id_is_unique() {
use crate::{Gas, GasWeight};
testing_env!(VMContextBuilder::new().signer_account_id(alice()).build());
let (_promise1, yield_id1) =
Promise::new_yield("on_callback1", vec![], Gas::from_tgas(5), GasWeight(1));
let (_promise2, yield_id2) =
Promise::new_yield("on_callback2", vec![], Gas::from_tgas(5), GasWeight(1));
assert_ne!(yield_id1, yield_id2, "Two yielded promises should have different YieldIds");
}
#[test]
#[ignore]
fn test_yielded_promise_can_chain_then() {
use crate::{Gas, GasWeight};
testing_env!(VMContextBuilder::new().signer_account_id(alice()).build());
{
let (yielded, _yield_id) =
Promise::new_yield("on_resume", vec![], Gas::from_tgas(5), GasWeight(1));
yielded.then(Promise::new(bob()).transfer(NearToken::from_yoctonear(1000))).detach();
}
let receipts = get_created_receipts();
let bob_receipt = receipts
.iter()
.find(|r| r.receiver_id == bob())
.expect("Should have created a receipt for bob()");
assert_eq!(bob_receipt.actions.len(), 1, "bob() receipt should have exactly 1 action");
let receipt_index = bob_receipt.actions[0].receipt_index().unwrap();
assert_eq!(
bob_receipt.actions[0],
MockAction::Transfer { receipt_index, deposit: NearToken::from_yoctonear(1000) }
);
}
}