Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
8ddb526
sc-payments update
andrei-marinica Dec 29, 2025
d5f6c22
crowdfunding update to v0.64.1
andrei-marinica Jan 14, 2026
ef66317
crowdfunding tutorial section
andrei-marinica Jan 14, 2026
db3fd67
github action upgrade
andrei-marinica Jan 14, 2026
83f167b
crowdfunding - final code page
andrei-marinica Jan 14, 2026
d932925
crowdfunding test cleanup
andrei-marinica Jan 15, 2026
8d23a5d
crowdfunding - part 3 with general token id
andrei-marinica Jan 15, 2026
2fbaaf0
crowdfunding - p2 vs p3 dedup
andrei-marinica Jan 15, 2026
34c7d73
crowdfunding test cleanup
andrei-marinica Jan 15, 2026
ed90707
crowdfunding - egld vs esdt tests
andrei-marinica Jan 15, 2026
542da2e
sc-payments update examples
andrei-marinica Jan 15, 2026
7d07eff
simplified payment annotation in examples
andrei-marinica Jan 15, 2026
8022ddb
payments clarification
andrei-marinica Jan 15, 2026
5fd86b7
typo
andrei-marinica Jan 15, 2026
f7d20ec
rust-tutorial-ci.sh fix
andrei-marinica Jan 15, 2026
c75a96c
crowdfunding code fix
andrei-marinica Jan 15, 2026
acc5e7f
rust tutorial ci fix
andrei-marinica Jan 15, 2026
23a6610
link fix
andrei-marinica Jan 15, 2026
df514b0
crowdfunding continuity fixes
andrei-marinica Jan 15, 2026
108d29b
relayed v3 note
sstanculeanu Jan 16, 2026
b8d4437
Merge pull request #1179 from multiversx/relayed-v3-note
sstanculeanu Jan 16, 2026
0216057
tutorial overview update
andrei-marinica Jan 16, 2026
1f940dd
Merge pull request #1177 from multiversx/new-payments
andrei-marinica Jan 16, 2026
5300b39
Merge pull request #1178 from multiversx/crowdfunding-update
andrei-marinica Jan 16, 2026
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
9 changes: 4 additions & 5 deletions .github/workflows/rust-tutorial-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,9 @@ jobs:
name: Rust tutorial code test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1
- uses: actions/checkout@v5
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
default: true
toolchain: nightly
toolchain: 1.92
- name: Run rust tests
run: ./scripts/rust-tutorial-ci.sh
run: ./testing/rust-tutorial-ci.sh
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
[workspace]
resolver = "3"

members = [
"testing/extract-tutorial-code",
]

exclude = [
"testing/crowdfunding-esdt",
"testing/crowdfunding",
]
153 changes: 141 additions & 12 deletions docs/developers/developer-reference/sc-payments.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@ title: Smart contract payments
We want to offer an overview on how smart contracts process payments. This includes two complementary parts: receiving tokens and sending them.

:::important important
On MultiversX it is impossible to send both EGLD and any ESDT token at the same time.
On MultiversX it is possible to send one or more tokens with any transaction. This includes EGLD, and it is also possible (though impractical) to send several payments of the same token at once.
:::


For this reason you will see no syntax for transferring both, neither when sending, nor receiving.
:::note note
Historically, it used to be impossible to send EGLD and ESDT at the same time, this is why some of the legacy APIs have this restriction. This restriction no longer applies since the [Spica release](https://multiversx.com/release/release-spica-patch-4-v1-8-12).
:::

---
Expand All @@ -29,7 +32,7 @@ There are two ways in which a smart contract can receive payments:

### Receiving payments directly

Sending EGLD and ESDT tokens directly to accounts works the same way for EOAs (extrernally owned accounts) as for smart contracts: the tokens are transferred from one account to the other without firing up the VM.
Sending EGLD and ESDT tokens directly to accounts works the same way for EOAs (externally owned accounts) as for smart contracts: the tokens are transferred from one account to the other without firing up the VM.

However, not all smart contracts are allowed to receive tokens directly. There is a flag that controls this, called "payable". This flag is part of the [code metadata](/developers/data/code-metadata), and is specified in the transaction that deploys or upgrades the smart contract.

Expand All @@ -39,13 +42,27 @@ The rationale for this is as follows: the MultiversX blockchain doesn't offer an

### Receiving payments via endpoints

The most common way for contracts to accept payments is by having endpoints annotated with the `#[payable(...)]` annotation.
The most common way for contracts to accept payments is by having endpoints annotated with the `#[payable]` annotation (or `#[payable("*")]`).

:::important important
The "payable" flag in the code metadata only refers to direct transfers. Transferring tokens via contract endpoint calls is not affected by it in any way.
:::

If an endpoint only accepts EGLD, it should be annotated with `#[payable("EGLD")]`:
To accept any kind of payment, annotate the endpoints with `#[payable]`:

```rust
#[endpoint]
#[payable]
fn accept_any_payment(&self) {
// ...
}
```

Usually on the first line there will be an instruction that processes, interprets, and validates the received payment ([see below](#call-value-methods))



If an endpoint only accepts EGLD, it can be annotated with `#[payable("EGLD")]`, although this is slowly falling out of favor.

```rust
#[endpoint]
Expand All @@ -55,36 +72,148 @@ fn accept_egld(&self) {
}
```

When annotated like this, the contract will reject any ESDT payment. Calling this function without any payment will work.

To accept any kind of payment, do annotate the endpoints with `#[payable("*")]`:
:::note Multi-transfer note
Note that it is currently possible to send two or more EGLD payments in the same transaction. The `#[payable("EGLD")]` annotation rejects that.
:::

This snippet is equivalent to:

```rust
#[endpoint]
#[payable("*")]
fn accept_any_payment(&self) {
// ...
#[payable]
fn accept_egld(&self) {
let payment_amount = self.call_value().egld();
// ...
}
```



:::note Hard-coded token identifier
It is also possible to hard-code a token identifier in the `payable`, e.g. `#[payable("MYTOKEN-123456")]`. It is rarely, if ever, used, tokens should normally be configured in storage, or at runtime.
:::

[comment]: # (mx-context-auto)

## Payment Types

The framework provides a unified approach to handling payments using the `Payment` type that treats EGLD and ESDT tokens uniformly. EGLD is represented as `EGLD-000000` token identifier, making all payment handling consistent.

**`Payment<A>`** - The primary payment type that combines:
- `token_identifier`: `TokenId<A>` - unified token identifier (EGLD serialized as "EGLD-000000")
- `token_nonce`: `u64` - token nonce for NFTs/SFTs, which is zero for all fungible tokens (incl. EGLD)
- `amount`: `NonZeroBigUint<A>` - guaranteed non-zero amount

**`PaymentVec<A>`** - A managed vector of `Payment<A>` objects, representing multiple payments in a single transaction.

[comment]: # (mx-context-auto)

## Call Value Methods

Additional restrictions on the incoming tokens can be imposed in the body of the endpoint, by calling the call value API. Most of these functions retrieve data about the received payment, while also stopping execution if the payment is not of the expected type.

[comment]: # (mx-context-auto)

### `all()` - Complete Payment Collection

`self.call_value().all()` retrieves all payments sent with the transaction as a `PaymentVec<A>`. It handles all tokens uniformly, including EGLD (represented as "EGLD-000000"). Never stops execution.

```rust
#[payable]
#[endpoint]
pub fn process_all_payments(&self) {
let payments = self.call_value().all();
for payment in payments.iter() {
// Handle each payment uniformly
self.process_payment(&payment.token_identifier, payment.token_nonce, &payment.amount);
}
}
```

[comment]: # (mx-context-auto)

### `single()` - Strict Single Payment

`self.call_value().single()` expects exactly one payment and returns it. Will halt execution if zero or multiple payments are received. Returns a `Payment<A>` object.

```rust
#[payable]
#[endpoint]
pub fn deposit(&self) {
let payment = self.call_value().single();
// Guaranteed to be exactly one payment
let token_id = &payment.token_identifier;
let amount = payment.amount;

self.deposits(&self.blockchain().get_caller()).set(&amount);
}
```

[comment]: # (mx-context-auto)

### `single_optional()` - Flexible Single Payment

`self.call_value().single_optional()` accepts either zero or one payment. Returns `Option<Payment<A>>` for graceful handling. Will halt execution if multiple payments are received.

```rust
#[payable]
#[endpoint]
pub fn execute_with_optional_fee(&self) {
match self.call_value().single_optional() {
Some(payment) => {
// Process the payment as fee
self.execute_premium_service(payment);
},
None => {
// Handle no payment scenario
self.execute_basic_service();
}
}
}
```

[comment]: # (mx-context-auto)

### `array()` - Fixed-Size Payment Array

`self.call_value().array<N>()` expects exactly N payments and returns them as a fixed-size array. Will halt execution if the number of payments doesn't match exactly.

```rust
#[payable]
#[endpoint]
pub fn swap(&self) {
// Expect exactly 2 payments for the swap
let [input_payment, fee_payment] = self.call_value().array();

require!(
input_payment.token_identifier != fee_payment.token_identifier,
"Input and fee must be different tokens"
);

self.execute_swap(input_payment, fee_payment);
}
```

[comment]: # (mx-context-auto)

## Legacy Call Value Methods

The following methods are available for backwards compatibility but may be deprecated in future versions:

- `self.call_value().egld_value()` retrieves the EGLD value transferred, or zero. Never stops execution.
- `self.call_value().all_esdt_transfers()` retrieves all the ESDT transfers received, or an empty list. Never stops execution.
- `self.call_value().multi_esdt<N>()` is ideal when we know exactly how many ESDT transfers we expect. It returns an array of `EsdtTokenPayment`. It knows exactly how many transfers to expect based on the return type (it is polymorphic in the length of the array). Will fail execution if the number of ESDT transfers does not match.
- `self.call_value().single_esdt()` expects a single ESDT transfer, fails otherwise. Will return the received `EsdtTokenPayment`. It is a special case of `multi_esdt`, where `N` is 1.
- `self.call_value().single_fungible_esdt()` further restricts `single_esdt` to only fungible tokens, so those with their nonce zero. Returns the token identifier and amount, as pair.
- `self.call_value().egld_or_single_esdt()` retrieves an object of type `EgldOrEsdtTokenPayment`. Will halt execution in case of ESDT multi-transfer.
- `self.call_value().egld_or_single_fungible_esdt()` further restricts `egld_or_single_esdt` to fungible ESDT tokens. It will return a pair of `EgldOrEsdtTokenIdentifier` and an amount.
- `self.call_value().any_payment()` is the most general payment retriever. Never stops execution. Returns an object of type `EgldOrMultiEsdtPayment`.
- `self.call_value().any_payment()` is the most general payment retriever. Never stops execution. Returns an object of type `EgldOrMultiEsdtPayment`. *(Deprecated since 0.64.1 - use `all()` instead)*

---

[comment]: # (mx-context-auto)

## Sending payments

We have seen how contracts can accommodate receiving tokens. Sending them is, in principle, even more straightforward, as it only involves specializing the `Payment` generic of the transaction using specific methods, or better said, attaching a payload to a regular transaction. Read more about payments [here](../transactions/tx-payment.md).
We have seen how contracts can accommodate receiving tokens. Sending them is, in principle, even more straightforward, as it only involves specializing the `Payment` generic of the transaction using specific methods, essentially attaching a payload to a regular transaction. Read more about payments [here](../transactions/tx-payment.md).
2 changes: 1 addition & 1 deletion docs/developers/developer-reference/sc-random-numbers.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ Example of BAD implementation:
#[endpoint(rollDie)]
fn roll_die(&self) {
// ...
let payment = self.call_value().egld_value();
let payment = self.call_value().egld();
let rand_nr = rand_source.next_u8();
if rand_nr % 6 == 0 {
let prize = payment * 2u32;
Expand Down
2 changes: 1 addition & 1 deletion docs/developers/meta/sc-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,7 @@ pub trait ForwarderQueue {
// ...

#[endpoint]
#[payable("*")]
#[payable]
fn forward_queued_calls(&self) {
while let Some(node) = self.queued_calls().pop_front() {
// ...
Expand Down
6 changes: 4 additions & 2 deletions docs/developers/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,10 @@ Below is a list of tutorials for building on MultiversX:
| [Build your first dApp in 15 minutes](/developers/tutorials/your-first-dapp) | Video + written tutorial on how to create your first dApp. |
| [Cryptozombies Tutorials](https://cryptozombies.io/en/multiversx) | Interactive way of learning how to write MultiversX Smart Contracts. |
| [Build a microservice for your dApp](/developers/tutorials/your-first-microservice) | Video + written tutorial on how to create your microservice. |
| [Building a Crowdfunding Smart Contract](/docs/developers/tutorials/crowdfunding-p1.md) | Write, build and test a simple smart contract. |
| [Enhancing the Crowdfunding Smart Contract](/docs/developers/tutorials/crowdfunding-p2.md) | Expand and refine the functionality of an existing contract.|
| [Crowdfunding Tutorial - Part 1: Setup & Basics](/docs/developers/tutorials/crowdfunding/crowdfunding-p1.md) | Write, build and test your first smart contract. |
| [Crowdfunding Tutorial - Part 2: Core Logic](/docs/developers/tutorials/crowdfunding/crowdfunding-p2.md) | Add endpoints, payments, validation and comprehensive testing.|
| [Crowdfunding Tutorial - Part 3: Extend to Any Token](/docs/developers/tutorials/crowdfunding/crowdfunding-p3.md) | Generalize the contract to accept any fungible token.|
| [Crowdfunding Tutorial - Final Code](/docs/developers/tutorials/crowdfunding/final-code.md) | Complete reference implementation with full source code.|
| [Staking contract Tutorial](/developers/tutorials/staking-contract) | Step by step tutorial on how to create a Staking Smart Contract. |
| [Energy DAO Tutorial](/developers/tutorials/energy-dao) | In depth analysis of the Energy DAO SC template. |
| [DEX Walkthrough](/developers/tutorials/dex-walkthrough) | In depth walkthrough of all the main DEX contracts. |
Expand Down
1 change: 1 addition & 0 deletions docs/developers/relayed-transactions.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ Therefore, in order to build such a transaction, one has to follow the next step
1. For a guarded relayed transaction, the guarded operation fee will also be consumed from the relayer.
2. Relayer must be different from guardian, in case of guarded sender.
3. Guarded relayers are not allowed.
4. Relayer address must be in the same shard as the transaction sender.
:::

### Example
Expand Down
12 changes: 6 additions & 6 deletions docs/developers/testing/rust/whitebox-legacy.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,14 @@ num-traits = "0.2"
hex = "0.4"
```

For this tutorial, we're going to use the crowdfunding SC, so it might be handy to have it open or clone the repository: https://github.com/multiversx/mx-sdk-rs/tree/master/contracts/examples/crowdfunding-esdt
For this tutorial, we're going to use the crowdfunding SC, so it might be handy to have it open or clone the repository: https://github.com/multiversx/mx-sdk-rs/tree/master/contracts/examples/crowdfunding

You need a `tests` and a `scenarios` folder in your contract. Create a `.rs` file in your `tests` folder.

In your newly created test file, add the following code (adapt the `crowdfunding_esdt` namespace, the struct/variable names, and the contract wasm path according to your contract):
In your newly created test file, add the following code (adapt the `crowdfunding` namespace, the struct/variable names, and the contract wasm path according to your contract):

```rust
use crowdfunding_esdt::*;
use crowdfunding::*;
use multiversx_sc::{
sc_error,
types::{Address, SCResult},
Expand All @@ -56,19 +56,19 @@ use multiversx_sc_scenario::{
DebugApi,
};

const WASM_PATH: &'static str = "crowdfunding-esdt/output/crowdfunding-esdt.wasm";
const WASM_PATH: &'static str = "crowdfunding/output/crowdfunding.wasm";

struct CrowdfundingSetup<CrowdfundingObjBuilder>
where
CrowdfundingObjBuilder:
'static + Copy + Fn() -> crowdfunding_esdt::ContractObj<DebugApi>,
'static + Copy + Fn() -> crowdfunding::ContractObj<DebugApi>,
{
pub blockchain_wrapper: BlockchainStateWrapper,
pub owner_address: Address,
pub first_user_address: Address,
pub second_user_address: Address,
pub cf_wrapper:
ContractObjWrapper<crowdfunding_esdt::ContractObj<DebugApi>, CrowdfundingObjBuilder>,
ContractObjWrapper<crowdfunding::ContractObj<DebugApi>, CrowdfundingObjBuilder>,
}
```

Expand Down
2 changes: 1 addition & 1 deletion docs/developers/testing/sc-debugging.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ For this tutorial, you will need:
- the [CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb) extension.
- A [Rust test](rust/sc-blackbox-example)

If you want to follow along, you can clone the [mx-sdk-rs](https://github.com/multiversx/mx-sdk-rs) repository and use the [crowdfunding-esdt](https://github.com/multiversx/mx-sdk-rs/tree/master/contracts/examples/crowdfunding-esdt) example.
If you want to follow along, you can clone the [mx-sdk-rs](https://github.com/multiversx/mx-sdk-rs) repository and use the [crowdfunding](https://github.com/multiversx/mx-sdk-rs/tree/master/contracts/examples/crowdfunding) example.

[comment]: # (mx-context-auto)

Expand Down
4 changes: 2 additions & 2 deletions docs/developers/transactions/tx-legacy-calls.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ mod callee_proxy {

#[multiversx_sc::proxy]
pub trait CalleeContract {
#[payable("*")]
#[payable]
#[endpoint(myPayableEndpoint)]
fn my_payable_endpoint(&self, arg: BigUint) -> BigUint;
}
Expand Down Expand Up @@ -224,7 +224,7 @@ Now that we specified the recipient address, the function and the arguments, it
Let's assume we want to call a `#[payable]` endpoint, with this definition:

```rust
#[payable("*")]
#[payable]
#[endpoint(myPayableEndpoint)]
fn my_payable_endpoint(&self, arg: BigUint) -> BigUint {
let payment = self.call_value().any_payment();
Expand Down
20 changes: 14 additions & 6 deletions docs/developers/transactions/tx-payment.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ References are also allowed. A slightly less common variation is the `ManagedRef
endpoint_name: ManagedBuffer,
args: MultiValueEncoded<ManagedBuffer>,
) {
let payment = self.call_value().egld_value(); // readonly BigUint managed reference
let payment = self.call_value().egld(); // readonly BigUint managed reference
self
.tx() // tx with sc environment
.to(to)
Expand Down Expand Up @@ -218,20 +218,28 @@ Sometimes we don't have ownership of the token identifier object, or amount, and
For brevity, instead of `payment(EsdtTokenPaymentRefs::new(&token_identifier, token_nonce, &amount))`, we can use `.single_esdt(&token_identifier, token_nonce, &amount)`.

```rust title=contract.rs
#[payable("*")]
#[payable]
#[endpoint]
fn send_esdt(&self, to: ManagedAddress) {
let (token_id, payment) = self.call_value().single_fungible_esdt();
let half = payment / BigUint::from(2u64);
let payment = self.call_value().single();
let half_payment = &payment.amount / 2u32;

self.tx()
.to(&to)
.single_esdt(&token_id, 0, &half)
.payment(PaymentRefs::new(
&payment.token_identifier,
0,
&half_payment,
))
.transfer();

self.tx()
.to(&self.blockchain().get_caller())
.single_esdt(&token_id, 0, &half)
.payment(PaymentRefs::new(
&payment.token_identifier,
0,
&half_payment,
))
.transfer();
}
```
Expand Down
Loading