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
81 changes: 81 additions & 0 deletions integration-tests/public/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# ink! Smart Contract Examples & Integration Tests

Welcome to the collection of example smart contracts and integration tests for [ink!](https://github.com/use-ink/ink), the relentless smart contract language for Polkadot and Substrate.

This directory contains a curated set of examples demonstrates everything from basic "Hello World" contracts to advanced upgradeability patterns and cross-chain messaging (XCM).

## Directory Structure

The examples are organized into categories to help you navigate from foundational concepts to complex implementations.

### Basics
*Fundamental building blocks for learning ink!.*

- **[`flipper`](basics/flipper)**: The classic "Hello World" of smart contracts. A simple boolean value you can flip.
- **[`incrementer`](basics/incrementer)**: Demonstrates simple state mutation with integers.
- **[`dns`](basics/dns)**: Shows how to use the storage `Mapping` type.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we put this under the folder use-case

- **[`events`](basics/events)**: Examples of defining and emitting events.
- **[`terminator`](basics/terminator)**: How to remove a contract from storage using `terminate`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we put this under the folder operations


### 🪙 Tokens
*Implementations of standard token interfaces.*

- **[`erc20`](tokens/erc20)**: Complete implementation of the ERC-20 fungible token standard.
- **[`erc721`](tokens/erc721)**: Non-Fungible Token (NFT) standard implementation.
- **[`erc1155`](tokens/erc1155)**: Multi-Token standard implementation.

### Traits
*Defining shared behavior using Rust traits.*

- **[`erc20`](traits/erc20)**: ERC-20 implemented using ink! trait definitions.
- **[`flipper`](traits/flipper)**: The flipper contract, but defined via a trait.
- **[`incrementer`](traits/incrementer)**: Trait-based incrementer.
- **[`dyn-cross-contract`](traits/dyn-cross-contract)**: How to make cross-contract calls using dynamic trait dispatch.

### Cross-Contract Interactions
*Calling other contracts from your contract.*

- **[`basic`](cross-contract/basic)**: Simple example of one contract calling another.
- **[`advanced`](cross-contract/advanced)**: Using call builders to set gas limits and storage deposits.
- **[`multi-caller`](cross-contract/multi-caller)**: A contract that can switch between calling different target contracts.
- **[`transfer`](cross-contract/transfer)**: Sending value (tokens) to another contract.
- **[`invocation`](cross-contract/invocation)**: Instantiating new contracts from within a contract.

### Storage
*Advanced storage layouts and data structures.*

- **[`basic`](storage/basic)**: Overview of available storage types.
- **[`complex`](storage/complex)**: Nested structs and Enums in storage.
- **[`lazyvec`](storage/lazyvec)**: Using `Lazy` vector types for gas optimization.
- **[`allocator`](storage/allocator)**: Using a custom heap allocator (e.g., `bumpalo`).

### Runtime & Chain
Copy link
Contributor

@Daanvdplas Daanvdplas Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Precompiles

*Interacting with the Substrate chain and runtime.*

- **[`call-contract`](runtime/call-contract)**: The runtime calling into a contract.
- **[`e2e-call`](runtime/e2e-call)**: A contract calling a runtime dispatchable (pallet function).
- **[`xcm`](runtime/xcm)**: Cross-Consensus Messaging examples.
- **[`precompile`](runtime/precompile)**: interacting with runtime precompiles.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The assets-precompile example should be here as well.


### Advanced
*Complex patterns and niche features.*

- **[`upgradeable`](advanced/upgradeable)**: Contracts that can upgrade their own code (`set_code_hash`).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be in the folder operations

- **[`custom-env`](advanced/custom-env)**: Defining custom chain extensions and environment types.
- **[`fuzzing`](advanced/fuzzing)**: Setup for property-based fuzz testing.
- **[`debugging`](advanced/debugging)**: Techniques for interpreting debug buffers.
Comment on lines +65 to +66
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be in the folder testing


## Running the Tests

Most examples in this directory are standard Rust crates. You can run their tests using Cargo.

```bash
cd basics/flipper
cargo test
```

For End-to-End (E2E) tests that require a running Substrate node (like `substrate-contracts-node`), look for `e2e_tests.rs` files and ensure your environment is set up correctly with the `PINK_Runtime` or a local node.

## Contributing

If you are adding a new example, please place it in the most appropriate category directory. Ensure it includes a `README.md` and basic tests.
File renamed without changes.
File renamed without changes.
30 changes: 30 additions & 0 deletions integration-tests/public/advanced/conditional/tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use super::Flip;
use super::conditional_compilation::ConditionalCompilation;

#[ink::test]
fn default_works() {
let flipper = ConditionalCompilation::new();
assert!(!flipper.get());
}

#[ink::test]
fn it_works() {
let mut flipper = ConditionalCompilation::new();
// Can call using universal call syntax using the trait.
assert!(!<ConditionalCompilation as Flip>::get(&flipper));
<ConditionalCompilation as Flip>::flip(&mut flipper);
// Normal call syntax possible to as long as the trait is in scope.
assert!(flipper.get());
}

#[cfg(feature = "foo")]
#[ink::test]
fn foo_works() {
let mut flipper = ConditionalCompilation::new_foo(false);

flipper.inherent_flip_foo();
assert!(flipper.get());

<ConditionalCompilation as Flip>::push_foo(&mut flipper, false);
assert!(!flipper.get())
}
63 changes: 63 additions & 0 deletions integration-tests/public/advanced/custom-env/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#![cfg_attr(not(feature = "std"), no_std, no_main)]

use ink::env::{
DefaultEnvironment,
Environment,
};

/// Our custom environment diverges from the `DefaultEnvironment` in the event topics
/// limit.
#[derive(Debug, Clone, PartialEq, Eq)]
#[ink::scale_derive(TypeInfo)]
pub enum EnvironmentWithManyTopics {}

impl Environment for EnvironmentWithManyTopics {
const NATIVE_TO_ETH_RATIO: u32 =
<DefaultEnvironment as Environment>::NATIVE_TO_ETH_RATIO;
const TRUST_BACKED_ASSETS_PRECOMPILE_INDEX: u16 =
<DefaultEnvironment as Environment>::TRUST_BACKED_ASSETS_PRECOMPILE_INDEX;
const POOL_ASSETS_PRECOMPILE_INDEX: u16 =
<DefaultEnvironment as Environment>::POOL_ASSETS_PRECOMPILE_INDEX;

type AccountId = <DefaultEnvironment as Environment>::AccountId;
type Balance = <DefaultEnvironment as Environment>::Balance;
type Hash = <DefaultEnvironment as Environment>::Hash;
type BlockNumber = <DefaultEnvironment as Environment>::BlockNumber;
type Timestamp = <DefaultEnvironment as Environment>::Timestamp;
type EventRecord = <DefaultEnvironment as Environment>::EventRecord;
}

#[ink::contract(env = crate::EnvironmentWithManyTopics)]
mod runtime_call {
/// Trivial contract with a single message that emits an event with many topics.
#[ink(storage)]
#[derive(Default)]
pub struct Topics;

/// An event that would be forbidden in the default environment, but is completely
/// valid in our custom one.
#[ink(event)]
#[derive(Default)]
pub struct EventWithTopics {
#[ink(topic)]
first_topic: Balance,
#[ink(topic)]
second_topic: Balance,
}

impl Topics {
#[ink(constructor)]
pub fn new() -> Self {
Default::default()
}

/// Emit an event with many topics.
#[ink(message)]
pub fn trigger(&mut self) {
self.env().emit_event(EventWithTopics::default());
}
}
}

#[cfg(test)]
mod tests;
77 changes: 77 additions & 0 deletions integration-tests/public/advanced/custom-env/tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
use super::runtime_call::{EventWithTopics, Topics, TopicsRef};
use super::EnvironmentWithManyTopics;

#[ink::test]
fn emits_event_with_many_topics() {
let mut contract = Topics::new();
contract.trigger();

let emitted_events = ink::env::test::recorded_events();
assert_eq!(emitted_events.len(), 1);

let emitted_event = <EventWithTopics as ink::scale::Decode>::decode(
&mut &emitted_events[0].data[..],
);

assert!(emitted_event.is_ok());
}

#[cfg(all(test, feature = "e2e-tests"))]
mod e2e_tests {
use super::*;
use ink_e2e::ContractsBackend;

type E2EResult<T> = Result<T, Box<dyn std::error::Error>>;

#[cfg(feature = "permissive-node")]
#[ink_e2e::test(environment = crate::EnvironmentWithManyTopics)]
async fn calling_custom_environment_works(mut client: Client) -> E2EResult<()> {
// given
let mut constructor = TopicsRef::new();
let contract = client
.instantiate("custom-environment", &ink_e2e::alice(), &mut constructor)
.submit()
.await
.expect("instantiate failed");
let mut call_builder = contract.call_builder::<Topics>();

// when
let message = call_builder.trigger();

let call_res = client
.call(&ink_e2e::alice(), &message)
.submit()
.await
.expect("call failed");

// then
assert!(call_res.contains_event("Revive", "ContractEmitted"));

Ok(())
}

#[cfg(not(feature = "permissive-node"))]
#[ink_e2e::test(environment = crate::EnvironmentWithManyTopics)]
async fn calling_custom_environment_fails_if_incompatible_with_node(
mut client: Client,
) -> E2EResult<()> {
// given
let mut constructor = TopicsRef::new();
let contract = client
.instantiate("custom-environment", &ink_e2e::alice(), &mut constructor)
.submit()
.await
.expect("instantiate failed");
let mut call_builder = contract.call_builder::<Topics>();

let message = call_builder.trigger();

// when
let call_res = client.call(&ink_e2e::alice(), &message).dry_run().await;

// then
assert!(call_res.is_err());

Ok(())
}
}
File renamed without changes.
114 changes: 114 additions & 0 deletions integration-tests/public/advanced/debugging/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
//! # Debugging Strategies
//!
//! This contract illustrates a number of strategies for debugging
//! contracts:
//!
//! * Emitting debugging events.
//! * The `pallet-revive` tracing API.
//! * Causing intentional reverts with a return value, in your contract.

#![cfg_attr(not(feature = "std"), no_std, no_main)]

#[ink::contract]
mod debugging_strategies {
use flipper::FlipperRef;
use ink::env::call::{
ExecutionInput,
Selector,
build_call,
build_create,
};
#[cfg(feature = "debug")]
use ink::prelude::{
borrow::ToOwned,
format,
string::String,
};

#[ink::event]
#[cfg(feature = "debug")]
pub struct DebugEvent {
message: String,
}

/// Storage of the contract.
#[ink(storage)]
#[derive(Default)]
pub struct DebuggingStrategies {
value: bool,
}

impl DebuggingStrategies {
#[ink(constructor)]
pub fn new() -> Self {
Self { value: true }
}

#[ink(message)]
pub fn get(&self) -> bool {
#[cfg(feature = "debug")]
self.env().emit_event(DebugEvent {
message: format!("received {:?}", self.env().transferred_value())
.to_owned(),
});
self.value
}

#[ink(message)]
pub fn intentional_revert(&self) {
#[cfg(feature = "debug")]
ink::env::return_value(
ink::env::ReturnFlags::REVERT,
&format!("reverting with info: {:?}", self.env().transferred_value()),
);
}

#[ink(message, payable)]
pub fn instantiate_and_call(&mut self, code_hash: ink::H256) -> bool {
let create_params = build_create::<FlipperRef>()
.code_hash(code_hash)
.endowment(0.into())
.exec_input(ExecutionInput::new(Selector::new(ink::selector_bytes!(
Abi::Ink,
"new"
))))
.returns::<FlipperRef>()
.params();

let other: FlipperRef = self.env()
.instantiate_contract(&create_params)
// todo
// we do this to make sure the instantiated contract is always at a
// different address
// .salt(self.env().addr().to_vec())
.unwrap_or_else(|error| {
panic!(
"Received an error from `pallet-revive` while instantiating: {error:?}"
)
})
.unwrap_or_else(|error| {
panic!("Received a `LangError` while instantiating: {error:?}")
});

let call = build_call()
.call(ink::ToAddr::to_addr(&other))
.transferred_value(0.into())
.exec_input(ExecutionInput::new(Selector::new(ink::selector_bytes!(
Abi::Ink,
"get"
))))
.returns::<bool>()
.params();

self.env()
.invoke_contract(&call)
.unwrap_or_else(|env_err| {
panic!("Received an error from the Environment: {env_err:?}")
})
.unwrap_or_else(|lang_err| panic!("Received a `LangError`: {lang_err:?}"))
}
}
}

#[cfg(test)]
mod tests;
Loading