Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Implements the API for the `pallet-revive` host functions `chain_id`, `balance_of`, `base_fee`, `origin`, `code_size`, `block_hash`, `block_author` - [#2719](https://github.com/use-ink/ink/pull/2719)
- Implement `From<ink::Address>` for "ink-as-dependency" contract refs - [#2728](https://github.com/use-ink/ink/pull/2728)
- Added apis to e2e crate to interact with pallet assets, together with a contract in integration tests using the assets precompile and node as backend. - [#2709](https://github.com/use-ink/ink/pull/2709)

### Changed
- Rename `ink_sandbox` crate to `ink_runtime`, `Sandbox` trait to `RuntimeEnv`, and `SandboxClient` to `RuntimeClient` for improved clarity. Also simplifies syntax for e2e tests, both runtime and node e2e tests.
Expand Down
108 changes: 62 additions & 46 deletions crates/e2e/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ mod contract_build;
mod contract_results;
mod error;
pub mod events;
mod macros;
mod node_proc;
mod subxt_client;
mod xts;
Expand Down Expand Up @@ -63,6 +64,10 @@ pub use contract_results::{
};
pub use ink_e2e_macro::test;
pub use ink_revive_types::evm::CallTrace;
pub use macros::{
ContractEventReader,
assert_last_event_internal,
};
pub use node_proc::{
TestNodeProcess,
TestNodeProcessBuilder,
Expand Down Expand Up @@ -100,6 +105,7 @@ use ink_primitives::{
H256,
types::AccountIdMapper,
};
use sp_core::crypto::AccountId32;
pub use sp_weights::Weight;
use std::{
cell::RefCell,
Expand Down Expand Up @@ -137,50 +143,6 @@ pub fn log_error(msg: &str) {
tracing::error!("[{}] {}", log_prefix(), msg);
}

/// Get an ink! [`ink_primitives::AccountId`] for a given keyring account.
pub fn account_id(account: Sr25519Keyring) -> ink_primitives::AccountId {
ink_primitives::AccountId::try_from(account.to_account_id().as_ref())
.expect("account keyring has a valid account id")
}

/// Returns the [`ink::Address`] for a given keyring account.
///
/// # Developer Note
///
/// We take the `AccountId` and return only the first twenty bytes, this
/// is what `pallet-revive` does as well.
pub fn address<E: Environment>(account: Sr25519Keyring) -> Address {
AccountIdMapper::to_address(account.to_account_id().as_ref())
}

/// Returns the [`ink::Address`] for a given account id.
///
/// # Developer Note
///
/// We take the `AccountId` and return only the first twenty bytes, this
/// is what `pallet-revive` does as well.
pub fn address_from_account_id<AccountId: AsRef<[u8]>>(account_id: AccountId) -> Address {
AccountIdMapper::to_address(account_id.as_ref())
}

/// Returns the [`ink::Address`] for a given `Keypair`.
///
/// # Developer Note
///
/// We take the `AccountId` and return only the first twenty bytes, this
/// is what `pallet-revive` does as well.
pub fn address_from_keypair<AccountId: From<[u8; 32]> + AsRef<[u8]>>(
keypair: &Keypair,
) -> Address {
let account_id: AccountId = keypair_to_account(keypair);
address_from_account_id(account_id)
}

/// Transforms a `Keypair` into an account id.
pub fn keypair_to_account<AccountId: From<[u8; 32]>>(keypair: &Keypair) -> AccountId {
AccountId::from(keypair.public_key().0)
}

/// Creates a call builder for `Contract`, based on an account id.
pub fn create_call_builder<Contract>(
acc_id: Address,
Expand All @@ -207,9 +169,57 @@ where
<<Contract as ContractCallBuilder>::Type<Abi> as FromAddr>::from_addr(acc_id)
}

/// Extension trait for converting various types to Address (H160).
/// Trait for converting various types into an `AccountId`.
///
/// This enables generic functions to accept multiple account representations
/// (e.g., `Keypair`, `AccountId32`, `ink_primitives::AccountId`) without
/// requiring callers to perform manual conversions.
///
/// Implementations extract the underlying 32-byte public key and convert it
/// to the target `AccountId` type.
pub trait IntoAccountId<TargetAccountId> {
/// Converts this type into the target account ID.
fn into_account_id(self) -> TargetAccountId;
}

impl IntoAccountId<AccountId32> for AccountId32 {
fn into_account_id(self) -> AccountId32 {
self
}
}

impl IntoAccountId<AccountId32> for &AccountId32 {
fn into_account_id(self) -> AccountId32 {
self.clone()
}
}

impl<AccountId> IntoAccountId<AccountId> for &ink_primitives::AccountId
where
AccountId: From<[u8; 32]>,
{
fn into_account_id(self) -> AccountId {
AccountId::from(*AsRef::<[u8; 32]>::as_ref(self))
}
}

impl<AccountId> IntoAccountId<AccountId> for &Keypair
where
AccountId: From<[u8; 32]>,
{
fn into_account_id(self) -> AccountId {
AccountId::from(self.public_key().0)
}
}

/// Trait for converting various types to an EVM-compatible `Address` (H160).
///
/// The conversion uses [`AccountIdMapper::to_address`] which applies different
/// strategies based on the account type:
/// - Ethereum-derived accounts (last 12 bytes are `0xEE`): extracts the first 20 bytes
/// - Sr25519-derived accounts: computes keccak256 hash and takes the last 20 bytes
pub trait IntoAddress {
/// Convert to an Address (H160).
/// Converts this type to an EVM-compatible address.
fn address(&self) -> Address;
}

Expand All @@ -225,3 +235,9 @@ impl IntoAddress for ink_primitives::AccountId {
AccountIdMapper::to_address(&bytes)
}
}

impl IntoAddress for AccountId32 {
fn address(&self) -> Address {
AccountIdMapper::to_address(self.as_ref())
}
}
208 changes: 208 additions & 0 deletions crates/e2e/src/macros.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
// Copyright (C) Use Ink (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.

//! Assertion helpers for ink! E2E tests.
//!
//! These macros provide convenient assertions similar to FRAME's testing macros,
//! adapted for contract call results.

/// Assert that a contract call succeeded without reverting.
///
/// This macro follows FRAME's `assert_ok!` convention for consistency across
/// the Polkadot ecosystem. It verifies that a contract call completed successfully
/// and did not revert.
///
/// # Variants
///
/// - `assert_ok!(result)` - Assert the call didn't revert
/// - `assert_ok!(result, expected)` - Assert the call didn't revert AND the return value
/// equals `expected`
///
/// # Examples
///
/// ```ignore
/// // Just assert success
/// let result = client.call(&alice, &contract_call.transfer(bob, amount))
/// .submit()
/// .await?;
/// assert_ok!(result);
///
/// // Assert success and check return value
/// let result = client.call(&alice, &contract_call.balance_of(bob))
/// .dry_run()
/// .await?;
/// assert_ok!(result, expected_balance);
/// ```
#[macro_export]
macro_rules! assert_ok {
($result:expr $(,)?) => {{
let result = $result;
if result.dry_run.did_revert() {
panic!(
"Expected call to succeed but it reverted.\nError: {:?}",
result.extract_error()
);
}
result
}};
($result:expr, $expected:expr $(,)?) => {{
let result = $result;
if result.dry_run.did_revert() {
panic!(
"Expected call to succeed but it reverted.\nError: {:?}",
result.extract_error()
);
}
assert_eq!(result.return_value(), $expected, "Return value mismatch");
result
}};
}

/// Assert that a contract call reverted with a specific error.
///
/// This macro follows FRAME's `assert_noop!` convention, which stands for
/// "assert no operation" - meaning the call should fail without changing state.
/// Since reverted contract calls don't mutate state, this verifies the call
/// reverted with the expected error message.
///
/// # Variants
///
/// - `assert_noop!(result, expected_error)` - Assert the call reverted with an error
/// containing `expected_error`
///
/// # Examples
///
/// ```ignore
/// let result = client.call(&alice, &contract_call.transfer(bob, huge_amount))
/// .submit()
/// .await?;
/// assert_noop!(result, "BalanceLow");
/// ```
#[macro_export]
macro_rules! assert_noop {
($result:expr, $expected_error:expr $(,)?) => {{
let result = $result;
if !result.dry_run.did_revert() {
panic!(
"Expected call to revert with '{}' but it succeeded.\nReturn value: {:?}",
$expected_error,
result.return_data()
);
}

let actual_error = result.extract_error();
if actual_error != Some($expected_error.to_string()) {
panic!(
"Expected error '{}' but got {:?}",
$expected_error, actual_error
);
}
result
}};
}

/// Assert that the last event from a contract call matches the expected event.
///
/// This macro extracts events from the contract result and compares the last
/// emitted event with the expected event structure by comparing encoded bytes.
///
/// # Examples
///
/// ```ignore
/// let result = client.call(&alice, &contract_call.transfer(bob_address, amount))
/// .submit()
/// .await?;
///
/// assert_last_event!(
/// &result,
/// Transfer {
/// from: contract.addr,
/// to: bob_address,
/// value: amount
/// }
/// );
/// ```
#[macro_export]
macro_rules! assert_last_event {
($result:expr, $expected_event:expr) => {{ $crate::assert_last_event_internal($result, $expected_event) }};
}

use crate::CallResult;
use ink_env::Environment;
use scale::{
Decode,
Encode,
};
use subxt::{
blocks::ExtrinsicEvents,
config::HashFor,
};

/// A trait for types that can expose the last contract-emitted event for assertions.
#[allow(dead_code)]
pub trait ContractEventReader {
fn fetch_last_contract_event(self) -> Result<Vec<u8>, String>;
}

impl<'a, E, V, C, Abi> ContractEventReader
for &'a CallResult<E, V, ExtrinsicEvents<C>, Abi>
where
E: Environment,
C: subxt::Config,
HashFor<C>: Into<sp_core::H256>,
{
fn fetch_last_contract_event(self) -> Result<Vec<u8>, String> {
let events = self
.contract_emitted_events()
.map_err(|err| format!("failed to get contract events: {err:?}"))?;

let last_event = events
.last()
.ok_or_else(|| "no contract events were emitted".to_string())?;

Ok(last_event.event.data.clone())
}
}

/// Shared implementation that decodes the last contract event and compares it against the
/// expected value.
#[allow(dead_code)]
pub fn assert_last_event_internal<R, E>(reader: R, expected_event: E)
where
R: ContractEventReader,
E: Decode + Encode + core::fmt::Debug,
{
let last_event_data = reader
.fetch_last_contract_event()
.unwrap_or_else(|err| panic!("Contract event assertion failed: {err}"));

let expected_bytes = expected_event.encode();

if expected_bytes != last_event_data {
let decoded_event =
E::decode(&mut &last_event_data[..]).unwrap_or_else(|error| {
panic!(
"failed to decode last contract event as {}: bytes={:?}, error={:?}",
core::any::type_name::<E>(),
last_event_data,
error
);
});

panic!(
"event mismatch!\nExpected: {:?}\nActual: {:?}",
expected_event, decoded_event
);
}
}
Loading
Loading