From 8ddb526237cba5cdab46e249c45e375b73a45c74 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Mon, 29 Dec 2025 15:43:21 +0200 Subject: [PATCH 1/5] sc-payments update --- .../developer-reference/sc-payments.md | 130 ++++++++++++++++-- 1 file changed, 121 insertions(+), 9 deletions(-) diff --git a/docs/developers/developer-reference/sc-payments.md b/docs/developers/developer-reference/sc-payments.md index 23f73ecb4..727c9a08e 100644 --- a/docs/developers/developer-reference/sc-payments.md +++ b/docs/developers/developer-reference/sc-payments.md @@ -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). ::: --- @@ -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. @@ -39,7 +42,7 @@ 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. @@ -55,13 +58,13 @@ fn accept_egld(&self) { } ``` -When annotated like this, the contract will reject any ESDT payment. Calling this function without any payment will work. +When annotated like this, the contract will only accept a single EGLD payment. -To accept any kind of payment, do annotate the endpoints with `#[payable("*")]`: +To accept any kind of payment, annotate the endpoints with `#[payable]`: ```rust #[endpoint] -#[payable("*")] +#[payable] fn accept_any_payment(&self) { // ... } @@ -71,7 +74,116 @@ fn accept_any_payment(&self) { 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`** - The primary payment type that combines: +- `token_identifier`: `TokenId` - 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` - guaranteed non-zero amount + +**`PaymentVec`** - A managed vector of `Payment` 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`. 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() { + let token_id = &payment.token_identifier; + let amount = payment.amount; + let nonce = &payment.token_nonce; + // Handle each payment uniformly + self.process_payment(token_id, nonce, 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` 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>` 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()` 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()` 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. @@ -79,7 +191,7 @@ Additional restrictions on the incoming tokens can be imposed in the body of the - `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.0 - use `all()` instead)* --- @@ -87,4 +199,4 @@ Additional restrictions on the incoming tokens can be imposed in the body of the ## 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). From 542da2e4355bf65677873a95af703d2e56834b77 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Thu, 15 Jan 2026 19:57:39 +0200 Subject: [PATCH 2/5] sc-payments update examples --- .../developer-reference/sc-random-numbers.md | 2 +- docs/developers/transactions/tx-payment.md | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/docs/developers/developer-reference/sc-random-numbers.md b/docs/developers/developer-reference/sc-random-numbers.md index 81d4105b0..11de4f421 100644 --- a/docs/developers/developer-reference/sc-random-numbers.md +++ b/docs/developers/developer-reference/sc-random-numbers.md @@ -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; diff --git a/docs/developers/transactions/tx-payment.md b/docs/developers/transactions/tx-payment.md index 174b36d28..98de014c5 100644 --- a/docs/developers/transactions/tx-payment.md +++ b/docs/developers/transactions/tx-payment.md @@ -111,7 +111,7 @@ References are also allowed. A slightly less common variation is the `ManagedRef endpoint_name: ManagedBuffer, args: MultiValueEncoded, ) { - 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) @@ -221,17 +221,25 @@ For brevity, instead of `payment(EsdtTokenPaymentRefs::new(&token_identifier, to #[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(); } ``` From 7d07eff4bda7f6e0f78a617c64e4b4500f1d969d Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Thu, 15 Jan 2026 20:00:14 +0200 Subject: [PATCH 3/5] simplified payment annotation in examples --- docs/developers/developer-reference/sc-payments.md | 8 ++++---- docs/developers/meta/sc-config.md | 2 +- docs/developers/transactions/tx-legacy-calls.md | 4 ++-- docs/developers/transactions/tx-payment.md | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/developers/developer-reference/sc-payments.md b/docs/developers/developer-reference/sc-payments.md index 727c9a08e..7c1ae9b89 100644 --- a/docs/developers/developer-reference/sc-payments.md +++ b/docs/developers/developer-reference/sc-payments.md @@ -100,7 +100,7 @@ Additional restrictions on the incoming tokens can be imposed in the body of the `self.call_value().all()` retrieves all payments sent with the transaction as a `PaymentVec`. It handles all tokens uniformly, including EGLD (represented as "EGLD-000000"). Never stops execution. ```rust -#[payable("*")] +#[payable] #[endpoint] pub fn process_all_payments(&self) { let payments = self.call_value().all(); @@ -121,7 +121,7 @@ pub fn process_all_payments(&self) { `self.call_value().single()` expects exactly one payment and returns it. Will halt execution if zero or multiple payments are received. Returns a `Payment` object. ```rust -#[payable("*")] +#[payable] #[endpoint] pub fn deposit(&self) { let payment = self.call_value().single(); @@ -140,7 +140,7 @@ pub fn deposit(&self) { `self.call_value().single_optional()` accepts either zero or one payment. Returns `Option>` for graceful handling. Will halt execution if multiple payments are received. ```rust -#[payable("*")] +#[payable] #[endpoint] pub fn execute_with_optional_fee(&self) { match self.call_value().single_optional() { @@ -163,7 +163,7 @@ pub fn execute_with_optional_fee(&self) { `self.call_value().array()` 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("*")] +#[payable] #[endpoint] pub fn swap(&self) { // Expect exactly 2 payments for the swap diff --git a/docs/developers/meta/sc-config.md b/docs/developers/meta/sc-config.md index 6d90a1365..2ddbbfaa7 100644 --- a/docs/developers/meta/sc-config.md +++ b/docs/developers/meta/sc-config.md @@ -427,7 +427,7 @@ pub trait ForwarderQueue { // ... #[endpoint] - #[payable("*")] + #[payable] fn forward_queued_calls(&self) { while let Some(node) = self.queued_calls().pop_front() { // ... diff --git a/docs/developers/transactions/tx-legacy-calls.md b/docs/developers/transactions/tx-legacy-calls.md index fbdd261a1..082c562ec 100644 --- a/docs/developers/transactions/tx-legacy-calls.md +++ b/docs/developers/transactions/tx-legacy-calls.md @@ -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; } @@ -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(); diff --git a/docs/developers/transactions/tx-payment.md b/docs/developers/transactions/tx-payment.md index 98de014c5..3eb0a6e68 100644 --- a/docs/developers/transactions/tx-payment.md +++ b/docs/developers/transactions/tx-payment.md @@ -218,7 +218,7 @@ 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 payment = self.call_value().single(); From 8022ddb1a33cddbe777fcc50b6248d4c79a210c4 Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Thu, 15 Jan 2026 20:23:46 +0200 Subject: [PATCH 4/5] payments clarification --- .../developer-reference/sc-payments.md | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/docs/developers/developer-reference/sc-payments.md b/docs/developers/developer-reference/sc-payments.md index 7c1ae9b89..3faf84507 100644 --- a/docs/developers/developer-reference/sc-payments.md +++ b/docs/developers/developer-reference/sc-payments.md @@ -48,7 +48,21 @@ The most common way for contracts to accept payments is by having endpoints anno 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] @@ -58,18 +72,24 @@ fn accept_egld(&self) { } ``` -When annotated like this, the contract will only accept a single EGLD payment. -To accept any kind of payment, 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) { - // ... +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. ::: @@ -105,11 +125,8 @@ Additional restrictions on the incoming tokens can be imposed in the body of the pub fn process_all_payments(&self) { let payments = self.call_value().all(); for payment in payments.iter() { - let token_id = &payment.token_identifier; - let amount = payment.amount; - let nonce = &payment.token_nonce; // Handle each payment uniformly - self.process_payment(token_id, nonce, amount); + self.process_payment(&payment.token_identifier, payment.token_nonce, &payment.amount); } } ``` From 5fd86b70a74859be018087c5b5eb29fb0d737b3c Mon Sep 17 00:00:00 2001 From: Andrei Marinica Date: Thu, 15 Jan 2026 20:27:44 +0200 Subject: [PATCH 5/5] typo --- docs/sdk-and-tools/sdk-js/sdk-js-cookbook-v14.md | 4 ++-- docs/sdk-and-tools/sdk-js/sdk-js-cookbook-v15.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/sdk-and-tools/sdk-js/sdk-js-cookbook-v14.md b/docs/sdk-and-tools/sdk-js/sdk-js-cookbook-v14.md index 3d2b8ee5a..8571ce1d8 100644 --- a/docs/sdk-and-tools/sdk-js/sdk-js-cookbook-v14.md +++ b/docs/sdk-and-tools/sdk-js/sdk-js-cookbook-v14.md @@ -3208,11 +3208,11 @@ We are going to assume we have an account at this point. If you don't, feel free { const secretKeyHex = "413f42575f7f26fad3317a778771212fdb80245850981e48b58a4f25e344e8f9"; const secretKey = UserSecretKey.fromString(secretKeyHex); - const publickKey = secretKey.generatePublicKey(); + const publicKey = secretKey.generatePublicKey(); const transaction = new Transaction({ nonce: 90n, - sender: publickKey.toAddress(), + sender: publicKey.toAddress(), receiver: Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"), value: 1000000000000000000n, gasLimit: 50000n, diff --git a/docs/sdk-and-tools/sdk-js/sdk-js-cookbook-v15.md b/docs/sdk-and-tools/sdk-js/sdk-js-cookbook-v15.md index c0ed54964..a84683128 100644 --- a/docs/sdk-and-tools/sdk-js/sdk-js-cookbook-v15.md +++ b/docs/sdk-and-tools/sdk-js/sdk-js-cookbook-v15.md @@ -3228,11 +3228,11 @@ We are going to assume we have an account at this point. If you don't, feel free { const secretKeyHex = "413f42575f7f26fad3317a778771212fdb80245850981e48b58a4f25e344e8f9"; const secretKey = UserSecretKey.fromString(secretKeyHex); - const publickKey = secretKey.generatePublicKey(); + const publicKey = secretKey.generatePublicKey(); const transaction = new Transaction({ nonce: 90n, - sender: publickKey.toAddress(), + sender: publicKey.toAddress(), receiver: Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"), value: 1000000000000000000n, gasLimit: 50000n,