Work with contract specs in Java, Python, and PHP
Introduction
Soroban smart contracts are powerful tools for building decentralized applications on the Stellar network. To interact with these contracts effectively, it's crucial to understand their specifications and how to use them in your programming language of choice.
A typical contract specification (spec) includes:
- Data types used by the contract
- Function definitions with their inputs and outputs
- Error types that the contract may return
These details guide how you interact with the contract, regardless of the programming language you're using.
Prerequisites
Before diving into contract interactions, ensure you have the following:
- Stellar CLI (
stellar
) installed - A Soroban-compatible SDK for your programming language. View the list of available SDKs to find one that suits your needs
- Access to a Stellar RPC server (local or on a test network)
For this guide, we will focus on the Java, Python, and PHP SDKs for reference, but the concepts can also be applied to other languages.
What are contract specs?
A contract spec is just like an ABI (Application Binary Interface) in Ethereum. It is a standardized description of a smart contract's interface, typically in JSON format. It defines the contract's functions, data structures, events, and errors in a way that external applications can understand and use.
This specification serves as a crucial bridge between the smart contract and client applications, enabling them to interact without needing to know the contract's internal implementation details.
Generating contract specs
The Stellar CLI provides a command to generate a contract spec from a contract's source code. This process is easy but requires you to have the Wasm binary of the contract.
Sometimes, you may not have access to the contract's source code or the ability to compile it. In such cases, you must use the stellar contract fetch
command to download the contract's Wasm binary and generate the spec.
Finally, we use the stellar bindings
command to generate the contract spec from the Wasm binary.
Fetching the contract binary
stellar contract fetch --network-passphrase 'Test SDF Network ; September 2015' --rpc-url https://soroban-testnet.stellar.org --id CONTRACT_ID --out-file contract.wasm
Generating the contract spec from Wasm
stellar contract bindings json --wasm contract.wasm > abi.json
Understanding the contract specification
The ABI (Application Binary Interface) specification for Stellar smart contracts includes several key components that define how to interact with the contract. Let's examine these in detail with examples:
-
Functions: Functions are defined with their name, inputs, and outputs. They represent the callable methods of the contract. They can be used for writing data to the contract and reading data from the contract.
Example:
{
"type": "function",
"name": "mint",
"inputs": [
{
"name": "contract",
"value": { "type": "address" }
},
{
"name": "minter",
"value": { "type": "address" }
},
{
"name": "to",
"value": { "type": "address" }
},
{
"name": "amount",
"value": { "type": "i128" }
}
],
"outputs": [
{
"type": "result",
"value": { "type": "tuple", "elements": [] },
"error": { "type": "error" }
}
]
}This defines a
mint
function that takes four parameters and returns either an empty tuple or an error. Notice the type of each parameter:address
for Stellar account addresses,i128
for 128-bit integers, etc. -
Structs: Structs define complex data types with multiple fields.
Example:
{
"type": "struct",
"name": "ClaimableBalance",
"fields": [
{
"name": "amount",
"value": { "type": "i128" }
},
{
"name": "claimants",
"value": {
"type": "vec",
"element": { "type": "address" }
}
},
{
"name": "time_bound",
"value": {
"type": "custom",
"name": "TimeBound"
}
},
{
"name": "token",
"value": { "type": "address" }
}
]
}This defines a
ClaimableBalance
struct with four fields. -
Unions: Unions represent variables that can be one of several types.
Example:
{
"type": "union",
"name": "DataKey",
"cases": [
{
"name": "Init",
"values": []
},
{
"name": "Balance",
"values": []
}
]
}This defines a
DataKey
union that can be eitherInit
orBalance
. -
Custom Types: Custom types refer to other defined types in the ABI.
Example:
{
"name": "time_bound",
"value": {
"type": "custom",
"name": "TimeBound"
}
}This refers to a custom
TimeBound
type defined elsewhere in the ABI. -
Vector Types: Vectors represent arrays of a specific type.
Example:
{
"name": "claimants",
"value": {
"type": "vec",
"element": { "type": "address" }
}
}This defines a vector of addresses.
-
Primitive Types: These include basic types like
i128
(128-bit integer),u64
(64-bit unsigned integer),address
, etc.Example:
{
"name": "amount",
"value": { "type": "i128" }
}
These specifications are crucial for encoding and decoding data when interacting with the contract. For example:
- When calling the
mint
function, you must provide four parameters: three addresses and a 128-bit integer. - If a function returns a
ClaimableBalance
, you would expect to receive a struct with an amount (i128), a vector of addresses (claimants), a TimeBound object, and an address (token). - If a function could return an
Error
, it will most likely fail at simulation and you won't need to decode the result.
Soroban types
Before we dive into interacting with Stellar smart contracts, it is important to note that Soroban has its own set of types that are used to interact with the contracts as described in this guide. Here are some of the common types:
u32
: Unsigned 32-bit integeru64
: Unsigned 64-bit integeri32
: Signed 32-bit integeri64
: Signed 64-bit integeru128
: Unsigned 128-bit integeri128
: Signed 128-bit integerbool
: Booleanstring
: UTF-8 encoded stringvec
: Variable-length arrayaddress
: Stellar account addressmap
: Key-value mapsymbol
: A small string used mainly for function names and map keys
In this guide and the SDKs, these types are represented as ScU32
, ScU64
, ScI32
, ScI64
, ScU128
, ScI128
, ScBool
, ScString
, ScVec
, ScAddress
, ScMap
, and ScSymbol
respectively.
Every other complex type can be derived using these basic types but these types do not really map to values in the programming languages. The Stellar SDKs provide helper classes to work with these types.
Working with native Soroban types
One of the most common tasks when working with Stellar smart contracts is converting between Stellar smart contract types and native types in your programming language. In this guide, we will go over some common conversions and show how they can be used to invoke contracts with the help of the contract spec.
In most SDKs, the ScVal
class or function is used to convert between Soroban types and native types.
The JSON code block shows the contract spec, while RUST code blocks show the contract for each example.
1. Invoking a contract function with no parameters
We will be using the increment
function of the sample increment contract to exemplify this. The increment
function takes no parameters and increments the counter by 1.
In this scenario, there is no need for conversions and passing the value null
as contract arguments is sufficient in most SDKs.
- Rust
- JSON
- Python
- Java
- PHP
#[contractimpl]
impl IncrementContract {
/// Increment increments an internal counter, and returns the value.
pub fn increment(env: Env) -> u32 {
// Get the current count.
let mut count: u32 = env.storage().instance().get(&COUNTER).unwrap_or(0); // If no value set, assume 0.
log!(&env, "count: {}", count);
// Increment the count.
count += 1;
// Save the count.
env.storage().instance().set(&COUNTER, &count);
env.storage().instance().extend_ttl(50, 100);
// Return the count to the caller.
count
}
}
[
{
"type": "function",
"doc": "Increment increments an internal counter, and returns the value.",
"name": "increment",
"inputs": [],
"outputs": [
{
"type": "u32"
}
]
}
]
# pip install --upgrade stellar-sdk
from stellar_sdk import SorobanServer, soroban_rpc, Account, Asset, Keypair, Network, TransactionBuilder
def send_transaction() -> soroban_rpc.SendTransactionResponse:
server = SorobanServer(server_url='https://soroban-testnet.stellar.org', client=None)
root_keypair = Keypair.from_secret(
"SXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
)
root_account = server.load_account("GBSBL6FBPX5UHKL4AZCPUU6PXKUBYMKRUN3L4YQ4V2CCWSE7YMN2HYPB")
contract_id = "CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE"
transaction = (
TransactionBuilder(
source_account=root_account,
network_passphrase=Network.TESTNET_NETWORK_PASSPHRASE,
base_fee=100,
)
.append_invoke_contract_function_op(contract_id,"increment")
# mark this transaction as valid only for the next 30 seconds
.set_timeout(30)
.build()
)
transaction.sign(root_keypair)
response = server.send_transaction(transaction)
return response
response = send_transaction()
print("status", response.status)
print("hash:", response.hash)
print("status:", response.status)
print("errorResultXdr:", response.error_result_xdr)
// implementation 'network.lightsail:stellar-sdk:0.44.0'
import org.stellar.sdk.AccountConverter;
import org.stellar.sdk.InvokeHostFunctionOperation;
import org.stellar.sdk.KeyPair;
import org.stellar.sdk.Network;
import org.stellar.sdk.SorobanServer;
import org.stellar.sdk.Transaction;
import org.stellar.sdk.TransactionBuilder;
import org.stellar.sdk.TransactionBuilderAccount;
import org.stellar.sdk.responses.sorobanrpc.SendTransactionResponse;
public class SendTransactionExample {
public static void main(String[] args) {
SorobanServer server = new SorobanServer("https://soroban-testnet.stellar.org");
try {
TransactionBuilderAccount account = server.getAccount("GBSBL6FBPX5UHKL4AZCPUU6PXKUBYMKRUN3L4YQ4V2CCWSE7YMN2HYPB");
KeyPair sourceKeyPair = KeyPair.fromSecretSeed("SXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");
String contractId = "CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE";
InvokeHostFunctionOperation operation = InvokeHostFunctionOperation.invokeContractFunctionOperationBuilder(contractId, "increment", null).build();
// Build the transaction
Transaction transaction = new TransactionBuilder(AccountConverter.enableMuxed(), account, Network.TESTNET)
.addOperation(operation)
.build();
// Sign the transaction
transaction.sign(sourceKeyPair);
// Send the transaction using the SorobanServer
SendTransactionResponse response = server.sendTransaction(transaction);
System.out.println(response.getStatus());
System.out.println(response.getHash());
System.out.println(response.getLatestLedger());
System.out.println(response.getLatestLedgerCloseTime());
} catch (Exception e) {
System.err.println("An error has occurred:");
e.printStackTrace();
}
}
}
<?php
namespace App\Models;
use Soneso\StellarSDK\TransactionBuilder;
use Soneso\StellarSDK\Soroban\SorobanServer;
use Soneso\StellarSDK\SEP\Derivation\Mnemonic;
use Soneso\StellarSDK\Crypto\KeyPair;
use Soneso\StellarSDK\Network;
use Soneso\StellarSDK\InvokeContractHostFunction;
use Soneso\StellarSDK\InvokeHostFunctionOperationBuilder;
use Soneso\StellarSDK\Soroban\Requests\SimulateTransactionRequest;
class Invoker
{
public function invoker2()
{
$server = new SorobanServer("https://soroban-testnet.stellar.org");
$accountASeed = "SXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
$accountAKeyPair = Keypair::fromSeed($accountASeed);
$accountAId = "GBWIPGMM27M7O43NIHS6MXMVBDDI6WUYXWCJX7FL7DBA6B4ITXBC6JPN";
$accountA = $server->getAccount($accountAId);
$network = Network::testnet();
$contractId = "CAMN24E6KNIXQBYPJJ4K7XRCUUJUMYSJRCSLZ2WOO6WJTSKIXAZWNYHK";
$invokeContractHostFunction = new InvokeContractHostFunction($contractId, "increment", null);
$builder = new InvokeHostFunctionOperationBuilder($invokeContractHostFunction);
$op = $builder->build();
$transaction = (new TransactionBuilder($accountA))
->addOperation($op)->build();
$request = new SimulateTransactionRequest($transaction);
$simulateResponse = $server->simulateTransaction($request);
$transaction->setSorobanTransactionData($simulateResponse->getTransactionData());
$transaction->addResourceFee($simulateResponse->minResourceFee);
$transaction->sign($accountAKeyPair, $network);
$server->sendTransaction($transaction);
}
}
Subsequent examples will show code blocks for using the contract spec only to reduce redundancy.
2. Invoking a contract function with one or more parameters
Generally, this involves passing in a native array
(not a ScVec
) of parameters to the contract function.
We will be using the hello
function of the sample Hello World contract to exemplify this.
We know from the spec that the hello
function takes a string parameter and returns a vector of strings. In this scenario, we need to convert the string parameter to a ScString
type before passing it to the contract.
This process is convenient using the ScVal
class or function in most SDKs.
- Rust
- JSON
- Python
- Java
- PHP
#[contract]
pub struct HelloContract;
#[contractimpl]
impl HelloContract {
pub fn hello(env: Env, to: String) -> Vec<String> {
vec![&env, String::from_str(&env, "Hello"), to]
}
}
[
{
"type": "function",
"doc": "",
"name": "hello",
"inputs": [
{
"doc": "",
"name": "to",
"value": {
"type": "string"
}
}
],
"outputs": [
{
"type": "vec",
"element": {
"type": "string"
}
}
]
}
]
from stellar_sdk import Keypair, Network, SorobanServer, TransactionBuilder, scval
.....
tx = (
TransactionBuilder(source, network_passphrase, base_fee=100)
.set_timeout(300)
.append_invoke_contract_function_op(
contract_id=contract_id,
function_name="hello",
parameters=[
scval.to_string("John"),
]
).build())
import org.stellar.sdk.scval.Scv;
import org.stellar.sdk.xdr.SCVal;
// .....
List<SCVal> contractArgs = new ArrayList<SCVal>();
contractArgs.add(Scv.toString("John"));
InvokeHostFunctionOperation operation = InvokeHostFunctionOperation
.invokeContractFunctionOperationBuilder(contractId, "hello", contractArgs).build();
TransactionBuilder transaction = new TransactionBuilder(source, Network.TESTNET);
Transaction tx = transaction.addOperation(operation).build();
$arg = \Soneso\StellarSDK\Xdr\XdrSCVal::forString("John");
$invokeContractHostFunction = new InvokeContractHostFunction($contractId, "hello", [$arg]);
$builder = new InvokeHostFunctionOperationBuilder($invokeContractHostFunction);
$op = $builder->build();
$transaction = (new TransactionBuilder($accountA))
->addOperation($op)->build();
3. Getting responses from contracts
Data returned from contracts is also in ScVal
format and need to be converted to native types in your programming language.
We will still be using the hello
function of the sample Hello World contract to exemplify this.
We know from the Spec that the hello
function takes a string parameter and returns a vec of strings. In this scenario, we need to convert the value returned from an ScVec
of ScString
type to array
of string
before making use of it.
Steps:
- Extract an
ScVec
from the return value - Extract each
ScString
from theScVec
- Convert each
ScString
to a native string
This process is convenient using the ScVal
class or function in most SDKs.
Ideally, to retrieve this value, we need to use the getTransaction
RPC method using the response hash of the transaction that invoked the contract function.
- Rust
- JSON
- Python
- Java
- PHP
#[contract]
pub struct HelloContract;
#[contractimpl]
impl HelloContract {
pub fn hello(env: Env, to: String) -> Vec<String> {
vec![&env, String::from_str(&env, "Hello"), to]
}
}
[
{
"type": "function",
"doc": "",
"name": "hello",
"inputs": [
{
"doc": "",
"name": "to",
"value": {
"type": "string"
}
}
],
"outputs": [
{
"type": "vec",
"element": {
"type": "string"
}
}
]
}
]
from stellar_sdk import SorobanServer, soroban_rpc
from stellar_sdk import xdr as stellar_xdr
from stellar_sdk.soroban_rpc import GetTransactionStatus
def get_transaction(hash: str) -> soroban_rpc.GetTransactionResponse:
server = SorobanServer(server_url='https://soroban-testnet.stellar.org', client=None)
tx = server.get_transaction(hash)
return tx
get_transaction_data = get_transaction("7e47c6ba2ebe53e156bc50c48e34302d49c91c04c465e8cd2b8a25219c2c8121")
if get_transaction_data.status == GetTransactionStatus.SUCCESS:
transaction_meta = stellar_xdr.TransactionMeta.from_xdr(
get_transaction_data.result_meta_xdr
)
result = transaction_meta.v3.soroban_meta.return_value
output = []
for x in result.vec.sc_vec:
decoded_string = x.str.sc_string.decode()
output.append(decoded_string)
print(f"transaction result: {output}")
else:
print(f"Transaction failed: {get_transaction_data.result_xdr}")
import org.stellar.sdk.responses.sorobanrpc.GetTransactionResponse;
import org.stellar.sdk.xdr.SCVal;
import org.stellar.sdk.xdr.TransactionMeta;
public static void main(String[] args) {
SorobanServer server = new SorobanServer("https://soroban-testnet.stellar.org");
try {
GetTransactionResponse tx = server
.getTransaction("7e47c6ba2ebe53e156bc50c48e34302d49c91c04c465e8cd2b8a25219c2c8121");
if (tx.getStatus() == GetTransactionResponse.GetTransactionStatus.SUCCESS) {
List<String> output = new ArrayList<String>();
String base64Xdr = tx.getResultMetaXdr();
// convert the string to a result
SCVal[] result = TransactionMeta.fromXdrBase64(base64Xdr).getV3()
.getSorobanMeta().getReturnValue().getVec()
.getSCVec();
for (SCVal x : result) {
output.add(x.getStr().getSCString().toString());
}
System.out.println("transaction result: " + output.toString());
} else {
System.out.println("Transaction failed: " + tx.getStatus());
}
} catch (Exception e) {
System.err.println("An error has occurred:");
e.printStackTrace();
}
}
<?php
use Soneso\StellarSDK\Soroban\Responses\GetTransactionResponse;
use Soneso\StellarSDK\Soroban\SorobanServer;
class Test {
public function getTx()
{
$txhash = "7e47c6ba2ebe53e156bc50c48e34302d49c91c04c465e8cd2b8a25219c2c8121";
$server = new SorobanServer("https://soroban-testnet.stellar.org");
$statusResponse = $server->getTransaction($txhash);
$status = $statusResponse->status;
$resultArr = [];
if ($status == GetTransactionResponse::STATUS_FAILED) {
print ("Transaction failed: " . $statusResponse->error . PHP_EOL);
} else if ($status == GetTransactionResponse::STATUS_SUCCESS) {
$resultValue = $statusResponse->getResultValue();
$resVec = $resultValue->vec;
foreach ($resVec as $strVal) {
$resultArr[] = $strVal->str;
}
print_r($resultArr);
}
return $resultArr;
}
}
Working with complex data types
As described in this guide, there are some other variants of data structure supported by Soroban. They are
Struct
with named fieldsStruct
with unnamed fieldsEnum
(Unit and Tuple Variants)Enum
(Integer Variants)
We would be looking at how these variants translate to the spec and how to construct them in the different SDKs.
Struct with named fields
Structs with named values when converted to ABI or spec are represented as a ScMap
where each value has the key in ScSymbol
and the value in the underlying type.
- Rust
- JSON
- Python
- Java
- PHP
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct State {
pub count: u32,
pub last_incr: u32,
}
[
{
"type": "struct",
"doc": "",
"name": "State",
"fields": [
{
"doc": "",
"name": "count",
"value": {
"type": "u32"
}
},
{
"doc": "",
"name": "last_incr",
"value": {
"type": "u32"
}
}
]
}
]
from stellar_sdk import scval
scval.to_map(
{
scval.to_symbol("count"): scval.to_u32(0),
scval.to_symbol("last_incr"): scval.to_u32(0),
}
)
import org.stellar.sdk.xdr.SCVal;
import org.stellar.sdk.scval.Scv;
import java.util.LinkedHashMap;
LinkedHashMap<SCVal, SCVal> map = new LinkedHashMap<SCVal, SCVal>();
map.put(Scv.toSymbol("count"), Scv.toUint32(0));
map.put(Scv.toSymbol("last_incr"), Scv.toUint32(0));
SCVal val = Scv.toMap(map);
<?php
use Soneso\StellarSDK\Xdr\XdrSCVal;
XdrSCVal::forMap(
[
XdrSCVal::forSymbol("count") => XdrSCVal::forU32(0),
XdrSCVal::forSymbol("last_incr") => XdrSCVal::forU32(0),
]
);
Struct with unnamed fields
Structs with unnamed values when converted to ABI or spec are represented as a ScVal
where each value has the underlying type.
- Rust
- JSON
- Python
- Java
- PHP
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct State(pub u32, pub u32);
[
{
"type": "struct",
"doc": "",
"name": "State",
"fields": [
{
"doc": "",
"value": {
"type": "u32"
}
},
{
"doc": "",
"value": {
"type": "u32"
}
}
]
}
]
from stellar_sdk import scval
scval.to_vec(
[
scval.to_uint32(0),
scval.to_uint32(0),
]
)
import org.stellar.sdk.xdr.SCVal;
import org.stellar.sdk.scval.Scv;
import java.util.LinkedHashMap;
List<SCVal> vec = new ArrayList<SCVal>();
vec.add(Scv.toUint32(0));
vec.add(Scv.toUint32(0));
SCVal val = Scv.toVec(vec);
<?php
use Soneso\StellarSDK\Xdr\XdrSCVal;
XdrSCVal::forVec(
[
XdrSCVal::forU32(0),
XdrSCVal::forU32(0),
]
);
Enum (unit and tuple variants)
Enums are generally represented with ScVec
, their unit types are represented as ScSymbol
and their tuple variants are represented as the underlying types.
- Rust
- JSON
- Python
- Java
- PHP
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Enum {
A,
B(u32),
}
[
{
"type": "union",
"doc": "",
"name": "Enum",
"cases": [
{
"doc": "",
"name": "A",
"values": []
},
{
"doc": "",
"name": "B",
"values": [
{
"type": "u32"
}
]
}
]
}
]
from stellar_sdk import scval
scval.to_vec(
[
scval.to_symbol("A"),
scval.to_map(
{
scval.to_symbol("B"): scval.to_uint32(0),
}
),
]
)
import org.stellar.sdk.xdr.SCVal;
import org.stellar.sdk.scval.Scv;
import java.util.LinkedHashMap;
List<SCVal> vec = new ArrayList<SCVal>();
vec.add(Scv.toSymbol("A"));
LinkedHashMap<SCVal, SCVal> map = new LinkedHashMap<SCVal, SCVal>();
map.put(Scv.toSymbol("B"), Scv.toUint32(0));
vec.add(Scv.toMap(map));
SCVal val = Scv.toVec(vec);
<?php
use Soneso\StellarSDK\Xdr\XdrSCVal;
XdrSCVal::forVec(
[
XdrSCVal::forSymbol("A"),
XdrSCVal::forMap(
[
XdrSCVal::forSymbol("B") => XdrSCVal::forU32(0),
]
),
]
);
Enum (integer variants)
Enums are generally represented with ScVec
, the integer variant has no keys so it's just a ScVec
of the underlying type.
- Rust
- JSON
- Python
- Java
- PHP
#[contracttype]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[repr(u32)]
pub enum Enum {
A = 1,
B = 2,
}
[
{
"type": "struct",
"doc": "",
"name": "Enum",
"fields": [
{
"doc": "",
"name": "A",
"type": "u32"
},
{
"doc": "",
"name": "B",
"type": "u32"
}
]
}
]
from stellar_sdk import scval
scval.to_vec(
[
scval.to_uint32(0),
scval.to_uint32(0),
]
)
import org.stellar.sdk.xdr.SCVal;
import org.stellar.sdk.scval.Scv;
import java.util.LinkedHashMap;
List<SCVal> vec = new ArrayList<SCVal>();
vec.add(Scv.toUint32(0));
vec.add(Scv.toUint32(0));
SCVal val = Scv.toVec(vec);
<?php
use Soneso\StellarSDK\Xdr\XdrSCVal;
XdrSCVal::forVec(
[
XdrSCVal::forU32(0),
XdrSCVal::forU32(0),
]
);
A complex example
Let's use the timelock contract example to show how to interact with a contract that has complex data types.
This example uses a TimeBound
struct that has a TimeBoundKind
enum as one of its fields, which are parameters to the deposit
function. This example combines most of the concepts we have discussed so far.
- Rust
- JSON
- Python
#[derive(Clone)]
#[contracttype]
pub enum TimeBoundKind {
Before,
After,
}
#[derive(Clone)]
#[contracttype]
pub struct TimeBound {
pub kind: TimeBoundKind,
pub timestamp: u64,
}
#[contracttype]
#[contractimpl]
impl ClaimableBalanceContract {
pub fn deposit(
env: Env,
from: Address,
token: Address,
amount: i128,
claimants: Vec<Address>,
time_bound: TimeBound,
) {}
}
[
{
"type": "union",
"doc": "",
"name": "TimeBoundKind",
"cases": [
{
"doc": "",
"name": "Before",
"values": []
},
{
"doc": "",
"name": "After",
"values": []
}
]
},
{
"type": "struct",
"doc": "",
"name": "TimeBound",
"fields": [
{
"doc": "",
"name": "kind",
"value": {
"type": "custom",
"name": "TimeBoundKind"
}
},
{
"doc": "",
"name": "timestamp",
"value": {
"type": "u64"
}
}
]
},
{
"type": "function",
"doc": "",
"name": "deposit",
"inputs": [
{
"doc": "",
"name": "from",
"value": {
"type": "address"
}
},
{
"doc": "",
"name": "token",
"value": {
"type": "address"
}
},
{
"doc": "",
"name": "amount",
"value": {
"type": "i128"
}
},
{
"doc": "",
"name": "claimants",
"value": {
"type": "vec",
"element": {
"type": "address"
}
}
},
{
"doc": "",
"name": "time_bound",
"value": {
"type": "custom",
"name": "TimeBound"
}
}
],
"outputs": []
}
]
from stellar_sdk import scval
secret = "SXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
rpc_server_url = "https://soroban-testnet.stellar.org:443"
contract_id = "CAIKIZOT2LXM2WBEPGTZTPHHTGVHGLEOSI4WE6BOHWIBHJOKHPMCOPLO"
network_passphrase = Network.TESTNET_NETWORK_PASSPHRASE
kp = Keypair.from_secret(secret)
soroban_server = SorobanServer(rpc_server_url)
source = soroban_server.load_account(kp.public_key)
# Let's build a transaction that invokes the `deposit` function.
tx = (
TransactionBuilder(source, network_passphrase, base_fee=1000)
.set_timeout(300)
.append_invoke_contract_function_op(
contract_id=contract_id,
function_name="deposit",
parameters=[
scval.to_address(kp.public_key),
scval.to_address("GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"),
scval.to_int128(1),
scval.to_vec(
[
scval.to_address("GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"),
]
),
scval.to_map(
{
scval.to_symbol("kind"): scval.to_vec(
[
scval.to_symbol("Before"),
]
),
scval.to_symbol("timestamp"): scval.to_uint64(12346),
}
),
],
)
.build()
)
Reading contract events
Reading contract events is similar to reading transaction results. You can use the getEvents
RPC method to get the list of events associated with a contract.
One common convention is that small strings like function names, enum keys, and event topics are represented as ScSymbol
in the contract spec.
However, event topics can be any scval
type depending on the contract implementation.
In the example below, we will be encoding the mint to ScSymbol
before querying it, and also encoding the addresses to ScAddress
. Even after getting the event, we will need to parse the topics and value to get the actual values again from xdr base 64 to their corresponding types before then converting it to native types.
- Rust
- Python
- bash
let address_1: &Address = "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX".into();
let address_2: &Address = "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX".into();
let count: i128 = 1;
env.events()
.publish((symbol_short!("mint"), address_1, address_2), count);
from stellar_sdk import SorobanServer, scval, stellar_xdr
from stellar_sdk.exceptions import NotFoundError, BadResponseError
from stellar_sdk.soroban_server import EventFilter, EventFilterType
def get_events():
server = SorobanServer("https://soroban-testnet.stellar.org")
# Define the request parameters
start_ledger = 835020
contract_id = "CDLYESZILKBHBRSPKQCQ3Q4K4N6MBI6UIQR3QXJ5L6WSYXC4EMTHSNNX"
topic = [
scval.to_symbol("mint").to_xdr(),
scval.to_address("GALIALRZJ5EU2IJJSIQEA3D3ZIEHK5HPBHZJFUEPTGQU3MYEKKIUINTY").to_xdr(),
scval.to_address("GC45QSBFYHGQUIWWQEOZ43INQGXX57CSSAABWRZ325H7MNFIFWZ56FD4").to_xdr(),
]
try:
# Use the get_events method directly
events_response = server.get_events(
start_ledger=start_ledger,
filters=[
EventFilter(
event_type=EventFilterType.CONTRACT,
contract_ids=[contract_id],
topics=[topic]
)
],
limit=20
)
# Process the response
print(f"Latest ledger: {events_response.latest_ledger}")
for event in events_response.events:
print(f"Event ID: {event.id}")
print(f"Contract ID: {event.contract_id}")
for _topic in event.topic:
if _topic is None:
continue
sc_val = stellar_xdr.SCVal.from_xdr(_topic)
if sc_val.sym is not None:
print(f"Topic: {scval.from_symbol(sc_val)}")
if sc_val.address is not None:
print(f"Topic: {scval.from_address(sc_val).address}")
if event.value is not None:
value_sc_val = stellar_xdr.SCVal.from_xdr(event.value)
if value_sc_val.i128 is not None:
print(f"Value: {scval.from_int128(value_sc_val)}")
print("---")
except NotFoundError:
print("No events found for the given parameters.")
except BadResponseError as e:
print(f"Error occurred: {str(e)}")
curl -X POST \
-H 'Content-Type: application/json' \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "getEvents",
"params": {
"startLedger": 1190000,
"filters": [
{
"type": "contract",
"contractIds": [
"CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"
],
"topics": [
[
"AAAADwAAAARtaW50",
"AAAAEgAAAAAAAAAAFoAuOU9JTSEpkiBAbHvKCHV07wnyktCPmaFNswRSkUQ=",
"AAAAEgAAAAAAAAAAudhIJcHNCiLWgR2ebQ2Br378UpAAG0c710/2NKgts98=",
]
]
}
],
"pagination": {
"limit": 20
}
}
}' \
https://soroban-testnet.stellar.org
Wrapping Up
As we've seen, working with Soroban smart contracts across different programming languages isn't rocket science, but it does require some careful attention to detail. The key takeaways:
- Always start with a solid understanding of your contract's spec
- Get comfortable with converting between native types and Soroban's quirky data structures
- Don't be intimidated by complex data types - they're just puzzles waiting to be solved
- When in doubt, consult your SDK's documentation for language-specific nuances
Guides in this category:
📄️ Use Docker to build and run dapps
Understand Docker and use it to build applications
📄️ Comprehensive frontend guide for Stellar dapps
Learn how to build functional frontend interfaces for Stellar dapps using React, Tailwind CSS, and the Stellar SDK.
📄️ Initialize a dapp using scripts
Set up initialization correctly to ensure seamless setup for your dapp
📄️ Create a frontend for your dapp using React
Connect dapp frontends to contracts and Freighter wallet using @soroban-react
📄️ Develop contract initialization frontend templates
Understand, find, and create your own frontend templates for use with Stellar CLI's `stellar contract init` command
📄️ Implement state archival in dapps
Learn how to implement state archival in your dapp
📄️ Work with contract specs in Java, Python, and PHP
A guide to understanding and interacting with Soroban smart contracts in different programming languages