
Vara Skills
- 363 installs
- 17 repo stars
- Updated July 13, 2026
- gear-foundation/vara-skills
vara-skills is a Gear Foundation agent skill bundle for Vara blockchain development and infrastructure management tasks that developers invoke when building or operating Vara network applications.
About
vara-skills is a skill entry in gear-foundation/vara-skills described for development and infrastructure management on the Vara blockchain ecosystem. Public catalog metadata is minimal and no readme excerpt was available, so specifics should be verified in the repository before production use. Developers working on Gear Protocol or Vara-based decentralized applications may reach for it when agents need guided workflows for chain tooling, deployment, or infra tasks native to that stack. Treat confidence as moderate until the repo SKILL.md confirms commands, scripts, and prerequisites. Pair with Web3 debugging and smart-contract testing practices when extending blockchain backends.
- vara-skills
- Development
Vara Skills by the numbers
- 363 all-time installs (skills.sh)
- Ranked #1,130 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Jul 24, 2026 (Skillselion catalog sync)
npx skills add https://github.com/gear-foundation/vara-skills --skill vara-skillsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 363 |
|---|---|
| repo stars | ★ 17 |
| Last updated | July 13, 2026 |
| Repository | gear-foundation/vara-skills ↗ |
How do you build on the Vara blockchain network?
For development and infrastructure management.
Who is it for?
Developers building or operating applications on the Gear Foundation Vara blockchain who need agent-guided infra and development workflows.
Skip if: Non-blockchain .NET or web projects with no Vara or Gear Protocol integration requirements.
When should I use this skill?
Development or infrastructure work targets the Vara blockchain ecosystem and Gear Foundation tooling is in scope.
What you get
Vara development workflows, infrastructure management steps, and Gear ecosystem integration guidance
- Vara development workflows
- Infrastructure management guidance
Files
Vara Skills
This wrapper exposes the same provisional vara-skills pack to OpenClaw.
The pack is self-contained inside this repo: load the local handbook and local skills instead of depending on sibling repositories.
Start Here
Load the repo router at ../SKILL.md, then begin with ship-sails-app.
Route By Task
| Builder Need | Skill |
|---|---|
| Local Rust or Gear setup | ../skills/sails-dev-env/SKILL.md |
| New app from scratch | ../skills/sails-new-app/SKILL.md |
| Feature or behavior change in existing repo | ../skills/sails-feature-workflow/SKILL.md |
| Message flow, replies, or async behavior | ../skills/gear-message-execution/SKILL.md |
| Service or program architecture | ../skills/sails-architecture/SKILL.md |
| IDL or generated client issues | ../skills/sails-idl-client/SKILL.md |
gtest authoring or debugging | ../skills/sails-gtest/SKILL.md |
| Local-node smoke after green tests | ../skills/sails-local-smoke/SKILL.md |
| React or TypeScript frontend | ../skills/sails-frontend/SKILL.md |
| Fungible token with awesome-sails | ../skills/awesome-sails-vft/SKILL.md |
| Wallet interactions, deploy, vouchers | ../skills/vara-wallet/SKILL.md |
Scope
This pack is for standard Gear/Vara Sails builders. The catalog is provisional and should be treated as a measured candidate set, not a frozen public taxonomy.
{
"name": "vara-skills",
"owner": {
"name": "Gear Foundation"
},
"metadata": {
"version": "3.0.0",
"description": "Official Gear/Vara Sails builder skill pack marketplace for Claude Code."
},
"plugins": [
{
"name": "vara-skills",
"source": "./",
"version": "3.0.0",
"description": "Gear/Vara Sails builder skill pack for Claude Code — 24 skills for smart contract development on Vara Network.",
"author": {
"name": "Gear Foundation"
},
"homepage": "https://github.com/gear-foundation/vara-skills",
"repository": "https://github.com/gear-foundation/vara-skills",
"license": "MIT",
"keywords": ["vara", "gear", "sails", "blockchain", "smart-contracts", "claude"],
"category": "blockchain",
"tags": ["vara", "gear", "sails", "blockchain", "smart-contracts", "wasm"]
}
]
}
{
"name": "vara-skills",
"description": "Gear/Vara Sails builder skill pack for Claude Code — 24 skills for smart contract development on Vara Network.",
"author": {
"name": "Gear Foundation"
},
"homepage": "https://github.com/gear-foundation/vara-skills",
"repository": "https://github.com/gear-foundation/vara-skills",
"license": "MIT",
"keywords": ["vara", "gear", "sails", "blockchain", "smart-contracts", "claude"]
}
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
verify:
name: Verify
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Run verification suite
run: make verify
name: Release
on:
push:
tags:
- 'v*'
permissions:
contents: write
jobs:
verify:
name: Verify & Release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Run verification suite
run: make verify
- name: Extract version from tag
id: version
run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
- name: Verify tag matches marketplace.json
run: |
META_VER=$(python3 -c "import json; print(json.load(open('.claude-plugin/marketplace.json'))['metadata']['version'])")
if [ "$META_VER" != "${{ steps.version.outputs.version }}" ]; then
echo "::error::Tag version (${{ steps.version.outputs.version }}) does not match marketplace.json ($META_VER)"
exit 1
fi
- name: Verify tag matches VERSION file
run: |
FILE_VER=$(cat VERSION | tr -d '[:space:]')
if [ "$FILE_VER" != "${{ steps.version.outputs.version }}" ]; then
echo "::error::Tag version (${{ steps.version.outputs.version }}) does not match VERSION file ($FILE_VER)"
exit 1
fi
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
name: v${{ steps.version.outputs.version }}
generate_release_notes: true
draft: false
prerelease: false
.DS_Store
.worktrees/
docs/plans/
AGENTS.md
Purpose
This repository is the provisional product repo for a portable Vara Sails-builder skill pack. It owns builder-facing skill content, shared references, validation scripts, and installation surfaces for Codex first, with Claude and OpenClaw packaging added in-repo.
Working Rules
- Use
writing-skillsbefore creating or editing any skill inskills/. - Apply TDD to repository behavior: add or update tests before changing scripts, validation logic, or published skill contracts.
- Keep each skill directory flat and include
SKILL.md,assets/,references/, andscripts/. - Prefer shared repository references in
references/and shared templates inassets/; skill files may reference them via relative paths. - Do not embed machine-specific absolute paths in published docs or skills unless the file is an explicit local validation target.
- Treat the current public skill catalog as provisional until the sibling eval repo proves which candidate skills create real uplift.
- Keep the first builder path focused on standard Gear/Vara Sails, not Vara.eth-first workflows.
Verification
- Run
make verifybefore claiming the repo is ready. - Keep installation working through
scripts/install-codex-skills.sh. - Keep repository docs aligned with the product split: this repo owns skills and packaging; the sibling eval repo owns measurement.
Architecture Note
Summary
Program And Service Boundaries
State Ownership
Message Flow
Routing And Public Interface
- Existing public routes that must remain stable
- New routes introduced by this release
- Any intentionally deprecated routes
- Whether any method signature or reply shape changes are proposed
Event Contract
- Existing events that must remain stable
- Any new event surface introduced by this release
- Whether any existing event payload changes are proposed
- Whether event versioning is required
Generated Client Or IDL Impact
- Does this release require IDL regeneration
- Which clients, scripts, or tools consume the IDL
- Whether old and new generated clients must coexist during cutover
Contract Version And Status Surface
- How the contract exposes version information
- Whether the contract has lifecycle status such as
ActiveorReadOnly - Whether old-version writes must be disabled after cutover
Off-Chain Components
- Frontend program-id and config impact
- Indexer subscription or decoder impact
- Any automation or scripts affected by the new version
Release And Cutover Plan
- Deploy order
- Frontend switch strategy
- Indexer switch strategy
- Whether the old version remains queryable
- Whether writes to the old version are disabled
Failure And Recovery Paths
- Rollback target
- How to revert frontend and indexer back to the previous version
- What happens if the new version is deployed but not adopted
Open Questions
Gtest Report
Target Workspace
Commands Run
Failing Cases
Fix Summary
Final Green State
Remaining Gaps
Feature Spec
Problem
User Goal
In Scope
Out of Scope
Actors
State Changes
Messages And Replies
Events
Invariants
Edge Cases
Acceptance Criteria
Task Plan
Goal
Preconditions
Ordered Tasks
Dependencies
Verification Steps
Review Checkpoints
Rollback Notes
#!/usr/bin/env bash
# vara-skills-update-check — periodic version check for all skills.
#
# Output (one line, or nothing):
# JUST_UPGRADED <old> <new> — marker found from recent upgrade
# UPGRADE_AVAILABLE <old> <new> — remote VERSION differs from local
# (nothing) — up to date, snoozed, disabled, or check skipped
#
# Env overrides (for testing):
# VARA_SKILLS_DIR — override auto-detected skills root
# VARA_SKILLS_REMOTE_URL — override remote VERSION URL
# VARA_SKILLS_STATE_DIR — override ~/.vara-skills state directory
# VARA_SKILLS_UPDATE_CHECK — set to "false" to disable checks
set -euo pipefail
VARA_SKILLS_DIR="${VARA_SKILLS_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
STATE_DIR="${VARA_SKILLS_STATE_DIR:-$HOME/.vara-skills}"
CACHE_FILE="$STATE_DIR/last-update-check"
MARKER_FILE="$STATE_DIR/just-upgraded-from"
SNOOZE_FILE="$STATE_DIR/update-snoozed"
VERSION_FILE="$VARA_SKILLS_DIR/VERSION"
REMOTE_URL="${VARA_SKILLS_REMOTE_URL:-https://raw.githubusercontent.com/gear-foundation/vara-skills/main/VERSION}"
# ─── Force flag (busts cache + snooze) ────────────────────────
if [ "${1:-}" = "--force" ]; then
rm -f "$CACHE_FILE"
rm -f "$SNOOZE_FILE"
fi
# ─── Step 0: Check if updates are disabled ────────────────────
if [ "${VARA_SKILLS_UPDATE_CHECK:-}" = "false" ]; then
exit 0
fi
# ─── Snooze helper ────────────────────────────────────────────
# check_snooze <remote_version>
# Returns 0 if snoozed (should stay quiet), 1 if not snoozed.
#
# Snooze file format: <version> <level> <epoch>
# Level durations: 1=24h, 2=48h, 3+=7d
# New version (version mismatch) resets snooze.
check_snooze() {
local remote_ver="$1"
if [ ! -f "$SNOOZE_FILE" ]; then
return 1
fi
local snoozed_ver snoozed_level snoozed_epoch
snoozed_ver="$(awk '{print $1}' "$SNOOZE_FILE" 2>/dev/null || true)"
snoozed_level="$(awk '{print $2}' "$SNOOZE_FILE" 2>/dev/null || true)"
snoozed_epoch="$(awk '{print $3}' "$SNOOZE_FILE" 2>/dev/null || true)"
# Validate: all three fields must be non-empty
if [ -z "$snoozed_ver" ] || [ -z "$snoozed_level" ] || [ -z "$snoozed_epoch" ]; then
return 1
fi
# Validate: level and epoch must be integers
case "$snoozed_level" in *[!0-9]*) return 1 ;; esac
case "$snoozed_epoch" in *[!0-9]*) return 1 ;; esac
# New version dropped? Ignore snooze.
if [ "$snoozed_ver" != "$remote_ver" ]; then
return 1
fi
# Compute snooze duration based on level
local duration
case "$snoozed_level" in
1) duration=86400 ;; # 24 hours
2) duration=172800 ;; # 48 hours
*) duration=604800 ;; # 7 days (level 3+)
esac
local now
now="$(date +%s)"
local expires=$(( snoozed_epoch + duration ))
if [ "$now" -lt "$expires" ]; then
return 0 # still snoozed
fi
return 1 # snooze expired
}
# ─── Step 1: Read local version ──────────────────────────────
LOCAL=""
if [ -f "$VERSION_FILE" ]; then
LOCAL="$(cat "$VERSION_FILE" 2>/dev/null | tr -d '[:space:]')"
fi
if [ -z "$LOCAL" ]; then
exit 0 # No VERSION file → skip check
fi
# ─── Step 2: Check "just upgraded" marker ─────────────────────
if [ -f "$MARKER_FILE" ]; then
OLD="$(cat "$MARKER_FILE" 2>/dev/null | tr -d '[:space:]')"
rm -f "$MARKER_FILE"
rm -f "$SNOOZE_FILE"
mkdir -p "$STATE_DIR"
echo "UP_TO_DATE $LOCAL" > "$CACHE_FILE"
if [ -n "$OLD" ]; then
echo "JUST_UPGRADED $OLD $LOCAL"
fi
exit 0
fi
# ─── Step 3: Check cache freshness ───────────────────────────
# UP_TO_DATE: 60 min TTL (detect new releases quickly)
# UPGRADE_AVAILABLE: 720 min TTL (keep nagging)
if [ -f "$CACHE_FILE" ]; then
CACHED="$(cat "$CACHE_FILE" 2>/dev/null || true)"
case "$CACHED" in
UP_TO_DATE*) CACHE_TTL=60 ;;
UPGRADE_AVAILABLE*) CACHE_TTL=720 ;;
*) CACHE_TTL=0 ;; # corrupt → force re-fetch
esac
STALE=$(find "$CACHE_FILE" -mmin +$CACHE_TTL 2>/dev/null || true)
if [ -z "$STALE" ] && [ "$CACHE_TTL" -gt 0 ]; then
case "$CACHED" in
UP_TO_DATE*)
CACHED_VER="$(echo "$CACHED" | awk '{print $2}')"
if [ "$CACHED_VER" = "$LOCAL" ]; then
exit 0
fi
;;
UPGRADE_AVAILABLE*)
CACHED_OLD="$(echo "$CACHED" | awk '{print $2}')"
if [ "$CACHED_OLD" = "$LOCAL" ]; then
CACHED_NEW="$(echo "$CACHED" | awk '{print $3}')"
if check_snooze "$CACHED_NEW"; then
exit 0 # snoozed — stay quiet
fi
echo "$CACHED"
exit 0
fi
;;
esac
fi
fi
# ─── Step 4: Slow path — fetch remote version ────────────────
mkdir -p "$STATE_DIR"
REMOTE=""
REMOTE="$(curl -sf --max-time 5 "$REMOTE_URL" 2>/dev/null || true)"
REMOTE="$(echo "$REMOTE" | tr -d '[:space:]')"
# Validate: must look like a version number (reject HTML error pages)
if ! echo "$REMOTE" | grep -qE '^[0-9]+\.[0-9.]+$'; then
# Invalid or empty response — assume up to date
echo "UP_TO_DATE $LOCAL" > "$CACHE_FILE"
exit 0
fi
if [ "$LOCAL" = "$REMOTE" ]; then
echo "UP_TO_DATE $LOCAL" > "$CACHE_FILE"
exit 0
fi
# Versions differ — upgrade available
echo "UPGRADE_AVAILABLE $LOCAL $REMOTE" > "$CACHE_FILE"
if check_snooze "$REMOTE"; then
exit 0 # snoozed — stay quiet
fi
# ─── Pre-refresh marketplace clone for plugin installs ───────
# Claude Code's `plugin update` reads the local HEAD of the
# marketplace clone, not origin. If the clone is stale, `update`
# thinks the installed version is already latest. Fix: reset the
# clone to origin/main so `claude plugin update` sees the new version.
_MKT="$HOME/.claude/plugins/marketplaces/vara-skills"
if [ -d "$_MKT/.git" ]; then
git -C "$_MKT" fetch origin --quiet 2>/dev/null || true
git -C "$_MKT" reset --hard origin/main --quiet 2>/dev/null || true
fi
echo "UPGRADE_AVAILABLE $LOCAL $REMOTE"
Changelog
All notable changes to this project will be documented in this file.
[3.0.0] - 2026-05-18
Verified against sails @ 1.0.0. Promotes the pack baseline from sails-rs 0.10.3 to sails-rs 1.0.0 and adds two new ethexe skills.
Added
sails-ethexe-architecture— ethexe feature planning and boundary isolation skillsails-ethexe-implementer— ethexe-specific Rust implementation skill (Syscall gating,#[export(ethabi|payable)],emit_eth_event)
Updated
- Skill pack baseline bumped to
sails-rs 1.0.0throughout (sails-rs-imports.md,sails-cheatsheet.md,ship-sails-app/SKILL.md) - New references:
sails-syscall-mapping.md,sails-idl-v2-syntax.md,sails-header-wire-format.md,sails-ethexe-patterns.md sails-gtest-and-local-validation.mdEvent Listener Pattern updated: service client now implementsListenerdirectlysails-rust-implementer: mandatesSyscall::*for all runtime accessors; adds#[sails_type]guidance; adds ethexe hand-off guardrailgear-sails-production-patterns.md: converted toSyscall::*throughout
Fixed
@entry-idannotation (hyphen) corrected to@entry_id(underscore) insails-idl-v2-syntax.mdandsails-header-wire-format.md— parser only accepts underscore formSyscall::*inconsistency in Sails service code:sails-cheatsheet.md,awesome-sails-token-patterns.md,ship-sails-app/SKILL.mdall updated (gstd-level references kept onexec::*/msg::*)- Five broken
../../docs/relative links across four files replaced with upstream GitHub URLs (or local reference paths) - Skill count updated to 23 in
plugin.json,marketplace.json, andCLAUDE.md
[2.2.0] - 2026-04-27
vara-wallet 0.15.0 was the first npm publish since 0.10.0; 0.16.0 followed five days later with the agent-UX hardening pass. This release captures both surfaces in one cut. Skipping a separate 2.1.x for the 0.13-0.15 work because none of those versions reached npm.
Updated — vara-wallet skill, 0.13/0.14 surface
- IDL Resolution: documented the on-chain WASM extraction path for v2 programs (
gearProgram.originalCodeStorage), local cache at~/.vara-wallet/idl-cache/, and bundled VFT/Rivr DEX IDLs. v2 programs no longer need--idlafter first call. - Quick Reference: added
discoverintrospection,--dry-runpayload encoding (no signing/submit),--args-file(with stdin support),idl importfor seeding the cache,subscribe messagesIDL-aware decoding, andwatchevent streaming. - Workflow examples: dry-run preview, stdin args-file for nested JSON, IDL-aware event monitoring.
- Error Recovery: added
AMBIGUOUS_EVENT,INVALID_ARGS_SOURCE,STDIN_IS_TTY,CONFLICTING_OPTIONS,PROGRAM_ERROR(then refreshed in the 0.16 pass below).
Updated — vara-wallet skill, 0.15.0 surface
- Units vocabulary unified to
human/rawacrosstransfer,vft, anddex. Legacyvara/tokenliterals retired and now error withINVALID_UNITS. --dry-runand--estimatecompose oncall(account required); previously mutually exclusive.subscribe messages --typerenamed to--eventfor consistency withwatch.metaStorageUrlconfig key andVARA_META_STORAGEenv var removed; meta-storage IDL fallback dropped.
Updated — vara-wallet skill, 0.16.0 surface
- New "Structured Errors (0.16+)" section:
reason+programMessageJSON shape,Result::unwrapstrip, jq case-switch with pre-0.16 fallback. - Error Recovery: added
INVALID_ARGS_FORMATandINVALID_ADDRESSrows; refreshedPROGRAM_ERRORandIDL_NOT_FOUNDrows for the new structured fields and the "This is a v1 contract" diagnostic. - IDL Resolution: framed v1 path as the expected route for stable Sails 0.10.x builders, not as a v2 fallback.
- Setup: minimum vara-wallet version pinned to 0.16.0.
- Guardrails:
calculateGasfailures now classified asPROGRAM_ERRORinstead of opaque gas errors.
[2.1.0] - 2026-04-02
Updated
vara-walletskill updated for v0.9.0:--networkshorthand,configCLI,--estimate, connection timeout,--idl/--init/--argsconstructor encoding, faucet command, program list default limit, SS58 outputsails-local-smokeskill updated with--network localandconfig set network localalternativessails-gtest-and-local-validationreference updated with--network local, IDL-based deploy, multi-constructor--initflag
[2.0.0] - 2026-03-29
Changed (BREAKING)
- Baseline reverted to `sails-rs 0.10.2` (stable) on main branch. The 1.0.0-beta.2 content is preserved on the
sails-betabranch. - Reason: beta.2 ecosystem has unresolved blockers (unpublished npm packages, vara-wallet v2 IDL incompatibility, no delayed message header helper). Stable builders should not hit these issues.
- All beta-specific patterns (ReflectHash, binary header protocol, IDL V2, edition 2024) moved to
sails-betabranch - Build.rs: reverted to standalone
sails_rs::build_wasm()pattern - Troubleshooting table kept but trimmed to stable-relevant errors only
cargo sails newremains the default bootstrap command
Added
sails-betabranch created as the home for all 1.0.0-beta.2 content, including friction fixes (troubleshooting, constructor payload, delayed message versioning, JS ecosystem status)- Cross-version notes in references pointing to
sails-betabranch for beta patterns - Post-deploy verification guidance in local-smoke skill and reference
- BTreeMap key types pitfall in sails-idl-client skill
- Troubleshooting table for top 3 stable compile errors in sails-cheatsheet
[1.4.0] - 2026-03-27
Added
- Sails 1.0.0-beta baseline: ReflectHash derive pattern, Sails Header Protocol reference, IDL V2 format guide, new
#[export]options (overrides,entry_id,throws) sails-new-app: Troubleshooting section for broken scaffold recovery with fallback manual bootstrap sequencesails-idl-client: Generated Client Pitfalls section coveringno_stddouble-injection in hand-assembled workspaces and customBTreeMapkey type decoding issuessails-gtest: Common Pitfalls section covering program balance accounting with existential deposit- IDL V2 Format section in
sails-idl-client-pipeline.mdwith syntax overview (version header, Rust-like types, service-scoped types,@query,throws,@partial) cargo sails client-jsCLI andcargo sails idl -nflag documented- 0.10.x legacy notes in reference files where patterns differ from 1.0.0-beta (build.rs, ReflectHash, header protocol)
- Test assertions for new skill sections (Troubleshooting, Generated Client Pitfalls, Common Pitfalls, ReflectHash)
Changed
- Scaffold command updated from
cargo sails programtocargo sails newacross all skills, references, README, and tests (9 content locations + 3 test assertions) - Version baseline updated from
sails-rs 0.10.2tosails-rs 1.0.0-beta.1across all references and skills - Install command pinned to
cargo install sails-cli@1.0.0-beta.1 --lockedinsails-dev-env - Root program
build.rspattern updated to chainedbuild_wasm()+ClientBuilder::from_wasm_path().build_idl() - Canonical workspace layout updated with new
src/lib.rswasm re-export pattern
[1.3.1] - 2026-03-26
Added
references/voucher-and-signless-flows.md— procedural-first reference covering voucher lifecycle, signless sessions, EZ-transactions, JS API surface, on-chain extrinsics, testing guidance, and failure modes- Builder recipes for voucher-only, signless session, and full gasless+signless flows
- Cross-references to voucher doc from ship-sails-app, sails-feature-workflow, sails-frontend, sails-local-smoke, and vara-wallet skills
- Test assertions for voucher reference content, cross-references, and wiki URL leakage prevention
Changed
- Demoted inline voucher prose in ship-sails-app and sails-feature-workflow to one-line pointers to the canonical reference
- Corrected
api.voucher.existssignature to(accountId, programId)andapi.voucher.issuereturn type documentation - Added backend sponsor service as explicit prerequisite for EZ-Transactions
Fixed
- Duplicate
## Environment Contractheading in sails-frontend-and-gear-js.md
[1.3.0] - 2026-03-26
Added
- Auto-update check that runs on each skill invocation via preamble in entry-point skills
bin/vara-skills-update-checkscript with cache TTL, snooze system, and graceful degradationskills/vara-skills-upgrade/skill for inline and standalone upgrade flowsVERSIONfile as single source of truth for version, kept in sync withmarketplace.json- 15 new tests for update check script covering all code paths
make test-updatetarget and VERSION tag verification in release workflow
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
What This Repo Is
A portable, self-contained skill pack for Gear/Vara Sails smart contract builders. It ships as markdown skills, shared reference docs, and validation tooling — packaged for Claude Code (plugin), Codex, and OpenClaw.
Current version: 3.0.0 (sails-rs 1.0.0). The skill catalog is provisional until the sibling vara-skills-evals repo proves uplift.
Commands
make verify # Run the full validation suite (layout, skills, parser, install, packaging, update)
make test-layout # Repo structure checks only
make test-skills # Skill validation + catalog + gstd-api-map tests
make test-parser # gtest output parser tests
make test-install # Codex install script tests
make test-packaging # Claude plugin metadata validation
make test-update # Auto-update check script testsAll tests are Python 3 (python3 tests/<file>.py). No pip dependencies required.
Architecture
Content layers
- `SKILL.md` (root) — top-level router that dispatches by builder intent to specific skills
- `skills/<name>/SKILL.md` — individual workflow or topic skills (24 total), each may contain
assets/subdirs - `references/` — self-contained handbook covering Gear execution, Sails architecture, IDL/client pipeline, gtest patterns, and more. Skills reference these via relative paths.
- `assets/` — canonical output templates (spec, architecture, task-plan, gtest report)
- `VERSION` — single-line version file, kept in sync with
marketplace.json - `bin/vara-skills-update-check` — bash script that checks for new versions on each skill invocation. State cached in
~/.vara-skills/(cache, snooze, upgrade marker). Controlled via env vars:VARA_SKILLS_UPDATE_CHECK=falseto disable,VARA_SKILLS_AUTO_UPGRADE=truefor silent upgrades.
Packaging surfaces
- Claude Code plugin:
.claude-plugin/plugin.json+marketplace.jsondefine the plugin. Skills underskills/are loaded directly. - Codex:
scripts/install-codex-skills.shinstalls skill directories locally. - OpenClaw:
openclaw-skill/SKILL.mdwraps the same content.
Validation
scripts/validate-skill.py— validates individual skill structurescripts/parse_test_output.py— parses gtest output for evidence collectionscripts/run_gtest.sh— runs gtest and captures outputtests/— Python test suite covering repo layout, skill contracts, catalog completeness, parser correctness, install scripts, and Claude packaging metadata
Working Rules (from AGENTS.md)
- Run
make verifybefore claiming the repo is ready. - Apply TDD: add or update tests before changing scripts, validation logic, or published skill contracts.
- Keep each skill directory flat with
SKILL.mdplus optionalassets/,references/,scripts/. - Prefer shared references in
references/and templates inassets/over duplicating content in skills. - The skill catalog is provisional — only measured winning skills should remain in the eventual public pack.
- Stay on the standard Gear/Vara Sails path. Vara.eth and ethexe work uses dedicated skills, not this pack.
Skill Routing
ship-sails-app is the primary entry skill for Claude Code. It routes standard builder tasks to the correct stage:
1. Planning: idea-to-spec → gear-architecture-planner or sails-architecture → task-decomposer 2. Implementation: sails-rust-implementer, sails-idl-client 3. Verification: sails-gtest or gtest-tdd-loop → sails-local-smoke (only after green gtest)
MIT License
Copyright (c) 2026 Gear Foundation
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
.PHONY: test-layout test-skills test-parser test-install test-packaging test-update verify verify-real verify-all
test-layout:
python3 tests/test_repo_layout.py
test-skills:
python3 tests/test_skill_validation.py
python3 tests/test_skill_catalog.py
python3 tests/test_gstd_api_map_skill.py
test-parser:
python3 tests/test_parse_test_output.py
test-install:
python3 tests/test_install_codex_skills.py
test-packaging:
python3 tests/test_packaging_metadata.py
test-update:
python3 tests/test_update_check.py
verify: test-layout test-skills test-parser test-install test-packaging test-update
verify-real:
bash scripts/verify-real-sails-program.sh
verify-all: verify verify-real
vara-skills — OpenClaw Skill
Portable standard Gear/Vara Sails builder guidance for OpenClaw.
Install
Copy SKILL.md into your OpenClaw skills directory, or point OpenClaw at this repo-local wrapper.
What It Does
The wrapper sends OpenClaw to the same root router used by Codex and Claude. Start at ../SKILL.md, then route through ship-sails-app into the narrower Sails-builder skills and the repo-local handbook.
Scope
- standard Gear/Vara Sails only
- new app flow and existing feature work
- message flow, replies, delays, and reservations
- architecture, IDL/client,
gtest, local smoke, and React frontend wiring
Notes
The public website is intentionally deferred until the measured first-wave catalog is stable.
vara-skills
vara-skills is a portable, self-contained skill pack for standard Gear/Vara Sails builders.
It is designed to help coding agents start from the right builder workflow, then pull the narrow Gear and Sails knowledge they need without depending on sibling repos or machine-local notes. The current public catalog is provisional and is expected to change as the eval suite identifies which candidate skills actually create uplift.
How It Works
Each skill is a markdown file.
SKILL.mdis the top-level router for the pack.skills/<name>/SKILL.mdis a narrower workflow or topic skill.references/is the self-contained handbook for Gear execution, Sails architecture, IDL/client generation,gtest, local validation, voucher/signless flows, and network configuration.
The pack is being narrowed toward standard Gear/Vara Sails app builders:
- preparing the local Rust and Gear toolchain
- turning feature ideas into spec, architecture, and task artifacts
- starting a new Sails app
- building features in an existing Sails app
- implementing approved Rust or Sails changes
- reasoning about Gear message flow and execution behavior
- choosing the right
gstdAPI when design depends on lower-level Gear behavior - getting architecture, IDL or client wiring,
gtest, and local-node validation right - building or extending a React frontend for a standard Sails app with Sails-JS and Gear-JS
- wiring gasless (voucher) and signless (session) UX flows
Installation
Quick Start (any agent)
npx skills add gear-foundation/vara-skillsWorks with Claude Code, Codex, and any agent that supports the skills CLI. Installs all 24 skills and the reference handbook in one command.
Codex
Clone the repo and install the local skills:
git clone git@github.com:gear-foundation/vara-skills.git
cd vara-skills
bash scripts/install-codex-skills.shThis installs the full Codex pack surface:
$CODEX_HOME/skills/vara-skills-> repo root, including the routerSKILL.md, sharedassets/, sharedreferences/, and helper scripts.$CODEX_HOME/skills/<skill-name>-> each installable skill directory underskills/.
Then start a new Codex session and use vara-skills when you want the pack router.
Claude Code Plugin
Claude Code can install this repo directly as a plugin. You do not need a separate Claude-specific fork.
Recommended install from GitHub:
/plugin marketplace add https://github.com/gear-foundation/vara-skills
/plugin install vara-skills@vara-skillsIf you are developing the plugin from a local checkout, run these commands from the repo root instead:
/plugin marketplace add .
/plugin install vara-skills@vara-skillsAfter local edits, reload the plugin without restarting Claude Code:
/reload-pluginsWhat Claude Code installs:
- the skill directories under
skills/ - the same shared
references/andassets/content those skills point at - the provisional standard Gear/Vara Sails builder workflow centered on
ship-sails-app
Important difference from Codex and OpenClaw: the repo-root SKILL.md is the portable pack router, but Claude Code loads plugin skills from skills/. In Claude Code, ship-sails-app is the broad entry skill that should trigger first for standard builder tasks.
OpenClaw
Use openclaw-skill/SKILL.md as the wrapper entrypoint for the same pack.
Skill Catalog
Router
ship-sails-app: default entry skill for standard Gear/Vara Sails builder work. Routes the user to the next correct stage instead of jumping straight into code.
Planning And Architecture
idea-to-spec: turns a rough request into a concrete spec artifact with actors, state changes, messages, replies, events, invariants, and acceptance criteria.gear-architecture-planner: maps an approved spec onto program boundaries, service boundaries, message flow, state ownership, and client or IDL implications.sails-architecture: focuses on Sails-specific service and program boundaries, state patterns, routing, and architecture tradeoffs for standard Sails repos.task-decomposer: breaks approved spec and architecture work into dependency-ordered implementation tasks with verification checkpoints.
Build And Implementation
sails-dev-env: prepares or repairs the local Rust, Wasm,cargo-sails, andgeartoolchain for standard Sails work.sails-new-app: greenfield workflow for creating a standard Sails workspace, typically starting fromcargo sails new <project-name>, without skipping the planning artifacts.sails-feature-workflow: stage-by-stage workflow for changing behavior in an existing Sails repo.sails-rust-implementer: implements approved Rust or Sails tasks while preserving routing, IDL, and async contract behavior.sails-idl-client: fixes or wires the IDL and generated client pipeline in app, client, or test crates.sails-frontend: builds or extends a React/TypeScript frontend for a Sails app using Sails-JS, generated clients, and Gear-JS.sails-indexer: builds or extends a read-side indexer and thin query API for a Sails app using IDL-driven event decoding, projected PostgreSQL read models, and optional on-chain query enrichment.awesome-sails-vft: adds a fungible token to a Sails app using awesome-sails building blocks, covering VFT crates, roles, events, and tests.sails-program-evolution: guides V1-to-V2 contract migration, interface evolution, and cutover planning for released Sails programs.vara-wallet: interacts with Vara Network on-chain — deploy programs, call Sails methods, manage wallets, transfer tokens, issue vouchers.
Verification And Runtime Behavior
sails-gtest: standard Sails-firstgtestverification flow using generated clients orGtestEnv.gtest-tdd-loop: red-green loop for deterministicgtestwork, using the repo scripts to capture failures and final green evidence.sails-local-smoke: typed local-node validation aftergtestis already green.gear-message-execution: focused reasoning about replies, delays, waitlist behavior, reservations, rollback, and async execution order.
Deep Capability Helpers
gear-gstd-api-map: design-time API chooser forgstd,gcore, andgsyswhen a spec or architecture depends on exact Gear messaging or execution primitives.
Maintenance
vara-skills-upgrade: checks for new pack versions and handles inline or standalone upgrades.
Recommended Workflows
New app workflow
ship-sails-app->sails-dev-envwhen the machine is not ready.ship-sails-app->sails-new-appto establish the greenfield path.- For a standard new Sails/Vara project, bootstrap the workspace with
cargo sails new <project-name>before custom wiring. idea-to-spec->gear-architecture-plannerorsails-architecture->task-decomposerto create the artifact chain indocs/plans/.sails-rust-implementerfor the approved code changes.sails-idl-clientif the generated interface path needs wiring or repair.sails-gtestorgtest-tdd-loopfor evidence-driven verification.sails-local-smokeonly after greengtest.
Existing feature workflow
ship-sails-app->sails-feature-workflowas the main router for existing Sails repos.idea-to-specfirst, thensails-architecturefor Sails-level structure.- Add
gear-gstd-api-mapwhen the feature depends on exactgstdAPI choice. - Add
gear-message-executionwhen replies, delays, reservations, or timeout behavior are part of the change. task-decomposer->sails-rust-implementer->sails-idl-client->sails-gtestorgtest-tdd-loop.sails-local-smokeonly after the typed test loop is green.
Verification workflow
- Use
sails-gtestfor the normal verification path. - Use
gtest-tdd-loopwhen the task must start from a failing test and produce parser-backed evidence from the repo scripts. - Use
sails-local-smokeonly aftergtest, not as a substitute for it.
Repo Structure
SKILL.mdcontains the top-level router.skills/contains installable skill directories.references/contains the self-contained handbook.assets/contains canonical output shapes for spec, architecture, task-plan, and gtest artifacts.scripts/contains install, validation, gtest execution, and parser helpers.
Milestone-One Evaluation
The first measured target is gpt-5.4.
The first full gpt-5.4 milestone-one suite now covers 12 cases across knowledge, codegen, workflow, and safety.
That suite produced 4 uplifts, 8 ties, and 0 regressions, with 2 artifact checks still recorded as not-run while compile-backed codegen execution remains scaffolded.
Measured winners in this run:
sails-default-pathgas-reservationno-low-level-bypassjs-client-from-idl(textual uplift only; artifact execution is still blocked)
Still tied or unresolved in the current pack:
rust-sails-compileaddress-format-ss58delayed-messagesidl-client-pathvoucher-signlesswaitlist-rentno-key-address-hallucinationsails-feature-flow
The supporting benchmark summary lives in the sibling vara-skills-evals repo at results/2026-03-11-gpt54-suite-report.md.
Verification
make verifyThis runs repository layout, skill-validator, skill-catalog, gstd-api-map, parser, install, packaging metadata, and update-check tests for the current product repo surface.
Current Direction
vara-skillsis the product repo for the pack- a sibling
vara-skills-evalsrepo owns benchmark definitions and uplift results gpt-5.4is the first evaluation target- the top-level router and candidate Sails-builder skills remain provisional until more targets are measured
- only measured winning skills should remain in the eventual first public pack
Awesome Sails Token Patterns
This note captures the reusable default patterns for adding fungible-token functionality to a standard Gear/Vara Sails app with awesome-sails.
Code snippets in this note are schematic patterns meant to guide composition and API shape. Adapt types, imports, and storage wrappers to the target repo instead of copying them blindly.
It is intentionally general and production-biased:
- preserve the standard VFT surface unless the spec explicitly needs more
- choose the smallest
awesome-sailstoken stack that satisfies the feature - keep token state explicit and composable
- keep typed events and generated clients on the normal path
- separate token logic from orchestration when the product has bridge, payment, staking, or other async workflows
Core Rule
Start from standard VFT-compatible behavior first.
On Vara, the VFT standard is the ERC-20 analogue. The default surface is:
ApproveTransferTransferFromAllowanceBalanceOfTotalSupplyNameSymbolDecimals
and the standard event surface is:
ApprovalTransfer
Do not start from a custom fungible-token design unless the spec clearly cannot fit the standard VFT model.
Choose The Smallest Awesome Sails Surface
awesome-sails is intentionally split into reusable token-related building blocks.
Use the smallest set that satisfies the feature:
vft: standard fungible-token behaviorvft_metadata: metadata access (name,symbol,decimals)vft_admin: privileged mint, burn, pause, and RBAC-secured admin operationsvft_extension: allowance cleanup,transfer_all, enumeration, and shard-management helpersvft_native_exchange: native value <-> VFT conversionvft_native_exchange_admin: admin and recovery flows around native exchangeaccess_control: explicit roles instead of ad hoc admin checksstorage: reusable storage wrappers for service compositionmsg_tracker: async message/status tracking when token flows are message-driven
Default decision order:
1. Start with vft. 2. Add vft_metadata if the token is user-facing. 3. Add vft_admin only when mint, burn, pause, or maintenance operations are real requirements. 4. Add vft_extension only when cleanup, enumeration, transfer_all, or shard helpers are needed. 5. Add vft_native_exchange only when wrapping/unwrapping native value is part of the product.
Dependency Pattern
Prefer explicit feature selection.
Meta-crate pattern
[dependencies]
sails-rs = { version = "*", default-features = false, features = ["gstd"] }
awesome-sails = { version = "x.y.z", default-features = false, features = [
"storage",
"vft",
"vft-metadata",
] }Add more features only when the spec needs them.
Direct subcrate pattern
Use direct crates when the project wants narrow imports or explicit dependency boundaries.
[dependencies]
sails-rs = { version = "*", default-features = false, features = ["gstd"] }
awesome-sails-storage = "x.y.z"
awesome-sails-vft-metadata = "x.y.z"
awesome-sails-utils = "x.y.z"This style is valid when the app composes token functionality from helpers rather than depending on the full meta-crate surface.
Storage Composition Pattern
The default reusable pattern is:
- keep balances, allowances, metadata, pause state, and admin/RBAC state in program-owned storage
- expose helper accessors that wrap those storages
- construct token-related services from wrappers instead of duplicating token logic across unrelated services
The following shape is schematic: Balances, Allowances, and Metadata represent app-defined storage types or wrappers chosen by the token implementation.
Minimal shape:
use sails_rs::{cell::RefCell, prelude::*};
use awesome_sails_storage::StorageRefCell;
pub struct Program {
balances: RefCell<Balances>,
allowances: RefCell<Allowances>,
metadata: RefCell<Metadata>,
pause: Pause,
}
pub struct TokenService<'a> {
balances: &'a RefCell<Balances>,
allowances: &'a RefCell<Allowances>,
metadata: &'a RefCell<Metadata>,
pause: &'a Pause,
}
impl<'a> TokenService<'a> {
fn balances_ref(&self) -> StorageRefCell<'_, Balances> {
StorageRefCell::new(self.balances)
}
fn allowances_ref(&self) -> StorageRefCell<'_, Allowances> {
StorageRefCell::new(self.allowances)
}
}Why this pattern is the default:
- state ownership is explicit
- services stay composable
- wrappers can be reused by VFT, metadata, admin, and extension layers
- the code scales better than one monolithic token service with hidden state
Metadata Pattern
Do not hand-roll metadata queries when the standard metadata service is enough.
Prefer delegating metadata through awesome-sails-vft-metadata:
use awesome_sails_vft_metadata::{Metadata, VftMetadata, VftMetadataExposure};
use awesome_sails_storage::StorageRefCell;
fn metadata_svc(&self) -> VftMetadataExposure<VftMetadata<StorageRefCell<'_, Metadata>>> {
VftMetadata::new(StorageRefCell::new(self.metadata)).expose(self.route())
}
#[export]
pub fn name(&self) -> String {
self.metadata_svc().name()
}
#[export]
pub fn symbol(&self) -> String {
self.metadata_svc().symbol()
}
#[export]
pub fn decimals(&self) -> u8 {
self.metadata_svc().decimals()
}Default rule: if the token is user-facing, metadata should usually be present and exposed in the standard way.
Event Pattern
Events are part of the token contract, not optional decoration.
Preserve the standard token events and add custom events only when the application has real domain facts beyond base token movement.
Minimal pattern:
#[event]
#[derive(Debug, Clone, Encode, Decode, TypeInfo, PartialEq, Eq)]
#[codec(crate = sails_rs::scale_codec)]
#[scale_info(crate = sails_rs::scale_info)]
pub enum Events {
Approval { owner: ActorId, spender: ActorId, value: U256 },
Transfer { from: ActorId, to: ActorId, value: U256 },
// Add only when the chosen token policy needs them:
Minted { to: ActorId, value: U256 },
Burned { from: ActorId, value: U256 },
}
#[service(events = Events)]
impl TokenService<'_> {}Default rules:
- keep
ApprovalandTransferwhen the service behaves like VFT - add
Minted,Burned,Paused,RoleGranted, or similar only when the feature set actually needs them - emit events from successful state-changing paths, not from queries
Exported Command Pattern
For stateful token commands, prefer Result<_, Error> plus #[export(unwrap_result)].
Minimal pattern:
#[service(events = Events)]
impl TokenService<'_> {
#[export(unwrap_result)]
pub fn transfer(&mut self, to: ActorId, value: U256) -> Result<bool, Error> {
let from = Syscall::message_source();
// mutate balances here
self.emit_event(Events::Transfer { from, to, value })
.map_err(|_| EmitError)?;
Ok(true)
}
#[export(unwrap_result)]
pub fn approve(&mut self, spender: ActorId, value: U256) -> Result<bool, Error> {
let owner = Syscall::message_source();
// mutate allowances here
self.emit_event(Events::Approval {
owner,
spender,
value,
})
.map_err(|_| EmitError)?;
Ok(true)
}
}Why this is the default:
- internal control flow stays typed and readable
- exported command failure remains fail-fast
- tests can explicitly expect fatal exported-path failures where appropriate
Admin Pattern
Do not introduce privileged token operations unless the spec actually needs them.
When the token needs mint, burn, pause, or operational maintenance, prefer the dedicated admin layer rather than growing ad hoc privileged methods.
Use an app-local admin guard only for small, clearly bounded cases. Escalate to RBAC when:
- there are multiple privileged concerns
- roles differ by capability
- grant/revoke is part of the design
- long-term maintainability matters
Default rule:
- fixed-supply token: no admin layer unless there is another real privileged operation
- mutable-supply token: prefer
vft_adminor an equivalent explicit RBAC design
Extension Pattern
vft_extension is optional.
Use it when the product truly needs one or more of:
- expired allowance cleanup
transfer_all- balance enumeration
- allowance enumeration
- explicit shard management
Do not add extension helpers to every token by default.
A token that only needs standard transfers, approvals, balances, supply, and metadata should stay smaller.
Native Exchange Pattern
vft_native_exchange is not a generic token default.
Use it only when the product explicitly requires:
- native value sent in -> VFT minted
- VFT burned -> native value returned
If the token does not wrap the chain’s native value, leave native exchange out.
Separate Token Logic From Orchestration
When the token participates in a larger async protocol, prefer this split:
- a dedicated token program or token service that owns balances, allowances, supply, metadata, and token-standard events
- a separate manager/orchestrator that owns bridge, payment, staking, claim, or delayed-message workflows
This pattern is strongly reinforced by the Gear bridge architecture:
- VFT owns token logic
- VFT-Manager owns bridge workflow
- the user first performs approve
- the manager then performs the higher-level protocol action
Default rule: if another subsystem needs to move user tokens, start from approval-based ingress before inventing a custom authorization flow.
For deployed bridge contract and token addresses, see vara-eth-bridge-contracts.md. For bridge flow mechanics and fee model, see vara-eth-bridge-flows.md.
Testing Defaults
A token integration is not complete until it covers the minimum matrix.
Base VFT behavior
- transfer success
- transfer failure on insufficient balance
- approve success
- allowance correctness
transfer_fromsuccess within allowancetransfer_fromfailure above allowance- total supply correctness
- metadata correctness when metadata is enabled
Event behavior
- assert Approval
- assert Transfer
- assert custom events only when that feature exists
Admin behavior
- authorized privileged action succeeds
- unauthorized privileged action fails
- mint/burn/pause flows are covered only when they exist
Extension behavior
- cleanup, enumeration, transfer_all, and shard operations are tested only when enabled
Fatal-path behavior
- for exported commands using #[export(unwrap_result)], assert fatal exported-path failures where appropriate
Anti-Patterns
- writing a custom fungible token before checking whether standard VFT plus one or two awesome-sails layers are enough
- mixing token storage, protocol orchestration, and bridge/payment workflow into one giant service
- adding admin, extension, or native-exchange features “just in case”
- treating Approval / Transfer events as optional on a VFT-like token
- hiding supply policy instead of deciding explicitly between fixed supply, mint/burn, or lock/unlock semantics
- bypassing generated clients on the normal token path
- making every app invent its own allowance and transfer semantics ad hoc
Practical Decision Order
When another agent has to choose quickly, bias toward this order:
- Preserve the standard VFT surface.
- Add metadata if the token is user-facing.
- Keep token state explicit and wrapper-based.
- Use typed events at the service boundary.
- Use
#[export(unwrap_result)]for stateful exported token commands. - Add admin only when privileged operations are real.
- Add extension only when cleanup, enumeration,
transfer_all, or shard helpers are required. - Add native exchange only when wrapping/unwrapping native value is part of the product.
- Split token logic from orchestration when the product has async protocol workflow.
- Test events and failure paths, not only final balances.
Program Evolution And State Migration For Gear/Sails
Goal
- Use this reference when the user asks how to evolve a Gear/Sails smart contract after deployment, how to release a new version safely, or how to migrate on-chain state from an old program to a new one without breaking frontend and indexer integrations.
- Frame the answer as a production maintenance problem, not only as a coding problem.
- Separate what the platform provides out of the box from what the project should implement as its own migration discipline.
- This reference focuses on released-contract evolution, cutover discipline, and state migration between program versions.
- It does not try to fully specify frontend rollout mechanics or indexer implementation details.
Core Model
- For Gear/Vara, treat upgradeability as deploying a new program version and explicitly migrating state, not as hot-swapping code inside one live program.
- Build recommendations around a
V1 -> V2 -> cutovermodel. - Treat deployment, state reading, migration batching, and cutover as separate concerns.
- Keep the public contract surface stable for as long as possible.
- When internal Rust names change but public names must stay stable, preserve routes with
#[export(route = "...")]. - When extending services, use
overrides,route, orentry_iddeliberately so inherited methods keep compatibility intent explicit. - Prefer program-owned business state passed into services via
RefCell; this keeps ownership explicit and makes versioned state and migration planning easier.
What Counts As Upgrade
- An upgrade is a release process with three parts:
- old contract
V1 - new contract
V2 - off-chain migrator script
V1is the source of truth for export.V2is deployed separately and is the destination for import.- The off-chain migrator reads old state and writes new state.
- Read state through the platform primitives when possible:
api.programState.readfor full-state reads or reads at a fixed block hashapi.programState.readUsingWasmfor selected state functions or custom paginated reads
What The Agent Must Recommend
- Always recommend a migration flow built on these rules:
- freeze writes on
V1 - export state in deterministic chunks
- import state into
V2in deterministic chunks - make imports idempotent
- verify counts, totals, and critical invariants
- activate
V2 - switch frontend and indexer to
V2 - Treat migration batches like any other retryable multi-step flow.
- Expect retries, partial completion,
Timeout, andRunOutOfGas. - Prefer tracked progress plus retry-safe behavior over one-shot bulk migration logic.
Minimal Status Model
- Do not require a separate
Migrationstatus unless the user explicitly wants it. - The default recommendation is:
ActiveReadOnlyReadOnlymeans user mutating calls are blocked but queries and export functions remain available.- This is usually enough for the old version because migration itself is an explicit export/import process and state reading is already handled separately by Vara APIs.
What Must Exist In The Old Contract V1
V1should contain:- admin-only
set_read_only(bool) - export methods for each business-relevant part of state
- a stats or invariants export method for validation
- no new writes once migration starts
- Prefer export APIs by business domain, not one giant dump.
- Good shapes include:
export_config()export_users_chunk(cursor, limit)export_positions_chunk(cursor, limit)export_balances_chunk(cursor, limit)export_active_accounts_chunk(cursor, limit)export_stats()- Use
api.programState.readwhen full-state reading is feasible. - Use
api.programState.readUsingWasmwhen only selected parts should be read or when large state should be paginated through custom state functions.
What Must Exist In The New Contract V2
V2should contain:- initial
ReadOnlymode until import is complete - admin-only import methods
- batch progress tracking
- idempotency checks
- conflict detection
- finalization step
- Good shapes include:
import_config(...)import_users_batch(batch_id, users)import_positions_batch(batch_id, positions)import_balances_batch(batch_id, balances)import_active_accounts_batch(batch_id, accounts)import_status()finalize_import()- If the same
batch_idis sent twice, prefer success without duplicating state changes. - If the same key or entity arrives with different data, fail with a conflict instead of silently overwriting.
- Keep normal business writes blocked until import verification passes and finalization is complete.
State Inventory Rules
- Before proposing migration, require a state inventory.
- For every field in the root program state and in nested structures, classify it as one of:
- migrate as-is
- migrate with transform
- rebuild after migration
- Make the user list every business-significant collection explicitly.
- This includes:
VecHashMapHashSet- nested combinations of them
- Do not assume caches, derived indexes, or recomputable aggregates need migration.
- Rebuild derived or cache-like data when it is safe and cheaper than migrating it.
- Keep the source of truth small and explicit.
How To Migrate Vec
- Treat
Vec<T>as an ordered sequence. - Export by index range using:
cursorlimitnext_cursor- Recommend chunked export/import by slices.
- This is the simplest collection to migrate because ordering is already defined.
How To Migrate HashMap
- Never recommend migrating a
HashMapby raw iteration order. - Migrate it as a deterministic sequence of entries, for example
Vec<(K, V)>. - Required rule:
- collect keys
- sort keys into a canonical order
- take a key range by cursor and limit
- export entries for those keys
- import entries into
V2 - If repeated key sorting would be too expensive, recommend one of these improvements:
- maintain a stable secondary index
- use
BTreeMapfor the new version - flatten nested maps into explicit migration records
- For nested structures like
HashMap<UserId, HashMap<OrderId, Order>>, flatten export into records such as(user_id, order_id, order)instead of trying to migrate one giant nested object. - Flattening keeps batching, retries, and conflict checks tractable.
How To Migrate HashSet
- Treat
HashSet<T>like a set of records, not a raw memory object. - Required rule:
- collect elements
- sort elements into canonical order
- export by cursor and limit
- import into
V2with duplicate-safe semantics - For large or critical membership sets, recommend
BTreeSetinV2when deterministic ordering and simpler validation are useful.
Snapshot Rule
- When possible, recommend freezing
V1and then reading state from a fixed snapshot block. - Use
api.programState.read({ at: ... })when snapshot consistency matters across batches. - This avoids drift between earlier and later export batches.
Verification Rule
- Always require post-import verification before activating
V2. - At minimum compare:
- entity counts
- total balances, reserves, or supply-like totals
- next-id counters
- config values
- membership set sizes
- optional checksum or hash over exported data
- Do not recommend switching traffic to
V2before these checks pass.
Compatibility Rule For Frontend And Indexer
- Treat public routes, method signatures, and event payloads as compatibility-sensitive surfaces.
- If the public surface must evolve, prefer one of these strategies:
- preserve old route names with
#[export(route = "...")] - use
overrides,route, orentry_idto maintain compatibility for inherited services - version events instead of silently changing payload shape
- Do not casually rename or reshape exported methods that current clients already use.
- Do not silently change event payload shape in place when an indexer already decodes it.
- Remember that Sails derives deterministic interface identifiers and entry identifiers from the canonical IDL and carries stable routing metadata in the header format.
Recommended Migration Algorithm
- When the user asks for a practical flow, recommend this order:
1. deploy V2 2. initialize V2 in ReadOnly 3. call V1.set_read_only(true) 4. record the snapshot block hash 5. export config and scalar state from V1 6. import config and scalar state into V2 7. export all business collections from V1 in deterministic chunks 8. import all chunks into V2 with idempotent batch methods 9. compare stats and invariants 10. call V2.finalize_import() 11. switch frontend and indexer to V2 12. keep V1 read-only for a rollback window if needed
- Treat rollback planning as part of the release, not as an afterthought.
Things The Agent Must Not Suggest
- Do not suggest replacing code in place as the default Gear/Sails strategy.
- Do not suggest one giant monolithic state dump for large production state.
- Do not suggest non-deterministic
HashMaporHashSetexport order. - Do not suggest import methods without idempotency.
- Do not suggest changing public routes or event shapes casually.
- Do not suggest migrating caches as if they were the source of truth.
Expected Output From The Agent
- When using this reference, the agent should produce:
- a clear split between
V1,V2, and off-chain migrator responsibilities - a state inventory plan
- export/import API design
- collection-specific migration rules
- verification checklist
- compatibility notes for frontend and indexer
- explicit mention of which parts are platform-supported and which are project conventions
Platform-Supported Pieces Versus Project Conventions
- Platform-supported primitives include:
- deploying a new program via upload or create flows
- reading full state
- reading selected state using state functions
- reading state at a specific block hash
- Project conventions in this reference include:
V1 -> V2 -> cutoverrelease disciplineActiveandReadOnlyas the default minimal status model- chunked deterministic export/import API design
- idempotent batch import semantics
- explicit verification before cutover
Default Conclusion
- If the user asks for the safest default pattern, recommend:
- immutable
V1 - deploy
V2 ReadOnlyfreeze- chunked deterministic export/import
- idempotent batches
- verification
- cutover
- This is the baseline production-friendly upgradability and migration pattern for Gear/Sails in this pack.
Delayed Message Pattern
Use Case
Use this pattern for future-block work such as reminders, auctions, inactivity cleanup, vesting steps, and timeout enforcement.
Canonical Self-Message Payload
For a Sails 1.0 self-call, build the same Sails Header v1 plus SCALE-encoded params that generated clients use. In generated Rust client code this is the io::<Call>::encode_call(route_idx, args...) helper:
use reminder_board_client::{
ReminderBoardClientProgram,
reminder_board::io::TriggerReminder,
};
let payload = TriggerReminder::encode_call(
ReminderBoardClientProgram::ROUTE_ID_REMINDER_BOARD,
id,
);- This is the header-routed byte shape to use when a delayed internal message cannot go through a generated client send directly.
- Keep the route constant and generated
iotype aligned with the exported Sails route. - If generated client code is unavailable, manually encode the Sails Header v1 followed by SCALE-encoded params. See
../../references/sails-header-wire-format.mdfor the header layout. - The legacy SCALE-string route form (
service,method, then args) is not a valid Sails 1.0 delayed self-message payload.
Sending The Delayed Message
// Dynamic gas: if this handler also does work, a fixed gas_limit will fail
// when execution already consumed most of the budget.
let gas_for_next = Syscall::gas_available() * 9 / 10;
msg::send_bytes_with_gas_delayed(Syscall::program_id(), payload, gas_for_next, 0, delay)
.expect("failed to schedule delayed self-message");- Use
Syscall::program_id()when the program is scheduling work for itself. - Use
Syscall::gas_available()to compute the gas budget dynamically. Do not use a fixedgas_limitfor self-scheduling loops — if the handler does work AND schedules the next tick, the remaining gas may be insufficient for the delayed message. A common pattern isSyscall::gas_available() * 9 / 10to reserve 90% of remaining gas for the next invocation. - Runtime accessors use
Syscall::*; outbound delayed sends still use the normalmsg::send*family. - Keep transferred value at
0unless the delayed route truly needs value.
Internal-Only Guard
- The internal-only check is
Syscall::message_source() == Syscall::program_id(). - Enforce it at the start of the exported handler so outside callers cannot trigger the internal route directly.
#[export]
pub fn trigger_reminder(&mut self, id: u64) {
assert_eq!(Syscall::message_source(), Syscall::program_id(), "internal only");
self.finish_trigger(id);
}Reservation And Gas Notes
- Use
ReservationIdonly when later execution budget must survive across blocks. - If a plain delayed send is enough, keep the flow simpler and derive gas from
Syscall::gas_available(). - Recompute or validate critical state inside the delayed handler instead of trusting stale assumptions from the scheduling block.
Gtest vs On-Chain Funding
- On a real node (local or remote), the program must hold VARA balance to pay for delayed message gas. Programs with zero balance cannot schedule delayed messages. Transfer VARA to the program address after deployment and before calling methods that schedule delayed work.
- In
gtest, delayed messages succeed without explicit program funding. This asymmetry is a common source of "works in test, fails on chain" bugs. Test the funding requirement in local-smoke validation.
Idempotency Rules
- Delayed routes must be safe if the target item was already canceled, completed, or cleaned up.
- Persist enough state to detect stale work.
- Prefer an idempotent no-op or explicit early return over a half-applied mutation.
Test Guidance
- Use
run_to_blockfor delay-sensitive assertions. - Assert both the scheduler-side effect and the eventual handler-side effect.
- If debugging the raw byte route, assert the payload starts with the Sails Header v1 bytes (
GM, version0x01, header length0x10) before blaming gas or timing.
Gear Builtin Actors — Reference
Builtin actors are chain-provided pseudo-programs: Substrate-native code registered by pallet-gear-builtin and reachable at a hardcoded ActorId. A standard Gear/Vara Sails program calls a builtin through the normal msg::send_for_reply family — but the request and reply types come from the gbuiltin-* helper crates, not from a Sails IDL.
Upstream docs: <https://wiki.gear.foundation/docs/developing/build/builtinactors>
What a builtin actor is
- Lives in the runtime, not in WASM. Has no program code, no memory, no
read_state. - Claims a
BuiltinId(au64). At registration time the pallet derives a stableActorIdfrom that id. - Dispatches incoming messages by decoding their payload into a per-actor
Requestenum and executing the mapped Substrate call. - Replies synchronously from the program's perspective: a success reply decodes as the actor's
Response(where one is defined) or is empty; a failure surfaces as an error reply with aBuiltinActorError. - Charges the sender's block gas allowance for the underlying extrinsic weight (
max_gas()).
Authoritative sources (paths relative to the gear repo root):
- Pallet:
vara/pallets/gear-builtin/src/lib.rs - Per-actor impls:
vara/pallets/gear-builtin/src/{bls12_381,staking,proxy}.rs - ETH bridge actor:
vara/pallets/gear-eth-bridge/(registered through the same tuple) - Helper crates (what a Sails program imports):
vara/sdk/builtins/{bls381,staking,proxy,eth-bridge}/src/lib.rs - Shared error type:
vara/sdk/builtins/common/src/lib.rs(crate namebuiltins-common) - Vara registration tuple:
vara/runtime/vara/src/lib.rslines 1174–1186
ActorId derivation
SEED = b"built/in" // 8 bytes, from pallet-gear-builtin
ActorId = blake2_256((SEED, builtin_id).encode())Source: vara/pallets/gear-builtin/src/lib.rs:165,212.
The builtin_id is a u64 assigned explicitly to each actor via ActorWithId<N, Actor> in the runtime's BuiltinActors tuple. The ActorId is a hash of that explicit N, not of the tuple position — reordering the tuple while keeping every N constant does not change any ActorId. What does change an ActorId: changing the numeric N for an actor, or switching to a runtime that assigns a different N. Treat hardcoded `ActorId` constants as bound to the `N` in the target runtime.
Vara runtime registry
Source: vara/runtime/vara/src/lib.rs:1174-1186.
| Builtin ID | Actor | Helper crate | Stable ActorId (current Vara runtime) |
|---|---|---|---|
| 1 | BLS12-381 | gbuiltin-bls381 | 0x6b6e292c382945e80bf51af2ba7fe9f458dcff81ae6075c46f9095e1bbecdc37 |
| 2 | Staking | gbuiltin-staking | 0x77f65ef190e11bfecb8fc8970fd3749e94bed66a23ec2f7a3623e785d0816761 |
| 3 | ETH Bridge | gbuiltin-eth-bridge | derived from id 3; see note below |
| 4 | Proxy | gbuiltin-proxy | 0x8263cd9fc648e101f1cd8585dc0b193445c3750a63bf64a39cdf58de14826299 |
ETH bridge note: the Vara runtime computes the bridge ActorId at startup via GearBuiltin::generate_actor_id(ETH_BRIDGE_BUILTIN_ID) (see vara/runtime/vara/src/lib.rs:1202-1205). Always reconfirm against that file for the target runtime version; do not copy a literal from another project.
Canonical calling pattern
A builtin call is a normal send_for_reply against a non-program ActorId:
use gbuiltin_staking::{Request, RewardAccount};
use gstd::{msg, ActorId};
use hex_literal::hex;
use parity_scale_codec::Encode;
const STAKING_BUILTIN: ActorId = ActorId::new(hex!(
"77f65ef190e11bfecb8fc8970fd3749e94bed66a23ec2f7a3623e785d0816761"
));
#[gstd::async_main]
async fn main() {
let value = msg::value();
let payload = Request::Bond {
value,
payee: RewardAccount::Program,
}
.encode();
let reply = msg::send_bytes_for_reply(STAKING_BUILTIN, &payload[..], 0, 0)
.expect("send failed")
.await
.expect("builtin returned error reply");
// `reply` is empty for Bond; for ActiveEra decode as gbuiltin_staking::Response.
}Notes:
- Prefer
send_bytes_for_replywhen encoding withparity_scale_codec::Encodedirectly. Use the typedsend_for_replyonly if you share a matching type the compiler can encode on your behalf. valuetransfers follow Substrate ED rules for the underlying pallet. StakingBondrequiresvalue≥ the network minimum bond.reply_depositargument is the last0in the example; set a non-zero value only if the service depends on downstream reply fees (rare for builtins).- An
Errfrom.awaitis a real error reply (typically aBuiltinActorError), not a transport failure. Treat it like any other error reply in thegear-messaging-and-replies.mdplaybook.
Per-actor reference
Staking (gbuiltin-staking)
Source: vara/sdk/builtins/staking/src/lib.rs. Broker example: examples/staking-broker/src/wasm.rs.
Request variants (SCALE-indexed):
Bond { value, payee }— index 0BondExtra { value }— index 1Unbond { value }— index 2WithdrawUnbonded { num_slashing_spans }— index 3Nominate { targets: Vec<ActorId> }— index 4Chill— index 5PayoutStakers { validator_stash, era }— index 6Rebond { value }— index 7SetPayee { payee }— index 8ActiveEra— index 9
RewardAccount: Staked | Program | Custom(ActorId) | None.
Response::ActiveEra { info: ActiveEraInfo, executed_at, executed_at_gear_block } is the only defined reply shape (for the ActiveEra request). Other requests reply with an empty payload on success.
Proxy (gbuiltin-proxy)
Source: vara/sdk/builtins/proxy/src/lib.rs. Broker example: examples/proxy-broker/src/wasm.rs.
Request variants:
AddProxy { delegate: ActorId, proxy_type: ProxyType }— index 0RemoveProxy { delegate: ActorId, proxy_type: ProxyType }— index 1
ProxyType: Any | NonTransfer | Governance | Staking | IdentityJudgement | CancelProxy (mirror of vara-runtime).
No Response type defined — success replies are empty; failures return BuiltinActorError via error reply.
BLS12-381 (gbuiltin-bls381)
Source: vara/sdk/builtins/bls381/src/lib.rs. Example: examples/bls381/src/wasm.rs.
Request variants carry Vec<u8> fields that are pre-encoded `ArkScale` payloads (from the ark-scale crate bridging arkworks ↔ SCALE). The helper crate re-exports ark_bls12_381, ark_ec, ark_ff, ark_scale, ark_serialize.
MultiMillerLoop { a, b }— index 0FinalExponentiation { f }— index 1MultiScalarMultiplicationG1 { bases, scalars }— index 2MultiScalarMultiplicationG2 { bases, scalars }— index 3ProjectiveMultiplicationG1 { base, scalar }— index 4ProjectiveMultiplicationG2 { base, scalar }— index 5AggregateG1 { points }— index 6MapToG2Affine { message }— index 7
Response mirrors each request variant with the same index and a single Vec<u8> (ArkScale-encoded) result. The REQUEST_* constants in the crate pin these indexes; gas charging is per-variant and depends on the decoded vector sizes.
ETH Bridge (gbuiltin-eth-bridge)
Source: vara/sdk/builtins/eth-bridge/src/lib.rs. Pallet: vara/pallets/gear-eth-bridge/.
Request:
SendEthMessage { destination: H160, payload: Vec<u8> }— index 0
Response:
EthMessageQueued { block_number: u32, hash: H256, nonce: U256, queue_id: u64 }— index 0
destination is an Ethereum 20-byte address. payload is the raw bridge payload (validated by the pallet). For bridge flows, addresses, and fee semantics see the separate vara-eth-bridge-flows.md and vara-eth-bridge-contracts.md references.
Reply decoding checklist
When debugging a builtin reply:
1. Confirm the sender targeted a builtin ActorId, not a program. If it was a program, this skill does not apply — go to gear-message-execution. 2. Identify the matching helper crate by source ActorId ↔ builtin id. 3. Classify: empty success, typed Response, or error reply. 4. Decode as gbuiltin_*::Response using parity_scale_codec::Decode — not a Sails generated client, not ProgramMetadata. There is no Sails Header framing on a builtin reply. 5. On error replies, inspect the payload as BuiltinActorError (defined in the builtins-common crate at vara/sdk/builtins/common/src/lib.rs; import as use builtins_common::BuiltinActorError;). Common variants: InsufficientGas when the caller-supplied gas_limit is below the call's weight; GasAllowanceExceeded when the block-level allowance is exhausted; InsufficientValue; DecodingError; and Custom(LimitedStr<'static>) for actor-specific errors.
Gas and ED
- Each builtin declares a
max_gas()covering the worst-case underlying extrinsic. The pallet upfront-charges this against the caller's message gas_limit (→InsufficientGasif the caller supplied too little) and against the current block allowance (→GasAllowanceExceededif the block is saturated). Both cases surface asBuiltinActorErrorreplies, not panics. - Value-bearing requests (staking
Bond, proxy ops with a deposit, etc.) follow the pallet's ED rules. First-time calls to a new builtin address require ED to be minted — runtime migrations do this for shipped builtins (vara/runtime/vara/src/migrations.rs→LockEdForBuiltin), so application code only needs to worry about ED on brand-new chains or newly-registered actors. - Keep reservations out of the critical path unless the calling flow already relies on them; a builtin reply typically arrives within the same message-queue pass.
Guardrails
- Do not hardcode an `ActorId` without citing a runtime file. Re-derive with
hash((b"built/in", id).encode())or copy fromvara/runtime/vara/src/lib.rs, and note the source. - Do not decode builtin replies with a Sails client. There is no Sails Header framing; use the helper crate's
Responsetype. - Do not call a builtin from a sync handler. The call is
_for_reply; the handler must be async. - Do not assume a reply shape across runtime versions. Helper crates evolve — pin to a workspace version that matches the target runtime.
- Do not wrap a builtin behind a service handler that loses idempotency. Broker services should carry their own retry and reconciliation state; the builtin has no memory.
- Do not confuse this with runtime-maintenance work. Adding or changing a builtin actor happens inside the gear repo (
vara/pallets/gear-builtin/+vara/runtime/vara/src/lib.rs) and is out of scope for this pack.
Gear Execution Model
Core Rules
- Gear and Vara programs are isolated actors with private state.
- State changes happen only while a program handles one incoming message.
- Sending a message enqueues work and returns a message id; it does not execute the queue immediately.
- The runtime drains the global message queue during block execution, so time and ordering are block-shaped concerns.
- In
gtest, the first message to a program is often initialization and still requires explicit block execution.
Message Lifecycle
1. A program enters handle, handle_reply, or another valid entrypoint for the current message. 2. It reads context such as source, value, payload, and prior reply linkage. 3. It stages outbound sends, replies, events, or delayed work while execution is still in progress. 4. The runtime materializes those outbound effects only after the current execution completes successfully.
Successful Execution And Rollback
- A successful execution commits state changes and materializes staged outbound messages, replies, and events.
- A panic or unrecoverable failure rolls back the current message execution, including state updates and staged outbound effects.
- Sails events follow the same rule because they are ordinary messages under the hood. If the command rolls back, the expected event is absent.
- This rollback boundary matters for design: mutate-before-send logic is safe only if failure panics and reverts the message.
Builder Implications
- Model multi-step workflows as distributed transactions across messages, not as one atomic call stack.
- Name actors, commands, queries, replies, events, and failure paths in specs before implementation starts.
- Use delayed messages for future block work when the program really must revisit state later.
- Use gas reservation only when future work needs a preserved execution budget across blocks.
- Prefer generated Sails clients for normal app paths so Sails Header routing and reply decoding stay aligned with the program contract.
Validation Implications
- Advance blocks explicitly in
gtest;sendalone is not execution. - Assert effects after the block that should have produced them.
- When smoke-testing against a local node, use the real deployed program id and observe the same async message boundaries you modeled in tests.
Program Lifecycle
1. Upload code — submit compiled Wasm to the network. The runtime validates the Wasm is well-formed and stores the code blob identified by its code hash. 2. Create program — send an init message that instantiates a program from the validated code. The runtime allocates state and runs the init handler. 3. Active — the program handles messages, mutates state, stages outbound sends, and emits events. Balance must stay above the existential deposit. 4. Exit — the program calls gr_exit to terminate, or init fails and the program is never activated. Locked deposit is released on exit.
A program that runs out of balance or is not accessed for the initial rent period risks being paused or removed by the rent system.
Rent
- Programs occupy on-chain state and must maintain at least the existential deposit (~1 VARA on mainnet).
- The initial rent period is approximately 173 days (5,000,000 blocks on Vara Network).
- If a program's balance drops below the threshold and the rent period expires, the program may be removed from active state.
- For long-lived programs, ensure the account is topped up periodically or funded sufficiently at creation.
Gear Gas Reservations And Waitlist
When To Reserve Gas
- Reserve gas when the design needs to preserve execution budget for future work across blocks.
- Typical cases are delayed recovery flows, self-messaging that must survive later execution, or async paths that resume after waiting.
- Reservation is not free, permanent, or a value transfer. It is a bounded gas budget tied to duration and usage.
Reservation Rules
- Create a reservation with an explicit amount and duration.
- Treat the reservation lifetime as part of the architecture, not an implementation footnote.
- Consuming, expiring, or unreserving the reservation changes what later messages can still do.
- Reusing an expired or already-consumed reservation id is a real failure mode and should be tested.
Waitlist Model
- The Waitlist is on-chain storage for messages that are waiting on conditions or later processing.
- Waitlisted messages are not a free parking lot and are not a normal mempool.
- Waitlist storage incurs rent or locked-fund cost over time, has expiry behavior, and is bounded by maximum duration rules.
- Designs that rely on indefinite prolonging of waitlisted messages are wrong.
Delayed Work Design
- Prefer delayed messages when the program must revisit state in a later block.
- Pair delayed execution with reservation only when the later path really needs preserved gas.
- Do not describe delayed automation as off-chain cron unless an off-chain agent is explicitly part of the design.
- On-chain, a program must have sufficient VARA balance to cover gas for delayed messages. Transfer VARA to the program address after deployment. This is not required in
gtest, which is a known test/production gap.
Testing And Accounting
- In
gtest, block advancement determines whether delayed work or expiry actually happens. - Use
spent_valueand related accounting assertions when gas burn or attached value affects behavior. - Keep sender funding, attached value, and existential-deposit constraints in the same reasoning frame.
Failure Signatures
- Invalid or expired reservation id
- Timeout followed by later side effects
- Balance assertions that ignore gas burn or ED behavior
- Waitlist assumptions that ignore rent, expiry, or maximum duration
Gear gstd API And Syscalls
Design first, debugging second: use this map to decide what a standard Gear program can do through gstd, then confirm wrapper or syscall details only when behavior is ambiguous.
Layer Map
| Layer | Role | Design Use |
|---|---|---|
gstd | Builder-facing API for standard programs | Start here for capability and contract choices |
gcore | Lower-level wrappers around raw calls | Use to confirm exact arguments, return values, and fallible behavior |
gsys | Raw gr_* syscall declarations | Use to confirm the actual host calls and syscall families |
Capability-First View
gstd::msg
Use msg when the design question is about payload handling, outbound messages, replies, or reply-await flows.
| Design intent | Prefer in gstd | gcore wrapper | gsys family |
|---|---|---|---|
| Read typed input | msg::load | gcore::msg::read | gr_read |
| Read raw bytes | msg::load_bytes | gcore::msg::read | gr_read |
| Send typed payload | msg::send | gcore::msg::send | gr_send |
| Send bytes | msg::send_bytes | gcore::msg::send | gr_send |
| Send with explicit gas | msg::send_with_gas, msg::send_bytes_with_gas | gcore::msg::send_with_gas | gr_send_wgas |
| Delay a send to a future block | msg::send_delayed, msg::send_bytes_delayed | gcore::msg::send_delayed | gr_send or gr_send_wgas with delay |
| Forward part of input | msg::send_input, msg::reply_input | gcore::msg::send_input, gcore::msg::reply_input | gr_send_input, gr_reply_input |
| Reply once | msg::reply, msg::reply_bytes | gcore::msg::reply | gr_reply |
| Reply with explicit gas | msg::reply_with_gas, msg::reply_bytes_with_gas | gcore::msg::reply_with_gas | gr_reply_wgas |
| Build an outbound message in parts | msg::MessageHandle::{init,push,push_input,commit} | gcore::msg::send_init, send_push, send_commit | gr_send_init, gr_send_push, gr_send_commit |
| Build a reply in parts | msg::reply_push, msg::reply_push_input, msg::reply_commit | gcore::msg::reply_push, reply_commit | gr_reply_push, gr_reply_commit |
| Await a reply in async code | _for_reply helpers such as msg::send_bytes_for_reply | same send wrappers plus async runtime locks | send syscalls plus reply deposit or reply-hook handling |
| Spend reserved gas on later messaging | reservation-backed send or reply variants | reservation send or reply wrappers | gr_reservation_send, gr_reservation_send_commit, gr_reservation_reply, gr_reservation_reply_commit |
Design note: prefer typed APIs for stable app contracts. Use bytes variants when the route is intentionally raw or when isolating codec or Sails Header routing bugs.
gstd::exec
Use exec when the design question is about current execution context, gas, waiting, waking, or termination.
| Design intent | Prefer in gstd or gcore | gsys syscall |
|---|---|---|
| Read environment variables | exec::env_vars | gr_env_vars |
| Read block height or timestamp | exec::block_height, exec::block_timestamp | gr_block_height, gr_block_timestamp |
| Inspect remaining gas | exec::gas_available | gr_gas_available |
| Read raw bytes from program state | exec::read_bytes | gr_read |
| Reserve gas for future system use | exec::system_reserve_gas | gr_system_reserve_gas |
| Inspect attached or available value | exec::value_available, msg::value | gr_value_available, gr_value |
Allocate reply deposit for handle_reply | exec::reply_deposit | gr_reply_deposit |
| Wait for future work | exec::wait, exec::wait_for, exec::wait_up_to | gr_wait, gr_wait_for, gr_wait_up_to |
| Wake a waited message | exec::wake | gr_wake |
| Read current program id | exec::program_id | gr_program_id |
| Read randomness | exec::random | gr_random |
| Exit and transfer value | exec::exit | gr_exit |
Design note: gr_wait and gr_wake are control syscalls. They shape state machines and future-block behavior, not synchronous function calls.
gstd::prog
Use prog when the design question is whether a program should create child programs and what init contract that child should receive.
| Design intent | Prefer in gstd | gcore wrapper | gsys syscall |
|---|---|---|---|
| Create child with bytes init payload | prog::create_program_bytes | gcore::prog::create_program | gr_create_program |
| Create child with typed init payload | prog::create_program | gcore::prog::create_program | gr_create_program |
| Delay creation | prog::create_program_bytes_delayed, prog::create_program_delayed | gcore::prog::create_program_delayed | gr_create_program with delay |
| Create with explicit gas | prog::create_program_bytes_with_gas, prog::create_program_with_gas | gcore::prog::create_program_with_gas | gr_create_program_wgas |
| Await init reply | _for_reply create helpers generated by wait_create_program_for_reply | same create wrappers plus async runtime | create syscall plus reply-await machinery |
Design note: creation is still message-based. The new program id and init message id exist before the child init reply is observed, so model that flow explicitly in specs and tests.
Reservations And Async Helpers
ReservationId::reserveand.unreserveexpose the reserve or unreserve path throughgcore::exec, which maps togr_reserve_gasandgr_unreserve_gas.- Reservation pools in
gstd::reservationshelp when the design needs several future-block gas buckets, but they do not remove expiry or waitlist constraints. #[gstd::async_init]and#[gstd::async_main]enable reply-await flows in standard programs; they still rely on message boundaries, reply hooks, and block-based progress.
Design Heuristics
1. Start from the contract: typed API, bytes API, staged send, delayed work, reservation-backed work, or child-program creation. 2. Check execution semantics before checking syscall names: rollback boundary, future blocks, reply timing, waitlist exposure, and gas lifetime. 3. Use gcore when the design needs exact parameter shape or you suspect fallible behavior matters. 4. Use gsys when you need the precise gr_send, gr_reply, gr_reserve_gas, gr_wait, or related names for debugging, audits, or low-level reasoning.
Guardrails
- Keep
gstdas the default vocabulary in specs, architecture notes, and builder guidance. - Do not design app flows directly in terms of raw syscall sequences unless the task is explicitly low-level.
- Treat
gcoreandgsysas explanation layers beneath the public API, not as the normal app-builder entry point. - This is not a full syscall-maintenance catalog; benchmark integrity and instrumentation coverage belong to lower-level workflow docs.
Gear Messaging And Replies
Send And Reply Families
- Use encoded send and reply APIs for normal SCALE-encoded payload types.
- Use bytes variants only when debugging route or codec mismatches, or when a raw transport path is intentional.
- Use delayed variants when execution must happen in a future block.
- Use reservation-backed variants when later execution needs preserved gas.
- Use staged payload flows only when the payload must be built incrementally.
- When a Sails program calls a Sails constructor or service, prefer generated clients or equivalent Sails Header-aware encoding rather than an ad hoc raw struct payload.
Message Lifecycle Checklist
1. Choose the payload style: typed, bytes, or forwarded input. 2. Choose delivery mode: immediate, delayed, explicit gas, or reservation-backed. 3. Choose interaction pattern: fire-and-forget or for_reply. 4. Choose reply mode: one-shot reply or staged push-plus-commit.
Async for_reply Flows
for_replyflows wait on a reply-producing message and return a future.- Timeouts are a distinct outcome and should be handled separately from error replies.
- A reply hook needs non-zero reply deposit to register.
- A timed-out future does not guarantee the reply hook never runs later if the reply eventually arrives.
Staged Payload Rules
- A staged send or reply is incomplete until its matching commit step runs.
- If a code path pushes payload pieces but never commits, the staged payload is dropped.
- Keep staged flow use narrow; it is easier to miss a commit than with one-shot replies.
Context-Specific Semantics
- Reply metadata belongs in reply-handler context only.
- Signal metadata belongs only in signal-handler context where that entrypoint is supported.
- Do not assume a generic message handler can safely read reply or signal context APIs.
Sails Routing Notes
- Generated Sails clients encode the Sails Header and SCALE body for you.
- The Sails 1.0 wire format is Sails Header v1 plus encoded data, so a bare raw struct is not a normal constructor or service payload.
- If a payload uses the wrong interface ID, entry ID, or route index, decode fails even when the payload body type is otherwise correct.
- Treat generated clients as the default path and use low-level byte encoding only to isolate transport or codec bugs.
Guardrails
- Outbound send and reply effects appear only after successful execution.
- Handle timeout, transport failure, and error reply as different branches.
- Always close staged payload paths with the correct commit call.
- Use
Syscall::gas_available()for service-code available-gas checks;msg::gas_available()is not the standard Gear API.
Gear/Sails Production Patterns
This note is a portable, production-biased companion to the narrower execution, architecture, IDL, and gtest references in this pack.
It is distilled from local research across official sails examples, reusable awesome-sails service patterns, and larger dapps codebases. The snippets below are intentionally inlined so another machine does not need those sibling repos.
Evidence weighting matters:
- Treat official
sailspatterns as the primary source for current framework defaults. - Treat
awesome-sailsas strong evidence for reusable service and storage patterns. - Treat
dappsas real-world evidence, but some of those projects are older or outdated. - Older
dappspatterns should not override current Sails-first defaults just because they exist in production code. - Do not copy older
dappscode blindly; validate it against the current Sails routing, client-generation, and state-management guidance in this pack.
Recommended Defaults
- Prefer program-owned state in
RefCellfields and pass references into services. - Keep
#[program]thin: constructor/init, service exposure, and reply wiring only. - Keep business rules, guards, and events inside
#[service]. - Prefer generated clients and IDL-backed routing over hand-built payload bytes.
- Treat stateful command failure as fatal: panic and let the current message revert.
- Use delayed self-messages and
ReservationIdonly when future-block execution is part of the design.
Fast Facts
- Standard Vara account flows use
SS58, not Ethereum0xaddresses. When chain-specific formatting matters, use Vara prefix137. - Pull
awesome-sailsfrom crates.io withdefault-features = false. If the program only needs pause, math, or map helpers, depend onawesome-sails-utilsdirectly. - Delayed work is block-based on chain. A program can send a delayed message to itself for a later block.
ReservationIdpreserves gas for future-block execution. A delayed step cannot spend the current message's gas later.- In
gtest, onesend()is not proof the whole async flow already settled. UseBlockRunModeand explicit block advancement when timing matters. - The waitlist is a temporary runtime holding area, not durable storage. Waitlisted messages pay rent or holding budget over blocks and can timeout or expire.
- Vouchers let a sponsor pay gas or fees for approved interactions. Signless shifts signing to a delegated, temporary, or session-style account.
Awesome Sails As An External Dependency
awesome-sails is not just local reference material. It is organized as publishable crates that can be added to a Sails project from crates.io.
Use it when an agent should compose proven building blocks instead of reimplementing them:
- role-based access control
- reusable storage abstractions
- VFT and related admin/exchange services
- message-status tracking for async flows
- pause wrappers, checked math, compact integer wrappers, and sharded maps
For project dependencies, prefer explicit features instead of the meta-crate default:
[dependencies]
sails-rs = { version = "*", default-features = false, features = ["gstd"] }
# Meta-crate, but feature-selected on purpose.
awesome-sails = { version = "x.y.z", default-features = false, features = [
"storage",
"access-control",
"vft",
"vft-admin",
"vft-extension",
"vft-metadata",
"vft-native-exchange",
"vft-native-exchange-admin",
"msg-tracker",
] }
# Separate utility crate when you want the low-level helpers directly.
awesome-sails-utils = "x.y.z"Important packaging facts:
- the
awesome-sailsmeta-crate defaults toall - for agent-authored production code, prefer
default-features = false - the meta-crate exposes feature flags for
storage,access-control,vft,vft-utils,vft-admin,vft-extension,vft-metadata,vft-native-exchange,vft-native-exchange-admin, andmsg-tracker - the meta-crate also has a
testfeature that forwards toawesome-sails-vft-utils/test awesome-sails-utilsis a separate crate, not a feature of the meta-crate
Feature/Crate Chooser
storage/awesome-sails-storage: choose this when a service should be generic over storage backends. It providesStorage,StorageMut,InfallibleStorage,InfallibleStorageMut, andStorageRefCell.access-control/awesome-sails-access-control: choose this when privileged operations outgrow a single-admin check. It providesAccessControl,RolesStorage, role hierarchy support, enumeration, and batch grant/revoke flows.vft-utils/awesome-sails-vft-utils: choose this when you need the underlying token storage/value types. It providesAllowances,Balances, compactAllowanceandBalancewrappers, shard-aware storage helpers, and related errors.vft/awesome-sails-vft: choose this for a standard VFT service with transfers, approvals, allowance handling, and balance/total-supply queries.vft-admin/awesome-sails-vft-admin: choose this when the token needs privileged mint, burn, pause, and admin-managed maintenance operations.vft-extension/awesome-sails-vft-extension: choose this when the token needstransfer_all, expired-allowance cleanup, balance/allowance enumeration, or explicit shard-management helpers.vft-metadata/awesome-sails-vft-metadata: choose this when the token should expose metadata like name, symbol, and decimals.vft-native-exchange/awesome-sails-vft-native-exchange: choose this when native value should mint VFT and burning VFT should return native value.vft-native-exchange-admin/awesome-sails-vft-native-exchange-admin: choose this when the exchange flow needs admin recovery,burn_from, or failed-value-transfer handling viahandle_reply.msg-tracker/awesome-sails-msg-tracker: choose this for async orchestration, saga-style flows, or any path that needs per-MessageIdstatus tracking. It providesMsgTracker,Pagination,MessageStorage,BTreeMapsupport, andFixedStoragefor bounded-capacity tracking.
Separate Utility Crate Chooser
awesome-sails-utils is the low-level helper crate. Reach for it when the project needs reusable primitives rather than a whole service.
error: shared error types such as a genericError,BadInput,BadOrigin,BadValue, andEmitError.map: a sharded-map implementation for controlled-capacity storage layouts.math:CheckedMath, compactLeBytes<N>integers,NonZero<T>, and the common numeric re-exports used across the token crates.pause:Pause,PausableRef,PausableStorage, and pause-related errors for emergency-stop style guards.macros: shared helper macros used by the ecosystem.
If an agent only needs CheckedMath, LeBytes, NonZero, PausableRef, or a sharded map, depend on awesome-sails-utils directly instead of pulling in the full service stack.
1. Program-Owned State Is The Default
This is the cleanest default for standard Gear/Sails programs. The program owns lifetime and storage; the service owns behavior.
use sails_rs::{cell::RefCell, prelude::*};
pub struct CounterData {
value: u32,
}
pub struct Program {
counter: RefCell<CounterData>,
}
pub struct CounterService<'a> {
counter: &'a RefCell<CounterData>,
}
impl<'a> CounterService<'a> {
pub fn new(counter: &'a RefCell<CounterData>) -> Self {
Self { counter }
}
}
#[service]
impl CounterService<'_> {
#[export]
pub fn add(&mut self, by: u32) -> u32 {
let mut data = self.counter.borrow_mut();
data.value += by;
data.value
}
}
#[program]
impl Program {
pub fn new() -> Self {
Self {
counter: RefCell::new(CounterData { value: 0 }),
}
}
pub fn counter(&self) -> CounterService<'_> {
CounterService::new(&self.counter)
}
}Why this is the default:
- state ownership is explicit at the program boundary
- constructor/setup is deterministic
- tests can inject or reset state cleanly
- services stay reusable and mostly stateless
2. Wrap Storage When Reusable Services Need It
awesome-sails shows a stronger reusable pattern: accept storage abstractions instead of hard-coding raw RefCell everywhere.
use awesome_sails_storage::StorageRefCell;
use awesome_sails_utils::pause::{PausableRef, Pause};
use sails_rs::{cell::RefCell, prelude::*};
pub struct Program {
pause: Pause,
balances: RefCell<Balances>,
}
impl Program {
pub fn balances(&self) -> PausableRef<'_, Balances> {
PausableRef::new(&self.pause, StorageRefCell::new(&self.balances))
}
pub fn token(&self) -> TokenService<'_> {
TokenService::new(self.balances())
}
}Use StorageRefCell, PausableRef, or similar wrappers when:
- the same business service should work with different storage backends
- you need guards around every mutation path
- reusable domain modules should be portable across programs
Production implication: expose helper methods like balances() or access_control_storage() on the program and construct the service from those helpers instead of borrowing raw fields everywhere.
3. Static Hidden State Is Allowed, But Not The Default
Some real apps use static hidden state to keep a service self-contained. This is valid, but it has sharper edges than program-owned state.
#![allow(static_mut_refs)]
use sails_rs::prelude::*;
static mut STORAGE: Option<MyStorage> = None;
#[derive(Default)]
struct MyStorage {
next_id: u64,
}
pub struct MyService;
impl MyService {
pub fn seed() {
unsafe {
STORAGE = Some(MyStorage::default());
}
}
fn storage_mut(&mut self) -> &'static mut MyStorage {
unsafe { STORAGE.as_mut().expect("MyService::seed() should be called") }
}
}
#[program]
impl Program {
pub fn new() -> Self {
MyService::seed();
Self
}
}If you choose static hidden state:
- the service must have an explicit
seed()orinit()path before first use - the program constructor must call that setup exactly once
- tests must re-seed or isolate initialization
- helper accessors should fail loudly if initialization was skipped
Costs:
- weaker test isolation
- more hidden coupling between constructor flow and service behavior
- easier async stale-state bugs
- harder refactors when multiple services need shared storage visibility
Use static hidden state only when the self-contained boundary is materially better than explicit program-owned state.
4. Hybrid State Is Common In Larger Apps
A practical production split is:
- program-owned
RefCell<SessionStorage>or other cross-cutting identity/session state - static hidden state for one dominant domain service
That hybrid model is acceptable when:
- one subsystem like session/signless routing needs explicit shared ownership
- the main game/service state is intentionally encapsulated behind one service
Guardrail: name the split explicitly in design docs. Hybrid state that is not documented becomes accidental architecture.
5. Keep #[program] Thin
The stable boundary is:
#[program]: constructors, service exposure, reply wiring#[service]: commands, queries, guards, events, domain logic
#[program]
impl Program {
pub fn new() -> Self {
Self::default()
}
pub fn handle_reply(&mut self) {
self.exchange_admin().handle_reply();
}
pub fn exchange_admin(&self) -> ExchangeAdmin<'_> {
ExchangeAdmin::new(self.access_control(), self.balances())
}
}If a #[program] method starts making domain decisions beyond setup and exposure, the boundary is already drifting.
6. Use Shallow extends Composition
Composition is good. Inheritance-like complexity is not.
pub struct DogService {
mammal: MammalService,
walker: WalkerService,
}
impl From<DogService> for (MammalService, WalkerService) {
fn from(value: DogService) -> Self {
(value.mammal, value.walker)
}
}
#[service(extends = [MammalService, WalkerService], events = DogEvents)]
impl DogService {
#[export]
pub fn make_sound(&mut self) -> &'static str {
self.emit_event(DogEvents::Barked).unwrap();
"Woof! Woof!"
}
}Use extends when:
- the inherited API is part of the intended public contract
- route and event interactions stay understandable
- the service can clearly convert into the extended services
Do not use extends to hide a messy architecture. Keep it shallow and intentional.
7. Put Guards Near The Service Boundary
The stronger patterns keep access control, pause checks, and session validation close to exported methods.
#[service]
impl AdminService<'_> {
#[export]
pub fn add_admin(&mut self, admin: ActorId) {
self.ensure_is_admin();
self.storage_mut().admins.insert(admin);
}
fn ensure_is_admin(&self) {
assert!(self.storage().admins.contains(&Syscall::message_source()), "Not admin");
}
}Production defaults:
- grant bootstrap roles in constructor/init
- keep one guard helper per privileged concern
- fail before mutating state
- prefer reusable guard wrappers over repeated inline checks when the same rule protects multiple mutation paths
8. Define And Emit Typed Service Events
In Sails, events are a first-class service pattern, not an afterthought.
The stable default is:
- define a typed event enum with #[event]
- attach it to the service with #[service(events = Events)]
- emit events from state-changing command paths with self.emit_event(...)
This matches the framework model: events are declared per service, each enum variant represents a distinct event shape, and once a service declares events = ..., Sails generates the emit_event method for that service. These events are meant to notify off-chain consumers about state changes, and generated JS surfaces can expose them for subscription through the IDL-driven client layer.
#[sails_rs::event]
#[sails_rs::sails_type]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Events {
Created(ActorId),
Removed(u64),
}
#[service(events = Events)]
impl MyService {
#[export]
pub fn create(&mut self) {
self.emit_event(Events::Created(Syscall::message_source())).unwrap();
}
}Production rules:
- define one service-level event enum per emitted event surface
- emit events from successful state-changing command paths, not from read-only queries
- treat event variants as part of the public integration contract
- prefer structured fields over opaque blobs so indexers, frontends, and analytics code can evolve safely
- emit after the authoritative state transition is accepted, not before
- keep event naming domain-oriented and stable enough for downstream consumers
Guardrails:
- do not emit vague variants like Updated when the domain change should be explicit
- do not treat event payload layout as disposable once off-chain consumers depend on it
- do not emit success-style events on paths that may still fail or revert
emit_eventis only available inside theimplblock annotated with#[service(events = Events)]. Internal methods that need to emit events must be defined in that same annotatedimplblock, not in a separate plainimpl. They can be private (no#[export]).
9. Async Rules: Re-Read State After .await
Any shared mutable state read before .await may be stale after resume.
#[service]
impl ResourceService<'_> {
#[export]
pub async fn attach_part(&mut self, id: u32, part_id: u32) -> Result<(), Error> {
self.ensure_is_admin()?;
let base = self.storage().resources.get(&id).ok_or(Error::Missing)?.base;
let part = self
.catalog_client
.part(part_id)
.with_destination(base)
.await
.unwrap();
if part.is_none() {
return Err(Error::MissingPart);
}
let resource = self.storage_mut().resources.get_mut(&id).ok_or(Error::Missing)?;
resource.parts.push(part_id);
Ok(())
}
}Production rules:
- do not hold mutable-borrow assumptions across
.await - after
.await, re-read or revalidate shared state before mutating it - if a command cannot tolerate drift, redesign it into smaller messages or stricter validation steps
10. Default To #[export(unwrap_result)] For Fatal Command Paths
For stateful command handlers, the default Sails production style is to return Result<_, Error> from the service method and expose it with #[export(unwrap_result)].
That keeps the command path explicit in Rust, while preserving fail-fast semantics at the exported boundary.
#[service]
impl BattleService<'_> {
#[export(unwrap_result)]
pub fn cancel_tournament(&mut self) -> Result<(), Error> {
let event = self.cancel_tournament_impl()?;
self.emit_event(event).expect("Notification Error");
Ok(())
}
}Why this is the default:
- the method body keeps normal Result-based control flow
- domain failures stay typed and readable inside the implementation
- the exported command still fails fast instead of leaving partially-updated state alive
- the Sails boundary expresses the intended production semantics directly
Recommended practice:
- return Result from stateful command handlers when failure is part of the internal control flow
- use #[export(unwrap_result)] when command failure should revert the current execution
- expect UserspacePanic in tests for fatal exported command-path failures
- keep compensation or partial-success behavior only when the spec explicitly models it
11. Use Fail-Fast Command Semantics Allowed, But Not The Default
Manual fail-fast conversion is still valid, but it should be treated as an explicit exception rather than the primary style.
pub fn panicking<T, E: core::fmt::Debug, F: FnOnce() -> Result<T, E>>(f: F) -> T {
match f() {
Ok(v) => v,
Err(e) => panic!("{e:?}"),
}
}
#[service]
impl BattleService<'_> {
#[export]
pub fn cancel_tournament(&mut self) {
let event = panicking(|| self.cancel_tournament_impl());
self.emit_event(event).expect("Notification Error");
}
}This style is allowed when:
- the public method intentionally exposes no Result shape at all
- the code must adapt older service structure without a wider refactor
- a small compatibility layer is clearer than rewriting multiple internal helpers
Costs:
- the exported API hides the recoverable/internal error shape
- it is less idiomatic than using Sails export behavior directly
- agents may copy the helper mechanically even where #[export(unwrap_result)] is clearer
Default rule: prefer #[export(unwrap_result)] over hand-rolled panic wrappers unless the method shape has a strong reason not to return Result.
12. Generated Clients First, Raw Bytes Only As Escape Hatch
The stable default is IDL plus generated clients.
// build.rs (dedicated client crate)
fn main() {
sails_rs::build_client::<demo::DemoProgram>();
}let reply = DemoProgram::client(program_id)
.counter()
.add(10)
.with_reply_deposit(10_000_000_000)
.await
.unwrap();Use generated clients by default because they preserve:
- Sails Header routing correctness
- reply decoding
- test mocks
- consistency between Rust and TS consumers
Hand-roll raw bytes only at low-level integration boundaries or when isolating codec/routing bugs.
13. Delayed Work And ReservationId Need Explicit Design
Delayed self-messages and reservations are for genuinely future-block workflows, not for vague “maybe later” logic.
fn send_timeout_from_reservation(
reservation_id: ReservationId,
player_id: ActorId,
delay: u32,
) {
let request = battle_client::battle::io::AutomaticMove::encode_call(
battle_client::BattleClientProgram::ROUTE_ID_BATTLE,
player_id,
);
msg::send_bytes_delayed_from_reservation(
reservation_id,
Syscall::program_id(),
request,
0,
delay,
)
.expect("Error in sending message");
}fn send_cleanup(player: ActorId, gas: u64, delay: u32) {
let payload = game_client::game::io::RemoveInstance::encode_call(
game_client::GameClientProgram::ROUTE_ID_GAME,
player,
);
msg::send_bytes_with_gas_delayed(Syscall::program_id(), payload, gas, 0, delay)
.expect("Error in sending message");
}Production rules:
- use delayed self-messages for scheduled in-protocol work, not off-chain cron assumptions
- persist enough state to make the delayed handler idempotent or safely repeatable
- use
ReservationIdonly when later execution budget must survive across executions - unreserve or remove reservations when the lifecycle ends
- derive gas budgets from
Syscall::gas_available(), not hard-coded values. When a handler both does work AND schedules its own next invocation via delayed self-message, a fixed gas amount will fail if execution consumed most of the budget. Uselet gas_for_next = Syscall::gas_available() * 9 / 10;or similar dynamic calculation. - on-chain, a program must hold sufficient VARA balance to cover gas for delayed messages. Transfer VARA to the program address after deployment. In
gtest, delayed messages succeed without explicit program funding — this asymmetry is a common source of "works in test, fails on chain" bugs.
If delayed work is essential, the architecture should name:
- who sends the follow-up
- which route receives it
- which gas source pays for it
- how stale or duplicate messages are handled
14. Test Like Production, Not Like A Synchronous Call Stack
Use GtestEnv modes intentionally.
use sails_rs::client::{BlockRunMode, GtestEnv};
let env = GtestEnv::new(system, ACTOR_ID.into()).with_block_run_mode(BlockRunMode::Next);
let reply = program.counter().add(10).send_for_reply().unwrap();
env.run_next_block();
let value = reply.await.unwrap();Why BlockRunMode matters:
Autois convenient for simple one-step callsNextexposes whether the reply actually lands in the expected next blockManualis right when the test must drive multi-block behavior explicitly
Also prefer:
- unit-level service tests when state can be injected through
RefCellor wrappers - assertions on events and reply errors, not only local state
- explicit expectations for
UserspacePanicon fatal command paths
Practical Defaults For Agents
When another agent has to choose quickly, bias toward this order:
1. Program-owned state plus thin service wrappers. 2. StorageRefCell or PausableRef when reusable storage-aware services are justified. 3. Generated clients plus build.rs IDL/client generation. 4. Fail-fast command handlers that test for UserspacePanic. 5. Delayed self-messages only when the feature is genuinely block-shaped. 6. ReservationId only when a later execution budget must be preserved. 7. Static hidden state only with explicit seed() or init() discipline.
Anti-Patterns
- fat
#[program]methods that own domain logic - static hidden state without
seed()orinit()discipline - mixing generated clients with ad hoc raw payloads on the same normal path
- mutating shared state before
.awaitand assuming it is still authoritative afterward - delayed work without explicit gas-budget and replay/idempotency thinking
- deep
extendsgraphs that obscure the public contract - soft-error command handlers that leave partially-updated state alive
Gtest Cheatsheet
Baseline Flow
1. Create exactly one System. 2. Fund the sender before sending. 3. Submit or deploy the program. 4. Send messages and keep their ids. 5. Advance blocks explicitly. 6. Assert logs, mailbox state, reply behavior, and spent value.
Things To Not Assume
senddoes not execute the queue immediately.- The first message is often initialization.
- Time-dependent behavior needs explicit block advancement.
- Program funding and user funding are not interchangeable.
- Programs creating child programs via
create_program_bytesmust have balance to cover the child's existential deposit. Fund the parent program before the factory call. GtestEnvconsumes theSystem, making post-deploysystem.mint_to()awkward. Fund programs as part of the deploy fixture before wrapping inGtestEnv, or use rawgtestAPIs for the funding step.
Child Program Creation
gstd::prog::create_program_bytes(code_id, salt, payload, value)requires the calling program to hold sufficient balance. Withvalue = 0, the call still fails if the child needs existential deposit.- In
gtest, fund the parent program withsystem.mint_to(parent_program_id, amount)before invoking the factory method. gstd::progis not re-exported throughsails_rs::gstd. Addgstdas a direct dependency for program creation primitives.
Sails-First Testing
- Prefer generated clients in
GtestEnvover hand-authored payload bytes. - Assert one constructor path and one command/query path in the happy case.
- Add event assertions when the service exposes events.
- Use a deterministic block run mode when the workflow depends on async replies or exits.
What The Core Skills Should Capture
- The task plan must call out required gtest coverage.
- The implementer should not claim completion without a green local gtest loop.
- The gtest loop skill should summarize failures in a form that is patchable by an agent.
See Also
references/sails-gtest-and-local-validation.mdreferences/gear-gas-reservations-and-waitlist.md
Gtest Patterns
Default Path
- Prefer
GtestEnvand generated clients for standard Sails verification. - Drop to raw
gtest::Programcalls only when you are debugging routing, payload encoding, failure logs, or timing at a lower level than the generated client exposes.
Raw send_bytes Mental Model
Program::send_bytes(...)returns aMessageId.System::run_next_block()returns aBlockRunResult.- Replies and failures are inspected from that
BlockRunResult, not from thesend_bytesreturn value.
let msg_id = program.send_bytes(sender, payload);
let r = system.run_next_block();
assert!(!r.failed.contains(&msg_id));
let reply = r
.log()
.iter()
.find(|entry| entry.reply_to() == Some(msg_id))
.expect("reply log missing");Init Semantics
The first message sent to a program is always treated as init, even if the program has no explicit init function. Tests should account for this when choosing the first payload.
What To Inspect
r.log()contains the block logs and replies that landed in that block.r.succeed,r.failed, andr.not_executedare the first outcome buckets to inspect.r.failed.contains(&msg_id)is the first quick check for command-path failure.reply.reply_code()tells you whether the reply was a success or an error.reply.reply_to()links a reply log back to the original message id.r.contains(&expected_log)is the quickest structured assertion when aLogpattern is enough.r.decoded_log::<T>()is useful when you want typed log inspection instead of raw bytes.- The payload still needs decoding after you extract the matching reply log entry.
Panic Assertions
- For fatal-path validation, prefer
r.assert_panicked_with(msg_id, "...")when you want to assert userspace panic text. - Use raw
reply_code()inspection when you need finer-grained low-level failure analysis.
Multi-Block And Delayed Behavior
- Use
system.run_to_block(n)when delayed sends, wakeups, or reservation expiry depend on block height. run_next_block()is enough only when the full effect should land in the very next block.- For Sails
GtestEnv, chooseBlockRunMode::NextorBlockRunMode::Manualwhen the test must expose exact timing. run_scheduled_tasks(n)advances blocks while processing scheduled tasks only, without draining the message queue.run_next_block_with_allowance(...)is useful for low-level gas/allowance timing tests.
Reply Decoding Reminder
- Generated clients handle reply prefixes for you.
- In raw tests for Sails programs, reply payloads may still include Sails routing framing (service + method + result), so decode them with Sails-aware helpers when the test is not intentionally validating raw framing.
- If the goal is not codec or routing debugging, go back to the generated client path.
Setup Ergonomics
Program<'_>borrowsSystem, so a helper usually cannot return(System, Program<'_>)without running into lifetime friction.- Prefer either:
- a helper that returns only
System, then buildPrograminside each test - a helper that performs the setup inline per test when the borrow would otherwise escape
- Also remember that only one System instance is allowed per thread.
Failure Patterns
UserspacePanicis often the expected assertion target for fatal command-path validation.RanOutOfGasand similar reply codes are best asserted through reply logs plusfailed.contains(&msg_id).- For low-level debugging, record the
MessageId, the block you ran, and the relevantBlockRunResultevidence in the test note.
Program Funding And Child Creation
- Programs that create child programs via
gstd::prog::create_program_bytesneed balance in their account. The child program requires existential deposit even whenvalue = 0is passed. - In
gtest, fund the parent program before the factory call:
// Fund parent program so it can afford child existential deposit
system.mint_to(parent_program_id, 100_000_000_000_000);
// Now the factory method can call create_program_bytes successfully
let msg_id = program.send_bytes(sender, create_child_payload);
let r = system.run_next_block();
assert!(!r.failed.contains(&msg_id));GtestEnvwrapsSystem, making directmint_tocalls awkward after deployment. Either:- Fund the program before wrapping in
GtestEnv. - Use raw
gtestAPIs for the funding step alongsideGtestEnvfor typed calls. NotEnoughValueingtestfor child creation almost always means the parent program is unfunded.
Sails Cheatsheet
Release Baseline
- Treat
sails-rs 1.0.0as the current standard baseline for this pack. If the target repo pins a different version, follow the repo version. - Public service methods must be marked with
#[export]to become Sails routes. - Event enums should use
#[sails_rs::event]. - Event emission should use
emit_event, not older renamed patterns.
Program Shape
#[program]owns constructors that returnSelfand exposes services.#[service]owns business logic and exported commands or queries.- One application has one
#[program], but it may expose multiple services.
Export Rules
- Treat
#[export]as required for every publicly callable service method. &mut selfexports are commands and may mutate state.&selfexports are queries and should be read-only.#[export(route = "...")]is the stable routing contract for services or methods; by default, exposed service and method names are converted to PascalCase.#[export]supports additional options in Sails 1.0:scale,ethabi,payablefor ethexe paths. See../../skills/sails-ethexe-implementer/SKILL.md.CommandReply<T>is the value-returning command path.- Service events should be emitted with
self.emit_event(...).
Event Rules
- Declare event enums with
#[sails_rs::event]. - Attach the event enum to the service with
#[service(events = Events)]. - Emit service events through
self.emit_event(...). - Treat events as part of the public contract: name them after meaningful state transitions, not internal implementation steps.
- Events are only published if the emitting command completes successfully.
- Outgoing event payloads follow the Sails route-framed layout: service name, event name, then event data.
Common Pitfalls
Syscall::message_source()andSyscall::program_id()requireuse sails_rs::Syscall;. Theprelude::*does not re-exportSyscall. Missing this import causesunresolved importerrors when writing guards or delayed-message helpers. UseSyscall::*in preference to rawmsg::source()/exec::program_id()calls.emit_eventis generated by the#[service(events = Events)]macro and is only available inside that annotatedimplblock. Internal helper methods that need to emit events must be defined in the same#[service(events = Events)] implblock (without#[export]). A separate plainimplblock cannot callemit_event.sails_rs::collections::BTreeMapis ano_stdre-export. Somestdmethods likedrain()are not available. Usekeys().cloned().collect::<Vec<_>>()then iterate and remove as a workaround for drain-like patterns.gstd::prog(includingcreate_program_bytes) is not re-exported throughsails_rs::gstd. If a Sails program needs to create child programs, addgstdas a direct dependency:gstd = "1.10.0".- IDL-visible types require
ReflectHashderive (included automatically via#[sails_rs::sails_type]). Prefer#[sails_rs::sails_type]over manual derive stacks for service types, command/query params, and event payloads. - A function returning
CommandReply<()>that also accepts value (payable) must send a manual reply. The framework does not auto-reply forCommandReply<()>when value is attached. If the reply is missing, the caller's value transfer succeeds but no reply is delivered.
Troubleshooting: Common Compile Errors
| Error Message | Cause | Fix |
|---|---|---|
Could not find parity-scale-codec | Shared types derive Encode/Decode without re-path attributes | Add #[codec(crate = sails_rs::scale_codec)] and #[scale_info(crate = sails_rs::scale_info)] to the type |
#[panic_handler] function required | Top-level src/lib.rs missing the wasm re-export | Add #[cfg(target_arch = "wasm32")] pub use app_crate::wasm::*; to the root crate src/lib.rs |
service attribute requires at least one public method with #[export] | Service impl block has no exported methods | Add #[export] to at least one public method in the #[service] impl |
These errors appear in order of frequency from builder feedback.
Program-Level Payable
- Use
#[program(payable)]if the program must accept value on an empty payload without routing to a service.
Advanced Export Helpers
#[export(unwrap_result)]allows internal use ofResult<T, E>and?while exposing the unwrapped success path to clients.
IDL And Clients
- Sails generates IDL from Rust types at build time.
- Generated clients are the default typed integration surface for Rust and TypeScript.
- Generated clients encode the correct payloads for constructor and service calls using the Sails Header plus SCALE body.
- The IDL uses V2 syntax (see
../../references/sails-idl-v2-syntax.md) and messages use a binary header protocol (see../../references/sails-header-wire-format.md). - Architecture decisions must keep exported DTO names distinct from service names.
- Events are part of the public interface and should map to meaningful state transitions.
Skill Implications
- Specs should talk in terms of program constructors, service routes, commands, queries, and events.
- Specs should name the chosen state ownership pattern instead of leaving storage implicit.
- Architecture plans should keep
#[program]thin and push logic into services. - Implementation guidance should prefer generated clients or other Sails Header-aware encoding over raw payload handling.
See Also
references/sails-rs-imports.mdreferences/sails-program-and-service-architecture.mdreferences/sails-idl-client-pipeline.md
Sails Program And Service Architecture
Baseline Structure
#[program]should stay thin: constructors returningSelf, wiring, and service exposure only.#[service]should own business logic, stateful commands, read-only queries, and events.- One application has one
#[program], but it may expose multiple services by business boundary.
State Ownership Patterns
- Name the state ownership pattern explicitly in the architecture note instead of leaving storage implied.
- Program-owned state is the preferred default because ownership is explicit and test setup is simpler.
- Hidden static service state is acceptable only when deterministic seeding and test isolation are controlled; it is an allowed alternative, not a required Sails default.
- Use shallow
extendscomposition when it improves reuse; do not build a maze of inherited service surfaces.
Routes And Public Contract
- Treat exported service and method routes as a compatibility surface.
- Use explicit route policy when compatibility matters across refactors.
- Keep exported DTO names distinct from service names so generated clients stay readable.
- Events should represent externally meaningful state transitions, not every internal branch.
Async And Failure Rules
- Any state read before
awaitmay be stale after resume. - Revalidate or re-read shared mutable state after
awaitbefore mutating it. - In stateful command paths, treat transport failure and error reply as fatal unless the design explicitly models compensation.
- Panic is a valid transaction boundary for one message: state, replies, and events from that execution roll back together.
Recommended Defaults
- Use program-owned state plus service wrappers.
- Keep constructor shape, state ownership, and service exposure aligned so the program contract is obvious from the
#[program]surface. - Keep access-control checks centralized in service-local guard helpers.
- Emit events close to the successful state transition they describe.
- Prefer fail-fast command semantics over soft-error partial commits.
Common Anti-Patterns
- Fat
#[program]methods with domain logic - Mixed route policy with no stability intent
- Static state without deterministic seeding
- Async stale-state bugs after
await - Mutate-before-send logic that ignores rollback on failure
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
python3 "${ROOT}/tests/test_repo_layout.py"
3.0.0