as-soroban-sdk
v1.1.0
Published
AssemblyScript SDK for writing contracts for Soroban.
Downloads
13
Maintainers
Readme
Stellar Soroban SDK for AssemblyScript
This AssemblyScript SDK is for writing contracts for Soroban. Soroban is a smart contracts platform from Stellar that is designed with purpose and built to perform.
Quick Start
1. Setup a new project
Set up a new AssemblyScript project as described in the AssemblyScript Book
$ mkdir hello
$ cd hello
$ npm init
$ npm install --save-dev assemblyscript
$ npx asinit .
2. Install the SDK
$ npm install as-soroban-sdk
3. Write your contract
You can now write your contract in the ./assembly/index.ts
file. For example:
import {SmallSymbolVal, VecObject, fromSmallSymbolStr} from 'as-soroban-sdk/lib/value';
import {Vec} from 'as-soroban-sdk/lib/vec';
export function hello(to: SmallSymbolVal): VecObject {
let vec = new Vec();
vec.pushFront(fromSmallSymbolStr("Hello"));
vec.pushBack(to);
return vec.getHostObject();
}
Next you need to add a contract.json
file to the project. It must contain the environment metadata and spec for your contract.
{
"functions": [
{
"name" : "hello",
"arguments": [{"name": "to", "type": "symbol"}],
"returns" : "vec[symbol]"
}
]
}
Finally, edit the asconfig.json
file of your project. Replace its content with the following:
{
"extends": "as-soroban-sdk/sdkasconfig",
"targets": {
"release": {
"outFile": "build/release.wasm",
"textFile": "build/release.wat"
},
"debug": {
"outFile": "build/debug.wasm",
"textFile": "build/debug.wat"
}
}
}
4. Build your contract
$ npm run asbuild:release
You can find the generated .wasm
(WebAssembly) file in the build
folder. You can also find the .wat
file there (Text format of the .wasm
).
5. Run your contract
To run the contract, you must first install the official stellar cli
as described in this setup guid. The stellar cli
needs cargo to be installed. You will not need rust or cargo for implementing smart contracts with this SDK.
Next, after you installed the stellar cli
, deploy your contract to testnet:
stellar contract deploy \
--wasm build/release.wasm \
--source SAIPPNG3AGHSK2CLHIYQMVBPHISOOPT64MMW2PQGER47SDCN6C6XFWQM \
--rpc-url https://soroban-testnet.stellar.org \
--network-passphrase "Test SDF Network ; September 2015"
This returns the ID of the contract, starting with a C. Similar to this:
CC4DZNN2TPLUOAIRBI3CY7TGRFFCCW6GNVVRRQ3QIIBY6TM6M2RVMBMC
Next let's invoke:
stellar -q contract invoke \
--source SAIPPNG3AGHSK2CLHIYQMVBPHISOOPT64MMW2PQGER47SDCN6C6XFWQM \
--rpc-url https://soroban-testnet.stellar.org \
--network-passphrase "Test SDF Network ; September 2015" \
--id <your contract id here> \
-- hello --to friend
The above hello contract
example implementation can be found as a complete example project here.
Features and limitations
In the Build your own SDK chapter of the official Soroban documentation, one can find the requirements for a Soroban SDK.
This Assembly Script Soroban SDK can help you with:
- Value Conversions
- Host functions
- SDK Types
- User Defined Errors
- Meta Generation
- Contract Spec Generation
- Testing
Value Conversions
When calling a contract function the host will only pass so called host values. In the SDK code a host value is simply called Val
. A host value is a 64-bit integer carrying a bit-packed disjoint union of several cases, each identified by a different tag value (e.g. i32
, u32
, symbol
, timestamp
, bool
etc. or object handles
such as references to vectors, maps, bytes, strings that live in the host).
You can read more about host values and their types in CAP-0046.
The SDK can encode and decode host values. For example converting primitives like i32:
import * as val from "as-soroban-sdk/lib/value";
// primitives
let xi32 = val.toI32(hostValue);
let xHostValue = val.fromI32(xi32);
// static values
let isTrue = val.fromBool(hostValue);
let hostValBool = val.toBool(isTrue);
// objects
let isVecObj = val.isVec(hostValue);
// this hostValue is a object handle referencing a vector object that lives on the host
if (isVecObj) {
let myVec = new Vec(hostVal); // init a (SDK Type) Vec from the handle.
myVec.pushFront(val.fromSmallSymbolStr("Hello"));
let hostValVecObj = myVec.getHostObject();
}
// symbols
let myHostValSymbol = val.fromSmallSymbolStr("Hello");
// etc.
Host functions
The host functions defined in env.json are functions callable from within the WASM Guest environment (where the contract code runs). The SDK makes them available to contracts to call in two versions. First directly
as defined by env.ts and second in a wrapped
version so that contracts have a nicer interface and less abstraction.
For example:
- Directly:
// see lib/env.ts
export declare function call(contract: AddressObject, func: Symbol, args: VecObject): Val;
- Wrapped:
// see lib/contract.ts
function callContractById(id: string, func: string, args: Vec): Val
Depending on the use case, you can decide which version makes more sense in your contract implementation.
SDK Types
Following types are supported by this SDK: Map
, Vec
, Bytes
, Str
, Sym
.
For example work with a vector:
let vec = new Vec();
vec.pushFront(fromSmallSymbolStr("Hello"));
vec.pushBack(fromSmallSymbolStr("friend"));
return vec.getHostObject();
// returns the host value referencing the vector object stored in the host
or a map:
let myMap = new Map();
myMap.put(fromU32(1), fromSmallSymbolStr("Hello"));
myMap.put(fromU32(2), fromSmallSymbolStr("friend"));
myMap.put(vec.getHostObject(), fromTrue());
return myMap.getHostObject();
Errors
Errors are host values that are composed of an error type (such as "contract error" or "storage error") and error code (u32). This SDK helps you to create and parse such errors. For example:
// traps with user defined error of type
// "contract error" and error code 12 (u32)
context.failWithErrorCode(12);
or
if(isError(hostVal) && getErrorType(hostVal) == errorTypeContract) {
return fromU32(getErrorCode(hostVal))
}
See also: as-soroban-examples
Contract spec generation
Contracts should contain a WASM custom section with name contractspecv0
and containing a serialized stream of SCSpecEntry
. There should be a SCSpecEntry
for every function, struct, and union exported by the contract.
The AS Soroban SDK simplifies the generation of the custom section by providing the possibility to enter the functions spec in the contract.json
file. See also Understanding contract metadata.
Contract meta generation
Contracts may optionally contain a Wasm custom section with name contractmetav0
and containing a serialized SCMetaEntry
. Contracts may store any metadata in the entries that can be used by applications and tooling off-network.
The AssemblyScript Soroban SDK simplifies the generation of the custom section by providing the possibility to add meta entries in the contract.json
file. See also Understanding contract metadata.
User Defined Types
Currently the SDK supports only simple enums from scratch.
For example:
enum ALLOWED_AGE_RANGE {
MIN = 18,
MAX = 99
}
enum AGE_ERR_CODES {
TOO_YOUNG = 1,
TOO_OLD = 2
}
export function checkAge(age: I32Val): Symbol {
let age2check = toI32(age);
if (age2check < ALLOWED_AGE_RANGE.MIN) {
failWithErrorCode(AGE_ERR_CODES.TOO_YOUNG);
}
if (age2check > ALLOWED_AGE_RANGE.MAX) {
failWithErrorCode(AGE_ERR_CODES.TOO_OLD);
}
return fromSmallSymbolStr("OK");
}
However, one can create own user defined types with ease by translating them into Maps or Vectors using the SDK.
Testing
Testing can be done to some extent by using events and the stellar-cli.
See the testing example that demonstrates a simple way to test a contract.
Logging
You can log for purpose of debugging.
Log an utf8 string
import * as context from 'as-soroban-sdk/lib/context';
context.logStr("Today is a sunny day!");
Log a string message and values
import * as context from 'as-soroban-sdk/lib/context';
let values = new Vec();
values.pushBack(val.fromI32(30));
values.pushBack(val.fromSmallSymbolStr("celsius"));
context.log("Today temperature:", values);
Log a value only
import * as context from 'as-soroban-sdk/lib/context';
context.logValue(val.fromI128Pieces(100,100));
To see the output of the logging, invoke the contract function with the cli.
E.g.:
stellar contract invoke \
--rpc-url https://soroban-testnet.stellar.org \
--network-passphrase "Test SDF Network ; September 2015" \
--source-account SAIPPNG3AGHSK2CLHIYQMVBPHISOOPT64MMW2PQGER47SDCN6C6XFWQM \
--id CBUP4VF23Z2GL5C5B7P6QHSNIE2VL7JOC6CM5HEH7IDQUPWZHRJWZS3P \
-- logging
See also: logging example
Publishing events
This AssemblyScript Soroban SDK makes it easy to publish events from a contract. This can also be very useful for testing.
context.publishSimpleEvent("STATUS", val.fromU32(1));
or
let topicsVec = new Vec();
topicsVec.pushBack(val.fromSmallSymbolStr("TOPIC1"));
topicsVec.pushBack(val.fromSmallSymbolStr("TOPIC2"));
topicsVec.pushBack(val.fromSmallSymbolStr("TOPIC3"));
let dataVec = new Vec();
dataVec.pushBack(val.fromU32(223));
dataVec.pushBack(val.fromU32(222));
dataVec.pushBack(val.fromU32(221));
context.publishEvent(topicsVec, dataVec.getHostObject());
See also: as-soroban-examples
Understanding contract metadata
To be able to run a contract, the compiled .wasm
file needs to contain the web assembly module environment metadata and contract spec.
They need to be attached to the .wasm
module. Therefore we need the contract.json
file.
The SDK parses the contract.json
file when compiling the contract and converts it to the needed data structures to be added to the .wasm
module. This is done by using an AssemblyScript transform (see: transforms.mjs).
Required field is the functions
array in a contract.json
file located in the root directory of your assembly script project.
Additionally one can also provide optional contract metadata with the meta
array.
Example:
{
"functions": [
{
"name" : "hello",
"arguments": [{"name": "to", "type": "symbol"}],
"returns" : "vec[symbol]"
}
],
"meta": [
{
"key" : "name",
"value" : "hello word"
},
{
"key" : "version",
"value" : "1.1.0"
},
{
"key" : "description",
"value" : "my first contract"
}
]
}
You must define the contract spec for each function exported by your contract. In the upper example there is only one function named hello
. In addition, you must define the name, the arguments and the return value of the function, so that the host environment can execute it.
{
"functions": [
{
"name" : "hello",
"arguments": [{"name": "to", "type": "symbol"}],
"returns" : "vec[symbol]"
}
]
}
Supported argument types are currently: val
(any type of host value), u32
, i32
, u64
, i64
, u128
, i128
, u256
, i256
,bool
, symbol
, string
, error
, bytes
, void
, timepoint
, duration
, address
, option[valueType]
, result[okType, errorType]
, vec[elementType]
, map[keyType, valueType]
, set[elementType]
,bytesN[size]
, udt(name)
, tuple(value types separated by ;)
. If your function has no arguments, you can pass an empty array.
Supported return value types are the same as the supported argument types. If your function has no return value you must return void as a static raw value. You can obtain it by using val.fromVoid()
. For this case you should set "returns" : "void"
or remove "returns"
in the contract.json.
See also Contract Spec Generation and Contract Meta Generation
In addition to functions
, for more advanced use cases, one can optionally define udt (user defined types): structs
, errors
, enums
and unions
. For example:
{
"structs":[
{
"name" : "Signature",
"fields": [
{"name": "public_key", "type": "bytesN[32]"},
{"name": "signature", "type": "bytesN[64]"}
]
}
],
"errors":[
{
"name" : "AccError",
"cases": [
{"name": "NotEnoughSigners", "value": 1},
{"name": "NegativeAmount", "value": 2},
{"name": "BadSignatureOrder", "value": 3},
{"name": "UnknownSigner", "value": 4}
]
}
]
}
The generation is implemented in transform.mjs.
Examples
Instead of a tutorial, we have created a series of contract examples with many explanations. It is recommended that you work through the examples in the order shown here.
You can find contract examples in our as-soroban-examples repository.
| Example | Description | | :--- | :--- | | add example| Demonstrates how to write a simple contract, with a single function that takes two i32 inputs and returns their sum as an output. | | hello word example| demonstrates how to write a simple contract, with a single function that takes an input and returns a vector containing multiple host values. | | increment example| Demonstrates how to write a simple contract that stores data, with a single function that increments an internal counter and returns the value. It also shows how to manage contract data lifetimes and how to optimize contracts.| | events example| Demonstrates how to publish events from a contract.| | errors example| Demonstrates how to define and generate errors in a contract that invokers of the contract can understand and handle.| | logging example| Demonstrates how to log for the purpose of debugging.| | auth example| Demonstrates how to implement authentication and authorization using the Soroban Host-managed auth framework.| | cross contract call example| Demonstrates how to call a contract's function from another contract.| | deployer example| Demonstrates how to deploy contracts using a contract.| | upgrading contracts example| Demonstrates how to upgrade a wasm contract.| | testing example| Shows a simple way to test your contract.| | token example| Demonstrates how to write a token contract that implements the Stellar token interface.| | atomic swap example| Swaps two tokens between two authorized parties atomically while following the limits they set. This example demonstrates advanced usage of Soroban auth framework and assumes the reader is familiar with the auth example and with Soroban token usage.| | atomic swap batched example| Swaps a pair of tokens between the two groups of users that authorized the swap operation from the atomic swap example.| | timelock example| Demonstrates how to write a timelock and implements a greatly simplified claimable balance similar to the claimable balance feature available on Stellar.| | single offer sale example| The single offer sale example demonstrates how to write a contract that allows a seller to set up an offer to sell token A for token B to multiple buyers.| | liquidity pool example| Demonstrates how to write a constant product liquidity pool contract.| | custom account example| This example is an advanced auth example which demonstrates how to implement a simple account contract that supports multisig and customizable authorization policies.|
More examples can be found in the test cases
Deploying smart contracts to a Stellar Network (not locally)
To deploy your smart contract to a Stellar Network such as futurenet
, testnet
or main
, you can make use of the soroban cli as described here.
You can also use one of our Stellar SDKs to programatically deploy and invoke contracts on the Stellar Network:
Working with big numbers
If you need arithmetic and binary operations for working with big numbers such as {i,u}128 or {i,u}256 then there are some possibilities.
Recommended for 256 bit numbers
There are host functions available for working with {i,u}256 (see env.ts).
Recommended for 128 bit numbers
For working with {i,u}128 we have implemented arithm128.ts which offers many functions for working with positive and negative 128 bit numbers. They use the {i,u}256 functions on the host environment to perform the calculations. This is the recommended way to work with 128 bit numbers. All examples that need 128 bit numbers such as the token contract example are using the functions from arithm128.ts.
Other posibilities for 128 bit numbers
We have also implemented a couple of functions for working with {i,u}128 that are based of binary operations, performing the calculations in the guest environment. But this functions are limited, work only for positive {i,u}128 and may consume more ressources. They are inspired by as-bignum and ported to work with soroban. You can find them in val128.ts. In some cases they may be needed, e.g. where an implementation in arithm128.ts is missing (e.g. sqrt
). It is not recommended to use the val128.ts functions. However, if you need to do so, we recommend that you test your contract thoroughly.
Another option for working with {i,u}128 is to use the library as-bignum. However, it does not work with soroban right away, because some functions throw errors, which leads to an import statement in the compiled wasm code, which in turn is not accepted by the Soroban VM:
// wat representation
(import "env" "abort" (func $~lib/builtins/abort (param i32 i32 i32 i32)))
Such special imports need support from the environment, which is currently not available. See AS special imports.
But this can also be circumvented by implementing a special abort
function in the contract.
To do so, you must first declare it in your asconfig.json
file. E.g.
{
"extends": "as-soroban-sdk/sdkasconfig",
"targets": {
"release": {
"outFile": "build/release.wasm",
"textFile": "build/release.wat",
"use": "abort=assembly/index/myAbort"
},
"debug": {
"outFile": "build/debug.wasm",
"textFile": "build/debug.wat",
"use": "abort=assembly/index/myAbort"
}
}
}
Then implement the function in your contract:
function myAbort(
message: string | null,
fileName: string | null,
lineNumber: u32,
columnNumber: u32
): void {//...}
By doing so, the import statement will not be added to the wasm code during compilation. And when invoking the contract, no VmError(Instantiation)
will be thrown while using the as-bignum
functions.
Another option is of course to implement your own arithmetic functions, for example by porting them from as-bignum and removing the throwing of errors.