Skip to content

Commit 1f940dd

Browse files
Merge pull request #1177 from multiversx/new-payments
sc-payments update
2 parents b8d4437 + 5fd86b7 commit 1f940dd

7 files changed

Lines changed: 163 additions & 26 deletions

File tree

docs/developers/developer-reference/sc-payments.md

Lines changed: 141 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@ title: Smart contract payments
1010
We want to offer an overview on how smart contracts process payments. This includes two complementary parts: receiving tokens and sending them.
1111

1212
:::important important
13-
On MultiversX it is impossible to send both EGLD and any ESDT token at the same time.
13+
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.
14+
:::
15+
1416

15-
For this reason you will see no syntax for transferring both, neither when sending, nor receiving.
17+
:::note note
18+
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).
1619
:::
1720

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

3033
### Receiving payments directly
3134

32-
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.
35+
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.
3336

3437
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.
3538

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

4043
### Receiving payments via endpoints
4144

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

4447
:::important important
4548
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.
4649
:::
4750

48-
If an endpoint only accepts EGLD, it should be annotated with `#[payable("EGLD")]`:
51+
To accept any kind of payment, annotate the endpoints with `#[payable]`:
52+
53+
```rust
54+
#[endpoint]
55+
#[payable]
56+
fn accept_any_payment(&self) {
57+
// ...
58+
}
59+
```
60+
61+
Usually on the first line there will be an instruction that processes, interprets, and validates the received payment ([see below](#call-value-methods))
62+
63+
64+
65+
If an endpoint only accepts EGLD, it can be annotated with `#[payable("EGLD")]`, although this is slowly falling out of favor.
4966

5067
```rust
5168
#[endpoint]
@@ -55,36 +72,148 @@ fn accept_egld(&self) {
5572
}
5673
```
5774

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

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

6282
```rust
6383
#[endpoint]
64-
#[payable("*")]
65-
fn accept_any_payment(&self) {
66-
// ...
84+
#[payable]
85+
fn accept_egld(&self) {
86+
let payment_amount = self.call_value().egld();
87+
// ...
6788
}
6889
```
6990

91+
92+
7093
:::note Hard-coded token identifier
7194
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.
7295
:::
7396

97+
[comment]: # (mx-context-auto)
98+
99+
## Payment Types
100+
101+
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.
102+
103+
**`Payment<A>`** - The primary payment type that combines:
104+
- `token_identifier`: `TokenId<A>` - unified token identifier (EGLD serialized as "EGLD-000000")
105+
- `token_nonce`: `u64` - token nonce for NFTs/SFTs, which is zero for all fungible tokens (incl. EGLD)
106+
- `amount`: `NonZeroBigUint<A>` - guaranteed non-zero amount
107+
108+
**`PaymentVec<A>`** - A managed vector of `Payment<A>` objects, representing multiple payments in a single transaction.
109+
110+
[comment]: # (mx-context-auto)
111+
112+
## Call Value Methods
113+
74114
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.
115+
116+
[comment]: # (mx-context-auto)
117+
118+
### `all()` - Complete Payment Collection
119+
120+
`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.
121+
122+
```rust
123+
#[payable]
124+
#[endpoint]
125+
pub fn process_all_payments(&self) {
126+
let payments = self.call_value().all();
127+
for payment in payments.iter() {
128+
// Handle each payment uniformly
129+
self.process_payment(&payment.token_identifier, payment.token_nonce, &payment.amount);
130+
}
131+
}
132+
```
133+
134+
[comment]: # (mx-context-auto)
135+
136+
### `single()` - Strict Single Payment
137+
138+
`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.
139+
140+
```rust
141+
#[payable]
142+
#[endpoint]
143+
pub fn deposit(&self) {
144+
let payment = self.call_value().single();
145+
// Guaranteed to be exactly one payment
146+
let token_id = &payment.token_identifier;
147+
let amount = payment.amount;
148+
149+
self.deposits(&self.blockchain().get_caller()).set(&amount);
150+
}
151+
```
152+
153+
[comment]: # (mx-context-auto)
154+
155+
### `single_optional()` - Flexible Single Payment
156+
157+
`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.
158+
159+
```rust
160+
#[payable]
161+
#[endpoint]
162+
pub fn execute_with_optional_fee(&self) {
163+
match self.call_value().single_optional() {
164+
Some(payment) => {
165+
// Process the payment as fee
166+
self.execute_premium_service(payment);
167+
},
168+
None => {
169+
// Handle no payment scenario
170+
self.execute_basic_service();
171+
}
172+
}
173+
}
174+
```
175+
176+
[comment]: # (mx-context-auto)
177+
178+
### `array()` - Fixed-Size Payment Array
179+
180+
`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.
181+
182+
```rust
183+
#[payable]
184+
#[endpoint]
185+
pub fn swap(&self) {
186+
// Expect exactly 2 payments for the swap
187+
let [input_payment, fee_payment] = self.call_value().array();
188+
189+
require!(
190+
input_payment.token_identifier != fee_payment.token_identifier,
191+
"Input and fee must be different tokens"
192+
);
193+
194+
self.execute_swap(input_payment, fee_payment);
195+
}
196+
```
197+
198+
[comment]: # (mx-context-auto)
199+
200+
## Legacy Call Value Methods
201+
202+
The following methods are available for backwards compatibility but may be deprecated in future versions:
203+
75204
- `self.call_value().egld_value()` retrieves the EGLD value transferred, or zero. Never stops execution.
76205
- `self.call_value().all_esdt_transfers()` retrieves all the ESDT transfers received, or an empty list. Never stops execution.
77206
- `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.
78207
- `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.
79208
- `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.
80209
- `self.call_value().egld_or_single_esdt()` retrieves an object of type `EgldOrEsdtTokenPayment`. Will halt execution in case of ESDT multi-transfer.
81210
- `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.
82-
- `self.call_value().any_payment()` is the most general payment retriever. Never stops execution. Returns an object of type `EgldOrMultiEsdtPayment`.
211+
- `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)*
83212

84213
---
85214

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

88217
## Sending payments
89218

90-
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).
219+
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).

docs/developers/developer-reference/sc-random-numbers.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ Example of BAD implementation:
9797
#[endpoint(rollDie)]
9898
fn roll_die(&self) {
9999
// ...
100-
let payment = self.call_value().egld_value();
100+
let payment = self.call_value().egld();
101101
let rand_nr = rand_source.next_u8();
102102
if rand_nr % 6 == 0 {
103103
let prize = payment * 2u32;

docs/developers/meta/sc-config.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,7 @@ pub trait ForwarderQueue {
427427
// ...
428428

429429
#[endpoint]
430-
#[payable("*")]
430+
#[payable]
431431
fn forward_queued_calls(&self) {
432432
while let Some(node) = self.queued_calls().pop_front() {
433433
// ...

docs/developers/transactions/tx-legacy-calls.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ mod callee_proxy {
151151

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

226226
```rust
227-
#[payable("*")]
227+
#[payable]
228228
#[endpoint(myPayableEndpoint)]
229229
fn my_payable_endpoint(&self, arg: BigUint) -> BigUint {
230230
let payment = self.call_value().any_payment();

docs/developers/transactions/tx-payment.md

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ References are also allowed. A slightly less common variation is the `ManagedRef
111111
endpoint_name: ManagedBuffer,
112112
args: MultiValueEncoded<ManagedBuffer>,
113113
) {
114-
let payment = self.call_value().egld_value(); // readonly BigUint managed reference
114+
let payment = self.call_value().egld(); // readonly BigUint managed reference
115115
self
116116
.tx() // tx with sc environment
117117
.to(to)
@@ -218,20 +218,28 @@ Sometimes we don't have ownership of the token identifier object, or amount, and
218218
For brevity, instead of `payment(EsdtTokenPaymentRefs::new(&token_identifier, token_nonce, &amount))`, we can use `.single_esdt(&token_identifier, token_nonce, &amount)`.
219219

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

227227
self.tx()
228228
.to(&to)
229-
.single_esdt(&token_id, 0, &half)
229+
.payment(PaymentRefs::new(
230+
&payment.token_identifier,
231+
0,
232+
&half_payment,
233+
))
230234
.transfer();
231235

232236
self.tx()
233237
.to(&self.blockchain().get_caller())
234-
.single_esdt(&token_id, 0, &half)
238+
.payment(PaymentRefs::new(
239+
&payment.token_identifier,
240+
0,
241+
&half_payment,
242+
))
235243
.transfer();
236244
}
237245
```

docs/sdk-and-tools/sdk-js/sdk-js-cookbook-v14.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3208,11 +3208,11 @@ We are going to assume we have an account at this point. If you don't, feel free
32083208
{
32093209
const secretKeyHex = "413f42575f7f26fad3317a778771212fdb80245850981e48b58a4f25e344e8f9";
32103210
const secretKey = UserSecretKey.fromString(secretKeyHex);
3211-
const publickKey = secretKey.generatePublicKey();
3211+
const publicKey = secretKey.generatePublicKey();
32123212

32133213
const transaction = new Transaction({
32143214
nonce: 90n,
3215-
sender: publickKey.toAddress(),
3215+
sender: publicKey.toAddress(),
32163216
receiver: Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"),
32173217
value: 1000000000000000000n,
32183218
gasLimit: 50000n,

docs/sdk-and-tools/sdk-js/sdk-js-cookbook-v15.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3228,11 +3228,11 @@ We are going to assume we have an account at this point. If you don't, feel free
32283228
{
32293229
const secretKeyHex = "413f42575f7f26fad3317a778771212fdb80245850981e48b58a4f25e344e8f9";
32303230
const secretKey = UserSecretKey.fromString(secretKeyHex);
3231-
const publickKey = secretKey.generatePublicKey();
3231+
const publicKey = secretKey.generatePublicKey();
32323232

32333233
const transaction = new Transaction({
32343234
nonce: 90n,
3235-
sender: publickKey.toAddress(),
3235+
sender: publicKey.toAddress(),
32363236
receiver: Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"),
32373237
value: 1000000000000000000n,
32383238
gasLimit: 50000n,

0 commit comments

Comments
 (0)