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
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)
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 or XDR 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.Stellar Contract Spec
When you compile a smart contract using the Rust SDK, the resulting Wasm file includes a special section containing a complete description of your contract’s interface types. This is often referred to as the contract’sspec or contract spec. Stellar smart contract specification, known as the contract spec, is a foundational element for interacting with contracts and for building dApps on the Stellar network. The contract Spec provides a robust and fully typed definitions for interacting with smart contracts, offering functionality equivalent to Ethereum’s ABI while addressing its limitations.
The contract spec serves as a standardized interface for interacting with Stellar smart contracts. Similar to Ethereum’s ABIs, but with key advantages:
- On-chain Availability: Every contract spec is stored on-chain
- Developer Comments: Comments from the contract author are preserved
- Seamless communication between contracts and external applications
- Ecosystem-wide compatibility with tools like wallets, explorers, and SDKs
Fully Typed Contract Definitions
The Contract Spec enforces fully typed definitions for all contract functions, inputs, and outputs. This ensures that:- Developers can define contract behavior explicitly, reducing ambiguity.
- Type mismatches and runtime errors are minimized, leading to more reliable smart contracts.
- Tools can provide intelligent suggestions and validations during development.
Comparison to Ethereum ABI
Stellar’s Contract Spec shares many similarities with Ethereum’s ABI but also introduces enhancements: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 thestellar 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.
The Stellar Lab has a Contract Explorer also provides the ability to view and to download a contract’s contract spec.
Fetching the contract binary
Generating the contract spec from Wasm
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:
This defines a
mintfunction that takes four parameters and returns either an empty tuple or an error. Notice the type of each parameter:addressfor Stellar account addresses,i128for 128-bit integers, etc. -
Structs: Structs define complex data types with multiple fields.
Example:
This defines a
ClaimableBalancestruct with four fields. -
Unions: Unions represent variables that can be one of several types.
Example:
This defines a
DataKeyunion that can be eitherInitorBalance. -
Custom Types: Custom types refer to other defined types in the ABI.
Example:
This refers to a custom
TimeBoundtype defined elsewhere in the ABI. -
Vector Types: Vectors represent arrays of a specific type.
Example:
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:
- When calling the
mintfunction, 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
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, theScVal 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 theincrement 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.
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 nativearray (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.
3. Getting responses from contracts
Data returned from contracts is also inScVal 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
ScVecfrom the return value - Extract each
ScStringfrom theScVec - Convert each
ScStringto a native string
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.
Working with complex data types
As described in this guide, there are some other variants of data structure supported by Soroban. They areStructwith named fieldsStructwith unnamed fieldsEnum(Unit and Tuple Variants)Enum(Integer Variants)
Struct with named fields
Structs with named values when converted to ABI or spec are represented as aScMap where each value has the key in ScSymbol and the value in the underlying type.
Struct with unnamed fields
Structs with unnamed values when converted to ABI or spec are represented as aScVal where each value has the underlying type.
Enum (unit and tuple variants)
Enums are generally represented withScVec, their unit types are represented as ScSymbol and their tuple variants are represented as the underlying types.
Enum (integer variants)
Enums are generally represented withScVec, the integer variant has no keys so it’s just a ScVec of the underlying type.
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 aTimeBound 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.
Reading contract events
Reading contract events is similar to reading transaction results. You can use thegetEvents 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.
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