sigil-go package will eventually expose.
Install
go mod init github.com/yourorg/sigil-quickstart
go get github.com/zeebo/blake3
Configuration
Use a small struct rather thaninit()-time globals so tests can stub it.
package sigil
import "time"
type Network struct {
ChainID string
RPCURL string
Timeout time.Duration
}
var (
Mainnet = Network{
ChainID: "sigil-mainnet-1",
RPCURL: "https://rpc.sigil.ml/rpc",
Timeout: 30 * time.Second,
}
Testnet = Network{
ChainID: "sigil-testnet-1",
RPCURL: "https://testnet-rpc.sigil.ml/rpc",
Timeout: 30 * time.Second,
}
)
export SIGIL_RPC_URL=https://testnet-rpc.sigil.ml/rpc
export SIGIL_CHAIN_ID=sigil-testnet-1
export SIGIL_SIGNING_KEY_HEX=... # 64 hex chars (Ed25519 seed)
export SIGIL_SENDER_DID=did:oas:sigil:agent:...
Identity
package sigil
import (
"crypto/ed25519"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"github.com/zeebo/blake3"
)
type Identity struct {
DID string
PublicKey ed25519.PublicKey
PrivateKey ed25519.PrivateKey
}
func NewAgent() (*Identity, error) {
pub, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return nil, fmt.Errorf("generate ed25519 key: %w", err)
}
addr := blake3.Sum256(pub)
return &Identity{
DID: fmt.Sprintf("did:oas:sigil:agent:%s", hex.EncodeToString(addr[:])),
PublicKey: pub,
PrivateKey: priv,
}, nil
}
func LoadAgent(seedHex string) (*Identity, error) {
seed, err := hex.DecodeString(seedHex)
if err != nil {
return nil, fmt.Errorf("decode seed: %w", err)
}
if len(seed) != ed25519.SeedSize {
return nil, errors.New("seed must be 32 bytes")
}
priv := ed25519.NewKeyFromSeed(seed)
pub, ok := priv.Public().(ed25519.PublicKey)
if !ok {
return nil, errors.New("public key extraction failed")
}
addr := blake3.Sum256(pub)
return &Identity{
DID: fmt.Sprintf("did:oas:sigil:agent:%s", hex.EncodeToString(addr[:])),
PublicKey: pub,
PrivateKey: priv,
}, nil
}
Reading chain state
Build a singleCall helper, then layer typed methods on top.
package sigil
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
)
type rpcRequest struct {
JSONRPC string `json:"jsonrpc"`
ID int `json:"id"`
Method string `json:"method"`
Params any `json:"params"`
}
type rpcResponse struct {
Result json.RawMessage `json:"result"`
Error *struct {
Code int `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
type RPCError struct {
Code int
Message string
}
func (e *RPCError) Error() string {
return fmt.Sprintf("rpc %d: %s", e.Code, e.Message)
}
func Call[T any](
ctx context.Context,
client *http.Client,
network Network,
method string,
params any,
) (T, error) {
var zero T
payload, err := json.Marshal(rpcRequest{
JSONRPC: "2.0", ID: 1, Method: method, Params: params,
})
if err != nil {
return zero, fmt.Errorf("marshal: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, network.RPCURL, bytes.NewReader(payload))
if err != nil {
return zero, fmt.Errorf("new request: %w", err)
}
req.Header.Set("content-type", "application/json")
res, err := client.Do(req)
if err != nil {
return zero, fmt.Errorf("post: %w", err)
}
defer res.Body.Close()
var envelope rpcResponse
if err := json.NewDecoder(res.Body).Decode(&envelope); err != nil {
return zero, fmt.Errorf("decode: %w", err)
}
if envelope.Error != nil {
return zero, &RPCError{Code: envelope.Error.Code, Message: envelope.Error.Message}
}
if len(envelope.Result) == 0 {
return zero, errors.New("missing result")
}
var out T
if err := json.Unmarshal(envelope.Result, &out); err != nil {
return zero, fmt.Errorf("unmarshal result: %w", err)
}
return out, nil
}
Typed queries
type BalanceResult struct {
DID string `json:"did"`
Balance uint64 `json:"balance"`
Nonce uint64 `json:"nonce"`
}
func GetBalance(
ctx context.Context, client *http.Client, network Network, did string,
) (BalanceResult, error) {
return Call[BalanceResult](ctx, client, network, "sigil_getBalance",
map[string]any{"did": did})
}
type NodeInfo struct {
Version string `json:"version"`
ChainID string `json:"chain_id"`
ProtocolVersion string `json:"protocol_version"`
}
func GetNodeInfo(ctx context.Context, client *http.Client, network Network) (NodeInfo, error) {
return Call[NodeInfo](ctx, client, network, "sigil_getNodeInfo", map[string]any{})
}
Signing and submitting transactions
The signing recipe is identical to every other SDK:- Build the canonical JSON object that matches
sigil_core::Transaction. - Prefix
sigil/transaction/v1\n. - Hash with BLAKE3 (32-byte output).
- Sign with Ed25519.
package sigil
import (
"crypto/ed25519"
"encoding/hex"
"encoding/json"
"time"
"github.com/zeebo/blake3"
)
const TxDomainTag = "sigil/transaction/v1\n"
type TxBody struct {
ChainID string `json:"chain_id"`
SenderDID string `json:"sender_did"`
Nonce uint64 `json:"nonce"`
GasLimit uint64 `json:"gas_limit"`
GasPrice uint64 `json:"gas_price"`
TxType map[string]any `json:"tx_type"`
Timestamp string `json:"timestamp"`
}
type SignedTx struct {
Tx TxBody `json:"tx"`
Signature string `json:"signature"`
SignerPublicKey string `json:"signer_public_key"`
}
func TransferTx(
chainID, senderDID, recipientDID string,
amountBaseUnits uint64,
nonce uint64,
memo *string,
) TxBody {
payload := map[string]any{
"recipient_did": recipientDID,
"amount": amountBaseUnits,
"memo": memo,
}
return TxBody{
ChainID: chainID,
SenderDID: senderDID,
Nonce: nonce,
GasLimit: 200_000,
GasPrice: 1,
TxType: map[string]any{"Transfer": payload},
Timestamp: time.Now().UTC().Format(time.RFC3339Nano),
}
}
func SignTx(tx TxBody, identity *Identity) (SignedTx, error) {
body, err := json.Marshal(tx)
if err != nil {
return SignedTx{}, err
}
transcript := append([]byte(TxDomainTag), body...)
digest := blake3.Sum256(transcript)
signature := ed25519.Sign(identity.PrivateKey, digest[:])
return SignedTx{
Tx: tx,
Signature: hex.EncodeToString(signature),
SignerPublicKey: hex.EncodeToString(identity.PublicKey),
}, nil
}
Submitting and waiting
type SendTxResult struct {
TxHash string `json:"tx_hash"`
}
func SendTransaction(
ctx context.Context, client *http.Client, network Network, signed SignedTx,
) (string, error) {
result, err := Call[SendTxResult](ctx, client, network, "sigil_sendTransaction", signed)
if err != nil {
return "", err
}
return result.TxHash, nil
}
type Receipt struct {
TxHash string `json:"tx_hash"`
BlockHeight *uint64 `json:"block_height"`
Status string `json:"status"`
GasUsed *uint64 `json:"gas_used"`
Error *string `json:"error"`
}
func WaitReceipt(
ctx context.Context, client *http.Client, network Network, txHash string,
) (Receipt, error) {
deadline := time.Now().Add(30 * time.Second)
for time.Now().Before(deadline) {
r, err := Call[Receipt](ctx, client, network, "sigil_getTransactionReceipt",
map[string]any{"tx_hash": txHash})
if err == nil && r.Status != "pending" {
return r, nil
}
select {
case <-time.After(1500 * time.Millisecond):
case <-ctx.Done():
return Receipt{}, ctx.Err()
}
}
return Receipt{}, fmt.Errorf("receipt for %s did not finalise", txHash)
}
Subscribing to events
WebSocket subscriptions are on the roadmap. Until then, pollsigil_getLatestBlock from a goroutine:
type Block struct {
Height uint64 `json:"height"`
}
func StreamBlocks(ctx context.Context, client *http.Client, network Network) <-chan Block {
ch := make(chan Block)
go func() {
defer close(ch)
var last uint64
for {
block, err := Call[Block](ctx, client, network, "sigil_getLatestBlock", map[string]any{})
if err == nil && block.Height > last {
last = block.Height
select {
case ch <- block:
case <-ctx.Done():
return
}
}
select {
case <-time.After(1500 * time.Millisecond):
case <-ctx.Done():
return
}
}
}()
return ch
}
Error handling
RPCError carries the JSON-RPC code. Inspect it before deciding to retry.
result, err := SendTransaction(ctx, client, sigil.Testnet, signed)
if err != nil {
var rpcErr *sigil.RPCError
if errors.As(err, &rpcErr) {
switch rpcErr.Code {
case -32002: // mempool full
// backoff and retry
case -32601: // method not found
return fmt.Errorf("RPC node does not support this method: %w", err)
}
}
return err
}
| Code | Meaning |
|---|---|
-32600 | Invalid JSON-RPC envelope |
-32601 | Method not found |
-32602 | Invalid params |
-32603 | Internal error (retryable) |
-32002 | Mempool full / rate-limited |
Worked example: send a payment with retry
package main
import (
"context"
"errors"
"fmt"
"log"
"net/http"
"os"
"strconv"
"time"
"github.com/yourorg/sigil-quickstart/sigil"
)
func sendPaymentWithRetry(
ctx context.Context,
client *http.Client,
network sigil.Network,
identity *sigil.Identity,
recipientDID string,
amountBaseUnits uint64,
) (string, error) {
bal, err := sigil.GetBalance(ctx, client, network, identity.DID)
if err != nil {
return "", fmt.Errorf("fetch nonce: %w", err)
}
tx := sigil.TransferTx(network.ChainID, identity.DID, recipientDID,
amountBaseUnits, bal.Nonce, nil)
signed, err := sigil.SignTx(tx, identity)
if err != nil {
return "", fmt.Errorf("sign: %w", err)
}
delay := 250 * time.Millisecond
for attempt := 0; attempt < 5; attempt++ {
hash, err := sigil.SendTransaction(ctx, client, network, signed)
if err == nil {
return hash, nil
}
var rpcErr *sigil.RPCError
if errors.As(err, &rpcErr) && (rpcErr.Code == -32002 || rpcErr.Code == -32603) && attempt < 4 {
time.Sleep(delay)
delay *= 2
continue
}
return "", err
}
return "", errors.New("unreachable")
}
func main() {
rpcURL := os.Getenv("SIGIL_RPC_URL")
if rpcURL == "" {
rpcURL = "https://testnet-rpc.sigil.ml/rpc"
}
network := sigil.Network{
ChainID: os.Getenv("SIGIL_CHAIN_ID"),
RPCURL: rpcURL,
Timeout: 30 * time.Second,
}
if network.ChainID == "" {
network.ChainID = "sigil-testnet-1"
}
identity, err := sigil.LoadAgent(os.Getenv("SIGIL_SIGNING_KEY_HEX"))
if err != nil {
log.Fatalf("load identity: %v", err)
}
recipient := os.Getenv("RECIPIENT_DID")
amount, err := strconv.ParseUint(os.Getenv("AMOUNT_BASE_UNITS"), 10, 64)
if err != nil {
log.Fatalf("AMOUNT_BASE_UNITS: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
client := &http.Client{Timeout: network.Timeout}
hash, err := sendPaymentWithRetry(ctx, client, network, identity, recipient, amount)
if err != nil {
log.Fatalf("send: %v", err)
}
fmt.Println("submitted:", hash)
}
Reference
- Wire format: JSON-RPC method index
- Canonical signing transcript:
sigil-wallet-core/src/lib.rs(Rust source — match its bytes) - Parity matrix: SDK overview