1.0.0 • Published 2 months ago

as-soroban-sdk v1.0.0

Weekly downloads
-
License
Apache-2.0
Repository
github
Last release
2 months ago

Stellar Soroban SDK for AssemblyScript

v1.0.0

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 soroban cli as described in this setup guid. The soroban 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 soroban cli, deploy your contract to testnet:

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

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

  1. Directly:
// see lib/env.ts
export declare function call(contract: AddressObject, func: Symbol, args: VecObject): Val;
  1. 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 soroban-cli.

See the testing example that demonstrates a simple way to test a contract.

Logging

You can log for purpose of debugging. Logs are only visible when executing contracts using soroban-cli. Do not use logs elsewhere.

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);

See also: as-soroban-examples

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.0.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, status, 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.

ExampleDescription
add exampleDemonstrates how to write a simple contract, with a single function that takes two i32 inputs and returns their sum as an output.
hello word exampledemonstrates how to write a simple contract, with a single function that takes an input and returns a vector containing multiple host values.
increment exampleDemonstrates 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 exampleDemonstrates how to publish events from a contract.
errors exampleDemonstrates how to define and generate errors in a contract that invokers of the contract can understand and handle.
logging exampleDemonstrates how to log for the purpose of debugging.
auth exampleDemonstrates how to implement authentication and authorization using the Soroban Host-managed auth framework.
cross contract call exampleDemonstrates how to call a contract's function from another contract.
deployer exampleDemonstrates how to deploy contracts using a contract.
upgrading contracts exampleDemonstrates how to upgrade a wasm contract.
testing exampleShows a simple way to test your contract.
token exampleDemonstrates how to write a token contract that implements the Stellar token interface.
atomic swap exampleSwaps 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 exampleSwaps a pair of tokens between the two groups of users that authorized the swap operation from the atomic swap example.
timelock exampleDemonstrates how to write a timelock and implements a greatly simplified claimable balance similar to the claimable balance feature available on Stellar.
single offer sale exampleThe 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 exampleDemonstrates how to write a constant product liquidity pool contract.
custom account exampleThis 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:

Using as-bignum for 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. On the one hand there are host functions available for working with {i,u}256 (see env.ts). Furthermore we have implemented a couple of functions for working with {i,u}128 into this SDK. But the functions from the SDK are limited, work only for positive {i,u}128 and will probably be removed soon.

A better 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.

1.0.0

2 months ago

0.4.0

5 months ago

0.3.0

6 months ago

0.2.3

9 months ago

0.2.5

8 months ago

0.2.4

8 months ago

0.2.2

11 months ago

0.2.1

12 months ago

0.2.0

12 months ago

0.1.9

12 months ago

0.1.8

1 year ago

0.1.0

1 year ago

0.1.2

1 year ago

0.1.1

1 year ago

0.0.7

1 year ago

0.0.6

1 year ago

0.0.5

2 years ago

0.0.4

2 years ago

0.0.3

2 years ago

0.0.2

2 years ago

0.0.1

2 years ago