← All standards
SEP-0048Source ↗

Contract Interface Specification

24 min · 5,274 words

#Simple Summary

A standard for contracts to self-describe their exported interface.

#Dependencies

None.

#Motivation

It is necessary for tooling, SDKs, and off-chain systems to be able to discover the functions exported by a contract. Tooling and SDKs must be able to generate client code for calling contracts. Off-chain systems must be able to present a human friendly interface describing a called contract interface.

All Wasm files contain a list of exported functions, but the contents of that list is primitive. The list includes their names, parameters, return values, but only the primitive types (e.g. i64, i32, u64, u32) of those values, and nothing about the Soroban host types (e.g. String, Symbol, Map, Vec, I128, U256, etc) or user-defined types that the functions accept.

It is also necessary for tooling, SDKs, and off-chain systems to be able to parse the events published by a contract. Tooling and SDKs must be able to generate code for parsing events. Off-chain systems must be able to present published events with the context of what each topic and data value means.

A richer description of the interface is needed to fully understand the interface, and to be able to recreate a contract interface exactly as it was originally coded.

#Abstract

This SEP defines a format for communicating about a contract's interface, as well as a common location to store the contract interface inside the Wasm files.

#Specification

#Wasm Custom Section

The contract interface is stored in one contractspecv0 Wasm custom section of the contract Wasm file.

#XDR Encoding

Each entry of the contract interface is structured and encoded using the SCSpecEntry type.

When encoding entries and storing them in the custom section they should be binary XDR encoded, appended to one another with no frame, no header, no delimiter, no prefix, including no length prefix. They should be in effect a stream of SCSpecEntry XDR binary encoded values.

Each SCSpecEntry describes a function, an event, or a user-defined type.

The following XDR types are specified:

const SC_SPEC_DOC_LIMIT = 1024;

enum SCSpecType
{
    SC_SPEC_TYPE_VAL = 0,

    // Types with no parameters.
    SC_SPEC_TYPE_BOOL = 1,
    SC_SPEC_TYPE_VOID = 2,
    SC_SPEC_TYPE_ERROR = 3,
    SC_SPEC_TYPE_U32 = 4,
    SC_SPEC_TYPE_I32 = 5,
    SC_SPEC_TYPE_U64 = 6,
    SC_SPEC_TYPE_I64 = 7,
    SC_SPEC_TYPE_TIMEPOINT = 8,
    SC_SPEC_TYPE_DURATION = 9,
    SC_SPEC_TYPE_U128 = 10,
    SC_SPEC_TYPE_I128 = 11,
    SC_SPEC_TYPE_U256 = 12,
    SC_SPEC_TYPE_I256 = 13,
    SC_SPEC_TYPE_BYTES = 14,
    SC_SPEC_TYPE_STRING = 16,
    SC_SPEC_TYPE_SYMBOL = 17,
    SC_SPEC_TYPE_ADDRESS = 19,
    SC_SPEC_TYPE_MUXED_ADDRESS = 20,

    // Types with parameters.
    SC_SPEC_TYPE_OPTION = 1000,
    SC_SPEC_TYPE_RESULT = 1001,
    SC_SPEC_TYPE_VEC = 1002,
    SC_SPEC_TYPE_MAP = 1004,
    SC_SPEC_TYPE_TUPLE = 1005,
    SC_SPEC_TYPE_BYTES_N = 1006,

    // User defined types.
    SC_SPEC_TYPE_UDT = 2000
};

struct SCSpecTypeOption
{
    SCSpecTypeDef valueType;
};

struct SCSpecTypeResult
{
    SCSpecTypeDef okType;
    SCSpecTypeDef errorType;
};

struct SCSpecTypeVec
{
    SCSpecTypeDef elementType;
};

struct SCSpecTypeMap
{
    SCSpecTypeDef keyType;
    SCSpecTypeDef valueType;
};

struct SCSpecTypeTuple
{
    SCSpecTypeDef valueTypes<12>;
};

struct SCSpecTypeBytesN
{
    uint32 n;
};

struct SCSpecTypeUDT
{
    string name<60>;
};

union SCSpecTypeDef switch (SCSpecType type)
{
case SC_SPEC_TYPE_VAL:
case SC_SPEC_TYPE_BOOL:
case SC_SPEC_TYPE_VOID:
case SC_SPEC_TYPE_ERROR:
case SC_SPEC_TYPE_U32:
case SC_SPEC_TYPE_I32:
case SC_SPEC_TYPE_U64:
case SC_SPEC_TYPE_I64:
case SC_SPEC_TYPE_TIMEPOINT:
case SC_SPEC_TYPE_DURATION:
case SC_SPEC_TYPE_U128:
case SC_SPEC_TYPE_I128:
case SC_SPEC_TYPE_U256:
case SC_SPEC_TYPE_I256:
case SC_SPEC_TYPE_BYTES:
case SC_SPEC_TYPE_STRING:
case SC_SPEC_TYPE_SYMBOL:
case SC_SPEC_TYPE_ADDRESS:
case SC_SPEC_TYPE_MUXED_ADDRESS:
    void;
case SC_SPEC_TYPE_OPTION:
    SCSpecTypeOption option;
case SC_SPEC_TYPE_RESULT:
    SCSpecTypeResult result;
case SC_SPEC_TYPE_VEC:
    SCSpecTypeVec vec;
case SC_SPEC_TYPE_MAP:
    SCSpecTypeMap map;
case SC_SPEC_TYPE_TUPLE:
    SCSpecTypeTuple tuple;
case SC_SPEC_TYPE_BYTES_N:
    SCSpecTypeBytesN bytesN;
case SC_SPEC_TYPE_UDT:
    SCSpecTypeUDT udt;
};

struct SCSpecUDTStructFieldV0
{
    string doc<SC_SPEC_DOC_LIMIT>;
    string name<30>;
    SCSpecTypeDef type;
};

struct SCSpecUDTStructV0
{
    string doc<SC_SPEC_DOC_LIMIT>;
    string lib<80>;
    string name<60>;
    SCSpecUDTStructFieldV0 fields<40>;
};

struct SCSpecUDTUnionCaseVoidV0
{
    string doc<SC_SPEC_DOC_LIMIT>;
    string name<60>;
};

struct SCSpecUDTUnionCaseTupleV0
{
    string doc<SC_SPEC_DOC_LIMIT>;
    string name<60>;
    SCSpecTypeDef type<12>;
};

enum SCSpecUDTUnionCaseV0Kind
{
    SC_SPEC_UDT_UNION_CASE_VOID_V0 = 0,
    SC_SPEC_UDT_UNION_CASE_TUPLE_V0 = 1
};

union SCSpecUDTUnionCaseV0 switch (SCSpecUDTUnionCaseV0Kind kind)
{
case SC_SPEC_UDT_UNION_CASE_VOID_V0:
    SCSpecUDTUnionCaseVoidV0 voidCase;
case SC_SPEC_UDT_UNION_CASE_TUPLE_V0:
    SCSpecUDTUnionCaseTupleV0 tupleCase;
};

struct SCSpecUDTUnionV0
{
    string doc<SC_SPEC_DOC_LIMIT>;
    string lib<80>;
    string name<60>;
    SCSpecUDTUnionCaseV0 cases<50>;
};

struct SCSpecUDTEnumCaseV0
{
    string doc<SC_SPEC_DOC_LIMIT>;
    string name<60>;
    uint32 value;
};

struct SCSpecUDTEnumV0
{
    string doc<SC_SPEC_DOC_LIMIT>;
    string lib<80>;
    string name<60>;
    SCSpecUDTEnumCaseV0 cases<50>;
};

struct SCSpecUDTErrorEnumCaseV0
{
    string doc<SC_SPEC_DOC_LIMIT>;
    string name<60>;
    uint32 value;
};

struct SCSpecUDTErrorEnumV0
{
    string doc<SC_SPEC_DOC_LIMIT>;
    string lib<80>;
    string name<60>;
    SCSpecUDTErrorEnumCaseV0 cases<50>;
};

struct SCSpecFunctionInputV0
{
    string doc<SC_SPEC_DOC_LIMIT>;
    string name<30>;
    SCSpecTypeDef type;
};

struct SCSpecFunctionV0
{
    string doc<SC_SPEC_DOC_LIMIT>;
    SCSymbol name;
    SCSpecFunctionInputV0 inputs<10>;
    SCSpecTypeDef outputs<1>;
};

enum SCSpecEventParamLocationV0
{
    SC_SPEC_EVENT_PARAM_LOCATION_DATA = 0,
    SC_SPEC_EVENT_PARAM_LOCATION_TOPIC_LIST = 1
};

struct SCSpecEventParamV0
{
    string doc<SC_SPEC_DOC_LIMIT>;
    string name<30>;
    SCSpecTypeDef type;
    SCSpecEventParamLocationV0 location;
};

enum SCSpecEventDataFormat
{
    SC_SPEC_EVENT_DATA_FORMAT_SINGLE_VALUE = 0,
    SC_SPEC_EVENT_DATA_FORMAT_VEC = 1,
    SC_SPEC_EVENT_DATA_FORMAT_MAP = 2
};

struct SCSpecEventV0
{
    string doc<SC_SPEC_DOC_LIMIT>;
    string lib<80>;
    SCSymbol name;
    SCSymbol prefixTopics<2>;
    SCSpecEventParamV0 params<50>;
    SCSpecEventDataFormat dataFormat;
};

enum SCSpecEntryKind
{
    SC_SPEC_ENTRY_FUNCTION_V0 = 0,
    SC_SPEC_ENTRY_UDT_STRUCT_V0 = 1,
    SC_SPEC_ENTRY_UDT_UNION_V0 = 2,
    SC_SPEC_ENTRY_UDT_ENUM_V0 = 3,
    SC_SPEC_ENTRY_UDT_ERROR_ENUM_V0 = 4,
    SC_SPEC_ENTRY_EVENT_V0 = 5
};

union SCSpecEntry switch (SCSpecEntryKind kind)
{
case SC_SPEC_ENTRY_FUNCTION_V0:
    SCSpecFunctionV0 functionV0;
case SC_SPEC_ENTRY_UDT_STRUCT_V0:
    SCSpecUDTStructV0 udtStructV0;
case SC_SPEC_ENTRY_UDT_UNION_V0:
    SCSpecUDTUnionV0 udtUnionV0;
case SC_SPEC_ENTRY_UDT_ENUM_V0:
    SCSpecUDTEnumV0 udtEnumV0;
case SC_SPEC_ENTRY_UDT_ERROR_ENUM_V0:
    SCSpecUDTErrorEnumV0 udtErrorEnumV0;
case SC_SPEC_ENTRY_EVENT_V0:
    SCSpecEventV0 eventV0;
};

Ref: https://github.com/stellar/stellar-xdr/blob/curr/Stellar-contract-spec.x

Note: This proposal references other XDR types, such as the ContractEvent XDR structure, that are not defined by this proposal. All other XDR types are defined by the Stellar protocol. Examples showing the structure of the other XDR is included to illustrate how the proposal interacts with the Stellar protocol. It is accurate as of Protocol 23, and may change over time.

#XDR Common Fields

Many of the XDR types that comprise the format of the contract interface have common fields.

The doc field is a human readable description of the type, field, or function. It is intended to be rendered into generated client code, or tooling, such that users and developers can understand the purpose of the type, field, or function.

The name field is the name of the type, field, or function. It is intended to be used in generated client code, or tooling, as the identifier for the type, field, or function.

The lib field is the name of the library that the type was imported from. It is mostly only usedul for contract SDK implementations that support importing the original library the type was defined in.

#XDR Spec Entry Kinds

#SC_SPEC_ENTRY_FUNCTION_V0

A function spec entry describes a contract function exported and callable.

The function name must match a name in the contract Wasm function table, with the same number of parameters as inputs, and the same number of return values as outputs.

The inputs field is a list of the function's input parameters.

The outputs field is a list of the function's return values.

struct SCSpecFunctionV0
{
    string doc<SC_SPEC_DOC_LIMIT>;
    SCSymbol name;
    SCSpecFunctionInputV0 inputs<10>;
    SCSpecTypeDef outputs<1>;
};

Each input parameter is described by the SCSpecFunctionInputV0 struct.

The type field is the type of the input parameter.

struct SCSpecFunctionInputV0
{
    string doc<SC_SPEC_DOC_LIMIT>;
    string name<30>;
    SCSpecTypeDef type;
};
Example

In the Soroban Rust SDK the above structure describes a function such as:

#[contractimpl]
impl MyContract {
    /// My function description.
    pub fn my_function(input: u64) -> Result<u64, Error> {
        ...
    }
}

Which will be encoded to the following SCSpecEntry XDR when stored in the custom section. The SCSpecEntry expressed as XDR-JSON:

{
  "function_v0": {
    "doc": "My function description.",
    "name": "my_function",
    "inputs": [
      {
        "doc": "",
        "name": "input",
        "type_": "u64"
      }
    ],
    "outputs": [
      {
        "result": {
          "ok_type": "u64",
          "error_type": "error"
        }
      }
    ]
  }
}

#SC_SPEC_ENTRY_UDT_STRUCT_V0

A user-defined type struct spec entry describes a user-defined type that has the properties of a Rust struct and that is used as a function parameter, or as a type within some other type that is a function parameter.

The fields field is a list of named fields.

struct SCSpecUDTStructV0
{
    string doc<SC_SPEC_DOC_LIMIT>;
    string lib<80>;
    string name<60>;
    SCSpecUDTStructFieldV0 fields<40>;
};

Each field is described by the SCSpecUDTStructFieldV0 struct.

The type field is the type of the field.

struct SCSpecUDTStructFieldV0
{
    string doc<SC_SPEC_DOC_LIMIT>;
    string name<30>;
    SCSpecTypeDef type;
};

A contract expecting this type, expects a SCVal to be a SCV_MAP, with each field stored in a map entry where the key is an SCVal SCV_SYMBOL containing the field name and the value is an SCVal containing the field value.

Example

In the Soroban Rust SDK the above structure describes a type such as:

/// My struct description.
#[contracttype]
pub struct MyStruct {
    /// My field1 description.
    pub field1: u64,
    /// My field2 description.
    pub field2: String,
}

Which will be encoded to the following SCSpecEntry XDR when stored in the custom section. The SCSpecEntry expressed as XDR-JSON:

{
  "udt_struct_v0": {
    "doc": "My struct description.",
    "lib": "",
    "name": "MyStruct",
    "fields": [
      {
        "doc": "My field1 description.",
        "name": "field1",
        "type_": "u64"
      },
      {
        "doc": "My field2 description.",
        "name": "field2",
        "type_": "String"
      }
    ]
  }
}

A contract expecting this type, expects the SCVal to be a SCV_MAP with two entries:

  1. A SCMapEntry with a key of SCV_SYMBOL field1, and value of SCV_U64.
  2. A SCMapEntry with a key of SCV_SYMBOL field2, and value of SCV_STRING.

For example, the SCVal in XDR, expressed in XDR-JSON:

{
  "map": [
    {
      "key": {
        "symbol": "field1"
      },
      "val": {
        "u64": 1
      }
    },
    {
      "key": {
        "symbol": "field2"
      },
      "val": {
        "string": "hello"
      }
    }
  ]
}

#SC_SPEC_ENTRY_UDT_UNION_V0

A user-defined type union spec entry describes a user-defined type that has the properties of a Rust enum with data containing variants and that is used as a function parameter, or as a type within some other type that is a function parameter.

The cases field is a list of union cases.

struct SCSpecUDTUnionV0
{
    string doc<SC_SPEC_DOC_LIMIT>;
    string lib<80>;
    string name<60>;
    SCSpecUDTUnionCaseV0 cases<50>;
};

Each case can be either a void case (no data) or a tuple case (contains data).

union SCSpecUDTUnionCaseV0 switch (SCSpecUDTUnionCaseV0Kind kind)
{
case SC_SPEC_UDT_UNION_CASE_VOID_V0:
    SCSpecUDTUnionCaseVoidV0 voidCase;
case SC_SPEC_UDT_UNION_CASE_TUPLE_V0:
    SCSpecUDTUnionCaseTupleV0 tupleCase;
};

A contract expecting this type, expects the SCVal to be a SCV_VEC, with the first element an SCV_SYMBOL containing the name of the variant, and the remaining elements the tuple values if any.

Example

In the Soroban Rust SDK the above structure describes a type such as:

/// My union description.
#[contracttype]
pub enum MyUnion {
    /// No data variant.
    NoData,
    /// With data variant.
    WithData(u64, String),
}

Which will be encoded to the following SCSpecEntry XDR when stored in the custom section. The SCSpecEntry expressed as XDR-JSON:

{
  "udt_union_v0": {
    "doc": "My union description.",
    "lib": "",
    "name": "MyUnion",
    "cases": [
      {
        "void_v0": {
          "doc": "No data variant.",
          "name": "NoData"
        }
      },
      {
        "tuple_v0": {
          "doc": "With data variant.",
          "name": "WithData",
          "type_": ["u64", "string"]
        }
      }
    ]
  }
}

A contract expecting this type, expects the SCVal to be a SCV_VEC.

If the value is NoData, the vec will have one element:

  1. A SCV_SYMBOL with the value NoData.

For example, the SCVal in XDR, expressed in XDR-JSON:

{
  "vec": [
    {
      "symbol": "NoData"
    }
  ]
}

If the value is WithData, the vec will have three elements:

  1. A SCV_SYMBOL with the value WithData.
  2. A SCV_U64 with the first tuple value.
  3. A SCV_STRING with the second tuple value.

For example, the SCVal in XDR, expressed in XDR-JSON:

{
  "vec": [
    {
      "symbol": "WithData"
    },
    {
      "u64": 1
    },
    {
      "string": "hello"
    }
  ]
}

#SC_SPEC_ENTRY_UDT_ENUM_V0

A user-defined type enum spec entry describes a user-defined type that has the properties of a Rust enum with C-like integer values and that is used as a function parameter, or as a type within some other type that is a function parameter.

The cases field is a list of enum cases.

struct SCSpecUDTEnumV0
{
    string doc<SC_SPEC_DOC_LIMIT>;
    string lib<80>;
    string name<60>;
    SCSpecUDTEnumCaseV0 cases<50>;
};

Each case is described by the SCSpecUDTEnumCaseV0 struct.

struct SCSpecUDTEnumCaseV0
{
    string doc<SC_SPEC_DOC_LIMIT>;
    string name<60>;
    uint32 value;
};

A contract expecting this type, expects the SCVal to be a SCV_U32 containing one of the values included in the enum's cases.

Example

In the Soroban Rust SDK the above structure describes a type such as:

/// My enum description.
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[repr(u32)]
pub enum Color {
    /// Red color.
    Red = 1,
    /// Green color.
    Green = 2,
    /// Blue color.
    Blue = 3,
}

Which will be encoded to the following SCSpecEntry XDR when stored in the custom section. The SCSpecEntry expressed as XDR-JSON:

{
  "udt_enum_v0": {
    "doc": "My enum description.",
    "lib": "",
    "name": "Color",
    "cases": [
      {
        "doc": "Red color.",
        "name": "Red",
        "value": 1
      },
      {
        "doc": "Green color.",
        "name": "Green",
        "value": 2
      },
      {
        "doc": "Blue color.",
        "name": "Blue",
        "value": 3
      }
    ]
  }
}

A contract expecting this type, expects the SCVal to be a SCV_U32.

  1. If the value is Red, the value will be 1.
  2. If the value is Green, the value will be 2.
  3. If the value is Blue, the value will be 3.

For example, the SCVal in XDR, expressed in XDR-JSON:

{
  "u32": 1
}

#SC_SPEC_ENTRY_UDT_ERROR_ENUM_V0

A user-defined type error enum spec entry describes a user-defined type that has the properties of a Rust enum with C-like integer values and is marked as being used for errors. It is used as a function parameter, or as a type within some other type that is a function parameter.

The cases field is a list of error enum cases.

struct SCSpecUDTErrorEnumV0
{
    string doc<SC_SPEC_DOC_LIMIT>;
    string lib<80>;
    string name<60>;
    SCSpecUDTErrorEnumCaseV0 cases<50>;
};

Each case is described by the SCSpecUDTErrorEnumCaseV0 struct.

struct SCSpecUDTErrorEnumCaseV0
{
    string doc<SC_SPEC_DOC_LIMIT>;
    string name<60>;
    uint32 value;
};

A contract emitting this value into diagnostic logs, will emit a SCVal of type SCV_ERROR with the SCError having type SCE_CONTRACT and the contractCode set to the value of the error enum case.

Example

In the Soroban Rust SDK the above structure describes a type such as:

/// My error enum description.
#[contracterror]
#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[repr(u32)]
pub enum Error {
    /// Invalid input error.
    InvalidInput = 1,
    /// Insufficient funds error.
    InsufficientFunds = 2,
    /// Unauthorized error.
    Unauthorized = 3,
}

Which will be encoded to the following SCSpecEntry XDR when stored in the custom section. The SCSpecEntry expressed as XDR-JSON:

{
  "udt_error_enum_v0": {
    "doc": "My error enum description.",
    "lib": "",
    "name": "Error",
    "cases": [
      {
        "doc": "Invalid input error.",
        "name": "InvalidInput",
        "value": 1
      },
      {
        "doc": "Insufficient funds error.",
        "name": "InsufficientFunds",
        "value": 2
      },
      {
        "doc": "Unauthorized error.",
        "name": "Unauthorized",
        "value": 3
      }
    ]
  }
}

A contract emitting this value into diagnostic logs, will emit a SCVal of type SCV_ERROR with the SCError having type SCE_CONTRACT and:

  1. If the value is InvalidInput, the contractCode will be 1.
  2. If the value is InsufficientFunds, the contractCode will be 2.
  3. If the value is Unauthorized, the contractCode will be 3.

For example, the SCVal in XDR, expressed in XDR-JSON:

{
  "error": {
    "contract": 1
  }
}

#SC_SPEC_ENTRY_EVENT_V0

An event spec entry describes an event that may be published by the contract.

The prefixTopics field contains a list of static values that are included as the first topic(s) of the event. The static values are defined in the spec as SCSymbol. Event parsers should use the static values to distinguish one type of event from another, and to match on events when filtering or mapping raw events to their specified equivalents. When matching, parsers should tolerate static topics being of the SCVal type SCV_SYMBOL or SCV_STRING because some contracts have emitted their topics as strings.

The params field is a list of the event's parameters, which are dynamic values that will be included in the published event. When an event is published to the ContractEvent XDR, event parameters are located either in the event's topic list or in the event's data payload. Event parameters located in the event's topic list appear after the prefixTopics. Event parameters located in the event's data are structured according to the dataFormat field. Event parsers can use the types of parameters to distinguish one type of event from another in the case where events share the same prefix topics, as is the case in some contract interfaces, e.g. SEP-41.

The dataFormat field specifies how parameters marked with location DATA are encoded in the event's data SCVal field.

struct SCSpecEventV0
{
    string doc<SC_SPEC_DOC_LIMIT>;
    string lib<80>;
    SCSymbol name;
    SCSymbol prefixTopics<2>;
    SCSpecEventParamV0 params<50>;
    SCSpecEventDataFormat dataFormat;
};

Each event parameter is described by the SCSpecEventParamV0 struct.

The location field specifies whether the parameter is stored in the event's data value (DATA) or topic list (TOPIC_LIST).

struct SCSpecEventParamV0
{
    string doc<SC_SPEC_DOC_LIMIT>;
    string name<30>;
    SCSpecTypeDef type;
    SCSpecEventParamLocationV0 location;
};
enum SCSpecEventParamLocationV0
{
    SC_SPEC_EVENT_PARAM_LOCATION_DATA = 0,
    SC_SPEC_EVENT_PARAM_LOCATION_TOPIC_LIST = 1
};

The data value of the event is structured according to the SCSpecEventDataFormat.

  • SINGLE_VALUE: The data value contains a single SCVal of any type. The event may have exactly zero or one parameters that are located in data. If the event has zero parameters located in data the value should be an SCV_VOID. If the event has one parameter located in data, the value should be the value of the parameter.
  • VEC: The data value contains an SCV_VEC with the values of the data parameters in declaration order.
  • MAP: The data value contains an SCV_MAP with each parameter stored in a map entry with the key being the parameter name stored as SCV_SYMBOL and the value being the parameter value.
enum SCSpecEventDataFormat
{
    SC_SPEC_EVENT_DATA_FORMAT_SINGLE_VALUE = 0,
    SC_SPEC_EVENT_DATA_FORMAT_VEC = 1,
    SC_SPEC_EVENT_DATA_FORMAT_MAP = 2
};
Example

In the Soroban Rust SDK the above structure describes an event such as:

/// Transfer event published when tokens are transferred.
#[contractevent]
pub struct Transfer {
    /// The sender of the tokens.
    #[topic] pub from: Address,
    /// The recipient of the tokens.
    #[topic] pub to: Address,
    /// The amount transferred.
    pub amount: i128,
    /// The muxed identified of the recipient.
    pub to_muxed_id: u64,
}

The schema will be encoded to the following SCSpecEntry XDR when stored in the custom section. The SCSpecEntry expressed as XDR-JSON:

{
  "event_v0": {
    "doc": "Transfer event published when tokens are transferred.",
    "lib": "",
    "name": "Transfer",
    "prefixTopics": ["transfer"],
    "params": [
      {
        "doc": "The sender of the tokens.",
        "name": "from",
        "type": "address",
        "location": "topic_list"
      },
      {
        "doc": "The recipient of the tokens.",
        "name": "to",
        "type": "address",
        "location": "topic_list"
      },
      {
        "doc": "The amount transferred.",
        "name": "amount",
        "type": "i128",
        "location": "data"
      },
      {
        "doc": "The muxed identifier of the recipient.",
        "name": "to_muxed_id",
        "type": "i128",
        "location": "data"
      }
    ],
    "data_format": "map"
  }
}

When used to publish an event:

Transfer {
    from: Address::from_str("GB..."),
    to: Address::from_str("GC..."),
    amount: 123,
    to_muxed_id: 456,
}.publish(env);

It is equivalent to:

let from = Address::from_str(env, "GA...");
let to = Address::from_str(env, "GB...");
let amount = 123i128;
let to_muxed_id = 456u64;
env.events().publish(
    // Topics:
    (symbol_short!("transfer"), from, to),
    // Data:
    Map::<Symbol, Val>::from_array(
        env,
        [
            (symbol_short!("amount"), amount.into_val(env)),
            (Symbol::new(env, "to_muxed_id"), to_muxed_id.into_val(env)),
        ],
    ),
);

And maps to the following ContractEvent XDR:

{
  "ext": "v0",
  "contract_id": "CA...",
  "type_": "contract",
  "body": {
    "v0": {
      "topics": [
        { "symbol": "transfer" },
        { "address": "GB..." },
        { "address": "GC..." }
      ],
      "data": {
        "map": [
          {
            "key": { "symbol": "amount" },
            "val": { "i128": "123" }
          },
          {
            "key": { "symbol": "to_muxed_id" },
            "val": { "u64": "456" }
          }
        ]
      }
    }
  }
}

Note: The Soroban Rust SDK, as demonstrated above, defaults to setting a single prefixTopics that is the snake_case transformation of the struct name. This is not a feature of this proposal, but a feature of the SDK. The name of the struct and the prefix topics are distinct in the SCSpecEventV0 XDR. The default behaviour can be changed. See the SDK documentation for more examples. For any other SDKs or tooling that generate an event specification from a type, it is recommended to follow the same pattern that encourages a good default of having each event identifiable by the first topic based off the name. When doing so, transform the type name into snake_case format for consistency with the Soroban Rust SDK.

#XDR Spec Types

This section describes all the types that can be used in function parameters, return values, and as fields in user-defined types.

#SC_SPEC_TYPE_VAL

A generic value type that could be any type. This is typically used when the contract needs to accept any type of value.

A contract expecting this type, expects a SCVal of any variant.

#SC_SPEC_TYPE_BOOL

A boolean type that can be either true or false.

A contract expecting this type, expects a SCVal to be a SCV_BOOL.

For example, the SCVal in XDR, expressed in XDR-JSON:

{
  "bool": true
}

#SC_SPEC_TYPE_VOID

A void type that represents the absence of a value. This is typically used for functions that do not return anything.

A contract expecting this type, expects the SCVal to be a SCV_VOID.

For example, the SCVal in XDR, expressed in XDR-JSON:

"void"

#SC_SPEC_TYPE_ERROR

A generic error type. This is typically used to indicate that a function can return an error without specifying the exact error type.

A contract expecting this type, expects the SCVal to be a SCV_ERROR.

For example, the SCVal in XDR, expressed in XDR-JSON:

{
  "error": {
    "contract": 1
  }
}

#SC_SPEC_TYPE_U32

An unsigned 32-bit integer.

A contract expecting this type, expects the SCVal to be a SCV_U32.

For example, the SCVal in XDR, expressed in XDR-JSON:

{
  "u32": 1
}

#SC_SPEC_TYPE_I32

A signed 32-bit integer.

A contract expecting this type, expects the SCVal to be a SCV_I32.

For example, the SCVal in XDR, expressed in XDR-JSON:

{
  "i32": 1
}

#SC_SPEC_TYPE_U64

An unsigned 64-bit integer.

A contract expecting this type, expects the SCVal to be a SCV_U64.

For example, the SCVal in XDR, expressed in XDR-JSON:

{
  "u64": 1
}

#SC_SPEC_TYPE_I64

A signed 64-bit integer.

A contract expecting this type, expects the SCVal to be a SCV_I64.

For example, the SCVal in XDR, expressed in XDR-JSON:

{
  "i64": 1
}

#SC_SPEC_TYPE_TIMEPOINT

A point in time represented as the number of seconds since the Unix epoch (January 1, 1970 00:00:00 UTC).

A contract expecting this type, expects the SCVal to be a SCV_TIMEPOINT.

For example, the SCVal in XDR, expressed in XDR-JSON:

{
  "timepoint": 1
}

#SC_SPEC_TYPE_DURATION

A duration represented as the number of seconds.

A contract expecting this type, expects the SCVal to be a SCV_DURATION.

For example, the SCVal in XDR, expressed in XDR-JSON:

{
  "duration": 1
}

#SC_SPEC_TYPE_U128

An unsigned 128-bit integer.

A contract expecting this type, expects the SCVal to be a SCV_U128.

For example, the SCVal in XDR, expressed in XDR-JSON:

{
  "u128": {
    "hi": 0,
    "lo": 1
  }
}

#SC_SPEC_TYPE_I128

A signed 128-bit integer.

A contract expecting this type, expects the SCVal to be a SCV_I128.

For example, the SCVal in XDR, expressed in XDR-JSON:

{
  "i128": {
    "hi": 0,
    "lo": 1
  }
}

#SC_SPEC_TYPE_U256

An unsigned 256-bit integer.

A contract expecting this type, expects the SCVal to be a SCV_U256.

For example, the SCVal in XDR, expressed in XDR-JSON:

{
  "u256": {
    "hi_hi": 0,
    "hi_lo": 0,
    "lo_hi": 0,
    "lo_lo": 1
  }
}

#SC_SPEC_TYPE_I256

A signed 256-bit integer.

A contract expecting this type, expects the SCVal to be a SCV_I256.

For example, the SCVal in XDR, expressed in XDR-JSON:

{
  "i256": {
    "hi_hi": 0,
    "hi_lo": 0,
    "lo_hi": 0,
    "lo_lo": 1
  }
}

#SC_SPEC_TYPE_BYTES

A variable-length array of bytes.

A contract expecting this type, expects the SCVal to be a SCV_BYTES.

For example, the SCVal in XDR, expressed in XDR-JSON:

{
  "bytes": "00000001"
}

#SC_SPEC_TYPE_STRING

An unencoded string.

A contract expecting this type, expects the SCVal to be a SCV_STRING.

For example, the SCVal in XDR, expressed in XDR-JSON:

{
  "string": "hello"
}

#SC_SPEC_TYPE_SYMBOL

A symbol is a string-like type that is optimized for equality comparison rather than content manipulation.

A contract expecting this type, expects the SCVal to be a SCV_SYMBOL.

For example, the SCVal in XDR, expressed in XDR-JSON:

{
  "symbol": "hello"
}

#SC_SPEC_TYPE_ADDRESS

An address in the Stellar network.

A contract expecting this type, expects the SCVal to be a SCV_ADDRESS with the contained SCAddress being of variant SC_ADDRESS_TYPE_ACCOUNT or SC_ADDRESS_TYPE_CONTRACT.

For example, the SCVal in XDR, expressed in XDR-JSON:

{
  "address": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"
}
{
  "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4"
}

#SC_SPEC_TYPE_MUXED_ADDRESS

A muxed or virtual address in the Stellar network.

A contract expecting this type, expects the SCVal to be a SCV_ADDRESS with the contained SCAddress being of variant SC_ADDRESS_TYPE_ACCOUNT, SC_ADDRESS_TYPE_CONTRACT, or SC_ADDRESS_TYPE_MUXED_ACCOUNT.

For example, the SCVal in XDR, expressed in XDR-JSON:

{
  "address": "MAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB5IG"
}

#SC_SPEC_TYPE_OPTION

An option type that represents either a value of the specified type or no value (None/null).

struct SCSpecTypeOption
{
    SCSpecTypeDef valueType;
};

A contract expecting this type, expects the SCVal to be either:

  • SCV_VOID, meaning the value is not set.
  • The SCVal type specified by the valueType field.

For example, the SCVal in XDR when not set, expressed in XDR-JSON:

"void"

For example, the SCVal in XDR when set with valueType SC_SPEC_TYPE_U64, expressed in XDR-JSON:

{
  "u64": 1
}

#SC_SPEC_TYPE_RESULT

A result type that represents either a success value of one type or an error value of another type.

struct SCSpecTypeResult
{
    SCSpecTypeDef okType;
    SCSpecTypeDef errorType;
};

A contract expecting this type, expects the SCVal to be either:

  • The SCVal type specified by the okType. The value of okType must not be an SCV_ERROR.
  • The SCVal type specified by the errorType. The value of errorType must be of a type, either the SC_SPEC_TYPE_ERROR or SC_SPEC_TYPE_UDT referencing a user-defined error type. In both cases mapping to a SCV_ERROR.

For example, the SCVal in XDR when ok with the okType SC_SPEC_TYPE_U64, expressed in XDR-JSON:

{
  "u64": 1
}

For example, the SCVal in XDR when error, expressed in XDR-JSON:

{
  "error": {
    "contract": 1
  }
}

#SC_SPEC_TYPE_VEC

A vector type that represents a collection of elements of the same type.

Note that the type may be SC_SPEC_TYPE_VAL to represent a vector of any type.

struct SCSpecTypeVec
{
    SCSpecTypeDef elementType;
};

A contract expecting this type, expects the SCVal to be a SCV_VEC with all elements of the elementType.

Contracts might not validate that the elements are all the elementType.

For example, the SCVal in XDR when elementType SC_SPEC_TYPE_U64, expressed in XDR-JSON:

{
  "vec": [
    {
      "u64": 1
    },
    {
      "u64": 2
    }
  ]
}

#SC_SPEC_TYPE_MAP

A map type that represents a collection of key-value pairs where all keys have the same type and all values have the same type.

Note that either or both the key and value type may be SC_SPEC_TYPE_VAL to specify that they have no type constraint.

struct SCSpecTypeMap
{
    SCSpecTypeDef keyType;
    SCSpecTypeDef valueType;
};

A contract expecting this type, expects the SCVal to be a SCV_MAP with all keys of the keyType and all values of the valueType. Note that contracts might not validate that the map entry key and values are of the specified types.

For example, the SCVal in XDR when keyType SC_SPEC_TYPE_SYMBOL and valueType SC_SPEC_TYPE_U64, expressed in XDR-JSON:

{
  "map": [
    {
      "key": {
        "symbol": "key"
      },
      "val": {
        "u64": 1
      }
    }
  ]
}

#SC_SPEC_TYPE_TUPLE

A tuple type that represents a fixed-size collection of elements of potentially different types.

struct SCSpecTypeTuple
{
    SCSpecTypeDef valueTypes<12>;
};

A contract expecting this type, expects the SCVal to be a SCV_VEC with each entry having the type as specified by the type specified at the same index in the valuesTypes vararray.

For example, the SCVal in XDR when valueTypes SC_SPEC_TYPE_SYMBOL and SC_SPEC_TYPE_U64, expressed in XDR-JSON:

{
  "vec": [
    {
      "symbol": "hello"
    },
    {
      "u64": 1
    }
  ]
}

#SC_SPEC_TYPE_BYTES_N

A fixed-size array of bytes.

struct SCSpecTypeBytesN
{
    uint32 n;
};

A contract expecting this type, expects the SCVal to be a SCV_BYTES with the length being exactly n.

For example, the SCVal in XDR when n is 32, expressed in XDR-JSON:

{
  "bytes": "0000000000000000000000000000000000000000000000000000000000000000"
}

#SC_SPEC_TYPE_UDT

A user-defined type. This is a reference to a type that is defined elsewhere in the contract spec.

struct SCSpecTypeUDT
{
    string name<60>;
};

A contract expecting this type, expects the SCVal to be the type as specified by the user-defined type with the same name as the name field. See the user-defined type sections for how they are structured:

#Example Usage

#Soroban Rust SDK (soroban-sdk)

Contract specs are automatically inserted in code with the Rust soroban-sdk by using the contractimpl, contracttype, and contracterror macros.

#Stellar CLI (stellar-cli)

Contract specs are inspectable when using the Stellar CLI stellar contract info interface command.

#Limitations

#No Claims to SEP Implementations

This proposal does not support a contract claiming to implement any specific interface. SEP-47 provides a way for off-chain systems for discover which SEPs a contract intends to implement.

#Design Rationale

#Custom Sections

The Soroban Environment uses custom sections to store meta information about a contract's Soroban Environment compatibility. That was documented in CAP-46-1.

Custom sections are additional binary sections of Wasm files that can be used for any purpose. The Wasm when executing does not have access to the custom section and is not affected by it.

#XDR Encoding

The XDR encoding is the format used to encode contract interface data. XDR (External Data Representation) is a standard data serialization format that ensures data compatibility across different computer architectures and systems. It is the same format used by the Stellar network for encoding transactions and other data structures.

The contract interface uses XDR to encode the structure of functions and types, allowing tools, SDKs, and clients to reliably decode and understand a contract's interface regardless of the implementation details or the platform used to build the contract.

The XDR is extendable at multiple points and other types other than strings can be trivially added when required. The schema includes version tags in enum discriminants to support backward compatibility as the specification evolves.

#XDR Stream Encoding

The entries are stream encoded, by appending them one after the other without frame, header, or prefix for compatibility with building the custom sections with the Rust compiler. The Rust compiler allows Rust code to embed data encoded at compile time into the Wasm custom section by using the #[link_section = "custom-section-name"] attribute on static byte arrays. When multiple static byte arrays specify the same link section the bytes are appended to any existing section.

XDR types when encoded have a well defined size because every element in an XDR type is either fixed size, contains a length field to specify the size, or is prefixed with discriminants that deterministically branch on selecting the size through the next type. As such no length prefix or counter are required.

#Security Concerns

Contracts may contain spec entries that do not align with the actual functions exported by the contract. A contract may include spec entries for funtions that do not exist. Or a contract may omit spec entries for functions that do exist.

#Changelog

  • v1.1.0: Add support for event specifications.
  • v1.0.0: Status changed to Active.
  • v0.1.0: Initial draft capturing the status quo as implemented in soroban-sdk.