Skip to content
Draft
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 @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add `ink_revive_types` (and remove `pallet-revive` dependency from `ink_e2e`) - [#2657](https://github.com/use-ink/ink/pull/2657)
- non-allocating Solidity ABI encoder - [#2655](https://github.com/use-ink/ink/pull/2655)
- Implement XCM precompile, stabilize XCM API - [#2687](https://github.com/use-ink/ink/pull/2687)
- Add `ink_precompiles` crate with ERC-20 assets precompile interface - [#2686](https://github.com/use-ink/ink/pull/2686)

### Changed
- Marks the `pallet-revive` host function `account_id` stable - [#2578](https://github.com/use-ink/ink/pull/2578)
Expand Down
37 changes: 37 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ members = [
"crates/ink/ir",
"crates/ink/macro",
"crates/metadata",
"crates/precompiles",
"crates/revive-types",
"crates/prelude",
"crates/primitives",
Expand Down Expand Up @@ -115,6 +116,7 @@ ink_env = { version = "=6.0.0-beta", path = "crates/env", default-features = fal
ink_ir = { version = "=6.0.0-beta", path = "crates/ink/ir", default-features = false }
ink_macro = { version = "=6.0.0-beta", path = "crates/ink/macro", default-features = false }
ink_metadata = { version = "=6.0.0-beta", path = "crates/metadata", default-features = false }
ink_precompiles = { version = "=6.0.0-beta", path = "crates/precompiles", default-features = false }
ink_prelude = { version = "=6.0.0-beta", path = "crates/prelude", default-features = false }
ink_primitives = { version = "=6.0.0-beta", path = "crates/primitives", default-features = false }
ink_revive_types = { version = "=6.0.0-beta", path = "crates/revive-types", default-features = false }
Expand Down
177 changes: 177 additions & 0 deletions crates/e2e/src/assertions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
// 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 E2E tests with node backend.
//!
//! These macros provide convenient assertions similar to the sandbox test framework.

/// Assert that a contract call succeeded without reverting.
///
/// This works with `CallResult` types returned from contract calls via the node backend.
///
/// # Examples
///
/// ```ignore
/// let result = client.call(&alice, &contract_call.transfer(bob_address, amount))
/// .submit()
/// .await?;
/// assert_ok!(&result);
/// ```
#[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()
);
}
}};
}

/// Assert that a contract call reverted with a specific error message.
///
/// This works with `CallResult` types returned from contract calls via the node backend.
///
/// # Examples
///
/// ```ignore
/// let result = client.call(&alice, &contract_call.transfer(bob_address, huge_amount))
/// .submit()
/// .await?;
/// assert_noop!(&result, "BalanceLow");
/// ```
#[macro_export]
macro_rules! assert_noop {
($result:expr, $expected_err:expr) => {{
let result = $result;
if !result.dry_run.did_revert() {
panic!(
"Expected call to revert with '{}' but it succeeded",
$expected_err
);
}

let actual_error = result.extract_error();
if let Some(error) = actual_error {
if !error.contains($expected_err) {
panic!(
"Expected error containing '{}' but got: {}",
$expected_err, error
);
}
} else {
panic!(
"Expected error containing '{}' but got no error",
$expected_err
);
}
}};
}

/// 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
);
}
}
25 changes: 25 additions & 0 deletions crates/e2e/src/contract_results.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,31 @@ impl<E: Environment, V, EventLog, Abi> CallResult<E, V, EventLog, Abi> {
pub fn return_data(&self) -> &[u8] {
&self.dry_run.exec_return_value().data
}

/// Returns the error from nested contract calls (e.g., precompile errors)
/// if available in the trace, otherwise returns the raw error data.
pub fn extract_error(&self) -> Option<String> {
if !self.dry_run.did_revert() {
return None;
}

// Check trace for error information
if let Some(trace) = &self.trace {
// // Check nested calls first (more specific errors)
for call in &trace.calls {
if let Some(error) = &call.error {
return Some(error.clone());
}
}

// Then check top-level error
if let Some(error) = &trace.error {
return Some(error.clone());
}
}
// Fallback to raw data
Some(format!("{:?}", self.return_data()))
}
}

// TODO(#xxx) Improve the `Debug` implementation.
Expand Down
37 changes: 37 additions & 0 deletions crates/e2e/src/conversions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use crate::sr25519::Keypair;
use sp_core::crypto::AccountId32;

/// Trait for types that can be converted into an `AccountId`.
pub trait IntoAccountId<TargetAccountId> {
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)
}
}
26 changes: 26 additions & 0 deletions crates/e2e/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,25 @@
html_favicon_url = "https://use.ink/crate-docs/favicon.png"
)]

mod assertions;
mod backend;
mod backend_calls;
mod builders;
mod client_utils;
mod contract_build;
mod contract_results;
mod conversions;
mod error;
pub mod events;
mod node_proc;
mod subxt_client;
mod xts;

pub use crate::contract_build::build_root_and_contract_dependencies;
pub use assertions::{
ContractEventReader,
assert_last_event_internal,
};
pub use backend::{
BuilderClient,
ChainBackend,
Expand Down Expand Up @@ -61,6 +67,7 @@ pub use contract_results::{
InstantiationResult,
UploadResult,
};
pub use conversions::IntoAccountId;
pub use ink_e2e_macro::test;
pub use ink_revive_types::evm::CallTrace;
pub use node_proc::{
Expand Down Expand Up @@ -206,3 +213,22 @@ where
{
<<Contract as ContractCallBuilder>::Type<Abi> as FromAddr>::from_addr(acc_id)
}

/// Extension trait for converting various types to Address (H160).
pub trait IntoAddress {
/// Convert to an Address (H160).
fn address(&self) -> Address;
}

impl IntoAddress for Keypair {
fn address(&self) -> Address {
AccountIdMapper::to_address(&self.public_key().0)
}
}

impl IntoAddress for ink_primitives::AccountId {
fn address(&self) -> Address {
let bytes = *AsRef::<[u8; 32]>::as_ref(self);
AccountIdMapper::to_address(&bytes)
}
}
Loading
Loading