Skip to content

Commit

Permalink
Add example which uses ext_transfer + ext_terminate (#554)
Browse files Browse the repository at this point in the history
* [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: Hero 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: Hero Bird <robin.freyler@gmail.com>
  • Loading branch information
Michael Müller and Robbepop authored Oct 30, 2020
1 parent aedd756 commit 89620e7
Show file tree
Hide file tree
Showing 15 changed files with 463 additions and 14 deletions.
8 changes: 8 additions & 0 deletions crates/env/src/engine/off_chain/db/accounts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
46 changes: 39 additions & 7 deletions crates/env/src/engine/off_chain/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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>(
Expand All @@ -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>(
Expand All @@ -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>
Expand Down
53 changes: 53 additions & 0 deletions crates/env/src/engine/off_chain/test_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
2 changes: 1 addition & 1 deletion crates/env/src/engine/off_chain/typed_encoded.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion crates/lang/ir/src/ir/attrs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand Down
2 changes: 1 addition & 1 deletion crates/lang/ir/src/ir/item/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand Down
4 changes: 2 additions & 2 deletions crates/lang/ir/src/ir/item_impl/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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(_))
Expand Down
2 changes: 1 addition & 1 deletion crates/lang/ir/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion crates/lang/src/env_access.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
///
Expand Down
9 changes: 9 additions & 0 deletions examples/contract-terminate/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# 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
36 changes: 36 additions & 0 deletions examples/contract-terminate/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
[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 = []
101 changes: 101 additions & 0 deletions examples/contract-terminate/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// 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");
}
}
}
9 changes: 9 additions & 0 deletions examples/contract-transfer/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# 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
Loading

0 comments on commit 89620e7

Please sign in to comment.