From b43a2b91c7a1549f708d2687c75c49786a6ba911 Mon Sep 17 00:00:00 2001 From: Elliot Voris Date: Thu, 21 Sep 2023 16:08:01 -0500 Subject: [PATCH] docs: provide example Stellar transactions to invoke a contract (#578) * docs: provide example Stellar transactions to invoke a contract This commit provides JavaScript and Python examples to build a transaction that invokes the `increment` function of a deployed example contract. Refs: #159 * format JS code example * editorial --------- Co-authored-by: Bri Wylde <92327786+briwylde08@users.noreply.github.com> --- .../invoking-contracts-with-transactions.mdx | 260 +++++++++++++++++- 1 file changed, 248 insertions(+), 12 deletions(-) diff --git a/docs/fundamentals-and-concepts/invoking-contracts-with-transactions.mdx b/docs/fundamentals-and-concepts/invoking-contracts-with-transactions.mdx index 830e278e..6a90b947 100644 --- a/docs/fundamentals-and-concepts/invoking-contracts-with-transactions.mdx +++ b/docs/fundamentals-and-concepts/invoking-contracts-with-transactions.mdx @@ -2,6 +2,7 @@ sidebar_position: 16 title: Interacting with Soroban via Stellar description: Invoke and deploy smart contracts with the InvokeHostFunctionOp operation. +toc_max_heading_level: 4 --- @@ -23,7 +24,242 @@ description: Invoke and deploy smart contracts with the InvokeHostFunctionOp ope /> -Stellar supports invoking and deploying contracts with a new Operation named `InvokeHostFunctionOp`. The [`soroban-cli`] abstracts these details away from the user, but SDKs do not yet and if you're building a dapp you'll probably find yourself building the XDR transaction to submit to the network. +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +## Example SDK Usage + +Some (but not all yet) of the Stellar SDKs have functions built-in to handle most of the process of building a Stellar transaction to interact with a Soroban smart contract. Below, we demonstrate in JavaScript and Python how to build and submit a Stellar transaction that will invoke an instance of the [increment example](../getting-started/storing-data) smart contract. + + + + +:::tip + +The [`soroban-client`](https://stellar.github.io/js-soroban-client/) JavaScript SDK is a new library that can be used alongside the existing JavaScript SDKs for Stellar. All you need to do is install it using your preferred package manager. + +```bash +npm install --save soroban-client +``` + +::: + +```js +(async () => { + const { + Keypair, + Contract, + Server, + TransactionBuilder, + Networks, + BASE_FEE, + } = require("soroban-client"); + + // The source account will be used to sign and send the transaction. + // GCWY3M4VRW4NXJRI7IVAU3CC7XOPN6PRBG6I5M7TAOQNKZXLT3KAH362 + const sourceKeypair = Keypair.fromSecret( + "SCQN3XGRO65BHNSWLSHYIR4B65AHLDUQ7YLHGIWQ4677AZFRS77TCZRB", + ); + + // Configure SorobanClient to use the `soroban-rpc` instance of your + // choosing. + const server = new Server("https://rpc-futurenet.stellar.org:443"); + + // Here we will use a deployed instance of the `increment` example contract. + const contractAddress = + "CBEOJUP5FU6KKOEZ7RMTSKZ7YLBS5D6LVATIGCESOGXSZEQ2UWQFKZW6"; + const contract = new Contract(contractAddress); + + // Transactions require a valid sequence number (which varies from one + // account to another). We fetch this sequence number from the RPC server. + const sourceAccount = await server.getAccount(sourceKeypair.publicKey()); + + // The transaction begins as pretty standard. The source account, minimum + // fee, and network passphrase are provided. + let builtTransaction = new TransactionBuilder(sourceAccount, { + fee: BASE_FEE, + networkPassphrase: Networks.FUTURENET, + }) + // The invocation of the `increment` function of our contract is added + // to the transaction. Note: `increment` doesn't require any parameters, + // but many contract functions do. You would need to provide those here. + .addOperation(contract.call("increment")) + // This transaction will be valid for the next 30 seconds + .setTimeout(30) + .build(); + + // We use the RPC server to "prepare" the transaction. This simulating the + // transaction, discovering the storage footprint, and updating the + // transaction to include that footprint. If you know the footprint ahead of + // time, you could manually use `addFootprint` and skip this step. + let preparedTransaction = await server.prepareTransaction(builtTransaction); + + // Sign the transaction with the source account's keypair. + preparedTransaction.sign(sourceKeypair); + + // Let's see the base64-encoded XDR of the transaction we just built. + console.log( + `Signed prepared transaction XDR: ${preparedTransaction + .toEnvelope() + .toXDR("base64")}`, + ); + + // Submit the transaction to the Soroban-RPC server. The RPC server will + // then submit the transaction into the network for us. Then we will have to + // wait, polling `getTransaction` until the transaction completes. + try { + let sendResponse = await server.sendTransaction(preparedTransaction); + console.log(`Sent transaction: ${JSON.stringify(sendResponse)}`); + + if (sendResponse.status === "PENDING") { + let getResponse = await server.getTransaction(sendResponse.hash); + // Poll `getTransaction` until the status is not "NOT_FOUND" + while (getResponse.status === "NOT_FOUND") { + console.log("Waiting for transaction confirmation..."); + // See if the transaction is complete + getResponse = await server.getTransaction(sendResponse.hash); + // Wait one second + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + + console.log(`getTransaction response: ${JSON.stringify(getResponse)}`); + + if (getResponse.status === "SUCCESS") { + // Make sure the transaction's resultMetaXDR is not empty + if (!getResponse.resultMetaXdr) { + throw "Empty resultMetaXDR in getTransaction response"; + } + // Find the return value from the contract and return it + let transactionMeta = getResponse.resultMetaXdr; + let returnValue = transactionMeta.v3().sorobanMeta().returnValue(); + console.log(`Transaction result: ${returnValue.value()}`); + } else { + throw `Transaction failed: ${getResponse.resultXdr}`; + } + } else { + throw sendResponse.errorResultXdr; + } + } catch (err) { + // Catch and report any errors we've thrown + console.log("Sending transaction failed"); + console.log(JSON.stringify(err)); + } +})(); +``` + + + + +:::tip + +The [`py-stellar-base`](https://stellar-sdk.readthedocs.io/en/soroban/) Python SDK has implemented experimental support for interacting with Soroban smart contracts. You can install it from the `soroban` branch of the library's [GitHub repository](https://github.com/StellarCN/py-stellar-base/tree/soroban). Note: If you already use this SDK for interacting with the Stellar network, you may want to consider installing the `soroban` branch in a virtualenv of some sort. + +```bash +pip install git+https://github.com/StellarCN/py-stellar-base.git@soroban +``` + +::: + +```py +import time + +from stellar_sdk import Keypair, Network, SorobanServer, TransactionBuilder, xdr as stellar_xdr +from stellar_sdk.exceptions import PrepareTransactionException +from stellar_sdk.soroban_rpc import GetTransactionStatus, SendTransactionStatus + +# The source account will be used to sign and send the transaction. +# GCWY3M4VRW4NXJRI7IVAU3CC7XOPN6PRBG6I5M7TAOQNKZXLT3KAH362 +source_keypair = Keypair.from_secret('SCQN3XGRO65BHNSWLSHYIR4B65AHLDUQ7YLHGIWQ4677AZFRS77TCZRB') + +# Configure SorobanClient to use the `soroban-rpc` instance of your choosing. +soroban_server = SorobanServer('https://rpc-futurenet.stellar.org') + +# Here we will use a deployed instance of the `increment` example contract. +contract_address = 'CBEOJUP5FU6KKOEZ7RMTSKZ7YLBS5D6LVATIGCESOGXSZEQ2UWQFKZW6' + +# Transactions require a valid sequence number (which varies from one account to +# another). We fetch this sequence number from the RPC server. +source_account = soroban_server.load_account(source_keypair.public_key) + +# The transaction begins as pretty standard. The source account, minimum fee, +# and network passphrase are provided. +built_transaction = ( + TransactionBuilder( + source_account=source_account, + base_fee=100, + network_passphrase=Network.FUTURENET_NETWORK_PASSPHRASE, + ) + # The invocation of the `increment` function of our contract is added to the + # transaction. Note: `increment` doesn't require any parameters, but many + # contract functions do. You would need to provide those here. + .append_invoke_contract_function_op( + contract_id=contract_address, + function_name="increment", + parameters=[], + ) + # This transaction will be valid for the next 30 seconds + .set_timeout(30) + .build() +) + +# We use the RPC server to "prepare" the transaction. This simulating the +# transaction, discovering the storage footprint, and updating the transaction +# to include that footprint. If you know the footprint ahead of time, you could +# manually use `addFootprint` and skip this step. +try: + prepared_transaction = soroban_server.prepare_transaction(built_transaction) +except PrepareTransactionException as e: + print(f"Exception preparing transaction: {e}") + raise e + +# Sign the transaction with the source account's keypair. +prepared_transaction.sign(source_keypair) + +# Let's see the base64-encoded XDR of the transaction we just built. +print(f"Signed prepared transaction XDR: {prepared_transaction.to_xdr()}") + +# Submit the transaction to the Soroban-RPC server. The RPC server will then +# submit the transaction into the network for us. Then we will have to wait, +# polling `getTransaction` until the transaction completes. +send_response = soroban_server.send_transaction(prepared_transaction) +print(f"Sent transaction: {send_response}") + +if send_response.status != SendTransactionStatus.PENDING: + raise Exception("sending transaction failed") + +# Poll `getTransaction` until the status is not "NOT_FOUND" +while True: + print("Waiting for transaction confirmation...") + # See if the transaction is complete + get_response = soroban_server.get_transaction(send_response.hash) + if get_response.status != GetTransactionStatus.NOT_FOUND: + break + # Wait one second + time.sleep(1) + +print(f"get_transaction response: {get_response}") + +if get_response.status == GetTransactionStatus.SUCCESS: + # Make sure the transaction's resultMetaXDR is not empty + assert get_response.result_meta_xdr is not None + + # Find the return value from the contract and return it + transaction_meta = stellar_xdr.TransactionMeta.from_xdr( + get_response.result_meta_xdr + ) + return_value = transaction_meta.v3.soroban_meta.return_value + output = return_value.u32.uint32 + print(f"Transaction result: {output}") +else: + print(f"Transaction failed: {get_response.result_xdr}") +``` + + + + +## XDR Usage + +Stellar supports invoking and deploying contracts with a new operation named `InvokeHostFunctionOp`. The [`soroban-cli`] abstracts these details away from the user, but not all SDKs do yet. If you're building a dapp you'll probably find yourself building the XDR transaction to submit to the network. The `InvokeHostFunctionOp` can be used to perform the following Soroban operations: @@ -37,7 +273,7 @@ There is only a single `InvokeHostFunctionOp` allowed per transaction. Contracts should be used to perform multiple actions atomically, for example, to deploy a new contract and initialize it atomically. -## InvokeHostFunctionOp +### InvokeHostFunctionOp The XDR of `HostFunction` and `InvokeHostFunctionOp` below can be found [here][XDR]. @@ -64,7 +300,7 @@ struct InvokeHostFunctionOp }; ``` -### Function +#### Function The `hostFunction` in `InvokeHostFunctionOp` will be executed by the Soroban host environment. The supported functions are: @@ -163,9 +399,9 @@ The `hostFunction` in `InvokeHostFunctionOp` will be executed by the Soroban hos Stellar asset. This is only supported when `executable == CONTRACT_EXECUTABLE_TOKEN`. Note, that the asset doesn't need to exist when this is applied, however the issuer of the asset will be the initial token administrator. Anyone can deploy asset contracts. -### Authorization Data +#### Authorization Data -Soroban [authorization framework](../fundamentals-and-concepts/authorization.mdx) +Soroban's [authorization framework](../fundamentals-and-concepts/authorization.mdx) provides a standardized way for passing authorization data to the contract invocations via `SorobanAuthorizationEntry` structures. @@ -188,7 +424,7 @@ case SOROBAN_CREDENTIALS_ADDRESS: `SorobanAuthorizationEntry` contains a tree of invocations with `rootInvocation` as a root. This tree is authorized by a user specified in `credentials`. -`SorobanAddressCredentials` have 2 options: +`SorobanAddressCredentials` have two options: - `SOROBAN_CREDENTIALS_SOURCE_ACCOUNT` - this simply uses the signature of the transaction (or operation, if any) source account and hence doesn't require any @@ -211,7 +447,7 @@ as a root. This tree is authorized by a user specified in `credentials`. `signatureExpirationLedger`, but it is no longer valid on `signatureExpirationLedger + 1`. It is recommended to keep this as small as viable, as it makes the transaction cheaper. - - `nonce` is an arbitrary value that is unique for all the signatures of + - `nonce` is an arbitrary value that is unique for all the signatures performed by `address` until `signatureExpirationLedger`. A good approach to generating this is to just use a random value. - `signatureArgs` - signature (or multiple signatures) that sign the 32-byte @@ -246,14 +482,14 @@ struct SorobanAuthorizedContractFunction ``` `SorobanAuthorizedInvocation` consists of the `function` that is being authorized -(either contract function or a host function) and authorized sub-invocations +(either contract function or a host function) and the authorized sub-invocations that `function` performs (if any). `SorobanAuthorizedFunction` has two variants: - `SOROBAN_AUTHORIZED_FUNCTION_TYPE_CONTRACT_FN` is a contract function that - includes the address of the contract, name of the function being invoked and - arguments of `require_auth`/`require_auth_for_args` call performed on behalf + includes the address of the contract, name of the function being invoked, and + arguments of the `require_auth`/`require_auth_for_args` call performed on behalf of the address. Note, that if `require_auth[_for_args]` wasn't called, there shouldn't be a `SorobanAuthorizedInvocation` entry in the transaction. - `SOROBAN_AUTHORIZED_FUNCTION_TYPE_CREATE_CONTRACT_HOST_FN` is authorization @@ -268,7 +504,7 @@ details). [envelope-xdr]: https://github.com/stellar/stellar-xdr/blob/e372df9f677961aac04c5a4cc80a3667f310b29f/Stellar-transaction.x#L703 [preflight-doc]: ../fundamentals-and-concepts/interacting-with-contracts.mdx#authorization -#### Stellar Account Signatures +##### Stellar Account Signatures `signatureArgs` format is user-defined for the [custom accounts], but it is protocol-defined for the Stellar accounts. @@ -287,7 +523,7 @@ pub struct AccountEd25519Signature { [structures]: https://github.com/stellar/rs-soroban-env/blob/99d8c92cdc7e5cd0f5311df8f88d04658ecde7d2/soroban-env-host/src/native_contract/account_contract.rs#L51 [custom accounts]: ../fundamentals-and-concepts/authorization.mdx#account-abstraction -## Transaction resources +### Transaction resources Every Soroban transaction has to have a `SorobanTransactionData` transaction [extension] populated. This is needed to compute the