Unverified Commit 89620e7d authored by Michael Müller's avatar Michael Müller Committed by GitHub
Browse files

Add example which uses ext_transfer + ext_terminate (#554)



* [chores] Fix typo: invokation ➜ invocation

* [chores] Fix typo: timstamp ➜ timestamp

* [chores] Fix typo: ininitialized ➜ initialized

* [env] Implement terminate_contract in off-chain env

* [examples] Add lock-until

* [env] Implement proper off-chain testing for ext_terminate

* Apply suggestions from code review

Co-authored-by: default avatarHero Bird <robin.freyler@gmail.com>

* [env] Fix Environment type

* [env] Derive Balance/AccountId from Environment

* [env] Fix types in macro

* [examples] Remove lock-until

* [examples] Add contract-terminate

* [examples] Add contract-transfer

* [examples] Make clippy happy

* [examples] Fix example name

* [examples] Remove Default impls

* [examples] Move macro to contract-terminate/test_utils

* [env] Migrate macro to fn which gets Environment type param

* [env] Add explanatory dev comment

* [examples] Allow clippy::new_without_default

Co-authored-by: default avatarHero Bird <robin.freyler@gmail.com>
parent aedd7561
Pipeline #112772 failed with stages
in 8 minutes and 44 seconds
......@@ -162,6 +162,14 @@ impl AccountsDb {
},
);
}
/// Removes an account.
pub fn remove_account<T>(&mut self, account_id: T::AccountId)
where
T: Environment,
{
self.accounts.remove(&OffAccountId::new(&account_id));
}
}
/// An account within the chain.
......
......@@ -203,7 +203,7 @@ impl EnvBackend for EnvInstance {
impl EnvInstance {
fn transfer_impl<T>(
&mut self,
destination: T::AccountId,
destination: &T::AccountId,
value: T::Balance,
) -> Result<()>
where
......@@ -220,18 +220,50 @@ impl EnvInstance {
}
let dst_value = self
.accounts
.get_or_create_account::<T>(&destination)
.get_or_create_account::<T>(destination)
.balance::<T>()?;
self.accounts
.get_account_mut::<T>(&src_id)
.expect("account of executed contract must exist")
.set_balance::<T>(src_value - value)?;
self.accounts
.get_account_mut::<T>(&destination)
.get_account_mut::<T>(destination)
.expect("the account must exist already or has just been created")
.set_balance::<T>(dst_value + value)?;
Ok(())
}
// Remove the calling account and transfer remaining balance.
//
// This function never returns. Either the termination was successful and the
// execution of the destroyed contract is halted. Or it failed during the termination
// which is considered fatal.
fn terminate_contract_impl<T>(&mut self, beneficiary: T::AccountId) -> !
where
T: Environment,
{
// Send the remaining balance to the beneficiary
let all: T::Balance = self.balance::<T>().expect("could not decode balance");
self.transfer_impl::<T>(&beneficiary, all)
.expect("transfer did not work ");
// Remove account
let contract_id = self.account_id::<T>().expect("could not decode account id");
self.accounts.remove_account::<T>(contract_id);
// The on-chain implementation would set a tombstone with a code hash here
// and remove the contract storage subsequently. Both is not easily achievable
// with our current off-chain env, hence we left it out here for the moment.
// Encode the result of the termination and panic with it.
// This enables testing for the proper result and makes sure this
// method returns `Never`.
let res = crate::test::ContractTerminationResult::<T> {
beneficiary,
transferred: all,
};
panic!(scale::Encode::encode(&res));
}
}
impl TypedEnvBackend for EnvInstance {
......@@ -349,7 +381,7 @@ impl TypedEnvBackend for EnvInstance {
T: Environment,
Args: scale::Encode,
{
unimplemented!("off-chain environment does not support contract invokation")
unimplemented!("off-chain environment does not support contract invocation")
}
fn eval_contract<T, Args, R>(
......@@ -375,11 +407,11 @@ impl TypedEnvBackend for EnvInstance {
unimplemented!("off-chain environment does not support contract instantiation")
}
fn terminate_contract<T>(&mut self, _beneficiary: T::AccountId) -> !
fn terminate_contract<T>(&mut self, beneficiary: T::AccountId) -> !
where
T: Environment,
{
unimplemented!("off-chain environment does not support contract termination")
self.terminate_contract_impl::<T>(beneficiary)
}
fn restore_contract<T>(
......@@ -398,7 +430,7 @@ impl TypedEnvBackend for EnvInstance {
where
T: Environment,
{
self.transfer_impl::<T>(destination, value)
self.transfer_impl::<T>(&destination, value)
}
fn random<T>(&mut self, subject: &[u8]) -> Result<T::Hash>
......
......@@ -363,3 +363,56 @@ where
Ok(callee)
})
}
/// The result of a successful contract termination.
#[derive(scale::Encode, scale::Decode)]
pub struct ContractTerminationResult<E>
where
E: Environment,
{
/// The beneficiary account who received the remaining value in the contract.
pub beneficiary: <E as Environment>::AccountId,
/// The value which was transferred to the `beneficiary`.
pub transferred: <E as Environment>::Balance,
}
#[cfg(feature = "std")]
use std::panic::UnwindSafe;
/// Tests if a contract terminates successfully after `self.env().terminate()`
/// has been called.
///
/// # Usage
///
/// ```no_compile
/// let should_terminate = move || your_contract.fn_which_should_terminate();
/// ink_env::test::assert_contract_termination::<ink_env::DefaultEnvironment, _>(
/// should_terminate,
/// expected_beneficiary,
/// expected_value_transferred_to_beneficiary
/// );
/// ```
///
/// See `examples/contract-terminate` for a complete usage example.
#[cfg(feature = "std")]
pub fn assert_contract_termination<T, F>(
should_terminate: F,
expected_beneficiary: T::AccountId,
expected_balance: T::Balance,
) where
T: Environment,
F: FnMut() + UnwindSafe,
<T as Environment>::AccountId: core::fmt::Debug,
<T as Environment>::Balance: core::fmt::Debug,
{
let value_any = ::std::panic::catch_unwind(should_terminate)
.expect_err("contract did not terminate");
let encoded_input: &Vec<u8> = value_any
.downcast_ref::<Vec<u8>>()
.expect("panic object can not be cast");
let res: ContractTerminationResult<T> =
scale::Decode::decode(&mut &encoded_input[..]).expect("input can not be decoded");
assert_eq!(res.beneficiary, expected_beneficiary);
assert_eq!(res.transferred, expected_balance);
}
......@@ -123,7 +123,7 @@ impl<M> TypedEncoded<M> {
}
}
/// Creates a new typed-encoded ininitialized by `value` of type `T`.
/// Creates a new typed-encoded initialized by `value` of type `T`.
pub fn new<T>(value: &T) -> Self
where
T: scale::Encode + 'static,
......
......@@ -453,7 +453,7 @@ where
{
let (ink_attrs, other_attrs) = ir::partition_attributes(attrs)?;
let normalized = ir::InkAttribute::from_expanded(ink_attrs).map_err(|err| {
err.into_combine(format_err!(parent_span, "at this invokation",))
err.into_combine(format_err!(parent_span, "at this invocation",))
})?;
normalized.ensure_first(is_valid_first).map_err(|err| {
err.into_combine(format_err!(
......
......@@ -108,7 +108,7 @@ impl TryFrom<syn::ItemStruct> for Event {
}
let normalized =
ir::InkAttribute::from_expanded(ink_attrs).map_err(|err| {
err.into_combine(format_err!(field_span, "at this invokation",))
err.into_combine(format_err!(field_span, "at this invocation",))
})?;
if !matches!(normalized.first().kind(), ir::AttributeArgKind::Topic) {
return Err(format_err!(
......
......@@ -177,7 +177,7 @@ impl ItemImpl {
if !ink_attrs.is_empty() {
let normalized =
ir::InkAttribute::from_expanded(ink_attrs).map_err(|err| {
err.into_combine(format_err!(impl_block_span, "at this invokation",))
err.into_combine(format_err!(impl_block_span, "at this invocation",))
})?;
if normalized
.ensure_first(&ir::AttributeArgKind::Implementation)
......@@ -295,7 +295,7 @@ impl TryFrom<syn::ItemImpl> for ItemImpl {
if !ink_attrs.is_empty() {
let normalized =
ir::InkAttribute::from_expanded(ink_attrs).map_err(|err| {
err.into_combine(format_err!(impl_block_span, "at this invokation",))
err.into_combine(format_err!(impl_block_span, "at this invocation",))
})?;
normalized.ensure_no_conflicts(|arg| {
!matches!(arg.kind(), ir::AttributeArgKind::Implementation | ir::AttributeArgKind::Namespace(_))
......
......@@ -18,7 +18,7 @@
//! parse, analyze and generate code for ink! smart contracts.
//!
//! The entry point for every ink! smart contract is the [`Contract`](`crate::ir::Contract`)
//! with its [`Config`](`crate::ir::Config`) provided in the initial invokation at
//! with its [`Config`](`crate::ir::Config`) provided in the initial invocation at
//! `#[ink::contract(... configuration ...)]`.
//!
//! The ink! IR tries to stay close to the original Rust syntactic structure.
......
......@@ -129,7 +129,7 @@ where
ink_env::gas_left::<T>().expect("couldn't decode gas left")
}
/// Returns the timstamp of the current block.
/// Returns the timestamp of the current block.
///
/// # Note
///
......
# Ignore build artifacts from the local tests sub-crate.
/target/
# Ignore backup files creates by cargo fmt.
**/*.rs.bk
# Remove Cargo.lock when creating an executable, leave it for libraries
# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock
Cargo.lock
\ No newline at end of file
[package]
name = "contract_terminate"
version = "3.0.0-rc1"
authors = ["Parity Technologies <admin@parity.io>"]
edition = "2018"
[dependencies]
ink_primitives = { version = "3.0.0-rc1", path = "../../crates/primitives", default-features = false }
ink_metadata = { version = "3.0.0-rc1", path = "../../crates/metadata", default-features = false, features = ["derive"], optional = true }
ink_env = { version = "3.0.0-rc1", path = "../../crates/env", default-features = false }
ink_storage = { version = "3.0.0-rc1", path = "../../crates/storage", default-features = false }
ink_lang = { version = "3.0.0-rc1", path = "../../crates/lang", default-features = false }
scale = { package = "parity-scale-codec", version = "1.3", default-features = false, features = ["derive"] }
scale-info = { version = "0.4", default-features = false, features = ["derive"], optional = true }
[lib]
name = "contract_terminate"
path = "lib.rs"
crate-type = ["cdylib"]
[features]
default = ["std"]
std = [
"ink_primitives/std",
"ink_metadata",
"ink_metadata/std",
"ink_env/std",
"ink_storage/std",
"ink_lang/std",
"scale/std",
"scale-info",
"scale-info/std",
]
ink-as-dependency = []
// Copyright 2018-2020 Parity Technologies (UK) Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! A smart contract which demonstrates behavior of the `self.env().terminate()`
//! function. It terminates itself once `terminate_me()` is called.
#![cfg_attr(not(feature = "std"), no_std)]
#![allow(clippy::new_without_default)]
use ink_lang as ink;
#[ink::contract]
pub mod just_terminates {
/// No storage is needed for this simple contract.
#[ink(storage)]
pub struct JustTerminate {}
impl JustTerminate {
/// Creates a new instance of this contract.
#[ink(constructor)]
pub fn new() -> Self {
Self {}
}
/// Terminates with the caller as beneficiary.
#[ink(message)]
pub fn terminate_me(&mut self) {
self.env().terminate_contract(self.env().caller());
}
}
#[cfg(test)]
mod tests {
use super::*;
use ink_env::{
call,
test,
};
use ink_lang as ink;
#[ink::test]
fn terminating_works() {
// given
let accounts = default_accounts();
let contract_id = ink_env::test::get_current_contract_account_id::<
ink_env::DefaultEnvironment,
>()
.expect("Cannot get contract id");
set_sender(accounts.alice);
set_balance(contract_id, 100);
let mut contract = JustTerminate::new();
// when
let should_terminate = move || contract.terminate_me();
// then
ink_env::test::assert_contract_termination::<ink_env::DefaultEnvironment, _>(
should_terminate,
accounts.alice,
100,
);
}
fn default_accounts(
) -> ink_env::test::DefaultAccounts<ink_env::DefaultEnvironment> {
ink_env::test::default_accounts::<ink_env::DefaultEnvironment>()
.expect("Off-chain environment should have been initialized already")
}
fn set_sender(sender: AccountId) {
let callee = ink_env::account_id::<ink_env::DefaultEnvironment>()
.unwrap_or([0x0; 32].into());
test::push_execution_context::<Environment>(
sender,
callee,
1000000,
1000000,
test::CallData::new(call::Selector::new([0x00; 4])), // dummy
);
}
fn set_balance(account_id: AccountId, balance: Balance) {
ink_env::test::set_account_balance::<ink_env::DefaultEnvironment>(
account_id, balance,
)
.expect("Cannot set account balance");
}
}
}
# Ignore build artifacts from the local tests sub-crate.
/target/
# Ignore backup files creates by cargo fmt.
**/*.rs.bk
# Remove Cargo.lock when creating an executable, leave it for libraries
# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock
Cargo.lock
\ No newline at end of file
[package]
name = "contract-transfer"
version = "3.0.0-rc1"
authors = ["Parity Technologies <admin@parity.io>"]
edition = "2018"
[dependencies]
ink_primitives = { version = "3.0.0-rc1", path = "../../crates/primitives", default-features = false }
ink_metadata = { version = "3.0.0-rc1", path = "../../crates/metadata", default-features = false, features = ["derive"], optional = true }
ink_env = { version = "3.0.0-rc1", path = "../../crates/env", default-features = false }
ink_storage = { version = "3.0.0-rc1", path = "../../crates/storage", default-features = false }
ink_lang = { version = "3.0.0-rc1", path = "../../crates/lang", default-features = false }
scale = { package = "parity-scale-codec", version = "1.3", default-features = false, features = ["derive"] }
scale-info = { version = "0.4", default-features = false, features = ["derive"], optional = true }
[lib]
name = "contract_transfer"
path = "lib.rs"
crate-type = ["cdylib"]
[features]
default = ["std"]
std = [
"ink_primitives/std",
"ink_metadata",
"ink_metadata/std",
"ink_env/std",
"ink_storage/std",
"ink_lang/std",
"scale/std",
"scale-info",
"scale-info/std",
]
ink-as-dependency = []
// Copyright 2018-2020 Parity Technologies (UK) Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! A smart contract which demonstrates behavior of the `self.env().transfer()` function.
//! It transfers some of it's balance to the caller.
#![cfg_attr(not(feature = "std"), no_std)]
#![allow(clippy::new_without_default)]
use ink_lang as ink;
#[ink::contract]
pub mod give_me {
/// No storage is needed for this simple contract.
#[ink(storage)]
pub struct GiveMe {}
/// The error types.
#[derive(Debug, PartialEq, Eq, scale::Encode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
pub enum Error {
/// Returned if the transfer failed.
TransferFailed,
/// Insufficient funds to execute transfer.
InsufficientFunds,
/// Transfer failed because it would have brought the contract's
/// balance below the subsistence threshold.
/// This is necessary to keep enough funds in the contract to
/// allow for a tombstone to be created.
BelowSubsistenceThreshold,
}
impl GiveMe {
/// Creates a new instance of this contract.
#[ink(constructor)]
pub fn new() -> Self {
Self {}
}
/// Transfers `value` amount of tokens to the caller.
///
/// # Errors
///
/// - Returns `Error::InsufficientFunds` in case the requested transfer of
/// `value` exceeds the contracts balance.
/// - Returns `Error::BelowSubsistenceThreshold` in case the requested transfer
/// of `value` would have brought the contract's balance below the subsistence
/// threshold.
/// - Returns `Error::TransferFailed` in case the transfer failed for another
/// reason.
#[ink(message)]
pub fn give_me(&mut self, value: Balance) -> Result<(), Error> {
if value > self.env().balance() {
return Err(Error::InsufficientFunds)
}
self.env()
.transfer(self.env().caller(), value)
.map_err(|err| {
match err {
ink_env::Error::BelowSubsistenceThreshold => {
Error::BelowSubsistenceThreshold
}
_ => Error::TransferFailed,
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use ink_env::{
call,
test,
};
use ink_lang as ink;
#[ink::test]
fn transfer_works() {
// given
let contract_balance = 100;
let accounts = default_accounts();
let mut give_me = create_contract(contract_balance);
// when
set_sender(accounts.eve);
set_balance(accounts.eve, 0);
assert_eq!(give_me.give_me(80), Ok(()));
// then
assert_eq!(get_balance(accounts.eve), 80);
}
#[ink::test]
fn transfer_fails_insufficient_funds() {
// given
let contract_balance = 100;
let accounts = default_accounts();
let mut give_me = create_contract(contract_balance);
// when
set_sender(accounts.eve);
let ret = give_me.give_me(120);
// then
assert_eq!(ret, Err(Error::InsufficientFunds));
}
/// Creates a new instance of `GiveMe` with `initial_balance`.
///
/// Returns the `contract_instance`.
fn create_contract(initial_balance: Balance) -> GiveMe {
let accounts = default_accounts();
let contract_id = ink_env::test::get_current_contract_account_id::<
ink_env::DefaultEnvironment,
>()
.expect("Cannot get contract id");
set_sender(accounts.alice);
set_balance(contract_id, initial_balance);
GiveMe::new()
}
fn set_sender(sender: AccountId) {
let callee = ink_env::account_id::<ink_env::DefaultEnvironment>()
.unwrap_or([0x0; 32].into());
test::push_execution_context::<Environment>(
sender,
callee,
1000000,
1000000,
test::CallData::new(call::Selector::new([0x00; 4])), // dummy
);
}
fn default_accounts(
) -> ink_env::test::DefaultAccounts<ink_env::DefaultEnvironment> {
ink_env::test::default_accounts::<ink_env::DefaultEnvironment>()
.expect("Off-chain environment should have been initialized already")
}
fn set_balance(account_id: AccountId, balance: Balance) {
ink_env::test::set_account_balance::<ink_env::DefaultEnvironment>(
account_id, balance,
)
.expect("Cannot set account balance");
}
fn get_balance(account_id: AccountId) -> Balance {
ink_env::test::get_account_balance::<ink_env::DefaultEnvironment>(account_id)
.expect("Cannot set account balance")
}
}
}