Skip to content
This repository has been archived by the owner on Aug 21, 2024. It is now read-only.

Commit

Permalink
docs: provide example Stellar transactions to invoke a contract (#578)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
ElliotFriend and briwylde08 authored Sep 21, 2023
1 parent ee13fce commit b43a2b9
Showing 1 changed file with 248 additions and 12 deletions.
260 changes: 248 additions & 12 deletions docs/fundamentals-and-concepts/invoking-contracts-with-transactions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
---

<head>
Expand All @@ -23,7 +24,242 @@ description: Invoke and deploy smart contracts with the InvokeHostFunctionOp ope
/>
</head>

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.

<Tabs>
<TabItem value="js" label="JavaScript">

:::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));
}
})();
```

</TabItem>
<TabItem value="py" label="Python">

:::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}")
```

</TabItem>
</Tabs>

## 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:

Expand All @@ -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].
Expand All @@ -64,7 +300,7 @@ struct InvokeHostFunctionOp
};
```

### Function
#### Function

The `hostFunction` in `InvokeHostFunctionOp` will be executed by the Soroban host environment. The supported functions are:

Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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
Expand Down

0 comments on commit b43a2b9

Please sign in to comment.