diff --git a/docs/developers/developer-reference/sc-contract-calls.md b/docs/developers/developer-reference/sc-contract-calls.md index 71bff64cb..0f862dd19 100644 --- a/docs/developers/developer-reference/sc-contract-calls.md +++ b/docs/developers/developer-reference/sc-contract-calls.md @@ -1,12 +1,47 @@ --- id: sc-contract-calls -title: Smart contract to smart contract calls +title: Smart contract calls --- [comment]: # (mx-abstract) ## Introduction +As programmers, we are used to calling functions all the time. Smart contract calls, however, are slightly trickier. Beside code execution, there can also be transfers of EGLD and ESDT tokens involved, gas needs to be specificed, and, finally, there are different flavors of calls. + +Any system for composing and sending these calls needs to be very versatile, to cater to all of the programmers' various needs. + +It is not so important _where_ that call comes from. Very often we need to call smart contracts from other smart contracts. However, testing systems and blockchain interactors also greatly benefit from such a system. + +The system we present below works just as well on- and off-chain. Think of it as a general Rust framework for specifying and sending calls in any context. + +:::note +The more primitive way to perform these calls is to just use the API methods directly, to serialize arguments in code and to specify endpoints as strings. But this does not give the compiler a chance to verify the correctness of the calls, and is very limited in how much we configure the call. It also normally leads to a lot of code duplication. For this reason we recommend always using contract call syntax when formatting transactions. +::: + + +--- + +[comment]: # (mx-context-auto) + +## Contract calls: base + +Smart contract calls at the blockchain level have no notion of arity, or data types. That is, the blockchain itself does not validate the number of arguments (or results), and each of these only appear as raw binary data fields. + +It is the contract that keeps track of the number of arguments, and deserializes them. If a transaction has the wrong number of arguments, it is only the contract itself that will be able to complain. If the types are off, it is only during deserialization that the contract will know. + +The description of a smart contract's inputs is known as the [ABI](/developers/data/abi), and lives off-chain. In short, the ABI is a collection of endpoint names, with argument names and type descriptions. To be able to effectively call a smart contract, it is useful to know its ABI. + +The equivalent of the ABI in the Rust world is a a helper trait, called a __proxy__. All it does is that it provides a typed interface to any smart contract, it takes the typed arguments and it serializes them according to the [MultiversX serialization format](/developers/data/serialization-overview). + +Let's take this very simple example: + +```rust +adder_proxy.add(3u32) +``` + +Here, we have a proxy to the adder contract. The `add` method doesn't call the smart contract directly. Instead, it produces a contract call object that contains the data field `add@03`, which is something that the blockchain can make sense of. We will see later how this contract call can end up actually being executed. But until then, let's see how we can get hold of one of these proxies. + This guide provides some examples on how to call a contract from another contract. More examples can be found in [the contract composability feature tests](https://github.com/multiversx/mx-sdk-rs/tree/master/contracts/feature-tests/composability). There are three ways of doing these calls: @@ -14,82 +49,339 @@ There are three ways of doing these calls: - writing the proxy manually - manually serializing the function name and arguments (not recommended) + + [comment]: # (mx-context-auto) -## Method #1: Importing the contract +### Proxies from contracts + +Whenever a smart contract is compiled, a proxy is also generated alongside it. This generated proxy is invisible, it comes from the procedural macros of the framework, specifically `#[multiversx_sc::contract]` and `#[multiversx_sc::module]`. -If you have access to the callee contract's code, importing the auto-generated proxy is easy. Simply import the contract (and any other modules the contract itself may use): +This means that if you have access to the crate of the target contract, you can simply import it, and you get access to the generated proxy automatically. ```toml [dependencies.contract-crate-name] path = "relative-path-to-contract-crate" ``` -If you want to use endpoints contained in an external module (i.e. in a different crate than the main contract) that the callee contract imports, you'll also have to add the module to the dependencies, the same way you added the main contract. +:::info important +Contract and module crates can be imported just like any other Rust crates either by: +- relative path; +- crate version, if it is released; +- git branch, tag or commit. + +Relative paths are the most common for contracts in the same workspace. +::: + +If the contract has modules with functionality that you may want to call, you will also need to import those. + +If the modules are in different crates than the target contract (and if the target contract doesn't somehow re-export them), you'll also have to add the module to the dependencies, the same way you added the target contract. + +These proxies are traits, just like the contracts themselves. The implementation is produced automatically, but nonetheless, this means that in order to call them, the proxy trait must be in scope. This is why you will see such imports everywhere these proxies are called: -Additionally, in your caller code, you have to add the following import: ```rust use module_namespace::ProxyTrait as _; ``` -If you use the rust-analyser VSCode extension, it might complain that it can't find this, but if you actually build the contract, the compiler can find it just fine. +If you use the [rust-analyser VSCode extension](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer), it might complain that it can't find this, but if you actually build the contract, the compiler can find it just fine. -Once you've imported the contract and any external modules it might use, you have to declare your proxy creator function: +Once you've imported the contract and any external modules it might use, you have to declare a proxy creator function in the contract: ```rust #[proxy] -fn contract_proxy(&self, sc_address: ManagedAddress) -> contract_namespace::Proxy; +fn callee_contract_proxy(&self, callee_sc_address: ManagedAddress) -> contract_namespace::Proxy; ``` +This function doesn't do much, it just tries to sort out the proxy trait imports, and neatly initializes the proxy for you. + This function creates an object that contains all the endpoints of the callee contract, and it handles the serialization automatically. Let's say you have the following endpoint in the contract you wish to call: ```rust -#[endpoint(myEndpoint)] -fn my_endpoint(&self, arg: BigUint) -> BigUint { +#[endpoint(caleeEndpoint)] +fn callee_endpoint(&self, arg: BigUint) -> BigUint { // implementation } ``` -To call this endpoint, you would do this in the caller contract: +To call this endpoint, you would write something like: + ```rust -let biguint_result = self.contract_proxy(callee_sc_address) - .my_endpoint(my_biguint_arg) - .execute_on_dest_context(); +self.callee_contract_proxy(callee_sc_address) + .callee_endpoint(my_biguint_arg) + .async_call() + .call_and_exit(); +``` + +We'll talk about `async_call` and `call_and_exit` later on. + +:::caution +Importing a smart contract crate only works if both contracts use the exact framework version. Otherwise, the compiler will (rightfully) complain that the interfaces do not perfectly match. + +In case the target contract is not under our control, it is often wiser to just manually compose the proxy of interest. +::: + + + +[comment]: # (mx-context-auto) + +### Manually specified proxies + +If we don't want to have a dependency to the target contract crate, or there is no access to this crate altogether, it is always possible for us to create such a proxy manually. This might also be desirable if the framework versions of the two contracts are different, or not under our control. + +Below we have an example of such a proxy: + +```rust +mod callee_proxy { + multiversx_sc::imports!(); + + #[multiversx_sc::proxy] + pub trait CalleeContract { + #[payable("*")] + #[endpoint(myPayableEndpoint)] + fn my_payable_endpoint(&self, arg: BigUint) -> BigUint; + } +} +``` + +The syntax is almost the same as for contracts, except the endpoints have no implementation. + +:::caution +Just like with smart contracts and modules, proxy declarations need to reside in their own module. This can either be a separate file, or an explicit declaration, like `mod callee_proxy {}` above. + +This is because there is quite a lot of code generated in the background that would interfere otherwise. +::: + +Manually declared proxies are no different from the auto-generated ones, so calling them looks the same. + +```rust +#[proxy] +fn callee_contract_proxy(&self, sc_address: ManagedAddress) -> callee_proxy::Proxy; +``` + + + +[comment]: # (mx-context-auto) + +### No proxy + +The point of the proxies is to help us build contract calls in a type-safe manner. But this is by no means compulsory. Sometimes we specifically want to build contract calls manually and serialize the arguments ourselves. + +We are looking to create a `ContractCallNoPayment` object. We'll discuss how to add the payments later. + +The `ContractCallNoPayment` has two type arguments: the API and the expected return type. If we are in a contract, the API will always be the same as for the entire contract. To avoid having to explicitly specify it, we can use the following syntax: + +```rust +let mut contract_call = self.send() + .contract_call::(to, endpoint_name); +contract_call.push_raw_argument(arg1_encoded); +contract_call.push_raw_argument(arg2_encoded); +``` + +If we are trying to create the same object outside of a smart contract, we do not have the `self.send()` API available, but we can always use the equivalent syntax: + +```rust +let mut contract_call = ContractCallNoPayment::::new(to, endpoint_name); +contract_call.push_raw_argument(arg1_encoded); +contract_call.push_raw_argument(arg2_encoded); +``` + + + + +[comment]: # (mx-context-auto) + +### Diagram + +Up to here we have created a contract call without payment, so an object of type `ContractCallNoPayment`, in the following ways: + +```mermaid +graph LR + gen-proxy[Generated Proxy] --> ccnp[ContractCallNoPayment] + man-proxy[Manual Proxy] --> ccnp + ccnp-new["ContractCallNoPayment::new(to, function)"] --> ccnp +``` + +--- + +[comment]: # (mx-context-auto) + +## Contract calls: payments + +Now that we specified the recipient address, the function and the arguments, it is time to add more configurations: token transfers and gas. + +Let's assume we want to call a `#[payable]` endpoint, with this definition: + +```rust +#[payable("*")] +#[endpoint(myPayableEndpoint)] +fn my_payable_endpoint(&self, arg: BigUint) -> BigUint { + let payment = self.call_value().any_payment(); + // ... +} +``` + +More on payable endpoints and simple transfers [here](/developers/developer-reference/sc-payments). This section refers to transfers during contract calls only. + + +[comment]: # (mx-context-auto) + +### EGLD transfer + +To add EGLD transfer to a contract call, simply append `.with_egld_transfer` to the builder pattern. + +```rust +self.callee_contract_proxy(callee_sc_address) + .my_payable_endpoint(my_biguint_arg) + .with_egld_transfer(egld_amount) +``` + +Note that this method returns a new type of object, `ContractCallWithEgld`, instead of `ContractCallNoPayment`. Having multiple contract call types has multiple advantages: +- We can restrict at compile time what methods are available in the builder. For instance, it is possible to add ESDT transfers to `ContractCallNoPayment`, but not to `ContractCallWithEgld`. We thus no longer need to enforce at runtime the restriction that EGLD and ESDT cannot coexist. This restriction is also more immediately obvious to developers. +- The contracts end up being smaller, because the compiler knows which kinds of transfers occur in the contract, and which do not. For instance, if a contract only ever transfers EGLD, there is not need for the code that prepares ESDT transfers in the contract. If the check had been done only at runtime, this optimisation would not have been possible. + + +[comment]: # (mx-context-auto) + +### ESDT transfers + +On the MultiversX blockchain, you can transfer multiple ESDT tokens at once. We create a single ESDT transfer type which works for both single- and multi-transfers. It is called `ContractCallWithMultiEsdt`. + +We can obtain such and object by starting with a `ContractCallNoPayment` and calling `with_esdt_transfer` once, or several times. The first such call will yield the `ContractCallWithMultiEsdt`, while subsequent calls simply add more ESDT transfers. + +:::info A note on arguments +There is more than one way to provide the arguments to `with_esdt_transfer`: +- as a tuple of the form `(token_identifier, nonce, amount)`; +- as a `EsdtTokenPayment` object. + +They contain the same data, but sometimes it is more convenient to use one, sometimes the other. +::: + +Example: + +```rust +let esdt_token_payment = self.call_value().single_esdt(); + +self.callee_contract_proxy(callee_sc_address) + .my_payable_endpoint(my_biguint_arg) + .with_esdt_transfer((token_identifier_1, 0, 100_000u32.into())) + .with_esdt_transfer((token_identifier_2, 1, 1u32.into())) + .with_esdt_transfer(esdt_token_payment) +``` + +In this example we passed the arguments both as a tuple and as an `EsdtTokenPayment` object. As we can see, when we already have an `EsdtTokenPayment` variable, it is easier to just pass it as it is. + +It is also possible to pass multiple ESDT transfers in one go, as follows: + +```rust +self.callee_contract_proxy(callee_sc_address) + .my_payable_endpoint(my_biguint_arg) + .with_multi_token_transfer(payments) +``` + +where `payments` is a `ManagedVec` of `EsdtTokenPayment`. + + + +[comment]: # (mx-context-auto) + +### Mixed transfers + +Sometimes we don't know at compile time what kind of transfers we are going to perform. For this reason, we also provide contract call types that work with both EGLD and ESDT tokens. + +First, we have the single transfer `ContractCallWithEgldOrSingleEsdt`, which can only handle a single ESDT transfer or EGLD. It used to be more popular before we introduced wrapped EGLD, but nowadays for most DeFi applications it is more convenient to just work with WEGLD and disallow EGLD transfers. + +```rust +let payment = self.call_value().egld_or_single_esdt(); +self.callee_contract_proxy(callee_sc_address) + .my_payable_endpoint(my_biguint_arg) + .with_egld_or_single_esdt_transfer(payment) +``` + +The most general such object is `ContractCallWithAnyPayment`, which can take any payment possible on the blockchain: either EGLD, or one or more ESDTs. + + +```rust +let payments = self.call_value().any_payment(); +self.callee_contract_proxy(callee_sc_address) + .my_payable_endpoint(my_biguint_arg) + .with_any_payment(payments) +``` + +[comment]: # (mx-context-auto) + +### Diagram + +To recap, these are all the various ways in which we can specify value transfers for a contract call: + +```mermaid +graph LR + ccnp[ContractCallNoPayment] + ccnp -->|".with_egld_transfer"| cc-egld[ContractCallWithEgld] + ccnp -->|".with_esdt_transfer"| cc-multi[ContractCallWithMultiEsdt] + cc-multi -->|".with_esdt_transfer"| cc-multi + ccnp -->|".with_multi_token_transfer"| cc-multi + ccnp -->|".with_any_payment"| cc-any[ContractCallWithAnyPayment] + ccnp -->|".with_egld_or_single_esdt_transfer"| cc-egld-single[ContractCallWithEgldOrSingleEsdt] ``` -This performs a synchronous call to the `callee_sc_address` contract, with the `my_biguint_arg` used as input for `arg: BigUint`. Notice how you also don't have to specify the `myEndpoint` name either. It's handled automatically. +--- + +[comment]: # (mx-context-auto) + +## Contract calls: gas + +Specifying gas is fairly straightforward. All contract call objects have a `with_gas_limit` method, so gas can be specified at any point. -After performing this call, you can execute some more code in the caller contract, using `biguint_result` as you wish. +Not all contract calls require explicit specification of the gas limit, leaving it out is sometimes fine. Notably: +- async calls will halt execution and consume all the remaining gas, so specifying the gas limit is not necessary for them; +- synchronous calls will by default simply use all the available gas as the upper limit, since unspent gas is returned to the caller anyway. -NOTE: Keep in mind that this only works for same-shard contracts. If the contracts are in different shards, you have to use async-calls or transfer-and-execute. +On the other hand, promises and transfer-execute calls do require gas to be explicitly specified. + +```rust +self.callee_contract_proxy(callee_sc_address) + .callee_endpoint(my_biguint_arg) + .with_gas_limit(gas_limit) +} +``` + +--- [comment]: # (mx-context-auto) -## Types of Contract to Contract calls +## Contract calls: execution + +There are several ways in which contract calls are launched from another contract. Currently they are: +- asynchronous calls: + - single asynchronous calls: + - promises (multiple asynchronous calls), + - transfer-execute calls, +- synchronous calls: + - executed on destination context, + - executed on destination context, readonly, + - executed on same context. + +Out of these, the asynchronous calls and the promises need some additional configuration, whereas the other can be launched right away. -There are two main types of contract-to-contract calls available at the moment: -- synchronous, same-shard calls, through execute_on_dest_context (as demonstrated above) -- asynchronous calls [comment]: # (mx-context-auto) ### Asynchronous calls -Asynchronous calls can be launched either through transfer_execute (in the case you don't care about the result) or through async_call when you want to save the result from the callee contract or perform some additional computation. Keep in mind logic in callbacks should be kept at a minimum, as they usually receive very little gas to perform their duty. +To perform an asynchronous call, the contract call needs to be first converted to an `AsyncCall` object, which holds additional configuration. All contract call objects have an `async_call` method that does this. -To launch a transfer and execute call using the above described proxy, you can simply replace `execute_on_dest_context` method with the `transfer_execute` method. Keep in mind that you can't get the returned `BigUint` in this case. +To finalize the call, you will need to call `.call_and_exit()`. At this point the current execution is terminated and the call launched. -If instead you want to launch an async call, you have to use the `async_call` method, and use the `call_and_exit()` method on the returned object. +A minimal asynchronous call could look like this: -Using the above example, your async call would look like this: ```rust #[endpoint] fn caller_endpoint(&self) { // other code here - self.contract_proxy(callee_sc_address) - .my_endpoint(my_biguint_arg) + self.callee_contract_proxy(callee_sc_address) + .callee_endpoint(my_biguint_arg) .async_call() .call_and_exit(); } @@ -99,10 +391,13 @@ fn caller_endpoint(&self) { ### Callbacks -If you want to perform some logic based on the result of the async call, or just some cleanup after the call, you have to declare a callback function. For example, let's say we want to do something based if the result is even, and something else if the result is odd, and do some cleanup in case of error. Our callback function would look something like this: +But before sending the asynchronous call, we most likely will want to also configure a callback for it. This is the only way that our contract will be able to react to the outcome of its execution. + +Let's imagine that `callee_endpoint` returns a `BigUint`, and we want to do something if the result is even, and something else if the result is odd. We also want to do some cleanup in case of error. Our callback function would look something like this: + ```rust #[callback] -fn my_endpoint_callback( +fn callee_endpoint_callback( &self, #[call_result] result: ManagedAsyncCallResult ) { @@ -122,31 +417,61 @@ fn my_endpoint_callback( } ``` -To assign this callback to the aforementioned async call, we hook it like this: +The `#[call_result]` argument interprets the output of the called endpoint and must almost always be of type `ManagedAsyncCallResult`. This type decodes the error status from the VM, more about it [here](/developers/data/multi-values#standard-multi-values). Its type argument must match the return type of the called endpoint. + +To assign this callback to the aforementioned async call, we hook it after `async_call`, but before `call_and_exit`: + ```rust #[endpoint] fn caller_endpoint(&self) { - // other code here + // previous code here - self.contract_proxy(callee_sc_address) - .my_endpoint(my_biguint_arg) + self.callee_contract_proxy(callee_sc_address) + .callee_endpoint(my_biguint_arg) .async_call() - .with_callback(self.callbacks().my_endpoint_callback()) + .with_callback(self.callbacks().callee_endpoint_callback()) .call_and_exit(); } ``` +:::caution +Callbacks should be prevented from failing, at all costs. Failed callbacks cannot be executed again, and can often lead smart contracts into a state of limbo, from where it is difficult to recover. + +For this reason we recommend keeping callback code as simple as possible. +::: + + +Callbacks can also receive payments, both EGLD and ESDT. They are always payable, there is never any need to annotate them with ``#[payable]`. They will receive payments if the called contract sends back tokens to the caller. In this case, they can query the received payments, just like a regular payable endpoint would. + +```rust +#[callback] +fn callee_endpoint_callback(&self, #[call_result] result: ManagedAsyncCallResult) { + let payment = self.call_value().any_payment(); + + // ... +} +``` + +:::note Note on implementation Even though, in theory, smart contract can only have ONE callback function, the Rust framework handles this for you by saving an ID for the callback function in storage when you fire the async call, and it knows how to retrieve the ID and call the correct function once the call returns. +::: + [comment]: # (mx-context-auto) -### Callback Arguments +### The callback closure + +Assume there is some additional context that we want to pass from our contract directly to the callback. We cannot rely on the called contract to do the job for us, we want to be in full control of this context. + +This context forms the contents of our callback closure. -Your callback may have additional arguments that are given to it at the time of launching the async call. These will be automatically saved before performing the initial async call, and they will be retrieved when the callback is called. Example: +More specifically, all callback arguments other than the `#[call_result]` will be passed to it before launching the call, they will be saved by the framework in the contract storage automatically, then given to the callback and deleted. + +Example: ```rust #[callback] -fn my_endpoint_callback( +fn callee_endpoint_callback( &self, original_caller: ManagedAddress, #[call_result] result: ManagedAsyncCallResult @@ -174,68 +499,67 @@ fn caller_endpoint(&self) { // other code here let caller = self.blockchain().get_caller(); - self.contract_proxy(callee_sc_address) - .my_endpoint(my_biguint_arg) + self.callee_contract_proxy(callee_sc_address) + .callee_endpoint(my_biguint_arg) .async_call() - .with_callback(self.callbacks().my_endpoint_callback(caller)) + .with_callback(self.callbacks().callee_endpoint_callback(caller)) .call_and_exit(); } ``` Notice how the callback now has an argument: + ```rust -self.callbacks().my_endpoint_callback(caller) +self.callbacks().callee_endpoint_callback(caller) ``` You can then use `original_caller` in the callback like any other function argument. + + [comment]: # (mx-context-auto) -### Payments in proxy arguments +### Promises -Let's say you want to call a `#[payable]` endpoint, with this definition: -```rust -#[payable("*")] -#[endpoint(myEndpoint)] -fn my_payable_endpoint(&self, arg: BigUint) -> BigUint { - let payment = self.call_value().egld_or_single_esdt(); - // implementation -} +Promises (or multi-async calls) are a new feature that will be introduced in mainnet release 1.6. They are very similar to the old asynchronous calls, with the following differences: +- launching them does not terminate current execution; +- there can be several launched from the same transaction. + +:::caution +Because this feature is currently not available on mainnet, contracts need to enable the "promises" feature flag in `Cargo.toml` to use the functionality: + +```toml +[dependencies.multiversx-sc] +version = "0.43.3" +features = ["promises"] ``` -To pass the payment, you can use the `with_egld_or_single_esdt_token_transfer` method: +This is to protect developers from accidentally creating contracts that depend on unreleased features. +::: + +The syntax is also very similar. The same example from above looks like this with promises: ```rust #[endpoint] -fn caller_endpoint(&self, token: EgldOrEsdtTokenIdentifier, nonce: u64, amount: BigUint) { +fn caller_endpoint(&self) { // other code here + let caller = self.blockchain().get_caller(); - self.contract_proxy(callee_sc_address) - .my_endpoint(token, nonce, amount, my_biguint_arg) - .with_egld_or_single_esdt_token_transfer(token, nonce, amount) - .async_call() - .call_and_exit(); + self.callee_contract_proxy(callee_sc_address) + .callee_endpoint(my_biguint_arg) + .with_gas_limit(gas_limit) + .async_call_promise() + .with_callback(self.callbacks().callee_endpoint_callback(caller)) + .with_extra_gas_for_callback(10_000_000) + .register_promise(); } -``` - -`with_egld_or_single_esdt_token_transfer` allows adding EGLD payment of a single ESDT token as payment. - -There are similar functions for other types of payments: -- `add_esdt_token_transfer` - for single ESDT transfers -- `with_egld_transfer` - for EGLD transfers -- `with_multi_token_transfer` - for ESDT multi-transfers - -[comment]: # (mx-context-auto) - -### Payments in callbacks - -If you expect to receive a payment instead of paying the contract, keep in mind callback functions are `#[payable]` by default, so you don't need to add the annotation: - -```rust -#[callback] -fn my_endpoint_callback(&self, #[call_result] result: ManagedAsyncCallResult) { - let payment = self.call_value().egld_or_single_esdt(); +#[promises_callback] +fn callee_endpoint_callback( + &self, + original_caller: ManagedAddress, + #[call_result] result: ManagedAsyncCallResult +) { match result { ManagedAsyncCallResult::Ok(value) => { if value % 2 == 0 { @@ -252,74 +576,253 @@ fn my_endpoint_callback(&self, #[call_result] result: ManagedAsyncCallResult(); ``` +We always need to specify the type that we want for the result. The framework will type-check that the requested result is compatible with the original one, but will not impose it upon us. For example, an endpoint might return a `u32` result, but we might choose to deserialize it as `u64` or `BigUint`. This is fine, since the types have similar semantics and the same representation. On the other hand, casting it to a `ManagedBuffer` will not be allowed. + +The method `execute_on_dest_context` is by far the more common when performing synchronous calls. The other alternatives are: +- `execute_on_dest_context_readonly` - enforces that the target contract does not change state, at blockchain level; +- `execute_on_same_context` - useful for library-like contracts, all changes are saved in the caller instead of the called contract. + + [comment]: # (mx-context-auto) -### Gas limit for execution +### Diagram + +To sum it all up, if we have a contract call object in a smart contract, these are the things that we can do to it: + +```mermaid +flowchart TB + cc[ContractCall] + cc --->|".async_call()"| async[AsyncCall] + async -->|".call_and_exit()"| exec-async["⚙️ Asynchronous call (legacy)"] + async -->|".with_callback"| async + cc --->|".async_call_promise()"| prom["AsyncCallPromises"] + prom -->|".with_callback"| prom + prom --> |".register_promise()"| exec-prom["⚙️ Asynchronous call (promise)"] + cc ---->|".transfer_execute()"| exec-te["⚙️ Transfer & execute"] + cc ---->|".execute_on_dest_context() + .execute_on_dest_context_readonly() + .execute_on_same_context()"| exec-dest["⚙️ Synchronous call"] + exec-dest --> result[Requested Result] +``` -`with_gas_limit` allows you to specify a gas limit for your call. By default, all gas left is passed, and any remaining is returned either for further execution (in case of sync calls) or for callback execution (for async calls). +--- [comment]: # (mx-context-auto) -### Method #2: Manually writing the proxy +## Contract calls: complete diagram + +To sum it all up, to properly set up a contract call from a contract, one needs to: +1. Get hold of a proxy. +2. Call it to get a basic contract call object. +3. Optionally, add EGLD or ESDT token transfers to it. +4. Optionally, also specify a gas limit for the call. +5. Launch it, either synchronously or asynchronously, in any one of a variety of flavors. + +Merging all these elements into one grand diagram, we get the following: + +```mermaid +flowchart TB + gen-proxy[Generated Proxy] --> cc + man-proxy[Manual Proxy] --> cc + ccnp-new["ContractCallNoPayment::new(to, function)"] --> cc + subgraph cc[ContractCall] + direction LR + ccnp[ContractCallNoPayment] + ccnp -->|".with_egld_transfer"| cc-egld[ContractCallWithEgld] + ccnp -->|".with_esdt_transfer"| cc-multi[ContractCallWithMultiEsdt] + cc-multi -->|".with_esdt_transfer"| cc-multi + ccnp -->|".with_multi_token_transfer"| cc-multi + ccnp -->|".with_any_payment"| cc-any[ContractCallWithAnyPayment] + ccnp -->|".with_egld_or_single_esdt_transfer"| cc-egld-single[ContractCallWithEgldOrSingleEsdt] + end + cc --->|".async_call()"| async[AsyncCall] + async -->|".call_and_exit()"| exec-async["⚙️ Asynchronous call (legacy)"] + async -->|".with_callback"| async + cc --->|".async_call_promise()"| prom["AsyncCallPromises"] + prom -->|".with_callback"| prom + prom --> |".register_promise()"| exec-prom["⚙️ Asynchronous call (promise)"] + cc ---->|".transfer_execute()"| exec-te["⚙️ Transfer & execute"] + cc ---->|".execute_on_dest_context() + .execute_on_dest_context_readonly() + .execute_on_same_context()"| exec-dest["⚙️ Synchronous call"] + exec-dest --> result[Requested Result] +``` + +--- + +[comment]: # (mx-context-auto) + +## Contract deploy: base + +A close relative of the contract call is the contract deploy call. It models the deployment or the upgrade of a smart contract. + +It shares a lot in common with the contract calls, with these notable differences: +- The endpoint name is always `init`. +- No ESDT transfers are allowed in `init`. +- They get executed slightly differently. + +```mermaid +flowchart TB + gen-proxy[Generated Proxy] --> cc + man-proxy[Manual Proxy] --> cc + ccnp-new["ContractDeploy::new"] --> cc + cc[ContractDeploy] + cc ---->|".deploy_contract() + .deploy_from_source()"| exec-deploy["⚙️ Deploy contract"] + exec-deploy --> result[Requested Result] + cc ---->|".deploy_contract() + .deploy_from_source()"| exec-upg["⚙️ Upgrade contract"] +``` + +The object encoding these calls is called `ContractDeploy`. Unlike the contract calls, there is a single such object. + +Creating this object is done in a similar fashion: either via proxies, or manually. Constructors in proxies naturally produce `ContractDeploy` objects: -Sometimes you don't have access to the callee contract code, or it's simply inconvenient to import it (different framework versions, for instance). In this case, you're going to have to manually declare your proxy. Let's use the same example endpoint as in the first method: ```rust mod callee_proxy { multiversx_sc::imports!(); #[multiversx_sc::proxy] pub trait CalleeContract { - #[payable("*")] - #[endpoint(myEndpoint)] - fn my_payable_endpoint(&self, arg: BigUint) -> BigUint; + #[init] + fn init(&self, arg: BigUint) -> BigUint; } } ``` -This is the only thing you'll have to do differently. The rest is the same, you declare a proxy builder, and the calls are all exactly the same as in method #1. ```rust -#[proxy] -fn contract_proxy(&self, sc_address: ManagedAddress) -> callee_proxy::Proxy; +self.callee_contract_proxy() + .init(my_biguint_arg) ``` +--- + [comment]: # (mx-context-auto) -### Method #3: Manual calls (NOT recommended) +## Contract deploy: configuration -If for some reason you don't want to use the contract proxies, you can create a `ContractCall` object by hand: +Just like with regular contract calls, we can specify the gas limit and perform EGLD transfers as part of the deploy as follows: ```rust -let mut contract_call = ContractCall::new( - self.api, - dest_sc_address, - ManagedBuffer::new_from_bytes(endpoint_name), -); +self.callee_contract_proxy(callee_sc_address) + .init(my_biguint_arg) + ``` -Where `dest_sc_address` is the address of the callee contract, and `endpoint_name` would be `b"myEndpoint"`. +```rust +self.callee_contract_proxy() + .init(my_biguint_arg) + .with_egld_transfer(egld_amount) + .with_gas_limit(gas_limit) +``` -From here, you would use `contract_call.push_endpoint_arg(&your_arg)` and the manual payment adder functions described in method #1 miscellaneous category. +--- -You would then use the same `execute_on_dest_context`, `transfer_execute` and `async_call` methods as in the automatically built `ContractCall` objects from before. +[comment]: # (mx-context-auto) + +## Contract deploy: execution -Only use this method if you REALLY have to, as it's very easy to make mistakes if you work with low-level code like this. [comment]: # (mx-context-auto) -## Conclusion +### Deploy + + +There are several ways to launch a contract deploy, different from a regular contract call. + +The simplest deploy operation we can perform is simply calling `deploy_contract`: + + +```rust +let (new_address, result) = self.callee_contract_proxy() + .init(my_biguint_arg) + .deploy_contract::(code, code_metadata); +``` + +:::important +Contract deploys always happen in the same shard as the deployer. They are therefore always [synchronous calls](#synchronous-calls) and we get the result right away. Just like for `execute_on_dest_context` we need to either write a result with an explicit result type, or give the result type as type argument. + +Contract upgrades, on the other hand, can be sent to a different shard, and are therefore in essence asynchronous calls. +::: + +The methods for executing contract deploys are as follows: +- `.deploy_contract(code, code_metadata)` - deploys a new contract with the code given by the contract. +- `.deploy_from_source(source_address, code_metadata)` - deploys a new contract with the same code as the code of the contract at `source_address`. The advantage is that the contract doesn't need to handle the new contract code, which could be quite a large data blob. This saves gas. It requires that we have the code already deployed somewhere else. + + +[comment]: # (mx-context-auto) + +### Upgrade + +To upgrade the contract we also need to specify the recipient address when setting up the `ContractDeploy` object, like so: + +```rust +self.callee_contract_proxy() + .contract(calee_contract_address) + .init(123, 456) + .with_egld_transfer(payment) + .upgrade_contract(code, code_metadata); +``` + +Note the `.contract(...)` method call. + +Just like deploy, upgrade also comes in two flavors: +- `.upgrade_contract(code, code_metadata)` - upgrades the target contract to the new code and sets the new code metadata. +- `.upgrade_from_source(source_address, code_metadata)` - updates the target contract with the same code as the code of the contract at `source_address`. -We hope this covers all the questions about how to call another contract from your contract. Use method #1 whenever possible, and method #2 if needed. Avoid method #3 as much as possible. diff --git a/docs/developers/developer-reference/sc-payments.md b/docs/developers/developer-reference/sc-payments.md new file mode 100644 index 000000000..dadfabd00 --- /dev/null +++ b/docs/developers/developer-reference/sc-payments.md @@ -0,0 +1,143 @@ +--- +id: sc-payments +title: Smart contract payments +--- + +[comment]: # (mx-abstract) + +## Some general points + +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. + +For this reason you will see no syntax for transferring both, neither when sending, nor receiving. +::: + + +--- + +[comment]: # (mx-context-auto) + +## Receiving payments + +There are two ways in which a smart contract can receive payments: +1. Like any regular account, directly, without any contract code being called; +2. Via an endpoint. + + +[comment]: # (mx-context-auto) + +### 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. + +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. + +The rationale for this is as follows: the MultiversX blockchain doesn't offer any mechanism to allow contracts to react to direct calls. This is because we wanted to keep direct calls simple, consistent, and with a predictable gas cost, in all contexts. Most contracts, however, will likely want to keep track of all the funds that are fed into them, so they do not want to accept payments without an opportunity to also change their internal state. + + +[comment]: # (mx-context-auto) + +### Receiving payments via endpoints + +The most common way for contracts to accept payments is by having endpoints annotated with the `#[payable(...)]` annotation. + +:::important important +The "payable" flag in the code metadata only refers to direct transfers. Trasferring 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")]`: + +```rust +#[endpoint] +#[payable("EGLD")] +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("*")]`: + +```rust +#[endpoint] +#[payable("*")] +fn accept_any_payment(&self) { + // ... +} +``` + +:::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. +::: + +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. +- `self.call_value().egld_value()` retrieves the EGLD value transfered, 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. +- `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`. + + + +--- + +[comment]: # (mx-context-auto) + +## Sending payments + + +We have seen how contracts can accomodate receiving tokens. Sending them is, in principle, even more straightforward, as it only involves calling methods from the `self.send()` component. + + +[comment]: # (mx-context-auto) + +### Sending payments directly + +Contracts can send tokens directly, without calling any endpoint at the destination, by using several methods from the API. + +The following methods will perform a transfer-execute call. This is a type of asynchronous call that does not provide a callback, or any other feedback from the receiver. For direct transferring of funds, this call type is ideal. + +- `self.send().direct_egld(to, amount)` sends EGLD directly to an address. If the amount is zero, execution will fail. +- `self.send().direct_non_zero_egld(to, amount)` sends EGLD directly to an address, if the amount is non-zero. Does nothing otherwise. +- `self.send().direct(to, token, nonce, amount)` sends EGLD or a single ESDT token directly to an address. The `token` argument is of type `EgldOrEsdtTokenIdentifier`. If the amount is zero, execution will fail. +- `self.send().direct_non_zero(to, token, nonce, amount)` sends EGLD or a single ESDT token directly to an address, if the amount is non-zero. Does nothing otherwise. +- `self.send().direct_esdt(to, token, nonce, amount)` sends a single ESDT token directly to an address. If the amount is zero, execution will fail. +- `self.send().direct_non_zero_esdt_payment(to, payment)` sends a single ESDT token directly to an address, if the amount is non-zero. Does nothing otherwise. +- `self.send().direct_multi(to, payments)` sends one or more ESDT tokens to destination. The `payments` argument is a list of such payments. If at least one of the amounts is zero, execution will fail. + +It is also possible to transfer tokens via an async call: +- `self.send().transfer_esdt_via_async_call(to, token, nonce, amount)` +- `self.send().transfer_esdt_non_zero_via_async_call(to, token, nonce, amount)` +- `self.send().transfer_multiple_esdt_via_async_call(to, payments)` + + +[comment]: # (mx-context-auto) + +### Sending payments to contract endpoints + +Sending tokens to a contract endpoint is a contract call, and we have a [type-safe and clean way of doing this](/developers/developer-reference/sc-contract-calls). + +Even if you do not know what the endpoint or the arguments will be at compile time, we still recommend creating a `ContractCall` first, and then decorating and sending it, as in [these examples](/developers/developer-reference/sc-contract-calls#no-proxy). + +However, a few direct API methods exist. Just to mention them: +- `self.send().direct_esdt_with_gas_limit(to, token_identifier, nonce, amount, gas, endpoint_name, arguments)` +- `self.send().direct_non_zero_esdt_with_gas_limit(to, token_identifier, nonce, amount, gas, endpoint_name, arguments)` +- `self.send().direct_with_gas_limit(to, token, nonce, amount, gas, endpoint_name, arguments)` +- `self.send().direct_non_zero_with_gas_limit(to, token, nonce, amount, gas, endpoint_name, arguments)` + + +[comment]: # (mx-context-auto) + +### Low-level API + +In the unlikely case that you still could not find an appropriate method of sending tokens using the methods in this page or [the contract calls page](/developers/developer-reference/sc-contract-calls), it is also possible to use the methods in `self.send_raw()` directly. + + diff --git a/sidebars.js b/sidebars.js index 9fbfec0a1..55d7d69e3 100644 --- a/sidebars.js +++ b/sidebars.js @@ -67,6 +67,7 @@ const sidebars = { items: [ "developers/developer-reference/sc-annotations", "developers/developer-reference/sc-modules", + "developers/developer-reference/sc-payments", "developers/developer-reference/sc-contract-calls", "developers/developer-reference/upgrading-smart-contracts", "developers/developer-reference/sc-api-functions", diff --git a/src/css/custom.css b/src/css/custom.css index d6db905fb..a7884fff6 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -290,3 +290,11 @@ h2.anchor code { } /* -------------------- End Markdown ------------------- */ + +/* ------------------- Start Mermaid ------------------- */ + +.docusaurus-mermaid-container { + text-align: center; +} + +/* -------------------- End Mermaid ------------------- */