Preamble (run first) If output shows : read and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise ask user with 3 options, write snooze if declined). If : tell user "Running vara-skills v{to} (upgraded from v{from})!" and continue. Vara Skills This repository is the portable router for the provisional pack. Use first when the task is about standard greenfield or unreleased Gear/Vara Sails application work. The repo is intended to be self-contained: use local handbook files before depending on sibling repositories or machine-local skill directories. What This Router Cover…

; then\n # Invalid or empty response — assume up to date\n echo \"UP_TO_DATE $LOCAL\" > \"$CACHE_FILE\"\n exit 0\nfi\n\nif [ \"$LOCAL\" = \"$REMOTE\" ]; then\n echo \"UP_TO_DATE $LOCAL\" > \"$CACHE_FILE\"\n exit 0\nfi\n\n# Versions differ — upgrade available\necho \"UPGRADE_AVAILABLE $LOCAL $REMOTE\" > \"$CACHE_FILE\"\nif check_snooze \"$REMOTE\"; then\n exit 0 # snoozed — stay quiet\nfi\n\n# ─── Pre-refresh marketplace clone for plugin installs ───────\n# Claude Code's `plugin update` reads the local HEAD of the\n# marketplace clone, not origin. If the clone is stale, `update`\n# thinks the installed version is already latest. Fix: reset the\n# clone to origin/main so `claude plugin update` sees the new version.\n_MKT=\"$HOME/.claude/plugins/marketplaces/vara-skills\"\nif [ -d \"$_MKT/.git\" ]; then\n git -C \"$_MKT\" fetch origin --quiet 2>/dev/null || true\n git -C \"$_MKT\" reset --hard origin/main --quiet 2>/dev/null || true\nfi\n\necho \"UPGRADE_AVAILABLE $LOCAL $REMOTE\"\n","content_type":"text/plain; charset=utf-8","language":null,"size":6273,"content_sha256":"59a858a2e9562f2fb8bd2d8d6a76a3bef2095730b7133321d6ed92768ea2ccf9"},{"filename":"CHANGELOG.md","content":"# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n## [3.0.0] - 2026-05-18\n\nVerified 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.\n\n### Added\n- `sails-ethexe-architecture` — ethexe feature planning and boundary isolation skill\n- `sails-ethexe-implementer` — ethexe-specific Rust implementation skill (Syscall gating, `#[export(ethabi|payable)]`, `emit_eth_event`)\n\n### Updated\n- Skill pack baseline bumped to `sails-rs 1.0.0` throughout (`sails-rs-imports.md`, `sails-cheatsheet.md`, `ship-sails-app/SKILL.md`)\n- New references: `sails-syscall-mapping.md`, `sails-idl-v2-syntax.md`, `sails-header-wire-format.md`, `sails-ethexe-patterns.md`\n- `sails-gtest-and-local-validation.md` Event Listener Pattern updated: service client now implements `Listener` directly\n- `sails-rust-implementer`: mandates `Syscall::*` for all runtime accessors; adds `#[sails_type]` guidance; adds ethexe hand-off guardrail\n- `gear-sails-production-patterns.md`: converted to `Syscall::*` throughout\n\n### Fixed\n- `@entry-id` annotation (hyphen) corrected to `@entry_id` (underscore) in `sails-idl-v2-syntax.md` and `sails-header-wire-format.md` — parser only accepts underscore form\n- `Syscall::*` inconsistency in Sails service code: `sails-cheatsheet.md`, `awesome-sails-token-patterns.md`, `ship-sails-app/SKILL.md` all updated (gstd-level references kept on `exec::*`/`msg::*`)\n- Five broken `../../docs/` relative links across four files replaced with upstream GitHub URLs (or local reference paths)\n- Skill count updated to 23 in `plugin.json`, `marketplace.json`, and `CLAUDE.md`\n\n## [2.2.0] - 2026-04-27\n\nvara-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.\n\n### Updated — vara-wallet skill, 0.13/0.14 surface\n- 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 `--idl` after first call.\n- Quick Reference: added `discover` introspection, `--dry-run` payload encoding (no signing/submit), `--args-file` (with stdin support), `idl import` for seeding the cache, `subscribe messages` IDL-aware decoding, and `watch` event streaming.\n- Workflow examples: dry-run preview, stdin args-file for nested JSON, IDL-aware event monitoring.\n- Error Recovery: added `AMBIGUOUS_EVENT`, `INVALID_ARGS_SOURCE`, `STDIN_IS_TTY`, `CONFLICTING_OPTIONS`, `PROGRAM_ERROR` (then refreshed in the 0.16 pass below).\n\n### Updated — vara-wallet skill, 0.15.0 surface\n- Units vocabulary unified to `human` / `raw` across `transfer`, `vft`, and `dex`. Legacy `vara` / `token` literals retired and now error with `INVALID_UNITS`.\n- `--dry-run` and `--estimate` compose on `call` (account required); previously mutually exclusive.\n- `subscribe messages --type` renamed to `--event` for consistency with `watch`.\n- `metaStorageUrl` config key and `VARA_META_STORAGE` env var removed; meta-storage IDL fallback dropped.\n\n### Updated — vara-wallet skill, 0.16.0 surface\n- New **\"Structured Errors (0.16+)\"** section: `reason` + `programMessage` JSON shape, `Result::unwrap` strip, jq case-switch with pre-0.16 fallback.\n- Error Recovery: added `INVALID_ARGS_FORMAT` and `INVALID_ADDRESS` rows; refreshed `PROGRAM_ERROR` and `IDL_NOT_FOUND` rows for the new structured fields and the \"This is a v1 contract\" diagnostic.\n- IDL Resolution: framed v1 path as the expected route for stable Sails 0.10.x builders, not as a v2 fallback.\n- Setup: minimum vara-wallet version pinned to 0.16.0.\n- Guardrails: `calculateGas` failures now classified as `PROGRAM_ERROR` instead of opaque gas errors.\n\n## [2.1.0] - 2026-04-02\n\n### Updated\n- `vara-wallet` skill updated for v0.9.0: `--network` shorthand, `config` CLI, `--estimate`, connection timeout, `--idl`/`--init`/`--args` constructor encoding, faucet command, program list default limit, SS58 output\n- `sails-local-smoke` skill updated with `--network local` and `config set network local` alternatives\n- `sails-gtest-and-local-validation` reference updated with `--network local`, IDL-based deploy, multi-constructor `--init` flag\n\n## [2.0.0] - 2026-03-29\n\n### Changed (BREAKING)\n- **Baseline reverted to `sails-rs 0.10.2` (stable)** on main branch. The 1.0.0-beta.2 content is preserved on the `sails-beta` branch.\n- 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.\n- All beta-specific patterns (ReflectHash, binary header protocol, IDL V2, edition 2024) moved to `sails-beta` branch\n- Build.rs: reverted to standalone `sails_rs::build_wasm()` pattern\n- Troubleshooting table kept but trimmed to stable-relevant errors only\n- `cargo sails new` remains the default bootstrap command\n\n### Added\n- `sails-beta` branch created as the home for all 1.0.0-beta.2 content, including friction fixes (troubleshooting, constructor payload, delayed message versioning, JS ecosystem status)\n- Cross-version notes in references pointing to `sails-beta` branch for beta patterns\n- Post-deploy verification guidance in local-smoke skill and reference\n- BTreeMap key types pitfall in sails-idl-client skill\n- Troubleshooting table for top 3 stable compile errors in sails-cheatsheet\n\n## [1.4.0] - 2026-03-27\n\n### Added\n- Sails 1.0.0-beta baseline: ReflectHash derive pattern, Sails Header Protocol reference, IDL V2 format guide, new `#[export]` options (`overrides`, `entry_id`, `throws`)\n- `sails-new-app`: Troubleshooting section for broken scaffold recovery with fallback manual bootstrap sequence\n- `sails-idl-client`: Generated Client Pitfalls section covering `no_std` double-injection in hand-assembled workspaces and custom `BTreeMap` key type decoding issues\n- `sails-gtest`: Common Pitfalls section covering program balance accounting with existential deposit\n- IDL V2 Format section in `sails-idl-client-pipeline.md` with syntax overview (version header, Rust-like types, service-scoped types, `@query`, `throws`, `@partial`)\n- `cargo sails client-js` CLI and `cargo sails idl -n` flag documented\n- 0.10.x legacy notes in reference files where patterns differ from 1.0.0-beta (build.rs, ReflectHash, header protocol)\n- Test assertions for new skill sections (Troubleshooting, Generated Client Pitfalls, Common Pitfalls, ReflectHash)\n\n### Changed\n- Scaffold command updated from `cargo sails program` to `cargo sails new` across all skills, references, README, and tests (9 content locations + 3 test assertions)\n- Version baseline updated from `sails-rs 0.10.2` to `sails-rs 1.0.0-beta.1` across all references and skills\n- Install command pinned to `cargo install [email protected] --locked` in `sails-dev-env`\n- Root program `build.rs` pattern updated to chained `build_wasm()` + `ClientBuilder::from_wasm_path().build_idl()`\n- Canonical workspace layout updated with new `src/lib.rs` wasm re-export pattern\n\n## [1.3.1] - 2026-03-26\n\n### Added\n- `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\n- Builder recipes for voucher-only, signless session, and full gasless+signless flows\n- Cross-references to voucher doc from ship-sails-app, sails-feature-workflow, sails-frontend, sails-local-smoke, and vara-wallet skills\n- Test assertions for voucher reference content, cross-references, and wiki URL leakage prevention\n\n### Changed\n- Demoted inline voucher prose in ship-sails-app and sails-feature-workflow to one-line pointers to the canonical reference\n- Corrected `api.voucher.exists` signature to `(accountId, programId)` and `api.voucher.issue` return type documentation\n- Added backend sponsor service as explicit prerequisite for EZ-Transactions\n\n### Fixed\n- Duplicate `## Environment Contract` heading in sails-frontend-and-gear-js.md\n\n## [1.3.0] - 2026-03-26\n\n### Added\n- Auto-update check that runs on each skill invocation via preamble in entry-point skills\n- `bin/vara-skills-update-check` script with cache TTL, snooze system, and graceful degradation\n- `skills/vara-skills-upgrade/` skill for inline and standalone upgrade flows\n- `VERSION` file as single source of truth for version, kept in sync with `marketplace.json`\n- 15 new tests for update check script covering all code paths\n- `make test-update` target and VERSION tag verification in release workflow\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":8783,"content_sha256":"562df568a39a873ca3cb2d15d04c29894a8b50f6b6627ad17e0c48a627f69880"},{"filename":"CLAUDE.md","content":"# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## What This Repo Is\n\nA 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.\n\nCurrent version: **3.0.0** (sails-rs 1.0.0). The skill catalog is provisional until the sibling `vara-skills-evals` repo proves uplift.\n\n## Commands\n\n```bash\nmake verify # Run the full validation suite (layout, skills, parser, install, packaging, update)\nmake test-layout # Repo structure checks only\nmake test-skills # Skill validation + catalog + gstd-api-map tests\nmake test-parser # gtest output parser tests\nmake test-install # Codex install script tests\nmake test-packaging # Claude plugin metadata validation\nmake test-update # Auto-update check script tests\n```\n\nAll tests are Python 3 (`python3 tests/\u003cfile>.py`). No pip dependencies required.\n\n## Architecture\n\n### Content layers\n\n- **`SKILL.md`** (root) — top-level router that dispatches by builder intent to specific skills\n- **`skills/\u003cname>/SKILL.md`** — individual workflow or topic skills (24 total), each may contain `assets/` subdirs\n- **`references/`** — self-contained handbook covering Gear execution, Sails architecture, IDL/client pipeline, gtest patterns, and more. Skills reference these via relative paths.\n- **`assets/`** — canonical output templates (spec, architecture, task-plan, gtest report)\n- **`VERSION`** — single-line version file, kept in sync with `marketplace.json`\n- **`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=false` to disable, `VARA_SKILLS_AUTO_UPGRADE=true` for silent upgrades.\n\n### Packaging surfaces\n\n- **Claude Code plugin**: `.claude-plugin/plugin.json` + `marketplace.json` define the plugin. Skills under `skills/` are loaded directly.\n- **Codex**: `scripts/install-codex-skills.sh` installs skill directories locally.\n- **OpenClaw**: `openclaw-skill/SKILL.md` wraps the same content.\n\n### Validation\n\n- `scripts/validate-skill.py` — validates individual skill structure\n- `scripts/parse_test_output.py` — parses gtest output for evidence collection\n- `scripts/run_gtest.sh` — runs gtest and captures output\n- `tests/` — Python test suite covering repo layout, skill contracts, catalog completeness, parser correctness, install scripts, and Claude packaging metadata\n\n## Working Rules (from AGENTS.md)\n\n- Run `make verify` before claiming the repo is ready.\n- Apply TDD: add or update tests before changing scripts, validation logic, or published skill contracts.\n- Keep each skill directory flat with `SKILL.md` plus optional `assets/`, `references/`, `scripts/`.\n- Prefer shared references in `references/` and templates in `assets/` over duplicating content in skills.\n- The skill catalog is provisional — only measured winning skills should remain in the eventual public pack.\n- Stay on the standard Gear/Vara Sails path. Vara.eth and ethexe work uses dedicated skills, not this pack.\n\n## Skill Routing\n\n`ship-sails-app` is the primary entry skill for Claude Code. It routes standard builder tasks to the correct stage:\n\n1. **Planning**: `idea-to-spec` → `gear-architecture-planner` or `sails-architecture` → `task-decomposer`\n2. **Implementation**: `sails-rust-implementer`, `sails-idl-client`\n3. **Verification**: `sails-gtest` or `gtest-tdd-loop` → `sails-local-smoke` (only after green gtest)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3696,"content_sha256":"93a83b944698ab45a1adffdab6fb09fd79642e7b9cdf1ce22608e676a5383874"},{"filename":"Makefile","content":".PHONY: test-layout test-skills test-parser test-install test-packaging test-update verify verify-real verify-all\n\ntest-layout:\n\tpython3 tests/test_repo_layout.py\n\ntest-skills:\n\tpython3 tests/test_skill_validation.py\n\tpython3 tests/test_skill_catalog.py\n\tpython3 tests/test_gstd_api_map_skill.py\n\ntest-parser:\n\tpython3 tests/test_parse_test_output.py\n\ntest-install:\n\tpython3 tests/test_install_codex_skills.py\n\ntest-packaging:\n\tpython3 tests/test_packaging_metadata.py\n\ntest-update:\n\tpython3 tests/test_update_check.py\n\nverify: test-layout test-skills test-parser test-install test-packaging test-update\n\nverify-real:\n\tbash scripts/verify-real-sails-program.sh\n\nverify-all: verify verify-real\n","content_type":"text/plain; charset=utf-8","language":"makefile","size":693,"content_sha256":"2e4e380d96e6f26c727f954c7abc92975d8f45e70dd7ffd52b28162e66897dcb"},{"filename":"README.md","content":"# vara-skills\n\n`vara-skills` is a portable, self-contained skill pack for standard Gear/Vara Sails builders.\n\nIt 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.\n\n## How It Works\n\nEach skill is a markdown file.\n\n- `SKILL.md` is the top-level router for the pack.\n- `skills/\u003cname>/SKILL.md` is a narrower workflow or topic skill.\n- `references/` is the self-contained handbook for Gear execution, Sails architecture, IDL/client generation, `gtest`, local validation, voucher/signless flows, and network configuration.\n\nThe pack is being narrowed toward standard Gear/Vara Sails app builders:\n\n- preparing the local Rust and Gear toolchain\n- turning feature ideas into spec, architecture, and task artifacts\n- starting a new Sails app\n- building features in an existing Sails app\n- implementing approved Rust or Sails changes\n- reasoning about Gear message flow and execution behavior\n- choosing the right `gstd` API when design depends on lower-level Gear behavior\n- getting architecture, IDL or client wiring, `gtest`, and local-node validation right\n- building or extending a React frontend for a standard Sails app with Sails-JS and Gear-JS\n- wiring gasless (voucher) and signless (session) UX flows\n\n## Installation\n\n### Quick Start (any agent)\n\n```bash\nnpx skills add gear-foundation/vara-skills\n```\n\nWorks with Claude Code, Codex, and any agent that supports the `skills` CLI. Installs all 24 skills and the reference handbook in one command.\n\n### Codex\n\nClone the repo and install the local skills:\n\n```bash\ngit clone [email protected]:gear-foundation/vara-skills.git\ncd vara-skills\nbash scripts/install-codex-skills.sh\n```\n\nThis installs the full Codex pack surface:\n\n- `$CODEX_HOME/skills/vara-skills` -> repo root, including the router `SKILL.md`, shared `assets/`, shared `references/`, and helper scripts.\n- `$CODEX_HOME/skills/\u003cskill-name>` -> each installable skill directory under `skills/`.\n\nThen start a new Codex session and use `vara-skills` when you want the pack router.\n\n### Claude Code Plugin\n\nClaude Code can install this repo directly as a plugin. You do not need a separate Claude-specific fork.\n\nRecommended install from GitHub:\n\n```bash\n/plugin marketplace add https://github.com/gear-foundation/vara-skills\n/plugin install vara-skills@vara-skills\n```\n\nIf you are developing the plugin from a local checkout, run these commands from the repo root instead:\n\n```bash\n/plugin marketplace add .\n/plugin install vara-skills@vara-skills\n```\n\nAfter local edits, reload the plugin without restarting Claude Code:\n\n```bash\n/reload-plugins\n```\n\nWhat Claude Code installs:\n\n- the skill directories under `skills/`\n- the same shared `references/` and `assets/` content those skills point at\n- the provisional standard Gear/Vara Sails builder workflow centered on `ship-sails-app`\n\nImportant 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.\n\n### OpenClaw\n\nUse `openclaw-skill/SKILL.md` as the wrapper entrypoint for the same pack.\n\n## Skill Catalog\n\n### Router\n\n- `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.\n\n### Planning And Architecture\n\n- `idea-to-spec`: turns a rough request into a concrete spec artifact with actors, state changes, messages, replies, events, invariants, and acceptance criteria.\n- `gear-architecture-planner`: maps an approved spec onto program boundaries, service boundaries, message flow, state ownership, and client or IDL implications.\n- `sails-architecture`: focuses on Sails-specific service and program boundaries, state patterns, routing, and architecture tradeoffs for standard Sails repos.\n- `task-decomposer`: breaks approved spec and architecture work into dependency-ordered implementation tasks with verification checkpoints.\n\n### Build And Implementation\n\n- `sails-dev-env`: prepares or repairs the local Rust, Wasm, `cargo-sails`, and `gear` toolchain for standard Sails work.\n- `sails-new-app`: greenfield workflow for creating a standard Sails workspace, typically starting from `cargo sails new \u003cproject-name>`, without skipping the planning artifacts.\n- `sails-feature-workflow`: stage-by-stage workflow for changing behavior in an existing Sails repo.\n- `sails-rust-implementer`: implements approved Rust or Sails tasks while preserving routing, IDL, and async contract behavior.\n- `sails-idl-client`: fixes or wires the IDL and generated client pipeline in app, client, or test crates.\n- `sails-frontend`: builds or extends a React/TypeScript frontend for a Sails app using Sails-JS, generated clients, and Gear-JS.\n- `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.\n- `awesome-sails-vft`: adds a fungible token to a Sails app using awesome-sails building blocks, covering VFT crates, roles, events, and tests.\n- `sails-program-evolution`: guides V1-to-V2 contract migration, interface evolution, and cutover planning for released Sails programs.\n- `vara-wallet`: interacts with Vara Network on-chain — deploy programs, call Sails methods, manage wallets, transfer tokens, issue vouchers.\n\n### Verification And Runtime Behavior\n\n- `sails-gtest`: standard Sails-first `gtest` verification flow using generated clients or `GtestEnv`.\n- `gtest-tdd-loop`: red-green loop for deterministic `gtest` work, using the repo scripts to capture failures and final green evidence.\n- `sails-local-smoke`: typed local-node validation after `gtest` is already green.\n- `gear-message-execution`: focused reasoning about replies, delays, waitlist behavior, reservations, rollback, and async execution order.\n\n### Deep Capability Helpers\n\n- `gear-gstd-api-map`: design-time API chooser for `gstd`, `gcore`, and `gsys` when a spec or architecture depends on exact Gear messaging or execution primitives.\n\n### Maintenance\n\n- `vara-skills-upgrade`: checks for new pack versions and handles inline or standalone upgrades.\n\n## Recommended Workflows\n\n### New app workflow\n\n- `ship-sails-app` -> `sails-dev-env` when the machine is not ready.\n- `ship-sails-app` -> `sails-new-app` to establish the greenfield path.\n- For a standard new Sails/Vara project, bootstrap the workspace with `cargo sails new \u003cproject-name>` before custom wiring.\n- `idea-to-spec` -> `gear-architecture-planner` or `sails-architecture` -> `task-decomposer` to create the artifact chain in `docs/plans/`.\n- `sails-rust-implementer` for the approved code changes.\n- `sails-idl-client` if the generated interface path needs wiring or repair.\n- `sails-gtest` or `gtest-tdd-loop` for evidence-driven verification.\n- `sails-local-smoke` only after green `gtest`.\n\n### Existing feature workflow\n\n- `ship-sails-app` -> `sails-feature-workflow` as the main router for existing Sails repos.\n- `idea-to-spec` first, then `sails-architecture` for Sails-level structure.\n- Add `gear-gstd-api-map` when the feature depends on exact `gstd` API choice.\n- Add `gear-message-execution` when replies, delays, reservations, or timeout behavior are part of the change.\n- `task-decomposer` -> `sails-rust-implementer` -> `sails-idl-client` -> `sails-gtest` or `gtest-tdd-loop`.\n- `sails-local-smoke` only after the typed test loop is green.\n\n### Verification workflow\n\n- Use `sails-gtest` for the normal verification path.\n- Use `gtest-tdd-loop` when the task must start from a failing test and produce parser-backed evidence from the repo scripts.\n- Use `sails-local-smoke` only after `gtest`, not as a substitute for it.\n\n## Repo Structure\n\n- `SKILL.md` contains the top-level router.\n- `skills/` contains installable skill directories.\n- `references/` contains the self-contained handbook.\n- `assets/` contains canonical output shapes for spec, architecture, task-plan, and gtest artifacts.\n- `scripts/` contains install, validation, gtest execution, and parser helpers.\n\n## Milestone-One Evaluation\n\nThe first measured target is `gpt-5.4`.\n\nThe first full `gpt-5.4` milestone-one suite now covers `12` cases across `knowledge`, `codegen`, `workflow`, and `safety`.\n\nThat 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.\n\nMeasured winners in this run:\n\n- `sails-default-path`\n- `gas-reservation`\n- `no-low-level-bypass`\n- `js-client-from-idl` (textual uplift only; artifact execution is still blocked)\n\nStill tied or unresolved in the current pack:\n\n- `rust-sails-compile`\n- `address-format-ss58`\n- `delayed-messages`\n- `idl-client-path`\n- `voucher-signless`\n- `waitlist-rent`\n- `no-key-address-hallucination`\n- `sails-feature-flow`\n\nThe supporting benchmark summary lives in the sibling `vara-skills-evals` repo at `results/2026-03-11-gpt54-suite-report.md`.\n\n## Verification\n\n```bash\nmake verify\n```\n\nThis runs repository layout, skill-validator, skill-catalog, gstd-api-map, parser, install, packaging metadata, and update-check tests for the current product repo surface.\n\n## Current Direction\n\n- `vara-skills` is the product repo for the pack\n- a sibling `vara-skills-evals` repo owns benchmark definitions and uplift results\n- `gpt-5.4` is the first evaluation target\n- the top-level router and candidate Sails-builder skills remain provisional until more targets are measured\n- only measured winning skills should remain in the eventual first public pack\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":9927,"content_sha256":"e95ebd90b26c1b0b55b40002a820566d766c4f47000041e7743044e884b9a361"},{"filename":"references/awesome-sails-token-patterns.md","content":"# Awesome Sails Token Patterns\n\nThis note captures the reusable default patterns for adding fungible-token functionality to a standard Gear/Vara Sails app with `awesome-sails`.\n\nCode 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.\n\nIt is intentionally general and production-biased:\n\n- preserve the standard VFT surface unless the spec explicitly needs more\n- choose the smallest `awesome-sails` token stack that satisfies the feature\n- keep token state explicit and composable\n- keep typed events and generated clients on the normal path\n- separate token logic from orchestration when the product has bridge, payment, staking, or other async workflows\n\n## Core Rule\n\nStart from standard VFT-compatible behavior first.\n\nOn Vara, the VFT standard is the ERC-20 analogue. The default surface is:\n\n- `Approve`\n- `Transfer`\n- `TransferFrom`\n- `Allowance`\n- `BalanceOf`\n- `TotalSupply`\n- `Name`\n- `Symbol`\n- `Decimals`\n\nand the standard event surface is:\n\n- `Approval`\n- `Transfer`\n\nDo not start from a custom fungible-token design unless the spec clearly cannot fit the standard VFT model.\n\n## Choose The Smallest Awesome Sails Surface\n\n`awesome-sails` is intentionally split into reusable token-related building blocks.\n\nUse the smallest set that satisfies the feature:\n\n- `vft`: standard fungible-token behavior\n- `vft_metadata`: metadata access (`name`, `symbol`, `decimals`)\n- `vft_admin`: privileged mint, burn, pause, and RBAC-secured admin operations\n- `vft_extension`: allowance cleanup, `transfer_all`, enumeration, and shard-management helpers\n- `vft_native_exchange`: native value \u003c-> VFT conversion\n- `vft_native_exchange_admin`: admin and recovery flows around native exchange\n- `access_control`: explicit roles instead of ad hoc admin checks\n- `storage`: reusable storage wrappers for service composition\n- `msg_tracker`: async message/status tracking when token flows are message-driven\n\nDefault decision order:\n\n1. Start with `vft`.\n2. Add `vft_metadata` if the token is user-facing.\n3. Add `vft_admin` only when mint, burn, pause, or maintenance operations are real requirements.\n4. Add `vft_extension` only when cleanup, enumeration, `transfer_all`, or shard helpers are needed.\n5. Add `vft_native_exchange` only when wrapping/unwrapping native value is part of the product.\n\n## Dependency Pattern\n\nPrefer explicit feature selection.\n\n### Meta-crate pattern\n\n```toml\n[dependencies]\nsails-rs = { version = \"*\", default-features = false, features = [\"gstd\"] }\n\nawesome-sails = { version = \"x.y.z\", default-features = false, features = [\n \"storage\",\n \"vft\",\n \"vft-metadata\",\n] }\n```\n\nAdd more features only when the spec needs them.\n\n### Direct subcrate pattern\n\nUse direct crates when the project wants narrow imports or explicit dependency boundaries.\n\n```toml\n[dependencies]\nsails-rs = { version = \"*\", default-features = false, features = [\"gstd\"] }\n\nawesome-sails-storage = \"x.y.z\"\nawesome-sails-vft-metadata = \"x.y.z\"\nawesome-sails-utils = \"x.y.z\"\n```\nThis style is valid when the app composes token functionality from helpers rather than depending on the full meta-crate surface.\n\n## Storage Composition Pattern\n\nThe default reusable pattern is:\n\n- keep balances, allowances, metadata, pause state, and admin/RBAC state in program-owned storage\n- expose helper accessors that wrap those storages\n- construct token-related services from wrappers instead of duplicating token logic across unrelated services\n\nThe following shape is schematic: `Balances`, `Allowances`, and `Metadata` represent app-defined storage types or wrappers chosen by the token implementation.\n\nMinimal shape:\n\n```rust\nuse sails_rs::{cell::RefCell, prelude::*};\nuse awesome_sails_storage::StorageRefCell;\n\npub struct Program {\n balances: RefCell\u003cBalances>,\n allowances: RefCell\u003cAllowances>,\n metadata: RefCell\u003cMetadata>,\n pause: Pause,\n}\n\npub struct TokenService\u003c'a> {\n balances: &'a RefCell\u003cBalances>,\n allowances: &'a RefCell\u003cAllowances>,\n metadata: &'a RefCell\u003cMetadata>,\n pause: &'a Pause,\n}\n\nimpl\u003c'a> TokenService\u003c'a> {\n fn balances_ref(&self) -> StorageRefCell\u003c'_, Balances> {\n StorageRefCell::new(self.balances)\n }\n\n fn allowances_ref(&self) -> StorageRefCell\u003c'_, Allowances> {\n StorageRefCell::new(self.allowances)\n }\n}\n```\n\nWhy this pattern is the default:\n\n- state ownership is explicit\n- services stay composable\n- wrappers can be reused by VFT, metadata, admin, and extension layers\n- the code scales better than one monolithic token service with hidden state\n\n## Metadata Pattern\n\nDo not hand-roll metadata queries when the standard metadata service is enough.\n\nPrefer delegating metadata through `awesome-sails-vft-metadata`:\n\n```rust\nuse awesome_sails_vft_metadata::{Metadata, VftMetadata, VftMetadataExposure};\nuse awesome_sails_storage::StorageRefCell;\n\nfn metadata_svc(&self) -> VftMetadataExposure\u003cVftMetadata\u003cStorageRefCell\u003c'_, Metadata>>> {\n VftMetadata::new(StorageRefCell::new(self.metadata)).expose(self.route())\n}\n\n#[export]\npub fn name(&self) -> String {\n self.metadata_svc().name()\n}\n\n#[export]\npub fn symbol(&self) -> String {\n self.metadata_svc().symbol()\n}\n\n#[export]\npub fn decimals(&self) -> u8 {\n self.metadata_svc().decimals()\n}\n```\n\nDefault rule: if the token is user-facing, metadata should usually be present and exposed in the standard way.\n\n## Event Pattern\n\nEvents are part of the token contract, not optional decoration.\n\nPreserve the standard token events and add custom events only when the application has real domain facts beyond base token movement.\n\nMinimal pattern:\n\n```rust\n\n#[event]\n#[derive(Debug, Clone, Encode, Decode, TypeInfo, PartialEq, Eq)]\n#[codec(crate = sails_rs::scale_codec)]\n#[scale_info(crate = sails_rs::scale_info)]\npub enum Events {\n Approval { owner: ActorId, spender: ActorId, value: U256 },\n Transfer { from: ActorId, to: ActorId, value: U256 },\n\n // Add only when the chosen token policy needs them:\n Minted { to: ActorId, value: U256 },\n Burned { from: ActorId, value: U256 },\n}\n\n#[service(events = Events)]\nimpl TokenService\u003c'_> {}\n```\n\nDefault rules:\n\n- keep `Approval` and `Transfer` when the service behaves like VFT\n- add `Minted`, `Burned`, `Paused`, `RoleGranted`, or similar only when the feature set actually needs them\n- emit events from successful state-changing paths, not from queries\n\n## Exported Command Pattern\n\nFor stateful token commands, prefer `Result\u003c_, Error>` plus `#[export(unwrap_result)]`.\n\nMinimal pattern:\n\n```rust\n#[service(events = Events)]\nimpl TokenService\u003c'_> {\n #[export(unwrap_result)]\n pub fn transfer(&mut self, to: ActorId, value: U256) -> Result\u003cbool, Error> {\n let from = Syscall::message_source();\n\n // mutate balances here\n\n self.emit_event(Events::Transfer { from, to, value })\n .map_err(|_| EmitError)?;\n\n Ok(true)\n }\n\n #[export(unwrap_result)]\n pub fn approve(&mut self, spender: ActorId, value: U256) -> Result\u003cbool, Error> {\n let owner = Syscall::message_source();\n\n // mutate allowances here\n\n self.emit_event(Events::Approval {\n owner,\n spender,\n value,\n })\n .map_err(|_| EmitError)?;\n\n Ok(true)\n }\n}\n```\n\nWhy this is the default:\n\n- internal control flow stays typed and readable\n- exported command failure remains fail-fast\n- tests can explicitly expect fatal exported-path failures where appropriate\n\n## Admin Pattern\n\nDo not introduce privileged token operations unless the spec actually needs them.\n\nWhen the token needs mint, burn, pause, or operational maintenance, prefer the dedicated admin layer rather than growing ad hoc privileged methods.\n\nUse an app-local admin guard only for small, clearly bounded cases. Escalate to RBAC when:\n\n- there are multiple privileged concerns\n- roles differ by capability\n- grant/revoke is part of the design\n- long-term maintainability matters\n\nDefault rule:\n\n- fixed-supply token: no admin layer unless there is another real privileged operation\n- mutable-supply token: prefer `vft_admin` or an equivalent explicit RBAC design\n\n## Extension Pattern\n\n`vft_extension` is optional.\n\nUse it when the product truly needs one or more of:\n\n- expired allowance cleanup\n- `transfer_all`\n- balance enumeration\n- allowance enumeration\n- explicit shard management\n\nDo not add extension helpers to every token by default.\n\nA token that only needs standard transfers, approvals, balances, supply, and metadata should stay smaller.\n\n## Native Exchange Pattern\n\n`vft_native_exchange` is not a generic token default.\n\nUse it only when the product explicitly requires:\n\n- native value sent in -> VFT minted\n- VFT burned -> native value returned\n\nIf the token does not wrap the chain’s native value, leave native exchange out.\n\n## Separate Token Logic From Orchestration\n\nWhen the token participates in a larger async protocol, prefer this split:\n\n- a dedicated token program or token service that owns balances, allowances, supply, metadata, and token-standard events\n- a separate manager/orchestrator that owns bridge, payment, staking, claim, or delayed-message workflows\n\nThis pattern is strongly reinforced by the Gear bridge architecture:\n\n- VFT owns token logic\n- VFT-Manager owns bridge workflow\n- the user first performs approve\n- the manager then performs the higher-level protocol action\n\nDefault rule: if another subsystem needs to move user tokens, start from approval-based ingress before inventing a custom authorization flow.\n\nFor 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`.\n\n## Testing Defaults\n\nA token integration is not complete until it covers the minimum matrix.\n\n### Base VFT behavior\n\n- transfer success\n- transfer failure on insufficient balance\n- approve success\n- allowance correctness\n- `transfer_from` success within allowance\n- `transfer_from` failure above allowance\n- total supply correctness\n- metadata correctness when metadata is enabled\n\n### Event behavior\n\n- assert Approval\n- assert Transfer\n- assert custom events only when that feature exists\n\n### Admin behavior\n\n- authorized privileged action succeeds\n- unauthorized privileged action fails\n- mint/burn/pause flows are covered only when they exist\n\n### Extension behavior\n\n- cleanup, enumeration, transfer_all, and shard operations are tested only when enabled\n\n### Fatal-path behavior\n\n- for exported commands using #[export(unwrap_result)], assert fatal exported-path failures where appropriate\n\n## Anti-Patterns\n\n- writing a custom fungible token before checking whether standard VFT plus one or two awesome-sails layers are enough\n- mixing token storage, protocol orchestration, and bridge/payment workflow into one giant service\n- adding admin, extension, or native-exchange features “just in case”\n- treating Approval / Transfer events as optional on a VFT-like token\n- hiding supply policy instead of deciding explicitly between fixed supply, mint/burn, or lock/unlock semantics\n- bypassing generated clients on the normal token path\n- making every app invent its own allowance and transfer semantics ad hoc\n\n## Practical Decision Order\n\nWhen another agent has to choose quickly, bias toward this order:\n\n- Preserve the standard VFT surface.\n- Add metadata if the token is user-facing.\n- Keep token state explicit and wrapper-based.\n- Use typed events at the service boundary.\n- Use `#[export(unwrap_result)]` for stateful exported token commands.\n- Add admin only when privileged operations are real.\n- Add extension only when cleanup, enumeration, `transfer_all`, or shard helpers are required.\n- Add native exchange only when wrapping/unwrapping native value is part of the product.\n- Split token logic from orchestration when the product has async protocol workflow.\n- Test events and failure paths, not only final balances.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":12029,"content_sha256":"e6a469f4f1779c4e758ea399f39879a68a9fe4b724282fc8a71da969e621d85e"},{"filename":"references/contract-interface-evolution.md","content":"# Program Evolution And State Migration For Gear/Sails\n\n## Goal\n\n- 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.\n- Frame the answer as a production maintenance problem, not only as a coding problem.\n- Separate what the platform provides out of the box from what the project should implement as its own migration discipline.\n- This reference focuses on released-contract evolution, cutover discipline, and state migration between program versions.\n- It does not try to fully specify frontend rollout mechanics or indexer implementation details.\n\n## Core Model\n\n- For Gear/Vara, treat upgradeability as deploying a new program version and explicitly migrating state, not as hot-swapping code inside one live program.\n- Build recommendations around a `V1 -> V2 -> cutover` model.\n- Treat deployment, state reading, migration batching, and cutover as separate concerns.\n- Keep the public contract surface stable for as long as possible.\n- When internal Rust names change but public names must stay stable, preserve routes with `#[export(route = \"...\")]`.\n- When extending services, use `overrides`, `route`, or `entry_id` deliberately so inherited methods keep compatibility intent explicit.\n- Prefer program-owned business state passed into services via `RefCell`; this keeps ownership explicit and makes versioned state and migration planning easier.\n\n## What Counts As Upgrade\n\n- An upgrade is a release process with three parts:\n - old contract `V1`\n - new contract `V2`\n - off-chain migrator script\n- `V1` is the source of truth for export.\n- `V2` is deployed separately and is the destination for import.\n- The off-chain migrator reads old state and writes new state.\n- Read state through the platform primitives when possible:\n - `api.programState.read` for full-state reads or reads at a fixed block hash\n - `api.programState.readUsingWasm` for selected state functions or custom paginated reads\n\n## What The Agent Must Recommend\n\n- Always recommend a migration flow built on these rules:\n - freeze writes on `V1`\n - export state in deterministic chunks\n - import state into `V2` in deterministic chunks\n - make imports idempotent\n - verify counts, totals, and critical invariants\n - activate `V2`\n - switch frontend and indexer to `V2`\n- Treat migration batches like any other retryable multi-step flow.\n- Expect retries, partial completion, `Timeout`, and `RunOutOfGas`.\n- Prefer tracked progress plus retry-safe behavior over one-shot bulk migration logic.\n\n## Minimal Status Model\n\n- Do not require a separate `Migration` status unless the user explicitly wants it.\n- The default recommendation is:\n - `Active`\n - `ReadOnly`\n- `ReadOnly` means user mutating calls are blocked but queries and export functions remain available.\n- 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.\n\n## What Must Exist In The Old Contract `V1`\n\n- `V1` should contain:\n - admin-only `set_read_only(bool)`\n - export methods for each business-relevant part of state\n - a stats or invariants export method for validation\n - no new writes once migration starts\n- Prefer export APIs by business domain, not one giant dump.\n- Good shapes include:\n - `export_config()`\n - `export_users_chunk(cursor, limit)`\n - `export_positions_chunk(cursor, limit)`\n - `export_balances_chunk(cursor, limit)`\n - `export_active_accounts_chunk(cursor, limit)`\n - `export_stats()`\n- Use `api.programState.read` when full-state reading is feasible.\n- Use `api.programState.readUsingWasm` when only selected parts should be read or when large state should be paginated through custom state functions.\n\n## What Must Exist In The New Contract `V2`\n\n- `V2` should contain:\n - initial `ReadOnly` mode until import is complete\n - admin-only import methods\n - batch progress tracking\n - idempotency checks\n - conflict detection\n - finalization step\n- Good shapes include:\n - `import_config(...)`\n - `import_users_batch(batch_id, users)`\n - `import_positions_batch(batch_id, positions)`\n - `import_balances_batch(batch_id, balances)`\n - `import_active_accounts_batch(batch_id, accounts)`\n - `import_status()`\n - `finalize_import()`\n- If the same `batch_id` is sent twice, prefer success without duplicating state changes.\n- If the same key or entity arrives with different data, fail with a conflict instead of silently overwriting.\n- Keep normal business writes blocked until import verification passes and finalization is complete.\n\n## State Inventory Rules\n\n- Before proposing migration, require a state inventory.\n- For every field in the root program state and in nested structures, classify it as one of:\n - migrate as-is\n - migrate with transform\n - rebuild after migration\n- Make the user list every business-significant collection explicitly.\n- This includes:\n - `Vec`\n - `HashMap`\n - `HashSet`\n - nested combinations of them\n- Do not assume caches, derived indexes, or recomputable aggregates need migration.\n- Rebuild derived or cache-like data when it is safe and cheaper than migrating it.\n- Keep the source of truth small and explicit.\n\n## How To Migrate `Vec`\n\n- Treat `Vec\u003cT>` as an ordered sequence.\n- Export by index range using:\n - `cursor`\n - `limit`\n - `next_cursor`\n- Recommend chunked export/import by slices.\n- This is the simplest collection to migrate because ordering is already defined.\n\n## How To Migrate `HashMap`\n\n- Never recommend migrating a `HashMap` by raw iteration order.\n- Migrate it as a deterministic sequence of entries, for example `Vec\u003c(K, V)>`.\n- Required rule:\n - collect keys\n - sort keys into a canonical order\n - take a key range by cursor and limit\n - export entries for those keys\n - import entries into `V2`\n- If repeated key sorting would be too expensive, recommend one of these improvements:\n - maintain a stable secondary index\n - use `BTreeMap` for the new version\n - flatten nested maps into explicit migration records\n- For nested structures like `HashMap\u003cUserId, HashMap\u003cOrderId, Order>>`, flatten export into records such as `(user_id, order_id, order)` instead of trying to migrate one giant nested object.\n- Flattening keeps batching, retries, and conflict checks tractable.\n\n## How To Migrate `HashSet`\n\n- Treat `HashSet\u003cT>` like a set of records, not a raw memory object.\n- Required rule:\n - collect elements\n - sort elements into canonical order\n - export by cursor and limit\n - import into `V2` with duplicate-safe semantics\n- For large or critical membership sets, recommend `BTreeSet` in `V2` when deterministic ordering and simpler validation are useful.\n\n## Snapshot Rule\n\n- When possible, recommend freezing `V1` and then reading state from a fixed snapshot block.\n- Use `api.programState.read({ at: ... })` when snapshot consistency matters across batches.\n- This avoids drift between earlier and later export batches.\n\n## Verification Rule\n\n- Always require post-import verification before activating `V2`.\n- At minimum compare:\n - entity counts\n - total balances, reserves, or supply-like totals\n - next-id counters\n - config values\n - membership set sizes\n - optional checksum or hash over exported data\n- Do not recommend switching traffic to `V2` before these checks pass.\n\n## Compatibility Rule For Frontend And Indexer\n\n- Treat public routes, method signatures, and event payloads as compatibility-sensitive surfaces.\n- If the public surface must evolve, prefer one of these strategies:\n - preserve old route names with `#[export(route = \"...\")]`\n - use `overrides`, `route`, or `entry_id` to maintain compatibility for inherited services\n - version events instead of silently changing payload shape\n- Do not casually rename or reshape exported methods that current clients already use.\n- Do not silently change event payload shape in place when an indexer already decodes it.\n- Remember that Sails derives deterministic interface identifiers and entry identifiers from the canonical IDL and carries stable routing metadata in the header format.\n\n## Recommended Migration Algorithm\n\n- When the user asks for a practical flow, recommend this order:\n 1. deploy `V2`\n 2. initialize `V2` in `ReadOnly`\n 3. call `V1.set_read_only(true)`\n 4. record the snapshot block hash\n 5. export config and scalar state from `V1`\n 6. import config and scalar state into `V2`\n 7. export all business collections from `V1` in deterministic chunks\n 8. import all chunks into `V2` with idempotent batch methods\n 9. compare stats and invariants\n 10. call `V2.finalize_import()`\n 11. switch frontend and indexer to `V2`\n 12. keep `V1` read-only for a rollback window if needed\n- Treat rollback planning as part of the release, not as an afterthought.\n\n## Things The Agent Must Not Suggest\n\n- Do not suggest replacing code in place as the default Gear/Sails strategy.\n- Do not suggest one giant monolithic state dump for large production state.\n- Do not suggest non-deterministic `HashMap` or `HashSet` export order.\n- Do not suggest import methods without idempotency.\n- Do not suggest changing public routes or event shapes casually.\n- Do not suggest migrating caches as if they were the source of truth.\n\n## Expected Output From The Agent\n\n- When using this reference, the agent should produce:\n - a clear split between `V1`, `V2`, and off-chain migrator responsibilities\n - a state inventory plan\n - export/import API design\n - collection-specific migration rules\n - verification checklist\n - compatibility notes for frontend and indexer\n - explicit mention of which parts are platform-supported and which are project conventions\n\n## Platform-Supported Pieces Versus Project Conventions\n\n- Platform-supported primitives include:\n - deploying a new program via upload or create flows\n - reading full state\n - reading selected state using state functions\n - reading state at a specific block hash\n- Project conventions in this reference include:\n - `V1 -> V2 -> cutover` release discipline\n - `Active` and `ReadOnly` as the default minimal status model\n - chunked deterministic export/import API design\n - idempotent batch import semantics\n - explicit verification before cutover\n\n## Default Conclusion\n\n- If the user asks for the safest default pattern, recommend:\n - immutable `V1`\n - deploy `V2`\n - `ReadOnly` freeze\n - chunked deterministic export/import\n - idempotent batches\n - verification\n - cutover\n- This is the baseline production-friendly upgradability and migration pattern for Gear/Sails in this pack.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":10751,"content_sha256":"097f2aefb38e98d0b1d2026efd570406585715fa9b0a488ae2851d46e92374d0"},{"filename":"references/delayed-message-pattern.md","content":"# Delayed Message Pattern\n\n## Use Case\n\nUse this pattern for future-block work such as reminders, auctions, inactivity cleanup, vesting steps, and timeout enforcement.\n\n## Canonical Self-Message Payload\n\nFor 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::\u003cCall>::encode_call(route_idx, args...)` helper:\n\n```rust\nuse reminder_board_client::{\n ReminderBoardClientProgram,\n reminder_board::io::TriggerReminder,\n};\n\nlet payload = TriggerReminder::encode_call(\n ReminderBoardClientProgram::ROUTE_ID_REMINDER_BOARD,\n id,\n);\n```\n\n- This is the header-routed byte shape to use when a delayed internal message cannot go through a generated client send directly.\n- Keep the route constant and generated `io` type aligned with the exported Sails route.\n- If generated client code is unavailable, manually encode the Sails Header v1 followed by SCALE-encoded params. See `../../references/sails-header-wire-format.md` for the header layout.\n- The legacy SCALE-string route form (`service`, `method`, then args) is not a valid Sails 1.0 delayed self-message payload.\n\n## Sending The Delayed Message\n\n```rust\n// Dynamic gas: if this handler also does work, a fixed gas_limit will fail\n// when execution already consumed most of the budget.\nlet gas_for_next = Syscall::gas_available() * 9 / 10;\nmsg::send_bytes_with_gas_delayed(Syscall::program_id(), payload, gas_for_next, 0, delay)\n .expect(\"failed to schedule delayed self-message\");\n```\n\n- Use `Syscall::program_id()` when the program is scheduling work for itself.\n- Use `Syscall::gas_available()` to compute the gas budget dynamically. Do not use a fixed `gas_limit` for 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 is `Syscall::gas_available() * 9 / 10` to reserve 90% of remaining gas for the next invocation.\n- Runtime accessors use `Syscall::*`; outbound delayed sends still use the normal `msg::send*` family.\n- Keep transferred value at `0` unless the delayed route truly needs value.\n\n## Internal-Only Guard\n\n- The internal-only check is `Syscall::message_source() == Syscall::program_id()`.\n- Enforce it at the start of the exported handler so outside callers cannot trigger the internal route directly.\n\n```rust\n#[export]\npub fn trigger_reminder(&mut self, id: u64) {\n assert_eq!(Syscall::message_source(), Syscall::program_id(), \"internal only\");\n self.finish_trigger(id);\n}\n```\n\n## Reservation And Gas Notes\n\n- Use `ReservationId` only when later execution budget must survive across blocks.\n- If a plain delayed send is enough, keep the flow simpler and derive gas from `Syscall::gas_available()`.\n- Recompute or validate critical state inside the delayed handler instead of trusting stale assumptions from the scheduling block.\n\n## Gtest vs On-Chain Funding\n\n- 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.\n- 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.\n\n## Idempotency Rules\n\n- Delayed routes must be safe if the target item was already canceled, completed, or cleaned up.\n- Persist enough state to detect stale work.\n- Prefer an idempotent no-op or explicit early return over a half-applied mutation.\n\n## Test Guidance\n\n- Use `run_to_block` for delay-sensitive assertions.\n- Assert both the scheduler-side effect and the eventual handler-side effect.\n- If debugging the raw byte route, assert the payload starts with the Sails Header v1 bytes (`GM`, version `0x01`, header length `0x10`) before blaming gas or timing.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4001,"content_sha256":"3bef40e8ba4096ba3881f20a34f33f8bb53ffd36f5b03a5e13d08c4a0511ec18"},{"filename":"references/gear-builtin-actors.md","content":"# Gear Builtin Actors — Reference\n\nBuiltin 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.\n\nUpstream docs: \u003chttps://wiki.gear.foundation/docs/developing/build/builtinactors>\n\n## What a builtin actor is\n\n- Lives in the runtime, not in WASM. Has no program code, no memory, no `read_state`.\n- Claims a `BuiltinId` (a `u64`). At registration time the pallet derives a stable `ActorId` from that id.\n- Dispatches incoming messages by decoding their payload into a per-actor `Request` enum and executing the mapped Substrate call.\n- 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 a `BuiltinActorError`.\n- Charges the sender's block gas allowance for the underlying extrinsic weight (`max_gas()`).\n\nAuthoritative sources (paths relative to the gear repo root):\n- Pallet: `vara/pallets/gear-builtin/src/lib.rs`\n- Per-actor impls: `vara/pallets/gear-builtin/src/{bls12_381,staking,proxy}.rs`\n- ETH bridge actor: `vara/pallets/gear-eth-bridge/` (registered through the same tuple)\n- Helper crates (what a Sails program imports): `vara/sdk/builtins/{bls381,staking,proxy,eth-bridge}/src/lib.rs`\n- Shared error type: `vara/sdk/builtins/common/src/lib.rs` (crate name `builtins-common`)\n- Vara registration tuple: `vara/runtime/vara/src/lib.rs` lines 1174–1186\n\n## ActorId derivation\n\n```text\nSEED = b\"built/in\" // 8 bytes, from pallet-gear-builtin\nActorId = blake2_256((SEED, builtin_id).encode())\n```\n\nSource: `vara/pallets/gear-builtin/src/lib.rs:165,212`.\n\nThe `builtin_id` is a `u64` assigned explicitly to each actor via `ActorWithId\u003cN, 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.**\n\n## Vara runtime registry\n\nSource: `vara/runtime/vara/src/lib.rs:1174-1186`.\n\n| Builtin ID | Actor | Helper crate | Stable `ActorId` (current Vara runtime) |\n|------------|--------------|----------------------|----------------------------------------------------------------------|\n| 1 | BLS12-381 | `gbuiltin-bls381` | `0x6b6e292c382945e80bf51af2ba7fe9f458dcff81ae6075c46f9095e1bbecdc37` |\n| 2 | Staking | `gbuiltin-staking` | `0x77f65ef190e11bfecb8fc8970fd3749e94bed66a23ec2f7a3623e785d0816761` |\n| 3 | ETH Bridge | `gbuiltin-eth-bridge`| derived from id `3`; see note below |\n| 4 | Proxy | `gbuiltin-proxy` | `0x8263cd9fc648e101f1cd8585dc0b193445c3750a63bf64a39cdf58de14826299` |\n\nETH 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.\n\n## Canonical calling pattern\n\nA builtin call is a normal `send_for_reply` against a non-program `ActorId`:\n\n```rust\nuse gbuiltin_staking::{Request, RewardAccount};\nuse gstd::{msg, ActorId};\nuse hex_literal::hex;\nuse parity_scale_codec::Encode;\n\nconst STAKING_BUILTIN: ActorId = ActorId::new(hex!(\n \"77f65ef190e11bfecb8fc8970fd3749e94bed66a23ec2f7a3623e785d0816761\"\n));\n\n#[gstd::async_main]\nasync fn main() {\n let value = msg::value();\n let payload = Request::Bond {\n value,\n payee: RewardAccount::Program,\n }\n .encode();\n\n let reply = msg::send_bytes_for_reply(STAKING_BUILTIN, &payload[..], 0, 0)\n .expect(\"send failed\")\n .await\n .expect(\"builtin returned error reply\");\n\n // `reply` is empty for Bond; for ActiveEra decode as gbuiltin_staking::Response.\n}\n```\n\nNotes:\n- Prefer `send_bytes_for_reply` when encoding with `parity_scale_codec::Encode` directly. Use the typed `send_for_reply` only if you share a matching type the compiler can encode on your behalf.\n- `value` transfers follow Substrate ED rules for the underlying pallet. Staking `Bond` requires `value` ≥ the network minimum bond.\n- `reply_deposit` argument is the last `0` in the example; set a non-zero value only if the service depends on downstream reply fees (rare for builtins).\n- An `Err` from `.await` is a real error reply (typically a `BuiltinActorError`), not a transport failure. Treat it like any other error reply in the `gear-messaging-and-replies.md` playbook.\n\n## Per-actor reference\n\n### Staking (`gbuiltin-staking`)\n\nSource: `vara/sdk/builtins/staking/src/lib.rs`. Broker example: `examples/staking-broker/src/wasm.rs`.\n\n`Request` variants (SCALE-indexed):\n- `Bond { value, payee }` — index 0\n- `BondExtra { value }` — index 1\n- `Unbond { value }` — index 2\n- `WithdrawUnbonded { num_slashing_spans }` — index 3\n- `Nominate { targets: Vec\u003cActorId> }` — index 4\n- `Chill` — index 5\n- `PayoutStakers { validator_stash, era }` — index 6\n- `Rebond { value }` — index 7\n- `SetPayee { payee }` — index 8\n- `ActiveEra` — index 9\n\n`RewardAccount`: `Staked | Program | Custom(ActorId) | None`.\n\n`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.\n\n### Proxy (`gbuiltin-proxy`)\n\nSource: `vara/sdk/builtins/proxy/src/lib.rs`. Broker example: `examples/proxy-broker/src/wasm.rs`.\n\n`Request` variants:\n- `AddProxy { delegate: ActorId, proxy_type: ProxyType }` — index 0\n- `RemoveProxy { delegate: ActorId, proxy_type: ProxyType }` — index 1\n\n`ProxyType`: `Any | NonTransfer | Governance | Staking | IdentityJudgement | CancelProxy` (mirror of `vara-runtime`).\n\nNo `Response` type defined — success replies are empty; failures return `BuiltinActorError` via error reply.\n\n### BLS12-381 (`gbuiltin-bls381`)\n\nSource: `vara/sdk/builtins/bls381/src/lib.rs`. Example: `examples/bls381/src/wasm.rs`.\n\n`Request` variants carry `Vec\u003cu8>` 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`.\n\n- `MultiMillerLoop { a, b }` — index 0\n- `FinalExponentiation { f }` — index 1\n- `MultiScalarMultiplicationG1 { bases, scalars }` — index 2\n- `MultiScalarMultiplicationG2 { bases, scalars }` — index 3\n- `ProjectiveMultiplicationG1 { base, scalar }` — index 4\n- `ProjectiveMultiplicationG2 { base, scalar }` — index 5\n- `AggregateG1 { points }` — index 6\n- `MapToG2Affine { message }` — index 7\n\n`Response` mirrors each request variant with the same index and a single `Vec\u003cu8>` (`ArkScale`-encoded) result. The `REQUEST_*` constants in the crate pin these indexes; gas charging is per-variant and depends on the decoded vector sizes.\n\n### ETH Bridge (`gbuiltin-eth-bridge`)\n\nSource: `vara/sdk/builtins/eth-bridge/src/lib.rs`. Pallet: `vara/pallets/gear-eth-bridge/`.\n\n`Request`:\n- `SendEthMessage { destination: H160, payload: Vec\u003cu8> }` — index 0\n\n`Response`:\n- `EthMessageQueued { block_number: u32, hash: H256, nonce: U256, queue_id: u64 }` — index 0\n\n`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.\n\n## Reply decoding checklist\n\nWhen debugging a builtin reply:\n\n1. 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`.\n2. Identify the matching helper crate by source `ActorId` ↔ builtin id.\n3. Classify: empty success, typed `Response`, or error reply.\n4. 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.\n5. 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\u003c'static>)` for actor-specific errors.\n\n## Gas and ED\n\n- Each builtin declares a `max_gas()` covering the worst-case underlying extrinsic. The pallet upfront-charges this against the caller's message gas_limit (→ `InsufficientGas` if the caller supplied too little) and against the current block allowance (→ `GasAllowanceExceeded` if the block is saturated). Both cases surface as `BuiltinActorError` replies, not panics.\n- 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.\n- 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.\n\n## Guardrails\n\n- **Do not hardcode an `ActorId` without citing a runtime file.** Re-derive with `hash((b\"built/in\", id).encode())` or copy from `vara/runtime/vara/src/lib.rs`, and note the source.\n- **Do not decode builtin replies with a Sails client.** There is no Sails Header framing; use the helper crate's `Response` type.\n- **Do not call a builtin from a sync handler.** The call is `_for_reply`; the handler must be async.\n- **Do not assume a reply shape across runtime versions.** Helper crates evolve — pin to a workspace version that matches the target runtime.\n- **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.\n- **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.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":10879,"content_sha256":"08fd4a9c9c7e48db10f94d12e86ca219ac32b72e532543337a5e5a86cc8743a3"},{"filename":"references/gear-execution-model.md","content":"# Gear Execution Model\n\n## Core Rules\n\n- Gear and Vara programs are isolated actors with private state.\n- State changes happen only while a program handles one incoming message.\n- Sending a message enqueues work and returns a message id; it does not execute the queue immediately.\n- The runtime drains the global message queue during block execution, so time and ordering are block-shaped concerns.\n- In `gtest`, the first message to a program is often initialization and still requires explicit block execution.\n\n## Message Lifecycle\n\n1. A program enters `handle`, `handle_reply`, or another valid entrypoint for the current message.\n2. It reads context such as source, value, payload, and prior reply linkage.\n3. It stages outbound sends, replies, events, or delayed work while execution is still in progress.\n4. The runtime materializes those outbound effects only after the current execution completes successfully.\n\n## Successful Execution And Rollback\n\n- A successful execution commits state changes and materializes staged outbound messages, replies, and events.\n- A panic or unrecoverable failure rolls back the current message execution, including state updates and staged outbound effects.\n- Sails events follow the same rule because they are ordinary messages under the hood. If the command rolls back, the expected event is absent.\n- This rollback boundary matters for design: mutate-before-send logic is safe only if failure panics and reverts the message.\n\n## Builder Implications\n\n- Model multi-step workflows as distributed transactions across messages, not as one atomic call stack.\n- Name actors, commands, queries, replies, events, and failure paths in specs before implementation starts.\n- Use delayed messages for future block work when the program really must revisit state later.\n- Use gas reservation only when future work needs a preserved execution budget across blocks.\n- Prefer generated Sails clients for normal app paths so Sails Header routing and reply decoding stay aligned with the program contract.\n\n## Validation Implications\n\n- Advance blocks explicitly in `gtest`; `send` alone is not execution.\n- Assert effects after the block that should have produced them.\n- When smoke-testing against a local node, use the real deployed program id and observe the same async message boundaries you modeled in tests.\n\n## Program Lifecycle\n\n1. **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.\n2. **Create program** — send an init message that instantiates a program from the validated code. The runtime allocates state and runs the init handler.\n3. **Active** — the program handles messages, mutates state, stages outbound sends, and emits events. Balance must stay above the existential deposit.\n4. **Exit** — the program calls `gr_exit` to terminate, or init fails and the program is never activated. Locked deposit is released on exit.\n\nA program that runs out of balance or is not accessed for the initial rent period risks being paused or removed by the rent system.\n\n## Rent\n\n- Programs occupy on-chain state and must maintain at least the existential deposit (~1 VARA on mainnet).\n- The initial rent period is approximately 173 days (5,000,000 blocks on Vara Network).\n- If a program's balance drops below the threshold and the rent period expires, the program may be removed from active state.\n- For long-lived programs, ensure the account is topped up periodically or funded sufficiently at creation.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3549,"content_sha256":"46eef1337accf1d1ce28a185f7af928fb6df09d9802e901e417a3de75ff5d2f8"},{"filename":"references/gear-gas-reservations-and-waitlist.md","content":"# Gear Gas Reservations And Waitlist\n\n## When To Reserve Gas\n\n- Reserve gas when the design needs to preserve execution budget for future work across blocks.\n- Typical cases are delayed recovery flows, self-messaging that must survive later execution, or async paths that resume after waiting.\n- Reservation is not free, permanent, or a value transfer. It is a bounded gas budget tied to duration and usage.\n\n## Reservation Rules\n\n- Create a reservation with an explicit amount and duration.\n- Treat the reservation lifetime as part of the architecture, not an implementation footnote.\n- Consuming, expiring, or unreserving the reservation changes what later messages can still do.\n- Reusing an expired or already-consumed reservation id is a real failure mode and should be tested.\n\n## Waitlist Model\n\n- The Waitlist is on-chain storage for messages that are waiting on conditions or later processing.\n- Waitlisted messages are not a free parking lot and are not a normal mempool.\n- Waitlist storage incurs rent or locked-fund cost over time, has expiry behavior, and is bounded by maximum duration rules.\n- Designs that rely on indefinite prolonging of waitlisted messages are wrong.\n\n## Delayed Work Design\n\n- Prefer delayed messages when the program must revisit state in a later block.\n- Pair delayed execution with reservation only when the later path really needs preserved gas.\n- Do not describe delayed automation as off-chain cron unless an off-chain agent is explicitly part of the design.\n- 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.\n\n## Testing And Accounting\n\n- In `gtest`, block advancement determines whether delayed work or expiry actually happens.\n- Use `spent_value` and related accounting assertions when gas burn or attached value affects behavior.\n- Keep sender funding, attached value, and existential-deposit constraints in the same reasoning frame.\n\n## Failure Signatures\n\n- Invalid or expired reservation id\n- Timeout followed by later side effects\n- Balance assertions that ignore gas burn or ED behavior\n- Waitlist assumptions that ignore rent, expiry, or maximum duration\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2273,"content_sha256":"f743cf9063f0c041c695766e1ec3003c00d1b62a1c53330094f11491806ca975"},{"filename":"references/gear-gstd-api-and-syscalls.md","content":"# Gear gstd API And Syscalls\n\nDesign 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.\n\n## Layer Map\n\n| Layer | Role | Design Use |\n| --- | --- | --- |\n| `gstd` | Builder-facing API for standard programs | Start here for capability and contract choices |\n| `gcore` | Lower-level wrappers around raw calls | Use to confirm exact arguments, return values, and fallible behavior |\n| `gsys` | Raw `gr_*` syscall declarations | Use to confirm the actual host calls and syscall families |\n\n## Capability-First View\n\n### `gstd::msg`\n\nUse `msg` when the design question is about payload handling, outbound messages, replies, or reply-await flows.\n\n| Design intent | Prefer in `gstd` | `gcore` wrapper | `gsys` family |\n| --- | --- | --- | --- |\n| Read typed input | `msg::load` | `gcore::msg::read` | `gr_read` |\n| Read raw bytes | `msg::load_bytes` | `gcore::msg::read` | `gr_read` |\n| Send typed payload | `msg::send` | `gcore::msg::send` | `gr_send` |\n| Send bytes | `msg::send_bytes` | `gcore::msg::send` | `gr_send` |\n| Send with explicit gas | `msg::send_with_gas`, `msg::send_bytes_with_gas` | `gcore::msg::send_with_gas` | `gr_send_wgas` |\n| 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 |\n| Forward part of input | `msg::send_input`, `msg::reply_input` | `gcore::msg::send_input`, `gcore::msg::reply_input` | `gr_send_input`, `gr_reply_input` |\n| Reply once | `msg::reply`, `msg::reply_bytes` | `gcore::msg::reply` | `gr_reply` |\n| Reply with explicit gas | `msg::reply_with_gas`, `msg::reply_bytes_with_gas` | `gcore::msg::reply_with_gas` | `gr_reply_wgas` |\n| 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` |\n| 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` |\n| 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 |\n| 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` |\n\nDesign 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.\n\n### `gstd::exec`\n\nUse `exec` when the design question is about current execution context, gas, waiting, waking, or termination.\n\n| Design intent | Prefer in `gstd` or `gcore` | `gsys` syscall |\n| --- | --- | --- |\n| Read environment variables | `exec::env_vars` | `gr_env_vars` |\n| Read block height or timestamp | `exec::block_height`, `exec::block_timestamp` | `gr_block_height`, `gr_block_timestamp` |\n| Inspect remaining gas | `exec::gas_available` | `gr_gas_available` |\n| Read raw bytes from program state | `exec::read_bytes` | `gr_read` |\n| Reserve gas for future system use | `exec::system_reserve_gas` | `gr_system_reserve_gas` |\n| Inspect attached or available value | `exec::value_available`, `msg::value` | `gr_value_available`, `gr_value` |\n| Allocate reply deposit for `handle_reply` | `exec::reply_deposit` | `gr_reply_deposit` |\n| Wait for future work | `exec::wait`, `exec::wait_for`, `exec::wait_up_to` | `gr_wait`, `gr_wait_for`, `gr_wait_up_to` |\n| Wake a waited message | `exec::wake` | `gr_wake` |\n| Read current program id | `exec::program_id` | `gr_program_id` |\n| Read randomness | `exec::random` | `gr_random` |\n| Exit and transfer value | `exec::exit` | `gr_exit` |\n\nDesign note: `gr_wait` and `gr_wake` are control syscalls. They shape state machines and future-block behavior, not synchronous function calls.\n\n### `gstd::prog`\n\nUse `prog` when the design question is whether a program should create child programs and what init contract that child should receive.\n\n| Design intent | Prefer in `gstd` | `gcore` wrapper | `gsys` syscall |\n| --- | --- | --- | --- |\n| Create child with bytes init payload | `prog::create_program_bytes` | `gcore::prog::create_program` | `gr_create_program` |\n| Create child with typed init payload | `prog::create_program` | `gcore::prog::create_program` | `gr_create_program` |\n| Delay creation | `prog::create_program_bytes_delayed`, `prog::create_program_delayed` | `gcore::prog::create_program_delayed` | `gr_create_program` with delay |\n| 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` |\n| 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 |\n\nDesign 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.\n\n### Reservations And Async Helpers\n\n- `ReservationId::reserve` and `.unreserve` expose the reserve or unreserve path through `gcore::exec`, which maps to `gr_reserve_gas` and `gr_unreserve_gas`.\n- Reservation pools in `gstd::reservations` help when the design needs several future-block gas buckets, but they do not remove expiry or waitlist constraints.\n- `#[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.\n\n## Design Heuristics\n\n1. Start from the contract: typed API, bytes API, staged send, delayed work, reservation-backed work, or child-program creation.\n2. Check execution semantics before checking syscall names: rollback boundary, future blocks, reply timing, waitlist exposure, and gas lifetime.\n3. Use `gcore` when the design needs exact parameter shape or you suspect fallible behavior matters.\n4. 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.\n\n## Guardrails\n\n- Keep `gstd` as the default vocabulary in specs, architecture notes, and builder guidance.\n- Do not design app flows directly in terms of raw syscall sequences unless the task is explicitly low-level.\n- Treat `gcore` and `gsys` as explanation layers beneath the public API, not as the normal app-builder entry point.\n- This is not a full syscall-maintenance catalog; benchmark integrity and instrumentation coverage belong to lower-level workflow docs.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":6846,"content_sha256":"27ffd18b6f878b569cd34ecacc179fd1f440588493e77213c369baf5f9cb8508"},{"filename":"references/gear-messaging-and-replies.md","content":"# Gear Messaging And Replies\n\n## Send And Reply Families\n\n- Use encoded send and reply APIs for normal SCALE-encoded payload types.\n- Use bytes variants only when debugging route or codec mismatches, or when a raw transport path is intentional.\n- Use delayed variants when execution must happen in a future block.\n- Use reservation-backed variants when later execution needs preserved gas.\n- Use staged payload flows only when the payload must be built incrementally.\n- 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.\n\n## Message Lifecycle Checklist\n\n1. Choose the payload style: typed, bytes, or forwarded input.\n2. Choose delivery mode: immediate, delayed, explicit gas, or reservation-backed.\n3. Choose interaction pattern: fire-and-forget or `for_reply`.\n4. Choose reply mode: one-shot reply or staged push-plus-commit.\n\n## Async `for_reply` Flows\n\n- `for_reply` flows wait on a reply-producing message and return a future.\n- Timeouts are a distinct outcome and should be handled separately from error replies.\n- A reply hook needs non-zero reply deposit to register.\n- A timed-out future does not guarantee the reply hook never runs later if the reply eventually arrives.\n\n## Staged Payload Rules\n\n- A staged send or reply is incomplete until its matching commit step runs.\n- If a code path pushes payload pieces but never commits, the staged payload is dropped.\n- Keep staged flow use narrow; it is easier to miss a commit than with one-shot replies.\n\n## Context-Specific Semantics\n\n- Reply metadata belongs in reply-handler context only.\n- Signal metadata belongs only in signal-handler context where that entrypoint is supported.\n- Do not assume a generic message handler can safely read reply or signal context APIs.\n\n## Sails Routing Notes\n\n- Generated Sails clients encode the Sails Header and SCALE body for you.\n- 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.\n- If a payload uses the wrong interface ID, entry ID, or route index, decode fails even when the payload body type is otherwise correct.\n- Treat generated clients as the default path and use low-level byte encoding only to isolate transport or codec bugs.\n\n## Guardrails\n\n- Outbound send and reply effects appear only after successful execution.\n- Handle timeout, transport failure, and error reply as different branches.\n- Always close staged payload paths with the correct commit call.\n- Use `Syscall::gas_available()` for service-code available-gas checks; `msg::gas_available()` is not the standard Gear API.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2693,"content_sha256":"17fe92903b839b43f618cf408e43452c1082f2558afeae3d9292dee38a72d49f"},{"filename":"references/gear-sails-production-patterns.md","content":"# Gear/Sails Production Patterns\n\nThis note is a portable, production-biased companion to the narrower execution, architecture, IDL, and gtest references in this pack.\n\nIt 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.\n\nEvidence weighting matters:\n\n- Treat official `sails` patterns as the primary source for current framework defaults.\n- Treat `awesome-sails` as strong evidence for reusable service and storage patterns.\n- Treat `dapps` as real-world evidence, but some of those projects are older or outdated.\n- Older `dapps` patterns should not override current Sails-first defaults just because they exist in production code.\n- Do not copy older `dapps` code blindly; validate it against the current Sails routing, client-generation, and state-management guidance in this pack.\n\n## Recommended Defaults\n\n- Prefer program-owned state in `RefCell` fields and pass references into services.\n- Keep `#[program]` thin: constructor/init, service exposure, and reply wiring only.\n- Keep business rules, guards, and events inside `#[service]`.\n- Prefer generated clients and IDL-backed routing over hand-built payload bytes.\n- Treat stateful command failure as fatal: panic and let the current message revert.\n- Use delayed self-messages and `ReservationId` only when future-block execution is part of the design.\n\n## Fast Facts\n\n- Standard Vara account flows use `SS58`, not Ethereum `0x` addresses. When chain-specific formatting matters, use Vara prefix `137`.\n- Pull `awesome-sails` from crates.io with `default-features = false`. If the program only needs pause, math, or map helpers, depend on `awesome-sails-utils` directly.\n- Delayed work is block-based on chain. A program can send a delayed message to itself for a later block.\n- `ReservationId` preserves gas for future-block execution. A delayed step cannot spend the current message's gas later.\n- In `gtest`, one `send()` is not proof the whole async flow already settled. Use `BlockRunMode` and explicit block advancement when timing matters.\n- 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.\n- Vouchers let a sponsor pay gas or fees for approved interactions. Signless shifts signing to a delegated, temporary, or session-style account.\n\n## Awesome Sails As An External Dependency\n\n`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.\n\nUse it when an agent should compose proven building blocks instead of reimplementing them:\n\n- role-based access control\n- reusable storage abstractions\n- VFT and related admin/exchange services\n- message-status tracking for async flows\n- pause wrappers, checked math, compact integer wrappers, and sharded maps\n\nFor project dependencies, prefer explicit features instead of the meta-crate default:\n\n```toml\n[dependencies]\nsails-rs = { version = \"*\", default-features = false, features = [\"gstd\"] }\n\n# Meta-crate, but feature-selected on purpose.\nawesome-sails = { version = \"x.y.z\", default-features = false, features = [\n \"storage\",\n \"access-control\",\n \"vft\",\n \"vft-admin\",\n \"vft-extension\",\n \"vft-metadata\",\n \"vft-native-exchange\",\n \"vft-native-exchange-admin\",\n \"msg-tracker\",\n] }\n\n# Separate utility crate when you want the low-level helpers directly.\nawesome-sails-utils = \"x.y.z\"\n```\n\nImportant packaging facts:\n\n- the `awesome-sails` meta-crate defaults to `all`\n- for agent-authored production code, prefer `default-features = false`\n- 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`, and `msg-tracker`\n- the meta-crate also has a `test` feature that forwards to `awesome-sails-vft-utils/test`\n- `awesome-sails-utils` is a separate crate, not a feature of the meta-crate\n\n### Feature/Crate Chooser\n\n- `storage` / `awesome-sails-storage`: choose this when a service should be generic over storage backends. It provides `Storage`, `StorageMut`, `InfallibleStorage`, `InfallibleStorageMut`, and `StorageRefCell`.\n- `access-control` / `awesome-sails-access-control`: choose this when privileged operations outgrow a single-admin check. It provides `AccessControl`, `RolesStorage`, role hierarchy support, enumeration, and batch grant/revoke flows.\n- `vft-utils` / `awesome-sails-vft-utils`: choose this when you need the underlying token storage/value types. It provides `Allowances`, `Balances`, compact `Allowance` and `Balance` wrappers, shard-aware storage helpers, and related errors.\n- `vft` / `awesome-sails-vft`: choose this for a standard VFT service with transfers, approvals, allowance handling, and balance/total-supply queries.\n- `vft-admin` / `awesome-sails-vft-admin`: choose this when the token needs privileged mint, burn, pause, and admin-managed maintenance operations.\n- `vft-extension` / `awesome-sails-vft-extension`: choose this when the token needs `transfer_all`, expired-allowance cleanup, balance/allowance enumeration, or explicit shard-management helpers.\n- `vft-metadata` / `awesome-sails-vft-metadata`: choose this when the token should expose metadata like name, symbol, and decimals.\n- `vft-native-exchange` / `awesome-sails-vft-native-exchange`: choose this when native value should mint VFT and burning VFT should return native value.\n- `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 via `handle_reply`.\n- `msg-tracker` / `awesome-sails-msg-tracker`: choose this for async orchestration, saga-style flows, or any path that needs per-`MessageId` status tracking. It provides `MsgTracker`, `Pagination`, `MessageStorage`, `BTreeMap` support, and `FixedStorage` for bounded-capacity tracking.\n\n### Separate Utility Crate Chooser\n\n`awesome-sails-utils` is the low-level helper crate. Reach for it when the project needs reusable primitives rather than a whole service.\n\n- `error`: shared error types such as a generic `Error`, `BadInput`, `BadOrigin`, `BadValue`, and `EmitError`.\n- `map`: a sharded-map implementation for controlled-capacity storage layouts.\n- `math`: `CheckedMath`, compact `LeBytes\u003cN>` integers, `NonZero\u003cT>`, and the common numeric re-exports used across the token crates.\n- `pause`: `Pause`, `PausableRef`, `PausableStorage`, and pause-related errors for emergency-stop style guards.\n- `macros`: shared helper macros used by the ecosystem.\n\nIf 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.\n\n## 1. Program-Owned State Is The Default\n\nThis is the cleanest default for standard Gear/Sails programs. The program owns lifetime and storage; the service owns behavior.\n\n```rust\nuse sails_rs::{cell::RefCell, prelude::*};\n\npub struct CounterData {\n value: u32,\n}\n\npub struct Program {\n counter: RefCell\u003cCounterData>,\n}\n\npub struct CounterService\u003c'a> {\n counter: &'a RefCell\u003cCounterData>,\n}\n\nimpl\u003c'a> CounterService\u003c'a> {\n pub fn new(counter: &'a RefCell\u003cCounterData>) -> Self {\n Self { counter }\n }\n}\n\n#[service]\nimpl CounterService\u003c'_> {\n #[export]\n pub fn add(&mut self, by: u32) -> u32 {\n let mut data = self.counter.borrow_mut();\n data.value += by;\n data.value\n }\n}\n\n#[program]\nimpl Program {\n pub fn new() -> Self {\n Self {\n counter: RefCell::new(CounterData { value: 0 }),\n }\n }\n\n pub fn counter(&self) -> CounterService\u003c'_> {\n CounterService::new(&self.counter)\n }\n}\n```\n\nWhy this is the default:\n\n- state ownership is explicit at the program boundary\n- constructor/setup is deterministic\n- tests can inject or reset state cleanly\n- services stay reusable and mostly stateless\n\n## 2. Wrap Storage When Reusable Services Need It\n\n`awesome-sails` shows a stronger reusable pattern: accept storage abstractions instead of hard-coding raw `RefCell` everywhere.\n\n```rust\nuse awesome_sails_storage::StorageRefCell;\nuse awesome_sails_utils::pause::{PausableRef, Pause};\nuse sails_rs::{cell::RefCell, prelude::*};\n\npub struct Program {\n pause: Pause,\n balances: RefCell\u003cBalances>,\n}\n\nimpl Program {\n pub fn balances(&self) -> PausableRef\u003c'_, Balances> {\n PausableRef::new(&self.pause, StorageRefCell::new(&self.balances))\n }\n\n pub fn token(&self) -> TokenService\u003c'_> {\n TokenService::new(self.balances())\n }\n}\n```\n\nUse `StorageRefCell`, `PausableRef`, or similar wrappers when:\n\n- the same business service should work with different storage backends\n- you need guards around every mutation path\n- reusable domain modules should be portable across programs\n\nProduction 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.\n\n## 3. Static Hidden State Is Allowed, But Not The Default\n\nSome real apps use static hidden state to keep a service self-contained. This is valid, but it has sharper edges than program-owned state.\n\n```rust\n#![allow(static_mut_refs)]\n\nuse sails_rs::prelude::*;\n\nstatic mut STORAGE: Option\u003cMyStorage> = None;\n\n#[derive(Default)]\nstruct MyStorage {\n next_id: u64,\n}\n\npub struct MyService;\n\nimpl MyService {\n pub fn seed() {\n unsafe {\n STORAGE = Some(MyStorage::default());\n }\n }\n\n fn storage_mut(&mut self) -> &'static mut MyStorage {\n unsafe { STORAGE.as_mut().expect(\"MyService::seed() should be called\") }\n }\n}\n\n#[program]\nimpl Program {\n pub fn new() -> Self {\n MyService::seed();\n Self\n }\n}\n```\n\nIf you choose static hidden state:\n\n- the service must have an explicit `seed()` or `init()` path before first use\n- the program constructor must call that setup exactly once\n- tests must re-seed or isolate initialization\n- helper accessors should fail loudly if initialization was skipped\n\nCosts:\n\n- weaker test isolation\n- more hidden coupling between constructor flow and service behavior\n- easier async stale-state bugs\n- harder refactors when multiple services need shared storage visibility\n\nUse static hidden state only when the self-contained boundary is materially better than explicit program-owned state.\n\n## 4. Hybrid State Is Common In Larger Apps\n\nA practical production split is:\n\n- program-owned `RefCell\u003cSessionStorage>` or other cross-cutting identity/session state\n- static hidden state for one dominant domain service\n\nThat hybrid model is acceptable when:\n\n- one subsystem like session/signless routing needs explicit shared ownership\n- the main game/service state is intentionally encapsulated behind one service\n\nGuardrail: name the split explicitly in design docs. Hybrid state that is not documented becomes accidental architecture.\n\n## 5. Keep `#[program]` Thin\n\nThe stable boundary is:\n\n- `#[program]`: constructors, service exposure, reply wiring\n- `#[service]`: commands, queries, guards, events, domain logic\n\n```rust\n#[program]\nimpl Program {\n pub fn new() -> Self {\n Self::default()\n }\n\n pub fn handle_reply(&mut self) {\n self.exchange_admin().handle_reply();\n }\n\n pub fn exchange_admin(&self) -> ExchangeAdmin\u003c'_> {\n ExchangeAdmin::new(self.access_control(), self.balances())\n }\n}\n```\n\nIf a `#[program]` method starts making domain decisions beyond setup and exposure, the boundary is already drifting.\n\n## 6. Use Shallow `extends` Composition\n\nComposition is good. Inheritance-like complexity is not.\n\n```rust\npub struct DogService {\n mammal: MammalService,\n walker: WalkerService,\n}\n\nimpl From\u003cDogService> for (MammalService, WalkerService) {\n fn from(value: DogService) -> Self {\n (value.mammal, value.walker)\n }\n}\n\n#[service(extends = [MammalService, WalkerService], events = DogEvents)]\nimpl DogService {\n #[export]\n pub fn make_sound(&mut self) -> &'static str {\n self.emit_event(DogEvents::Barked).unwrap();\n \"Woof! Woof!\"\n }\n}\n```\n\nUse `extends` when:\n\n- the inherited API is part of the intended public contract\n- route and event interactions stay understandable\n- the service can clearly convert into the extended services\n\nDo not use `extends` to hide a messy architecture. Keep it shallow and intentional.\n\n## 7. Put Guards Near The Service Boundary\n\nThe stronger patterns keep access control, pause checks, and session validation close to exported methods.\n\n```rust\n#[service]\nimpl AdminService\u003c'_> {\n #[export]\n pub fn add_admin(&mut self, admin: ActorId) {\n self.ensure_is_admin();\n self.storage_mut().admins.insert(admin);\n }\n\n fn ensure_is_admin(&self) {\n assert!(self.storage().admins.contains(&Syscall::message_source()), \"Not admin\");\n }\n}\n```\n\nProduction defaults:\n\n- grant bootstrap roles in constructor/init\n- keep one guard helper per privileged concern\n- fail before mutating state\n- prefer reusable guard wrappers over repeated inline checks when the same rule protects multiple mutation paths\n\n## 8. Define And Emit Typed Service Events\n\nIn Sails, events are a first-class service pattern, not an afterthought.\n\nThe stable default is:\n\n- define a typed event enum with #[event]\n- attach it to the service with #[service(events = Events)]\n- emit events from state-changing command paths with self.emit_event(...)\n\nThis 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.\n\n```rust\n#[sails_rs::event]\n#[sails_rs::sails_type]\n#[derive(Debug, Clone, PartialEq, Eq)]\npub enum Events {\n Created(ActorId),\n Removed(u64),\n}\n\n#[service(events = Events)]\nimpl MyService {\n #[export]\n pub fn create(&mut self) {\n self.emit_event(Events::Created(Syscall::message_source())).unwrap();\n }\n}\n```\nProduction rules:\n\n- define one service-level event enum per emitted event surface\n- emit events from successful state-changing command paths, not from read-only queries\n- treat event variants as part of the public integration contract\n- prefer structured fields over opaque blobs so indexers, frontends, and analytics code can evolve safely\n- emit after the authoritative state transition is accepted, not before\n- keep event naming domain-oriented and stable enough for downstream consumers\n\nGuardrails:\n\n- do not emit vague variants like Updated when the domain change should be explicit\n- do not treat event payload layout as disposable once off-chain consumers depend on it\n- do not emit success-style events on paths that may still fail or revert\n- `emit_event` is only available inside the `impl` block annotated with `#[service(events = Events)]`. Internal methods that need to emit events must be defined in that same annotated `impl` block, not in a separate plain `impl`. They can be private (no `#[export]`).\n\n## 9. Async Rules: Re-Read State After `.await`\n\nAny shared mutable state read before `.await` may be stale after resume.\n\n```rust\n#[service]\nimpl ResourceService\u003c'_> {\n #[export]\n pub async fn attach_part(&mut self, id: u32, part_id: u32) -> Result\u003c(), Error> {\n self.ensure_is_admin()?;\n\n let base = self.storage().resources.get(&id).ok_or(Error::Missing)?.base;\n\n let part = self\n .catalog_client\n .part(part_id)\n .with_destination(base)\n .await\n .unwrap();\n\n if part.is_none() {\n return Err(Error::MissingPart);\n }\n\n let resource = self.storage_mut().resources.get_mut(&id).ok_or(Error::Missing)?;\n resource.parts.push(part_id);\n Ok(())\n }\n}\n```\n\nProduction rules:\n\n- do not hold mutable-borrow assumptions across `.await`\n- after `.await`, re-read or revalidate shared state before mutating it\n- if a command cannot tolerate drift, redesign it into smaller messages or stricter validation steps\n\n## 10. Default To #[export(unwrap_result)] For Fatal Command Paths\n\nFor stateful command handlers, the default Sails production style is to return Result\u003c_, Error> from the service method and expose it with #[export(unwrap_result)].\n\nThat keeps the command path explicit in Rust, while preserving fail-fast semantics at the exported boundary.\n\n```rust\n#[service]\nimpl BattleService\u003c'_> {\n #[export(unwrap_result)]\n pub fn cancel_tournament(&mut self) -> Result\u003c(), Error> {\n let event = self.cancel_tournament_impl()?;\n self.emit_event(event).expect(\"Notification Error\");\n Ok(())\n }\n}\n```\n\nWhy this is the default:\n\n- the method body keeps normal Result-based control flow\n- domain failures stay typed and readable inside the implementation\n- the exported command still fails fast instead of leaving partially-updated state alive\n- the Sails boundary expresses the intended production semantics directly\n\nRecommended practice:\n\n- return Result from stateful command handlers when failure is part of the internal control flow\n- use #[export(unwrap_result)] when command failure should revert the current execution\n- expect UserspacePanic in tests for fatal exported command-path failures\n- keep compensation or partial-success behavior only when the spec explicitly models it\n\n\n## 11. Use Fail-Fast Command Semantics Allowed, But Not The Default\n\nManual fail-fast conversion is still valid, but it should be treated as an explicit exception rather than the primary style.\n\n```rust\npub fn panicking\u003cT, E: core::fmt::Debug, F: FnOnce() -> Result\u003cT, E>>(f: F) -> T {\n match f() {\n Ok(v) => v,\n Err(e) => panic!(\"{e:?}\"),\n }\n}\n\n#[service]\nimpl BattleService\u003c'_> {\n #[export]\n pub fn cancel_tournament(&mut self) {\n let event = panicking(|| self.cancel_tournament_impl());\n self.emit_event(event).expect(\"Notification Error\");\n }\n}\n```\n\nThis style is allowed when:\n\n- the public method intentionally exposes no Result shape at all\n- the code must adapt older service structure without a wider refactor\n- a small compatibility layer is clearer than rewriting multiple internal helpers\n\nCosts:\n\n- the exported API hides the recoverable/internal error shape\n- it is less idiomatic than using Sails export behavior directly\n- agents may copy the helper mechanically even where #[export(unwrap_result)] is clearer\n\nDefault rule: prefer #[export(unwrap_result)] over hand-rolled panic wrappers unless the method shape has a strong reason not to return Result.\n\n## 12. Generated Clients First, Raw Bytes Only As Escape Hatch\n\nThe stable default is IDL plus generated clients.\n\n```rust\n// build.rs (dedicated client crate)\nfn main() {\n sails_rs::build_client::\u003cdemo::DemoProgram>();\n}\n```\n\n```rust\nlet reply = DemoProgram::client(program_id)\n .counter()\n .add(10)\n .with_reply_deposit(10_000_000_000)\n .await\n .unwrap();\n```\n\nUse generated clients by default because they preserve:\n\n- Sails Header routing correctness\n- reply decoding\n- test mocks\n- consistency between Rust and TS consumers\n\nHand-roll raw bytes only at low-level integration boundaries or when isolating codec/routing bugs.\n\n## 13. Delayed Work And `ReservationId` Need Explicit Design\n\nDelayed self-messages and reservations are for genuinely future-block workflows, not for vague “maybe later” logic.\n\n```rust\nfn send_timeout_from_reservation(\n reservation_id: ReservationId,\n player_id: ActorId,\n delay: u32,\n) {\n let request = battle_client::battle::io::AutomaticMove::encode_call(\n battle_client::BattleClientProgram::ROUTE_ID_BATTLE,\n player_id,\n );\n\n msg::send_bytes_delayed_from_reservation(\n reservation_id,\n Syscall::program_id(),\n request,\n 0,\n delay,\n )\n .expect(\"Error in sending message\");\n}\n```\n\n```rust\nfn send_cleanup(player: ActorId, gas: u64, delay: u32) {\n let payload = game_client::game::io::RemoveInstance::encode_call(\n game_client::GameClientProgram::ROUTE_ID_GAME,\n player,\n );\n\n msg::send_bytes_with_gas_delayed(Syscall::program_id(), payload, gas, 0, delay)\n .expect(\"Error in sending message\");\n}\n```\n\nProduction rules:\n\n- use delayed self-messages for scheduled in-protocol work, not off-chain cron assumptions\n- persist enough state to make the delayed handler idempotent or safely repeatable\n- use `ReservationId` only when later execution budget must survive across executions\n- unreserve or remove reservations when the lifecycle ends\n- 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. Use `let gas_for_next = Syscall::gas_available() * 9 / 10;` or similar dynamic calculation.\n- 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.\n\nIf delayed work is essential, the architecture should name:\n\n- who sends the follow-up\n- which route receives it\n- which gas source pays for it\n- how stale or duplicate messages are handled\n\n## 14. Test Like Production, Not Like A Synchronous Call Stack\n\nUse `GtestEnv` modes intentionally.\n\n```rust\nuse sails_rs::client::{BlockRunMode, GtestEnv};\n\nlet env = GtestEnv::new(system, ACTOR_ID.into()).with_block_run_mode(BlockRunMode::Next);\nlet reply = program.counter().add(10).send_for_reply().unwrap();\n\nenv.run_next_block();\nlet value = reply.await.unwrap();\n```\n\nWhy `BlockRunMode` matters:\n\n- `Auto` is convenient for simple one-step calls\n- `Next` exposes whether the reply actually lands in the expected next block\n- `Manual` is right when the test must drive multi-block behavior explicitly\n\nAlso prefer:\n\n- unit-level service tests when state can be injected through `RefCell` or wrappers\n- assertions on events and reply errors, not only local state\n- explicit expectations for `UserspacePanic` on fatal command paths\n\n## Practical Defaults For Agents\n\nWhen another agent has to choose quickly, bias toward this order:\n\n1. Program-owned state plus thin service wrappers.\n2. `StorageRefCell` or `PausableRef` when reusable storage-aware services are justified.\n3. Generated clients plus `build.rs` IDL/client generation.\n4. Fail-fast command handlers that test for `UserspacePanic`.\n5. Delayed self-messages only when the feature is genuinely block-shaped.\n6. `ReservationId` only when a later execution budget must be preserved.\n7. Static hidden state only with explicit `seed()` or `init()` discipline.\n\n## Anti-Patterns\n\n- fat `#[program]` methods that own domain logic\n- static hidden state without `seed()` or `init()` discipline\n- mixing generated clients with ad hoc raw payloads on the same normal path\n- mutating shared state before `.await` and assuming it is still authoritative afterward\n- delayed work without explicit gas-budget and replay/idempotency thinking\n- deep `extends` graphs that obscure the public contract\n- soft-error command handlers that leave partially-updated state alive\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":23727,"content_sha256":"86645e222f0fe1c5c954b44aec3c4445e162a1e72d24ae74908ebe551616b666"},{"filename":"references/gtest-cheatsheet.md","content":"# Gtest Cheatsheet\n\n## Baseline Flow\n1. Create exactly one `System`.\n2. Fund the sender before sending.\n3. Submit or deploy the program.\n4. Send messages and keep their ids.\n5. Advance blocks explicitly.\n6. Assert logs, mailbox state, reply behavior, and spent value.\n\n## Things To Not Assume\n- `send` does not execute the queue immediately.\n- The first message is often initialization.\n- Time-dependent behavior needs explicit block advancement.\n- Program funding and user funding are not interchangeable.\n- Programs creating child programs via `create_program_bytes` must have balance to cover the child's existential deposit. Fund the parent program before the factory call.\n- `GtestEnv` consumes the `System`, making post-deploy `system.mint_to()` awkward. Fund programs as part of the deploy fixture before wrapping in `GtestEnv`, or use raw `gtest` APIs for the funding step.\n\n## Child Program Creation\n\n- `gstd::prog::create_program_bytes(code_id, salt, payload, value)` requires the calling program to hold sufficient balance. With `value = 0`, the call still fails if the child needs existential deposit.\n- In `gtest`, fund the parent program with `system.mint_to(parent_program_id, amount)` before invoking the factory method.\n- `gstd::prog` is not re-exported through `sails_rs::gstd`. Add `gstd` as a direct dependency for program creation primitives.\n\n## Sails-First Testing\n- Prefer generated clients in `GtestEnv` over hand-authored payload bytes.\n- Assert one constructor path and one command/query path in the happy case.\n- Add event assertions when the service exposes events.\n- Use a deterministic block run mode when the workflow depends on async replies or exits.\n\n## What The Core Skills Should Capture\n- The task plan must call out required gtest coverage.\n- The implementer should not claim completion without a green local gtest loop.\n- The gtest loop skill should summarize failures in a form that is patchable by an agent.\n\n## See Also\n- `references/sails-gtest-and-local-validation.md`\n- `references/gear-gas-reservations-and-waitlist.md`\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2067,"content_sha256":"65380582b341ed6b22c24baa6475a9cd13c8c1f2e2a0ec0ea1da41af7b3b3a2d"},{"filename":"references/gtest-patterns.md","content":"# Gtest Patterns\n\n## Default Path\n\n- Prefer `GtestEnv` and generated clients for standard Sails verification.\n- Drop to raw `gtest::Program` calls only when you are debugging routing, payload encoding, failure logs, or timing at a lower level than the generated client exposes.\n\n## Raw `send_bytes` Mental Model\n\n- `Program::send_bytes(...)` returns a `MessageId`.\n- `System::run_next_block()` returns a `BlockRunResult`.\n- Replies and failures are inspected from that `BlockRunResult`, not from the `send_bytes` return value.\n\n```rust\nlet msg_id = program.send_bytes(sender, payload);\nlet r = system.run_next_block();\n\nassert!(!r.failed.contains(&msg_id));\n\nlet reply = r\n .log()\n .iter()\n .find(|entry| entry.reply_to() == Some(msg_id))\n .expect(\"reply log missing\");\n```\n\n## Init Semantics\n\nThe first message sent to a program is always treated as init, even if the program has no explicit init function.\nTests should account for this when choosing the first payload.\n\n## What To Inspect\n\n- `r.log()` contains the block logs and replies that landed in that block.\n- `r.succeed`, `r.failed`, and `r.not_executed` are the first outcome buckets to inspect.\n- `r.failed.contains(&msg_id)` is the first quick check for command-path failure.\n- `reply.reply_code()` tells you whether the reply was a success or an error.\n- `reply.reply_to()` links a reply log back to the original message id.\n- `r.contains(&expected_log)` is the quickest structured assertion when a `Log` pattern is enough.\n- `r.decoded_log::\u003cT>()` is useful when you want typed log inspection instead of raw bytes.\n- The payload still needs decoding after you extract the matching reply log entry.\n\n## Panic Assertions\n\n- For fatal-path validation, prefer `r.assert_panicked_with(msg_id, \"...\")` when you want to assert userspace panic text.\n- Use raw `reply_code()` inspection when you need finer-grained low-level failure analysis.\n\n## Multi-Block And Delayed Behavior\n\n- Use `system.run_to_block(n)` when delayed sends, wakeups, or reservation expiry depend on block height.\n- `run_next_block()` is enough only when the full effect should land in the very next block.\n- For Sails `GtestEnv`, choose `BlockRunMode::Next` or `BlockRunMode::Manual` when the test must expose exact timing.\n- `run_scheduled_tasks(n)` advances blocks while processing scheduled tasks only, without draining the message queue.\n- `run_next_block_with_allowance(...)` is useful for low-level gas/allowance timing tests.\n\n## Reply Decoding Reminder\n\n- Generated clients handle reply prefixes for you.\n- 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.\n- If the goal is not codec or routing debugging, go back to the generated client path.\n\n## Setup Ergonomics\n\n- `Program\u003c'_>` borrows `System`, so a helper usually cannot return `(System, Program\u003c'_>)` without running into lifetime friction.\n- Prefer either:\n - a helper that returns only `System`, then build `Program` inside each test\n - a helper that performs the setup inline per test when the borrow would otherwise escape\n- Also remember that only one System instance is allowed per thread.\n\n## Failure Patterns\n\n- `UserspacePanic` is often the expected assertion target for fatal command-path validation.\n- `RanOutOfGas` and similar reply codes are best asserted through reply logs plus `failed.contains(&msg_id)`.\n- For low-level debugging, record the `MessageId`, the block you ran, and the relevant `BlockRunResult` evidence in the test note.\n\n## Program Funding And Child Creation\n\n- Programs that create child programs via `gstd::prog::create_program_bytes` need balance in their account. The child program requires existential deposit even when `value = 0` is passed.\n- In `gtest`, fund the parent program before the factory call:\n\n```rust\n// Fund parent program so it can afford child existential deposit\nsystem.mint_to(parent_program_id, 100_000_000_000_000);\n\n// Now the factory method can call create_program_bytes successfully\nlet msg_id = program.send_bytes(sender, create_child_payload);\nlet r = system.run_next_block();\nassert!(!r.failed.contains(&msg_id));\n```\n\n- `GtestEnv` wraps `System`, making direct `mint_to` calls awkward after deployment. Either:\n - Fund the program before wrapping in `GtestEnv`.\n - Use raw `gtest` APIs for the funding step alongside `GtestEnv` for typed calls.\n- `NotEnoughValue` in `gtest` for child creation almost always means the parent program is unfunded.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4588,"content_sha256":"e670983878293dc242427cf586aaa4e8c9f102eb5e3ec0c8d1c23a5ef3ac3c68"},{"filename":"references/sails-cheatsheet.md","content":"# Sails Cheatsheet\n\n## Release Baseline\n\n- Treat `sails-rs 1.0.0` as the current standard baseline for this pack. If the target repo pins a different version, follow the repo version.\n- Public service methods must be marked with `#[export]` to become Sails routes.\n- Event enums should use `#[sails_rs::event]`.\n- Event emission should use `emit_event`, not older renamed patterns.\n\n## Program Shape\n- `#[program]` owns constructors that return `Self` and exposes services.\n- `#[service]` owns business logic and exported commands or queries.\n- One application has one `#[program]`, but it may expose multiple services.\n\n## Export Rules\n- Treat `#[export]` as required for every publicly callable service method.\n- `&mut self` exports are commands and may mutate state.\n- `&self` exports are queries and should be read-only.\n- `#[export(route = \"...\")]` is the stable routing contract for services or methods; by default, exposed service and method names are converted to PascalCase.\n- `#[export]` supports additional options in Sails 1.0: `scale`, `ethabi`, `payable` for ethexe paths. See `../../skills/sails-ethexe-implementer/SKILL.md`.\n- `CommandReply\u003cT>` is the value-returning command path.\n- Service events should be emitted with `self.emit_event(...)`.\n\n## Event Rules\n- Declare event enums with `#[sails_rs::event]`.\n- Attach the event enum to the service with `#[service(events = Events)]`.\n- Emit service events through `self.emit_event(...)`.\n- Treat events as part of the public contract: name them after meaningful state transitions, not internal implementation steps.\n- Events are only published if the emitting command completes successfully.\n- Outgoing event payloads follow the Sails route-framed layout: service name, event name, then event data.\n\n## Common Pitfalls\n\n- `Syscall::message_source()` and `Syscall::program_id()` require `use sails_rs::Syscall;`. The `prelude::*` does not re-export `Syscall`. Missing this import causes `unresolved import` errors when writing guards or delayed-message helpers. Use `Syscall::*` in preference to raw `msg::source()` / `exec::program_id()` calls.\n- `emit_event` is generated by the `#[service(events = Events)]` macro and is only available inside that annotated `impl` block. Internal helper methods that need to emit events must be defined in the same `#[service(events = Events)] impl` block (without `#[export]`). A separate plain `impl` block cannot call `emit_event`.\n- `sails_rs::collections::BTreeMap` is a `no_std` re-export. Some `std` methods like `drain()` are not available. Use `keys().cloned().collect::\u003cVec\u003c_>>()` then iterate and remove as a workaround for drain-like patterns.\n- `gstd::prog` (including `create_program_bytes`) is not re-exported through `sails_rs::gstd`. If a Sails program needs to create child programs, add `gstd` as a direct dependency: `gstd = \"1.10.0\"`.\n- IDL-visible types require `ReflectHash` derive (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.\n- A function returning `CommandReply\u003c()>` that also accepts value (payable) must send a manual reply. The framework does not auto-reply for `CommandReply\u003c()>` when value is attached. If the reply is missing, the caller's value transfer succeeds but no reply is delivered.\n\n## Troubleshooting: Common Compile Errors\n\n| Error Message | Cause | Fix |\n|---|---|---|\n| `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 |\n| `#[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` |\n| `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 |\n\nThese errors appear in order of frequency from builder feedback.\n\n## Program-Level Payable\n- Use `#[program(payable)]` if the program must accept value on an empty payload without routing to a service.\n\n## Advanced Export Helpers\n- `#[export(unwrap_result)]` allows internal use of `Result\u003cT, E>` and `?` while exposing the unwrapped success path to clients.\n\n\n## IDL And Clients\n- Sails generates IDL from Rust types at build time.\n- Generated clients are the default typed integration surface for Rust and TypeScript.\n- Generated clients encode the correct payloads for constructor and service calls using the Sails Header plus SCALE body.\n- 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`).\n- Architecture decisions must keep exported DTO names distinct from service names.\n- Events are part of the public interface and should map to meaningful state transitions.\n\n## Skill Implications\n- Specs should talk in terms of program constructors, service routes, commands, queries, and events.\n- Specs should name the chosen state ownership pattern instead of leaving storage implicit.\n- Architecture plans should keep `#[program]` thin and push logic into services.\n- Implementation guidance should prefer generated clients or other Sails Header-aware encoding over raw payload handling.\n\n## See Also\n- `references/sails-rs-imports.md`\n- `references/sails-program-and-service-architecture.md`\n- `references/sails-idl-client-pipeline.md`\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":5601,"content_sha256":"e04548ad8b8e6b6f3e703f34db617cf55f5d026007c5b1c9c3ce60603724686e"},{"filename":"references/sails-ethexe-patterns.md","content":"# Sails Ethexe Patterns\n\nSource of truth: `rs/macros/core/src/service/ethexe.rs`, `rs/src/gstd/ethexe.rs`, `rs/src/solidity.rs`, `rs/sol-gen/`.\n\n## Feature Flag\n\nEnable with `sails-rs = { version = \"...\", features = [\"ethexe\"] }`. This pulls in:\n- `alloy-primitives` (B256, Selector)\n- `alloy-sol-types` (SolValue, SolType, ABI encoding)\n- Propagates through macros, type-registry, and reflect-hash\n\n## Standard Sails vs Ethexe Diff\n\n| Area | Standard Sails | Ethexe |\n|------|---------------|--------|\n| Type encoding | SCALE codec | ABI (`SolValue`) for ethabi transport, SCALE for scale transport |\n| Event emission | `emit_event(MyEvent::Variant)` | `emit_eth_event(MyEvent::Variant)` → sends to `ETH_EVENT_ADDR` |\n| Event traits | `SailsEvent` only | `SailsEvent` + `EthEvent` (both auto-generated by `#[event]`) |\n| Transport | Scale only | Scale + ethabi (dual or single via `#[export]` flags) |\n| Payable | Not available | `#[export(payable)]` or `#[export(ethabi, payable)]` |\n| Gated syscalls | Full set | `signal_from`, `signal_code`, `system_reserve_gas` unavailable |\n| Program entry | Sails Header routing | Sails Header + Solidity selector routing (`sol_init`, `sol_main`) |\n| Client gen | `sails-client-gen-v2` | + `sails-sol-gen` for Solidity interface generation |\n| Metadata | Route/entry IDs | + Solidity method/constructor/callback signatures |\n\n## Type Requirements\n\nTypes used in ethabi-exported methods must implement `SolValue` (from `alloy-sol-types`). Primitive types and tuples implement it automatically. For custom structs/enums:\n\n- `#[sails_type]` handles standard Sails derives (Encode, Decode, TypeInfo, ReflectHash)\n- Ethabi transport additionally requires the type to be ABI-encodable as a Solidity tuple\n- Parameters decode via `SolValue::abi_decode_params()`, encode via `abi_encode_sequence()`\n- Message IDs are encoded as `alloy_primitives::B256`\n\n## Transport Selection\n\n```rust\n#[export] // Both: scale + ethabi (default when ethexe on)\n#[export(scale)] // Scale only — no ethabi even with ethexe\n#[export(ethabi)] // Ethabi only\n#[export(scale, ethabi)] // Explicit dual\n#[export(ethabi, payable)] // Ethabi + payable\n#[export(scale, ethabi, payable)] // Dual + payable\n```\n\n- If no transport flag: both enabled\n- If any flag specified: only those listed\n- `payable` requires `ethabi` — error if scale-only\n\n## Payable Methods\n\n```rust\n#[export(payable)]\npub fn do_something_payable(&mut self, p1: u32) -> u32 { ... }\n```\n\nRuntime check: non-payable methods panic if `msg::value() > 0`. Payable methods skip this check.\n\nConstructors can also be payable:\n```rust\n#[export(payable)]\npub fn create_payable() -> Self { ... }\n```\n\n## Event Emission\n\n### Standard Gear events\n```rust\nself.emit_event(MyEvent::Something { value: 42 });\n```\n\n### Eth events (ethexe only)\n```rust\nself.emit_eth_event(MyEvent::Something { value: 42 });\n```\n\n- Sends to `ETH_EVENT_ADDR` (`0xffffffffffffffffffffffffffffffffffffffff`)\n- Encodes topics (keccak256 signature + indexed fields) + ABI-encoded data\n- Max 3 `#[indexed]` fields per event variant\n\n### Event Trait Generation\n\n`#[event]` macro generates both `SailsEvent` and `EthEvent` impls when ethexe is enabled:\n```rust\n#[event]\npub enum MyEvent {\n Something {\n #[indexed]\n who: ActorId,\n value: u64,\n },\n Simple,\n}\n```\n\n**Forbidden event variant names** (reserved by runtime):\nExecutableBalanceTopUpRequested, Message, MessageQueueingRequested, Reply, ReplyQueueingRequested, StateChanged, ValueClaimed, ValueClaimingRequested\n\n## Solidity Interface Generation\n\n`sails-sol-gen` generates Solidity contract interfaces from IDL:\n- Function stubs with correct ABI signatures\n- Event definitions with `indexed` fields\n- Constructor interfaces\n- `payable` annotation on applicable functions\n\nUsage: `generate_solidity_contract(idl_content, name)` → Solidity source\n\n## Program Entry Points\n\nEthexe programs generate additional entry points:\n- `sol_init()` — handles constructor calls via Solidity selector routing\n- `sol_main()` — handles service method calls via selector routing\n\nSelector computation: `keccak256(service_name + method_name + param_types)` → 4-byte selector\n\nCallback signatures: `keccak256(\"replyOn_\" + method_name + return_types)`\n\n## Gated Syscalls\n\nAvailable in ethexe: all `Syscall::*` methods except:\n- `Syscall::signal_from()` — `#[cfg(not(feature = \"ethexe\"))]`\n- `Syscall::signal_code()` — `#[cfg(not(feature = \"ethexe\"))]`\n- `Syscall::system_reserve_gas()` — `#[cfg(not(feature = \"ethexe\"))]`\n\n## Ethexe Workspace Structure\n\nSeparate workspace at `rs/ethexe/` with its own `Cargo.toml`. Example project layout:\n\n```\nrs/ethexe/\n├── Cargo.toml # Workspace root\n├── ethapp/ # Minimal ethexe program\n│ ├── Cargo.toml # sails-rs with features = [\"ethexe\"]\n│ ├── build.rs\n│ └── src/lib.rs\n├── ethapp_with_events/ # Program with eth event emission\n└── macros-tests/ # Macro snapshot tests\n```\n\nDependencies pattern:\n```toml\n[dependencies]\nsails-rs = { workspace = true, features = [\"ethexe\"] }\n\n[build-dependencies]\nsails-rs = { workspace = true, features = [\"ethexe\", \"wasm-builder\"] }\n\n[dev-dependencies]\nsails-rs = { workspace = true, features = [\"ethexe\", \"gtest\"] }\n```\n\n## Validation Rules\n\n- Service names validated against Solidity reserved keywords\n- PascalCase service names required\n- `#[indexed]` fields: max 3 per event variant\n- Forbidden event variant names (see above)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":5603,"content_sha256":"ab69bc6b32feca250e087688f13281f2ddcfc4071b7e38017a3b3a44009ed6a7"},{"filename":"references/sails-frontend-and-gear-js.md","content":"# Sails Frontend And Gear-JS\n\nThis note captures the standard frontend path for Vara Sails applications and the justified fallback path for lower-level Gear-JS work.\n\n## Contents\n\n- [Default Decision](#default-decision)\n- [Next.js App Router Compatibility](#nextjs-app-router-compatibility)\n- [Typical Package Surface](#typical-package-surface)\n- [Root Provider Composition](#root-provider-composition)\n- [Wallet And Account Flow](#wallet-and-account-flow)\n- [Typed Sails Client Path](#typed-sails-client-path)\n- [Queries](#queries)\n- [SCALE Decode Decision](#scale-decode-decision)\n- [Commands And Transactions](#commands-and-transactions)\n- [Events](#events)\n- [Low-Level Gear-JS Fallback](#low-level-gear-js-fallback)\n- [Vouchers, Gasless, And Signless](#vouchers-gasless-and-signless)\n- [Mailbox And Replies](#mailbox-and-replies)\n- [Environment Contract](#environment-contract)\n- [Default Review Checklist](#default-review-checklist)\n\n## Quick Start\n\nWhen a Sails app needs a frontend, use `create-vara-app`:\n\n```bash\nnpx create-vara-app my-dapp --idl path/to/service.idl\n```\n\nnpm: https://www.npmjs.com/package/create-vara-app | GitHub: https://github.com/gear-foundation/create-vara-app\n\nThis scaffolds a Vite + React + TypeScript frontend with typed wrappers, wallet integration, event subscriptions, and a debug panel.\n\nFor extending or repairing an existing frontend, skip to the Default Decision below.\n\n## Default Decision\n\nFor a standard Vara Sails frontend, use this order:\n\n1. The Sails `.idl` is the interface source of truth.\n2. Prefer `create-vara-app`'s scaffold for IDL-driven codegen with React components. For projects not using `create-vara-app`, use `cargo sails client-js` generated client code for typed access.\n3. Prefer `@gear-js/react-hooks` for React integration, queries, commands, and typed events.\n4. Use `useSails` only when you intentionally choose runtime IDL parsing instead of generated client code.\n5. Use `@gear-js/api` only as an explicit escape hatch for cases the typed Sails path does not cover.\n\nDo not start with manual SCALE payload assembly when an IDL and generated client are available.\nDo not switch to low-level Gear-JS just because a UI flow is custom.\n\n## Next.js App Router Compatibility\n\n`@gear-js/api`, `sails-js`, and `@polkadot/api` depend on browser globals and WASM. They break in React Server Components by default. When the frontend uses Next.js App Router:\n\n- Add Gear packages to `transpilePackages` in `next.config.js`:\n ```js\n const nextConfig = {\n transpilePackages: ['@gear-js/api', 'sails-js', '@polkadot/api'],\n webpack: (config) => {\n config.resolve.fallback = { ...config.resolve.fallback, fs: false, net: false, tls: false };\n return config;\n },\n };\n ```\n Note: this `webpack` configuration applies when Next.js uses webpack. If using Turbopack (default in Next.js 15+), the fallback configuration may differ.\n- Mark all Gear provider and hook files with `'use client'`.\n- Use dynamic imports with `ssr: false` for components that initialize Gear connections:\n ```tsx\n import dynamic from 'next/dynamic';\n const GearProviders = dynamic(() => import('./GearProviders'), { ssr: false });\n ```\n- If `@gear-js/react-hooks` or its subdependencies (e.g. wallet packages) cause server-build failures even after `transpilePackages`, fall back to a lower-level `sails-js` + `@gear-js/api` integration path with explicit client-only runtime boundaries. If falling back, commit to the lower-level path for the entire project. Do not mix hooks-based and non-hooks-based service access in the same codebase.\n- On the lower-level fallback path, use `await import('sails-js')` and `await import('@gear-js/api')` at runtime inside client components instead of top-level static imports, to prevent Next.js from bundling them into server page-data collection.\n- `next/font/google` fetches fonts from Google at build time. CI environments or proof-loop sandboxes without internet access will fail. Use local font files or `next/font/local` in offline environments.\n- `.next/types` verification order: run `next build` before `tsc --noEmit` because App Router generates route type files during the build step. Running typecheck first will fail on missing route types.\n\n## Typical Package Surface\n\nA typical Vara frontend built around Sails usually has three layers.\n\n### Core Sails layer\n- `sails-js`\n- `@gear-js/react-hooks`\n- `@gear-js/api`\n- `@polkadot/api`\n- `@tanstack/react-query`\n- React\n\n### Wallet and UI layer\nAdd these only if the app uses packaged wallet/UI components:\n- `@gear-js/ui`\n- `@gear-js/vara-ui`\n- `@gear-js/wallet-connect`\n\n### Optional low-level layer\nAdd low-level metadata, mailbox, voucher, or raw API helpers only when the app genuinely needs escape-hatch behavior.\n\nWhen using the transaction-builder path without React hooks (e.g. in a Node.js script, test harness, or Next.js API route), add `@polkadot/util` and `@polkadot/util-crypto` as explicit dependencies. These are normally pulled in transitively by `@gear-js/react-hooks`, but on the no-hooks path they must be listed explicitly or the build will fail with unresolved module errors.\n\nVersion resolution order:\n1. Follow the target repository lockfile first.\n2. Then check current `peerDependencies`, especially for `@gear-js/react-hooks`.\n3. If bootstrapping a new app, resolve a compatible package set from current package metadata before writing `package.json`.\n\nDo not pin Gear/Vara frontend package versions from memory.\n\n## Root Provider Composition\n\nThe normal React root is:\n\n- TanStack Query provider\n- Gear API provider\n- account provider\n- alert provider\n- router and app-specific providers\n\nThe official hooks documentation requires a TanStack `QueryClientProvider` together with `ApiProvider`, `AccountProvider`, and `AlertProvider`.\n\nA practical app wrapper looks like this:\n\n```tsx\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query';\nimport {\n ApiProvider,\n AccountProvider,\n AlertProvider,\n} from '@gear-js/react-hooks';\nimport { Alert, alertStyles } from '@gear-js/ui';\n\nconst queryClient = new QueryClient();\n\nexport function Providers({ children }: { children: React.ReactNode }) {\n return (\n \u003cQueryClientProvider client={queryClient}>\n \u003cApiProvider initialArgs={{ endpoint: import.meta.env.VITE_NODE_ADDRESS }}>\n \u003cAccountProvider appName=\"My Vara App\">\n \u003cAlertProvider template={Alert} containerClassName={alertStyles.root}>\n {children}\n \u003c/AlertProvider>\n \u003c/AccountProvider>\n \u003c/ApiProvider>\n \u003c/QueryClientProvider>\n );\n}\n```\nIf the app uses packaged Gear wallet/UI components, import their required styles at the app entrypoint before validating wallet UX.\n\nAlso validate required env values, such as node endpoint and program IDs, before rendering the provider tree. Missing env configuration must fail visibly, not silently.\n\nProject-specific providers such as auth, routing, balances, feature flags, or domain contexts should sit around or under this base tree.\n\n## Wallet And Account Flow\n\nFor a standard dApp, prefer browser wallet flows rather than local keyrings:\n\n- `useAccount` for detected wallets, selected account, login, and logout\n- `@gear-js/wallet-connect` `Wallet` component for a ready-made wallet button and modal\n\nIf you need a custom signer path, the generated Sails transaction builder also supports browser wallet injection:\n\n```ts\nimport { web3Accounts, web3FromSource } from '@polkadot/extension-dapp';\n\nconst [account] = await web3Accounts();\nconst injector = await web3FromSource(account.meta.source);\n\ntransaction.withAccount(account.address, { signer: injector.signer });\n```\n\nDo not place `GearKeyring.fromMnemonic`, seeds, or test mnemonics in a shipped browser frontend.\n\n### Required wallet readiness states\n\nAt minimum, the UI should distinguish these states:\n\n- `!isAccountReady`: wallet discovery is still loading\n- `isAccountReady && !isAnyWallet`: no Vara-compatible wallet extension found\n- `isAnyWallet && !account`: wallet exists but no account is connected\n- `account`: wallet connected and account ready for signing\n\nDo not render write actions as active until the account path is ready.\nDisabled actions should explain why they are disabled.\n\n## Typed Sails Client Path\n\nGenerate the client from the program IDL:\n\n```bash\ncargo sails client-js path/to/program.idl src/lib.ts\n```\n\nThen wire the generated `Program` class:\n\n```ts\nimport { useProgram } from '@gear-js/react-hooks';\nimport { Program } from '@/lib';\n\nconst { data: program } = useProgram({\n library: Program,\n id: programId,\n});\n```\n\nUse `useSails` only when you intentionally want runtime IDL parsing instead of a generated library.\n\nUse generated client code as the default for product code.\nReserve runtime IDL parsing for dynamic integrations, tooling, playgrounds, or cases where the IDL is not known at build time.\n\nIf the program `.idl` changes, regenerate the client before treating the frontend as up to date.\n\n### sails-js-parser Initialization\n\nWhen using `sails-js` programmatically for runtime IDL parsing (not the generated client path), the `Sails` class requires an explicit parser instance:\n\n```ts\nimport { Sails } from 'sails-js';\nimport { SailsIdlParser } from 'sails-js-parser';\n\nconst parser = await SailsIdlParser.new();\nconst sails = new Sails(parser);\nconst idl = await fetch('/program.idl').then(r => r.text());\nsails.parseIdl(idl);\n```\n\nWithout `sails-js-parser`, calling `parseIdl` will throw: `Parser not set. Use sails-js-parser package to initialize the parser and pass it to the Sails constructor.`\n\nIn Next.js, this code must run client-side only via a `'use client'` component or dynamic import with `ssr: false`. If `sails-js` exports its own parser in a future version, the separate `sails-js-parser` import may no longer be needed.\n\n## Queries\n\nFor Sails queries, prefer `useProgramQuery`:\n\n```ts\nconst { data, isLoading, error } = useProgramQuery({\n program,\n serviceName: 'counter',\n functionName: 'value',\n args: [],\n watch: false,\n});\n```\n\nGuidance:\n\n- Use `watch: false` by default.\n- Turn `watch: true` on only for screens that truly need live updates.\n- Use `watch: true` only for narrow screens that genuinely benefit from live updates.\n- Document the extra network traffic when enabling subscriptions.\n- Prefer query + explicit refetch after successful mutations as the default data freshness model.\n- Do not depend on broad live subscriptions for ordinary CRUD freshness.\n\nFor low-level reads, use:\n\n- `api.programState.read` with `ProgramMetadata` for the full state\n- `api.programState.readUsingWasm` for state functions backed by `state.meta.wasm`\n\n## SCALE Decode Decision\n\nWhen the frontend drops below the generated Sails path, classify the bytes before decoding:\n\n- standard Sails constructors, service calls, replies, and events:\n - prefer the generated client\n - use runtime `parseIdl` only when dynamic control is explicitly required\n\n- full state reads:\n - use `api.programState.read` with `ProgramMetadata`\n\n- state-function outputs:\n - use `api.programState.readUsingWasm` with `state.meta.wasm`\n\nDo not decode arbitrary Sails-facing bytes as a bare DTO until you have ruled out routing framing.\nDo not use `.idl` and `ProgramMetadata` interchangeably.\nDo not use full-state metadata where `state.meta.wasm` is the correct artifact.\n\n## Commands And Transactions\n\nFor standard Sails writes, prefer hooks over manual extrinsic assembly.\n\n### Send directly with hooks\n\n```ts\nconst { sendTransactionAsync } = useSendProgramTransaction({\n program,\n serviceName: 'counter',\n functionName: 'increment',\n});\n\nconst result = await sendTransactionAsync({\n args: [1],\n value: 0n,\n});\n\nconst reply = await result.response;\n```\n\nKey points:\n\n- `args` maps to the Sails function arguments.\n- `account` is optional when the connected wallet account should sign.\n- `value` defaults to `0`.\n- `gasLimit` is auto-calculated if omitted.\n- `voucherId` can be provided for voucher-backed sends.\n- The frontend should usually await the decoded `response`, not just the extrinsic submission.\n\n### Minimal UI mutation pattern with alerts\n\n```ts\nimport { useState } from 'react';\nimport {\n useAccount,\n useAlert,\n useSendProgramTransaction,\n} from '@gear-js/react-hooks';\n\ntype Props = {\n program: any;\n onSuccess: () => Promise\u003cvoid> | void;\n};\n\nexport function CreateCampaignButton({ program, onSuccess }: Props) {\n const alert = useAlert();\n const { account, isAnyWallet, isAccountReady } = useAccount();\n const [inlineError, setInlineError] = useState\u003cstring | null>(null);\n\n const { sendTransactionAsync, isPending } = useSendProgramTransaction({\n program,\n serviceName: 'crowdfunding',\n functionName: 'createCampaign',\n });\n\n const handleClick = async () => {\n setInlineError(null);\n\n if (!isAccountReady) {\n setInlineError('Wallets are still loading');\n return;\n }\n\n if (!isAnyWallet) {\n setInlineError('No Vara wallet extension found');\n return;\n }\n\n if (!account) {\n setInlineError('Connect a wallet first');\n return;\n }\n\n if (!program) {\n setInlineError('Program is not ready yet');\n return;\n }\n\n const alertId = alert.loading('Awaiting wallet confirmation...');\n\n try {\n const result = await sendTransactionAsync({\n args: [\n 'My campaign',\n 'Short description',\n 1_000_000_000_000n,\n 1000,\n [{ title: 'Prototype', amount: 1_000_000_000_000n }],\n ],\n value: 0n,\n awaitFinalization: true,\n });\n\n await result.response;\n\n alert.update(alertId, 'Campaign created successfully');\n await onSuccess();\n } catch (error) {\n const message =\n error instanceof Error ? error.message : 'Transaction failed';\n\n alert.update(alertId, message);\n setInlineError(message);\n }\n };\n\n return (\n \u003cdiv>\n {inlineError && \u003cp>{inlineError}\u003c/p>}\n\n \u003cbutton onClick={handleClick} disabled={isPending || !account}>\n {isPending ? 'Submitting...' : 'Create campaign'}\n \u003c/button>\n \u003c/div>\n );\n}\n```\n\n### Prepare before send\n\nUse `usePrepareProgramTransaction` when you need the transaction fee, gas limit, or extrinsic before the user confirms:\n\n```ts\nconst { prepareTransactionAsync } = usePrepareProgramTransaction({\n program,\n serviceName: 'counter',\n functionName: 'increment',\n});\n\nconst transaction = await prepareTransactionAsync({\n args: [1],\n});\n\nconst fee = await transaction.transactionFee();\n```\n\nThis is the right place for:\n\n- balance validation\n- fee previews\n- custom confirmation modals\n- voucher checks\n\nA common wrapper pattern is to run a balance check before calling `transaction.signAndSend()`, then await `response()` and raise a UI alert on failure.\n\n### Transaction builder without hooks\n\nOutside React hooks, the generated client returns a Sails transaction builder:\n\n```ts\nconst tx = program.counter.increment(1);\n\ntx.withAccount(account.address, { signer: injector.signer });\nawait tx.calculateGas();\n\nconst { response } = await tx.signAndSend();\nconst reply = await response();\n```\n\nUse this path for custom orchestration, batch flows, or non-React helpers.\n\n### API Surface Reference\n\nAs of `sails-js` 0.x & 1.0.x, validate these conventions against the installed version before wiring UI code:\n\n- `tx.signAndSend()` returns `{ response }` where `response` is a callable function that returns a promise of the decoded reply. Pattern: `const { response } = await tx.signAndSend(); const reply = await response();`\n- Do not use `sendAndWait()` or `withAccount(...).sendAndWait()` — these do not exist on the current `sails-js` transaction builder.\n- For the hooks path, `sendTransactionAsync` returns `{ response }` as a promise: `const result = await sendTransactionAsync({ args: [...] }); const reply = await result.response;`\n- Function arguments are positional, not wrapped in objects. If the Sails function takes `(config: GameConfig)`, call `CreateGame(configValue)` not `CreateGame({ config: configValue })`.\n- Query builder: `useProgramQuery({ program, serviceName, functionName, args: [] })`. Outside hooks: `program.serviceName.FunctionName(...args)` returns a query builder; call `.withAddress(programId)` then `.call()` to execute.\n- Validate the generated `Program` class surface before wiring: confirm `program.\u003cserviceName>.\u003cfunctionName>` exists. If it returns `undefined`, the generated client does not match the current IDL.\n\nIf the installed `sails-js` version differs from the conventions above, validate the actual API surface from the package exports before wiring.\n\n### Canonical mutation pattern\n\nEvery write action should follow this sequence:\n\n1. Check runtime prerequisites:\n - program instance is ready\n - wallet discovery is ready\n - wallet exists\n - account is connected\n2. Validate form and domain constraints before sending.\n3. Show a visible pending state.\n4. Send the transaction.\n5. Await the decoded `response`, not only extrinsic submission.\n6. Surface success or failure visibly.\n7. Refetch the affected queries after success.\n\nDo not rely only on console logs or implicit alerts.\n\n## Events\n\nUse `useProgramEvent` for typed event subscriptions:\n\n```ts\nuseProgramEvent({\n program,\n serviceName: 'counter',\n functionName: 'Incremented',\n onData: (event) => console.log(event),\n});\n```\n\nDo not subscribe globally to every event. Scope subscriptions to the screen or feature that actually needs them.\n\n## Low-Level Gear-JS Fallback\n\nUse low-level `@gear-js/api` only when there is a concrete reason the typed Sails path does not cover.\n\nLow-level Gear-JS is an escape hatch, not a parallel default architecture.\nDo not replace ordinary Sails command flows with raw message sends just because the UI is custom.\n\nUse low-level `@gear-js/api` when you need one of these cases:\n\n- direct `api.message.send` or `api.message.sendReply`\n- mailbox flows\n- voucher issue, revoke, or update flows\n- metadata parsing with `ProgramMetadata` or `getProgramMetadata`\n- full-state reads without a generated Sails client\n- dynamic multi-program or multi-IDL integrations\n\n### Connection\n\nFor direct API work, the docs note that `VaraApi` and `VaraTestnetApi` are preferable to the generic `GearApi` on Vara networks because runtime versions and signatures may differ. Keep that recommendation in mind when you are building non-hook or non-provider utilities.\n\n### Metadata\n\nUse `getProgramMetadata` with metadata hex when you need `ProgramMetadata`. The old `gear-meta` CLI is deprecated in the official docs.\n\n### Raw send\n\n```ts\nconst gas = await api.program.calculateGas.handle(\n account.address,\n programId,\n payload,\n 0,\n false,\n metadata,\n);\n\nconst extrinsic = api.message.send(\n {\n destination: programId,\n payload,\n gasLimit: gas.min_limit,\n value: 0,\n },\n metadata,\n);\n\nawait extrinsic.signAndSend(account, ({ events }) => {\n console.log(events);\n});\n```\n\nRules:\n\n- calculate gas before sending in production-like flows\n- prefer Sails command builders when the program is a Sails app\n- use low-level message send only when you need a true escape hatch\n\n### Gas Calculation Methods\n\n| Method | Use When |\n|--------|----------|\n| `api.program.calculateGas.handle(source, destination, payload, value, allowOtherPanics, metadata)` | Sending a message to an existing program |\n| `api.program.calculateGas.reply(source, messageId, payload, value, allowOtherPanics, metadata)` | Replying to a message in the user's mailbox |\n| `api.program.calculateGas.initUpload(source, code, payload, value, allowOtherPanics, metadata)` | Uploading code and initializing in one step |\n| `api.program.calculateGas.initCreate(source, codeId, payload, value, allowOtherPanics, metadata)` | Creating a program from already-uploaded code |\n\nAll methods return `{ min_limit, reserved, burned }`. Use `min_limit` as the gas limit for the extrinsic. Set `allowOtherPanics` to `false` for strict error checking or `true` to tolerate panics in downstream programs.\n\n### Raw state reads\n\n```ts\nconst state = await api.programState.read({ programId }, metadata);\n```\n\nOr:\n\n```ts\nconst state = await api.programState.readUsingWasm(\n {\n programId,\n fn_name: 'state_function',\n stateWasm,\n argument: { input: 'payload' },\n },\n stateMetadata,\n);\n```\n\n## Vouchers, Gasless, And Signless\n\nVoucher-aware flows exist at both levels:\n\n- hooks and Sails transaction builders accept `voucherId`\n- low-level Gear-JS exposes `api.voucher.issue`, `api.voucher.call`, `api.voucher.revoke`, and `api.voucher.update`\n\nFor product-level gasless and signless UX, the current docs point to the EZ-transactions package and its providers and hooks. Treat this as an explicit product decision rather than a hidden default.\n\nFor the canonical voucher lifecycle, builder recipes, signless sessions, and failure modes, see `references/voucher-and-signless-flows.md`.\n\n## Mailbox And Replies\n\nIf the frontend needs user mailbox handling rather than direct Sails commands:\n\n- use low-level mailbox APIs and extrinsics\n- remember that user-facing messages can land in the mailbox\n- `sendReply` and `claimValue` are separate flows from a normal command send\n\nThis is an advanced path. Name it explicitly in the plan instead of blending it into ordinary Sails UI work.\n\n## Environment Contract\n\nKeep the environment contract explicit and validated at startup.\n\nTypical frontend env keys include:\n- node endpoint, for example `VITE_NODE_ADDRESS`\n- one or more deployed program IDs\n- optional auxiliary contract IDs such as VFT program IDs\n\nRules:\n- validate required env values before rendering feature screens\n- fail visibly when endpoint or required program IDs are missing\n- document the intended network and deployment assumptions explicitly\n\n## Default Review Checklist\n\nBefore closing frontend work, verify:\n\n- every UI action is mapped to a Sails command, query, or event\n- the provider tree is present once at app root\n- the generated client matches the latest `.idl`\n- package versions were resolved from the target repo and current peer dependency expectations, not memory\n- wallet connect UI is visible, usable, and styled correctly\n- missing wallet/account states are rendered explicitly\n- one read path works in the UI\n- one write path shows disabled, pending, and success or error states\n- transaction flows await decoded replies where applicable\n- affected queries refetch after successful mutations\n- `watch` subscriptions are intentional\n- gas and fee assumptions are explicit\n- voucher or gasless usage is explicit\n- required env vars are validated\n- no secrets are embedded in the frontend\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":22717,"content_sha256":"0bb587f1d560755c6b4105d7e4b1ae15a83c90a7ce64e22aede453349b2324c0"},{"filename":"references/sails-gtest-and-local-validation.md","content":"# Sails Gtest And Local Validation\n\n## Canonical `gtest` Harness\n\n- Prefer the Sails-first path built around `GtestEnv` and generated clients.\n- A typical harness creates one `System`, funds the sender, submits the Wasm, builds `GtestEnv`, and deploys the program through the generated constructor path.\n- Use generated client methods for normal command and query assertions.\n\n## Raw `gtest` Escape Hatch\n\n- When you must debug below the generated client layer, remember the raw flow:\n - `Program::send_bytes*` returns a `MessageId`\n - `System::run_next_block()` returns a `BlockRunResult`\n - `BlockRunResult.log()` contains the reply log entries for that block\n - `BlockRunResult.failed` is the fast failure set for sent messages\n- Use `run_to_block` when delayed sends or multi-block progress matter more than a single next-block reply.\n\n## `BlockRunMode` Semantics\n\n- `Auto`: run enough blocks to extract tracked replies automatically.\n- `Next`: advance one block per reply extraction attempt; useful when timing matters.\n- `Manual`: no automatic block running, so the test must call block-advance methods explicitly.\n\nPick `Next` or `Manual` for timing-sensitive flows, delayed work, reply absence, redirects, or wait/reservation behavior.\n\n## What To Assert\n\n- Happy-path command and query behavior\n- Typed events through listener flow where the service exposes events\n- Reply and timeout behavior for async paths\n- Gas, value, or reservation-sensitive results when accounting matters\n- Real route or codec bugs with a low-level byte path only when generated clients are insufficient\n\n## Event Listener Pattern\n\n- If the service declares `#[service(events = ...)]`, prefer asserting events through the generated listener API.\n- In Sails 1.0, the service client implements `Listener` directly, without creating a separate listener object.\n- A common flow is:\n - obtain the service client\n - start listening before the command that should emit the event: `client.listen().await`\n - call the command\n - read the next event from the stream and assert its typed payload\n- Event assertions should be tied to the block that successfully executed the producing command.\n\n## Sails-Specific Edge Cases\n\n- `ReplyIsMissing` often means the wrong `BlockRunMode` or missing block advancement.\n- `UserspacePanic` can appear after an async path that already waited or sent internally.\n- A timeout result does not prove a later reply hook never runs.\n- Event assertions should happen after the producing block and only expect events on successful execution.\n- If you drop to raw `gtest`, inspect the matching `MessageId` in the `BlockRunResult` instead of expecting a direct typed return value.\n\n## Local Smoke — Primary Path (vara-wallet)\n\nThe fastest local smoke path uses `vara-wallet` for deployment and interaction. No Rust tooling needed beyond building the `.opt.wasm`.\n\n1. Start or reuse a local node on the default port (ws://localhost:9944).\n2. Set the local endpoint: `vara-wallet --network local` (per-command), `vara-wallet config set network local` (persists), or `export VARA_WS=ws://localhost:9944` (session). The default is mainnet — always override for local work.\n3. Import a funded dev account: `vara-wallet --network local wallet import --seed '//Alice' --name alice`.\n4. Deploy the `.opt.wasm` artifact with IDL-based constructor encoding:\n ```bash\n UPLOAD=$(vara-wallet --account alice program upload ./target/wasm32-unknown-unknown/release/my_program.opt.wasm --idl ./my_program.idl --args '[]')\n PROGRAM_ID=$(echo $UPLOAD | jq -r .programId)\n ```\n If the constructor does non-trivial work, add `--gas-limit \u003cn>`. Use `--init \u003cname>` when the IDL has multiple constructors.\n - **Post-deploy verification:** If the service exposes a query, call it immediately after deploy to verify the constructor ran. An uninitialized program returns default or empty state, or panics.\n5. If the program uses delayed messages, transfer VARA to the program address: `vara-wallet --account alice transfer $PROGRAM_ID 100`.\n6. Exercise one command and one query:\n ```bash\n vara-wallet --account alice call $PROGRAM_ID MyService/DoSomething --args '[\"hello\"]' --idl ./my_program.idl\n vara-wallet call $PROGRAM_ID MyService/GetState --args '[]' --idl ./my_program.idl\n ```\n7. Use local dev accounts such as `//Alice` and `//Bob` only for local smoke, and keep seed phrases or private keys out of committed docs.\n\n## Local Smoke — Secondary Path (Rust gclient)\n\nUse this path when the project already has a Rust test harness built on `gclient` and `GclientEnv`. For new projects, prefer the vara-wallet path above.\n\n1. Start or reuse a local node.\n2. Connect with `GearApi`:\n\n```rust\nuse gclient::{GearApi, WSAddress};\n\n// Local node on default port (ws://127.0.0.1:9944):\nlet api = GearApi::init(WSAddress::dev()).await?;\n\n// Custom port — port goes in the second arg, NOT in the URL:\nlet api = GearApi::init(WSAddress::new(\"ws://127.0.0.1\", 9944)).await?;\n```\n\nCommon API confusions:\n\n- `GclientEnv` has no `upload_code` method. Use `GearApi` directly for code upload and program deployment, then wrap in `GclientEnv` for typed client calls.\n- `GearApi::dev_from_path()` expects a filesystem path to the node binary, not a WebSocket URL.\n- `WSAddress::new(url, port)` — the port is the second argument. Do not embed the port in the URL string.\n\n3. Wrap the API in `GclientEnv` or the equivalent generated-client environment.\n4. Upload the `.opt.wasm`, deploy with a unique salt, and record the real program id. If the constructor does non-trivial work, override gas with an explicit limit — default `calculateGas()` may be insufficient.\n5. If the program uses delayed messages, transfer VARA to the program address before exercising commands. Unlike `gtest`, on-chain programs need balance to pay for future gas.\n6. Use the generated client for one command and one query path.\n7. Use local dev accounts such as `//Alice` and `//Bob` only for local smoke, and keep seed phrases or private keys out of committed docs.\n\n## JS/TS Deploy Pitfalls\n\nWhen deploying via the Sails JS client (`@gear-js/api` or `sails-js`):\n\n- `tx.withAccount(keyringPair)` — pass the keyring pair directly. Do not wrap it: `{ signer: keyringPair }` causes \"Invalid signer interface\" errors.\n- If init fails with \"Program has been terminated,\" the constructor ran out of gas. Override with `tx.withGas(200_000_000_000n)` or higher.\n- Programs that schedule delayed messages need VARA balance. After deploy, transfer funds: `api.balance.transfer(programId, amount)`.\n- Prefer `newCtorFromCode()` from the generated Sails client, which handles code upload and init in one transaction.\n\n## Setup Ergonomics\n\n- `sails_rs::gtest::constants` such as `DEFAULT_USER_ALICE`, `DEFAULT_USERS_INITIAL_BALANCE`, and `UNITS` improve readability in ordinary happy-path tests.\n- Treat them as convenience fixtures, not as part of the behavioral contract under test.\n- Use explicit custom accounts or balances when the test depends on caller identity, funding asymmetry, or boundary conditions.\n\n## Guardrails\n\n- Do not invent the deployed program id, voucher id, or account addresses.\n- Do not treat local-node smoke as a substitute for `gtest`.\n- Keep the smoke path typed unless you are isolating a transport bug.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":7308,"content_sha256":"b1a8fcad8ba049bd1366698ee62f9c3a1396676f03c4889d0b2d5427cd25d030"},{"filename":"references/sails-header-wire-format.md","content":"# Sails Header v1 Wire Format\n\nSource of truth: [docs/sails-header-v1-spec.md](https://github.com/gear-tech/sails/blob/master/docs/sails-header-v1-spec.md).\n\n## Overview\n\nEvery Sails 1.0 message begins with a 16-byte header prepended to the SCALE-encoded payload. The header lives entirely within the Gear message payload — no runtime or consensus changes required.\n\n## Base Header Layout\n\n```\nByte offset Field Size (bytes) Description\n0–1 Magic 2 ASCII \"GM\" (0x47 0x4D)\n2 Version 1 0x01\n3 Header length 1 Must be 0x10 in v1\n4–11 Interface ID 8 64-bit ID from interface hash\n12–13 Entry ID 2 Little-endian 16-bit entry identifier\n14 Route Index 1 0x00 = infer route if unambiguous\n15 Reserved 1 Must be 0x00 in v1\n>15 Payload variable SCALE-encoded params or body\n```\n\nExtension-sized headers are reserved for future versions. A v1 decoder must reject header lengths other than `0x10`.\n\n## Identifier Derivation\n\n### Interface ID\n\nDeterministic 64-bit hash derived from the canonical interface structure. Service names never influence `interface_id`. See [docs/reflect-hash-spec.md](https://github.com/gear-tech/sails/blob/master/docs/reflect-hash-spec.md) and [docs/interface-id-spec.md](https://github.com/gear-tech/sails/blob/master/docs/interface-id-spec.md).\n\n### Entry ID\n\nCommands, queries, and events are sorted lexicographically by name within their interface. `entry_id` is assigned sequentially starting at 0. Use `@entry_id: N` in IDL to override.\n\n### Route Index\n\n- `0x00` — infer route (valid only when exactly one matching `interface_id` instance exists)\n- Non-zero — explicit route selector mapped via per-program manifest\n\n## Impact on Clients\n\nGenerated clients encode/decode the header automatically. When debugging wire payloads, the first 16 bytes are the Sails Header, not SCALE data.\n\n## Interface ID Stability\n\n- Adding or removing methods/types **changes** the interface ID\n- Renaming public methods **changes** the interface ID because the exported function route name is hashed\n- `@entry_id` overrides allow pinning specific entry points across versions, but they do not preserve the interface ID after a method rename\n- Programs hosting V1 and V2 services simultaneously can use `route_idx` to disambiguate\n\n## Validation Checklist\n\n1. Magic: first two bytes are `0x47 0x4D`\n2. Version: `0x01`\n3. Header length: must equal `0x10` for v1; reject smaller, larger, or extension-sized v1 headers\n4. Reserved byte: must be `0x00` in v1\n5. Route inference (`0x00`): resolve only if exactly one matching instance exists\n\n## Wire Examples\n\nRoute inference (`route_idx = 0`):\n```\n47 4D 01 10 18 07 F6 E5 D4 C3 B2 A1 02 00 00 00\n^magic ^ver ^hlen ^interface_id LE ^entry_id ^route ^reserved\n```\n\nExplicit route (`route_idx = 2`):\n```\n47 4D 01 10 EF CD AB 89 67 45 23 01 05 00 02 00\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3067,"content_sha256":"9061af6674461b14822e4e317fb8b8a7502ba699d3fdeea81dc4a46e08061c30"},{"filename":"references/sails-idl-client-pipeline.md","content":"# Sails IDL Client Pipeline\n\n## Source Of Truth\n\n- Treat the program `.idl` as the typed interface contract.\n- Generated Rust and TypeScript clients should follow the `.idl`, not a hand-maintained parallel contract.\n- If the program interface changed, refresh the `.idl` and generated clients before deeper debugging.\n\n## `build.rs` Modes\n\n### Template Workspace Pattern\n\n- The root program crate's `build.rs` chains wasm build with IDL generation:\n ```rust\n fn main() {\n if let Some((_, wasm_path)) = sails_rs::build_wasm() {\n sails_rs::ClientBuilder::\u003capp::Program>::from_wasm_path(wasm_path).build_idl();\n }\n }\n ```\n- For dedicated Rust client crates, the normal path is:\n - `[build-dependencies] sails-rs = { version = \"...\", features = [\"build\"] }`\n - `fn main() { sails_rs::build_client::\u003cProgram>(); }`\n- If the repo needs custom generation controls, use:\n - `sails_rs::ClientBuilder::\u003cProgram>::from_env().build_idl().generate()`\n- Use direct `sails_idl_gen::generate_idl_to_file::\u003cProgram>(...)` plus\n `ClientGenerator::from_idl_path(...).generate_to(...)` only for explicitly manual or non-standard pipelines.\n\n### Shorthand Builder\n\n- `sails_rs::build_client::\u003cProgram>()`\n\nUse this when default paths and workspace layout are conventional.\n\n### Configurable Builder\n\n- `ClientBuilder::\u003cProgram>::from_env().build_idl().generate()`\n\nUse this when the repo needs controlled output paths or additional generation settings.\n\n### Manual Generation (Non-default)\n\n- `sails_idl_gen::generate_idl_to_file::\u003cProgram>(...)`\n- `ClientGenerator::from_idl_path(...).generate_to(...)`\n\nUse this only when the repo layout or artifact wiring is genuinely non-standard.\n\n## JavaScript And TypeScript Path\n\n- Use `sails-js` and `cargo sails client-js ...` for the normal JS or TS client flow.\n- The usual output includes `lib.ts` and typed program or service classes.\n- Pair the generated client with `GearApi` for node connectivity.\n- Use `parseIdl` only when a dynamic runtime path is explicitly needed instead of pre-generated files.\n\n## Frontend IDL Export Pattern\n\nFor projects where a frontend consumes the IDL generated by the Rust build pipeline:\n\n1. The Rust build produces the `.idl` via `build.rs` or `cargo sails idl`.\n2. Copy the `.idl` to the frontend project, for example `frontend/src/assets/program.idl` or `frontend/public/program.idl`.\n3. Generate the TypeScript client: `cargo sails client-js frontend/src/assets/program.idl frontend/src/lib.ts`.\n4. Check both the `.idl` snapshot and the generated `lib.ts` into version control so the frontend can build independently of the Rust workspace.\n\nAdd a script or Makefile target to automate the copy-and-regenerate step.\n\nCross-reference: `sails-frontend` and `sails-idl-client` skills both expect this handoff to be explicit.\n\nThe IDL uses V2 syntax with Rust-like types and service blocks (see `../../references/sails-idl-v2-syntax.md`). Messages use a binary header protocol (see `../../references/sails-header-wire-format.md`).\n\n## Pipeline Debugging Checklist\n\n1. Check `build.rs` before adding an ad hoc generation command.\n2. Confirm where the repo expects `.idl` output to land.\n3. Confirm whether the crate is a program or a dedicated client crate before changing Cargo features.\n4. Regenerate the Rust or TS client from the current `.idl`.\n5. Verify tests and smoke flows use the generated client instead of hand-built payload encoders.\n6. Keep generated artifacts deterministic and avoid unstable output locations.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3513,"content_sha256":"1196de45156b4b1943a07e3405e7e436ba674d9a56602f5bfb53e252263e6967"},{"filename":"references/sails-idl-v2-syntax.md","content":"# IDL v2 Syntax Reference\n\nSource of truth: [docs/idl-v2-spec.md](https://github.com/gear-tech/sails/blob/master/docs/idl-v2-spec.md).\n\n## Annotations\n\n### Global\n\nFile-level metadata, must appear at the start of the file:\n\n- `!@sails: 0.1.0` — IDL version\n- `!@author: me` — author\n- `!@git: ...` — source code URL\n- `!@version: 0.2.0` — protocol version\n\n### Include\n\n- `!@include: \u003cpath_to_idl>` — include another IDL file\n- `!@include: git://github.com/org/repo/file.idl` — include from git URL\n- Includes are deduplicated by source ID. Circular includes are prevented.\n\n### Local\n\nAnnotations placed above the related token:\n\n- `@query` — marks read-only functions\n- `@partial` — partial service subset (requires explicit `interface_id`)\n- `@entry_id: \u003cnumber>` — override automatic positional index\n- `@indexed` — marks indexed event fields\n- `@doc: text` (shortcut: `///`) — documentation\n\n## Types\n\n### Primitives\n\n`bool`, `char`, `String`, `u8`–`u128`, `i8`–`i128`\n\n### Collections\n\n- `[T]` — dynamic-length array (slice)\n- `[T; N]` — fixed-length array\n- `(T1, T2, ..)` — tuple\n- `()` — unit type\n\n### Structs\n\n```rust\nstruct Named { f1: u64, f2: Type2 } // named fields\nstruct Tuple(Type1, Type2); // tuple-like\nstruct Unit; // unit-like\n```\n\n### Enums\n\n```rust\nenum Type {\n Var1 { f1: Type1, f2: Type2 }, // struct-like\n Var2, // unit-like\n Var3(Type3), // tuple-like\n}\n```\n\n### Generics\n\n```rust\nstruct Point\u003cT> { x: T, y: T }\nenum Result\u003cT, E> { Ok(T), Err(E) }\n```\n\n### Built-in Primitives And Common Aliases\n\nBuilt-in primitives include `actor` = `ActorId([u8; 32])`, `code` = `CodeId([u8; 32])`, `messageid` = `MessageId([u8; 32])`, `u256` = `U256([u64; 4])`, `h160` = `H160([u8; 20])`, and `h256` = `H256([u8; 32])`.\n\n`map\u003cK,V>` and `set\u003cT>` are not built in. Declare them in a `types` block before use:\n\n```\ntypes {\n alias map\u003cK,V> = [(K,V)];\n alias set\u003cT> = [T];\n}\n```\n\n## Service\n\n```\nservice \u003cident> [@0x\u003cinterface_id>] {\n extends { Base1, Base2 }\n events { ... }\n functions { ... }\n types { ... }\n}\n```\n\n### Functions\n\n```\n/// Documentation\n[@query]\n[@entry_id: N]\nFuncName(param1: Type1, param2: Type2) [-> ReturnType] [throws ErrorType];\n```\n\n- `@query` — read-only, no state mutation\n- `@entry_id: N` — override automatic positional index (starts from 0)\n- `throws \u003cType>` — explicit error type\n\n### Events\n\nModeled as enum variants inside `events { ... }`:\n\n```\nevents {\n StatusChanged(Point\u003cu32>),\n Jubilee { amount: u64, bits: bitvec },\n Simple,\n}\n```\n\n- `@entry_id: N` overrides automatic event index\n\n### Partial Services\n\nFor generating a client targeting specific methods of a large contract:\n\n```\n@partial\nservice PartialService@0x1234567890abcdef {\n events {\n @entry_id: 2\n SomethingHappened(String);\n }\n functions {\n @entry_id: 5\n SomeMethod() -> bool;\n }\n}\n```\n\nRequires explicit `interface_id` and `entry_id` on each member.\n\n## Program\n\n```\nprogram \u003cident> {\n constructors {\n Create();\n WithOwner(owner: actor) throws ZeroOwnerError;\n }\n services {\n Canvas: Canvas;\n DemoCanvas: Canvas;\n }\n types { ... }\n}\n```\n\n## Full Example\n\n```\n!@sails: 0.1.0\n!@include: ownable.idl\n\n/// Canvas service\nservice Canvas {\n extends { Ownable, Pausable }\n\n events {\n StatusChanged(Point\u003cu32>),\n Jubilee { @indexed amount: u64, bits: bitvec },\n }\n\n functions {\n ColorPoint(point: Point\u003cu32>, color: Color) throws ColorError;\n KillPoint(point: Point\u003cu32>) -> bool throws String;\n @query\n Points(offset: u32, len: u32) -> map\u003cPoint\u003cu32>, PointStatus> throws String;\n @query\n PointStatus(point: Point\u003cu32>) -> Option\u003cPointStatus>;\n }\n\n types {\n alias map\u003cK,V> = [(K,V)];\n alias set\u003cT> = [T];\n struct Color { color: [u8; 4], space: ColorSpace }\n enum ColorError { InvalidSource, DeadPoint }\n struct Point\u003cT> { x: T, y: T }\n }\n}\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4111,"content_sha256":"8838a6e84fd67c79c98cd06dc5cccaa3b5b24d5e9399774b2643f472b5d48842"},{"filename":"references/sails-indexer-patterns.md","content":"# Sails Indexer Patterns\n\n> **Usage note**: This is a comprehensive reference handbook, not a document to load in full on every task. The `sails-indexer` skill cross-references specific sections by name for each workflow step. Consult the relevant section for the step at hand rather than reading top to bottom.\n\n## Decide Whether The Project Needs An Indexer First\n\nAdd an indexer only when the project needs a persistent read model that direct chain queries cannot provide cheaply or cleanly. Typical signals are historical views, activity feeds, pagination, filtering, joins across multiple programs, rolling aggregates, charts, rankings, or a read API that must stay fast even when the chain or RPC is slow.\n\nStay with direct chain queries when the frontend only needs a few live reads from a small number of programs and there is no requirement for replayable history, searchable lists, or derived windows. Do not build an indexer just because the project has events. Build one when projected read models are part of the product surface.\n\nBefore writing processor, handler, or schema code, make the decision explicit in plain project notes: either direct chain reads are enough, or a projected read model is required because the product needs durable history, filtering, joins, aggregates, or a fast dedicated read API.\n\nUse this reference when implementing a read-side indexer for a standard Gear/Vara Sails project.\n\nTreat the indexer as a projection pipeline:\n\n`archive or chain -> typed Gear event boundary -> Sails IDL decode -> project -> enrich -> persist -> expose thin GraphQL API`\n\nThis reference assumes GraphQL is the default read surface. Do not treat REST and GraphQL as interchangeable defaults. If another read surface is added for a specific integration, GraphQL should still remain the primary contract for frontend and developer inspection.\n\nThe most important rule is to preserve a clean boundary between:\n\n- what happened on chain\n- how it is decoded off chain\n- what gets persisted as the read model\n- how that read model is exposed to consumers\n\nThis reference uses a single canonical layout and flow. The names below are generic, but the structure is intentionally close to a real working implementation.\n\n---\n\n## Canonical Runtime Flow\n\n### 1. Start with configuration, not hardcoded endpoints\n\nKeep archive, RPC, rate limits, block range, and root program IDs in one place.\n\n```ts\n// src/config.ts\nimport { HexString } from \"@gear-js/api\";\nimport dotenv from \"dotenv\";\n\ndotenv.config();\n\nconst getEnv = (key: string, fallback?: string): string => {\n const value = process.env[key] || fallback;\n if (!value) {\n throw new Error(`Environment variable ${key} is not set`);\n }\n return value;\n};\n\nexport const config = {\n archiveUrl: getEnv(\n \"VARA_ARCHIVE_URL\",\n \"https://v2.archive.subsquid.io/network/vara-testnet\"\n ),\n rpcUrl: getEnv(\"VARA_RPC_URL\"),\n rateLimit: Number(getEnv(\"VARA_RPC_RATE_LIMIT\", \"20\")),\n fromBlock: Number(getEnv(\"VARA_FROM_BLOCK\", \"0\")),\n registryProgramId: getEnv(\"REGISTRY_PROGRAM_ID\") as HexString,\n};\n```\n\n`rpcUrl` should point to a Vara archive node endpoint. Subsquid performs historical queries during indexing and a live non-archive node is not sufficient. Keep the Subsquid gateway URL (`archiveUrl`) and the Vara archive RPC URL (`rpcUrl`) as separate config entries — they serve different ingestion roles but both require archive-capable backing.\n\nWhy this shape matters:\n\n- the processor must be restartable from a known `fromBlock`\n- program IDs must not be scattered through handlers\n- RPC and archive concerns should stay outside domain projection code\n\n---\n\n### 2. Build one shared batch processor and export its derived types\n\nEvery other file should consume the exported processor-derived types instead of redefining Subsquid payloads ad hoc.\n\n```ts\n// src/processor.ts\nimport {\n BlockHeader as _BlockHeader,\n DataHandlerContext,\n SubstrateBatchProcessor,\n SubstrateBatchProcessorFields,\n Event as _Event,\n Call as _Call,\n Extrinsic as _Extrinsic,\n} from \"@subsquid/substrate-processor\";\nimport { Store } from \"@subsquid/typeorm-store\";\nimport { hostname } from \"node:os\";\n\nimport { config } from \"./config\";\n\nexport const processor = new SubstrateBatchProcessor()\n .setGateway(config.archiveUrl)\n .setRpcEndpoint({\n url: config.rpcUrl,\n rateLimit: config.rateLimit,\n headers: {\n \"User-Agent\": hostname(),\n },\n })\n .setBlockRange({ from: config.fromBlock })\n .setFields({\n event: {\n args: true,\n extrinsic: true,\n call: true,\n },\n extrinsic: {\n hash: true,\n fee: true,\n signature: true,\n },\n call: {\n args: true,\n },\n block: {\n timestamp: true,\n },\n });\n\nexport type Fields = SubstrateBatchProcessorFields\u003ctypeof processor>;\nexport type BlockHeader = _BlockHeader\u003cFields> & { timestamp: number };\nexport type Event = _Event\u003cFields>;\nexport type Call = _Call\u003cFields>;\nexport type Extrinsic = _Extrinsic\u003cFields>;\nexport type ProcessorContext = DataHandlerContext\u003cStore, Fields>;\n```\n\nWhy this shape matters:\n\n- the entire indexer gets one consistent type boundary\n- handler code stays focused on projection work, not processor wiring\n- any future field expansion happens in one file\n\n---\n\n### 3. Create a typed boundary for Gear events before touching business payloads\n\nDo not let raw processor `Event` values leak through the whole codebase.\n\n```ts\n// src/types/gear-events.ts\nimport { Call, Event, Extrinsic } from \"../processor\";\n\nexport interface MessageQueuedExtrinsic extends Extrinsic {\n readonly hash: `0x${string}`;\n}\n\nexport interface MessageQueuedCall extends Omit\u003cCall, \"args\"> {\n readonly args: {\n readonly destination: `0x${string}`;\n readonly payload: `0x${string}`;\n readonly gasLimit: string;\n readonly value: string;\n };\n}\n\nexport type MessageQueuedEvent = Omit\u003cEvent, \"args\" | \"extrinsic\" | \"call\"> & {\n args: MessageQueuedArgs;\n extrinsic: MessageQueuedExtrinsic;\n call: MessageQueuedCall;\n};\n\nexport interface MessageQueuedArgs {\n readonly id: string;\n readonly source: string;\n readonly destination: string;\n readonly entry: \"Init\" | \"Handle\" | \"Reply\";\n}\n\nexport interface GearRunExtrinsic extends Extrinsic {\n readonly hash: `0x${string}`;\n}\n\nexport type UserMessageSentEvent = Omit\u003cEvent, \"args\" | \"extrinsic\"> & {\n args: UserMessageSentArgs;\n extrinsic: GearRunExtrinsic;\n};\n\nexport interface UserMessageSentArgs {\n readonly message: {\n readonly id: `0x${string}`;\n readonly source: `0x${string}`;\n readonly destination: `0x${string}`;\n readonly payload: `0x${string}`;\n readonly value: string;\n readonly details?: UserMessageSentDetails;\n };\n}\n\nexport interface UserMessageSentDetails {\n readonly code: {\n readonly __kind: \"Success\" | \"Error\";\n };\n readonly to: `0x${string}`;\n}\n```\n\nAdd explicit tracked-program descriptors as their own type instead of passing plain tuples around.\n\n```ts\n// src/types/tracked-program.ts\nexport interface TrackedProgramInfo {\n address: `0x${string}`;\n tags: `0x${string}`[];\n}\n```\n\nWhy this shape matters:\n\n- typed Gear boundary and typed projection boundary are different concerns\n- event filtering becomes safer and easier to review\n- generic projection code can depend on `TrackedProgramInfo` instead of domain-specific ad hoc objects\n\n---\n\n### 4. Keep chain event guards in small helpers\n\nThe handlers should not repeat event-name checks everywhere.\n\n```ts\n// src/helpers/is.ts\nimport { MessageQueuedEvent, UserMessageSentEvent } from \"../types\";\nimport { Event } from \"../processor\";\n\nexport function isUserMessageSentEvent(\n event: Event\n): event is UserMessageSentEvent {\n return event.name === \"Gear.UserMessageSent\";\n}\n\nexport function isMessageQueuedEvent(\n event: Event\n): event is MessageQueuedEvent {\n return event.name === \"Gear.MessageQueued\";\n}\n\nexport function isSailsEvent(event: UserMessageSentEvent): boolean {\n return !Boolean(event.args.message.details);\n}\n```\n\nWhy this shape matters:\n\n- handler code stays readable\n- protocol-level checks are centralized\n- the event classification logic is easy to test separately\n\n---\n\n### 5. Centralize all IDL decoding behind one decoder\n\nAll `.idl` parsing, prefix detection, event decoding, query encoding, and query result decoding should live in one place.\n\n```ts\n// src/sails-decoder.ts\nimport { isHex } from \"@subsquid/util-internal-hex\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { getFnNamePrefix, getServiceNamePrefix, Sails } from \"sails-js\";\nimport { SailsIdlParser } from \"sails-js-parser\";\nimport { MessageQueuedEvent, UserMessageSentEvent } from \"./types\";\n\ninterface Message {\n service: string;\n method: string;\n}\n\ninterface InputMessage\u003cT> extends Message {\n params: T;\n}\n\ninterface OutputMessage\u003cT> extends Message {\n payload: T;\n}\n\nexport class SailsDecoder {\n constructor(private readonly program: Sails) {}\n\n static async new(idlPath: string): Promise\u003cSailsDecoder> {\n if (!existsSync(idlPath)) {\n throw new Error(`File ${idlPath} does not exist`);\n }\n\n const parser = await SailsIdlParser.new();\n const sails = new Sails(parser);\n sails.parseIdl(readFileSync(idlPath, \"utf8\"));\n\n return new SailsDecoder(sails);\n }\n\n service(data: string): string {\n if (!isHex(data)) {\n throw new Error(`Invalid hex string: ${data}`);\n }\n return getServiceNamePrefix(data as `0x${string}`);\n }\n\n method(data: string): string {\n if (!isHex(data)) {\n throw new Error(`Invalid hex string: ${data}`);\n }\n return getFnNamePrefix(data as `0x${string}`);\n }\n\n decodeInput\u003cT>({\n call: {\n args: { payload },\n },\n }: MessageQueuedEvent): InputMessage\u003cT> {\n const service = this.service(payload);\n const method = this.method(payload);\n const params =\n this.program.services[service].functions[method].decodePayload\u003cT>(\n payload\n );\n\n return { service, method, params };\n }\n\n decodeOutput\u003cT>({\n args: {\n message: { payload },\n },\n }: UserMessageSentEvent): OutputMessage\u003cT> {\n const service = this.service(payload);\n const method = this.method(payload);\n const result =\n this.program.services[service].functions[method].decodeResult\u003cT>(payload);\n\n return { service, method, payload: result };\n }\n\n decodeEvent\u003cT>({\n args: {\n message: { payload },\n },\n }: UserMessageSentEvent): OutputMessage\u003cT> {\n const service = this.service(payload);\n const method = this.method(payload);\n const result = this.program.services[service].events[method]?.decode(payload);\n\n return { service, method, payload: result };\n }\n\n encodeQueryInput(service: string, fn: string, data: any[]): `0x${string}` {\n return this.program.services[service].queries[fn].encodePayload(...data);\n }\n\n decodeQueryOutput\u003cT>(service: string, fn: string, data: string): T {\n return this.program.services[service].queries[fn].decodeResult\u003cT>(\n data as `0x${string}`\n );\n }\n}\n```\n\nWhy this shape matters:\n\n- handlers never need to know payload prefix details\n- IDL-driven decode stays consistent across events, replies, and queries\n- query enrichment uses the same protocol boundary as event projection\n\n---\n\n### 6. Wire the runtime in `main.ts`, but do not place projection logic there\n\nThe entrypoint should only compose the processor, API, decoders, and handlers.\n\n```ts\n// src/main.ts\nimport { TypeormDatabase } from \"@subsquid/typeorm-store\";\nimport { GearApi } from \"@gear-js/api\";\n\nimport { BaseHandler } from \"./handlers/base\";\nimport { RegistryHandler, ProgramsHandler } from \"./handlers\";\nimport { config } from \"./config\";\nimport { processor } from \"./processor\";\n\nexport class GearProcessor {\n private readonly handlers: BaseHandler[] = [];\n\n public addUserMessageSent(programIds: string[]) {\n for (const id of programIds) {\n console.log(`[*] Adding UserMessageSent events for program ${id}`);\n }\n\n processor.addGearUserMessageSent({\n programId: undefined,\n extrinsic: true,\n call: true,\n });\n }\n\n public registerHandler(handler: BaseHandler) {\n this.handlers.push(handler);\n\n const programIds = handler.getUserMessageSentProgramIds();\n if (programIds.length > 0) {\n this.addUserMessageSent(programIds);\n }\n }\n\n public async run() {\n const db = new TypeormDatabase({\n supportHotBlocks: true,\n stateSchema: \"gear_processor\",\n });\n\n processor.run(db, async (ctx) => {\n for (const handler of this.handlers) {\n await handler.process(ctx);\n }\n\n for (const handler of this.handlers) {\n await handler.save();\n }\n });\n }\n}\n\nasync function main() {\n const api = await GearApi.create({ providerAddress: config.rpcUrl });\n const app = new GearProcessor();\n\n const programsHandler = new ProgramsHandler();\n const registryHandler = new RegistryHandler(\n config.registryProgramId,\n programsHandler\n );\n\n await registryHandler.init(api);\n await programsHandler.init(api);\n\n app.registerHandler(registryHandler);\n app.registerHandler(programsHandler);\n\n await app.run();\n}\n\nmain().catch((error) => {\n console.error(error);\n process.exit(1);\n});\n```\n\nWhy this shape matters:\n\n- startup order is explicit\n- handler lifecycle is deterministic\n- runtime composition stays separate from domain projection logic\n\n---\n\n### 7. Give every handler the same lifecycle contract\n\nThe clean pattern is:\n\n- `init()` for one-time startup work\n- `process(ctx)` for current batch work\n- `save()` for persistence\n- `clear()` for resetting batch-local state\n\n```ts\n// src/handlers/base.ts\nimport { Logger } from \"@subsquid/logger\";\nimport { GearApi } from \"@gear-js/api\";\nimport { ProcessorContext } from \"../processor\";\n\nexport abstract class BaseHandler {\n protected events: string[] = [];\n protected userMessageSentProgramIds: string[] = [];\n protected messageQueuedProgramIds: string[] = [];\n protected _logger!: Logger;\n protected _ctx!: ProcessorContext;\n\n public getEvents(): string[] {\n return this.events;\n }\n\n public getUserMessageSentProgramIds(): string[] {\n return this.userMessageSentProgramIds;\n }\n\n public getMessageQueuedProgramIds(): string[] {\n return this.messageQueuedProgramIds;\n }\n\n public init(_api: GearApi): Promise\u003cvoid> {\n return Promise.resolve();\n }\n\n abstract clear(): void;\n\n async process(ctx: ProcessorContext): Promise\u003cvoid> {\n this._ctx = ctx;\n this.clear();\n }\n\n abstract save(): Promise\u003cvoid>;\n}\n```\n\nWhy this shape matters:\n\n- batch processing and persistence are separated\n- handlers can keep controlled in-memory state between `process()` and `save()`\n- the same lifecycle works for discovery handlers and projection handlers\n\n---\n\n### 8. Split dynamic discovery from domain projection\n\nIf the topology is fixed from day one, skip discovery entirely. A single known program ID, or a small fixed set of program IDs, should be loaded from configuration and projected directly. Add a discovery handler only when the product truly creates or tracks programs dynamically.\n\nIf one program creates or registers child programs, keep that logic in a dedicated handler.\n\n```ts\n// src/handlers/registry.ts\nimport { isSailsEvent, isUserMessageSentEvent } from \"../helpers\";\nimport { ProcessorContext } from \"../processor\";\nimport { SailsDecoder } from \"../sails-decoder\";\nimport { UserMessageSentEvent, TrackedProgramInfo } from \"../types\";\nimport { BaseHandler } from \"./base\";\nimport { ProgramsHandler } from \"./programs\";\n\ninterface ProgramRegisteredPayload {\n program_id: `0x${string}`;\n tags: `0x${string}`[];\n}\n\nexport class RegistryHandler extends BaseHandler {\n private decoder!: SailsDecoder;\n\n constructor(\n private readonly registryProgramId: string,\n private readonly programsHandler: ProgramsHandler\n ) {\n super();\n this.userMessageSentProgramIds = [registryProgramId];\n }\n\n public async init(): Promise\u003cvoid> {\n this.decoder = await SailsDecoder.new(\"assets/registry.idl\");\n }\n\n public clear(): void {}\n\n public async save(): Promise\u003cvoid> {\n const entitiesToSave = this.programsHandler.getPrimaryEntitiesToSave();\n if (entitiesToSave.length > 0) {\n await this._ctx.store.save(entitiesToSave);\n }\n }\n\n public async process(ctx: ProcessorContext): Promise\u003cvoid> {\n await super.process(ctx);\n\n for (const block of ctx.blocks) {\n for (const event of block.events) {\n if (\n isUserMessageSentEvent(event) &&\n event.args.message.source === this.registryProgramId\n ) {\n await this.handleUserMessageSentEvent(event);\n }\n }\n }\n }\n\n private async handleUserMessageSentEvent(event: UserMessageSentEvent) {\n if (!isSailsEvent(event)) return;\n\n const { service, method, payload } =\n this.decoder.decodeEvent\u003cProgramRegisteredPayload>(event);\n\n if (service === \"Registry\" && method === \"ProgramRegistered\") {\n const info: TrackedProgramInfo = {\n address: payload.program_id,\n tags: payload.tags,\n };\n\n this.programsHandler.registerProgram(info);\n }\n }\n}\n```\n\nWhy this shape matters:\n\n- discovery and projection do not get tangled together\n- dynamic program creation works after restarts and replays\n- the domain handler can focus on projecting activity from tracked programs\n\n---\n\n### 9. Keep explicit runtime state per tracked program\n\nA projection handler usually needs memory for:\n\n- tracked program metadata\n- the current primary entity\n- pending activity records for this batch\n- pending snapshot records for this batch\n- dirty flags\n- last periodic-update timestamp\n- active or inactive status\n\n```ts\n// src/handlers/programs.ts\nimport { GearApi, HexString } from \"@gear-js/api\";\nimport { MoreThanOrEqual } from \"typeorm\";\n\nimport { BaseHandler } from \"./base\";\nimport { ProcessorContext } from \"../processor\";\nimport { isSailsEvent, isUserMessageSentEvent } from \"../helpers\";\nimport { SailsDecoder } from \"../sails-decoder\";\nimport { TrackedProgramInfo, UserMessageSentEvent } from \"../types\";\nimport {\n TrackedProgram,\n ActivityRecord,\n MetricSnapshot,\n Resource,\n ResourcePriceSnapshot,\n} from \"../model\";\n\ninterface TrackedProgramState {\n info: TrackedProgramInfo;\n entity: TrackedProgram | null;\n activity: Map\u003cstring, ActivityRecord>;\n snapshots: Map\u003cstring, MetricSnapshot>;\n isEntityUpdated: boolean;\n isSnapshotsUpdated: boolean;\n lastPeriodicUpdate: Date | null;\n isActive: boolean;\n}\n\nexport class ProgramsHandler extends BaseHandler {\n private decoder!: SailsDecoder;\n private api!: GearApi;\n private existingProgramsLoaded = false;\n private programs = new Map\u003cstring, TrackedProgramState>();\n private resources = new Map\u003cstring, Resource>();\n private prices = new Map\u003cstring, number>();\n private resourceIdsToSave = new Set\u003cstring>();\n private priceSnapshots = new Map\u003cstring, ResourcePriceSnapshot>();\n\n public async init(api: GearApi): Promise\u003cvoid> {\n this.api = api;\n this.decoder = await SailsDecoder.new(\"assets/program.idl\");\n }\n\n public registerProgram(info: TrackedProgramInfo): void {\n this.getOrCreateState(info);\n }\n\n public getPrimaryEntitiesToSave(): TrackedProgram[] {\n const out: TrackedProgram[] = [];\n\n for (const state of this.programs.values()) {\n if (state.isEntityUpdated && state.entity) {\n if (!state.isActive) {\n state.entity.isActive = false;\n }\n out.push(state.entity);\n }\n }\n\n return out;\n }\n\n public clear(): void {\n this.priceSnapshots.clear();\n this.resourceIdsToSave.clear();\n\n for (const state of this.programs.values()) {\n state.activity.clear();\n state.isEntityUpdated = false;\n state.isSnapshotsUpdated = false;\n\n if (!state.isActive) {\n this.programs.delete(state.info.address);\n }\n }\n }\n\n private getOrCreateState(info: TrackedProgramInfo): TrackedProgramState {\n const existing = this.programs.get(info.address);\n if (existing) return existing;\n\n const state: TrackedProgramState = {\n info,\n entity: null,\n activity: new Map(),\n snapshots: new Map(),\n isEntityUpdated: false,\n isSnapshotsUpdated: false,\n lastPeriodicUpdate: null,\n isActive: true,\n };\n\n this.programs.set(info.address, state);\n return state;\n }\n}\n```\n\nWhy this shape matters:\n\n- one tracked program can accumulate multiple writes inside one batch\n- dirty flags prevent unnecessary writes\n- restart-safe rehydration becomes straightforward\n\n---\n\n### 10. Rehydrate tracked programs from the database on first processing run\n\nThe projection handler must not depend on a perfect never-failing runtime. After a restart, it should rebuild in-memory tracking from persisted primary entities.\n\n```ts\npublic async loadExistingPrograms(ctx: ProcessorContext): Promise\u003cvoid> {\n const existing = await ctx.store.find(TrackedProgram);\n\n for (const entity of existing) {\n const info: TrackedProgramInfo = {\n address: entity.id as HexString,\n tags: entity.tags as HexString[],\n };\n\n const state = this.getOrCreateState(info);\n state.entity = entity;\n state.isActive = entity.isActive;\n }\n}\n```\n\nUse it at the top of `process()`.\n\n```ts\npublic async process(ctx: ProcessorContext): Promise\u003cvoid> {\n await super.process(ctx);\n\n if (!this.existingProgramsLoaded) {\n await this.loadExistingPrograms(ctx);\n this.existingProgramsLoaded = true;\n }\n\n if (!this.programs.size) {\n return;\n }\n\n // continue with current batch processing\n}\n```\n\nWhy this shape matters:\n\n- the indexer can restart without losing tracked program memory\n- discovery is no longer a one-time event that must remain in RAM forever\n- live processing and replay use the same code path\n\n---\n\n### 11. Initialize missing primary entities from on-chain queries\n\nApply this pattern only when the discovery event does not carry enough information to construct the primary entity — for example, when the event contains only a program address but the entity also requires fields like `title`, `owner`, or `status` that must come from a direct chain read. If the event payload is sufficient, build the entity from it directly and skip the on-chain query.\n\nIf the tracked program exists in memory but the primary projected entity is missing, initialize it from chain.\n\n```ts\nprivate async ensurePrimaryEntity(state: TrackedProgramState): Promise\u003cvoid> {\n if (state.entity || !state.isActive) {\n return;\n }\n\n try {\n const metadata = await this.queryProgramMetadata(state.info.address);\n\n state.entity = new TrackedProgram({\n id: state.info.address,\n title: metadata.title,\n owner: metadata.owner,\n status: metadata.status,\n tags: state.info.tags,\n totalCount: BigInt(metadata.totalCount),\n volume: 0,\n isActive: true,\n createdAt: new Date(),\n updatedAt: new Date(),\n });\n\n state.isEntityUpdated = true;\n } catch {\n state.isActive = false;\n state.isEntityUpdated = true;\n }\n}\n```\n\nThis is the right pattern when events tell you that a program exists, but some initial fields should come from a query.\n\nWhy this shape matters:\n\n- discovery and entity bootstrap are separated\n- chain state can confirm the canonical initial projection\n- failure can deactivate a tracked program cleanly\n\n---\n\n### 12. Initialize supporting entities lazily and cache them in memory\n\nIf the projection depends on related on-chain resources, load them from the database first and only then fall back to live queries.\n\n```ts\nprivate async ensureResource(resourceId: string): Promise\u003cvoid> {\n let resource = this.resources.get(resourceId);\n\n if (!resource) {\n resource = await this._ctx.store.findOneBy(Resource, { id: resourceId });\n }\n\n if (!resource) {\n resource = await this.createResourceFromContract(resourceId);\n }\n\n if (!resource) {\n throw new Error(`Failed to load resource ${resourceId}`);\n }\n\n this.resources.set(resource.id, resource);\n this.resourceIdsToSave.add(resource.id);\n}\n```\n\nWhy this shape matters:\n\n- repeat queries are avoided during one run\n- restart rehydration can reuse persisted rows\n- enrichment logic remains explicit and testable\n\n---\n\n### 13. Process a batch in a stable order\n\nThe canonical order is:\n\n1. clear batch-local memory\n2. rehydrate persisted tracked programs if needed\n3. bootstrap missing primary entities\n4. bootstrap supporting entities\n5. initialize latest snapshots\n6. perform periodic updates if needed\n7. iterate current block events\n8. persist batch results\n\nA concrete `process()` body can look like this:\n\n```ts\npublic async process(ctx: ProcessorContext): Promise\u003cvoid> {\n await super.process(ctx);\n\n if (!this.existingProgramsLoaded) {\n await this.loadExistingPrograms(ctx);\n this.existingProgramsLoaded = true;\n }\n\n if (!this.programs.size) {\n return;\n }\n\n const firstBlockTimestamp = new Date(ctx.blocks[0].header.timestamp);\n\n for (const state of this.programs.values()) {\n await this.ensurePrimaryEntity(state);\n await this.initResources(state);\n await this.initSnapshots(state, firstBlockTimestamp);\n }\n\n for (const block of ctx.blocks) {\n const blockTimestamp = new Date(block.header.timestamp);\n const blockNumber = BigInt(block.header.height);\n\n for (const state of this.programs.values()) {\n if (this.shouldPerformPeriodicUpdates(state, blockTimestamp)) {\n await this.performPeriodicUpdates(state, blockTimestamp, blockNumber);\n }\n }\n\n for (const event of block.events) {\n if (!isUserMessageSentEvent(event)) continue;\n\n const state = this.programs.get(event.args.message.source);\n if (!state || !state.isActive) continue;\n\n await this.handleUserMessageSentEvent(state, event, {\n blockNumber,\n blockTimestamp,\n });\n }\n }\n\n for (const state of this.programs.values()) {\n if (!state.entity || !state.isActive) continue;\n\n await this.updateRollingMetrics(state, firstBlockTimestamp);\n this.updateCurrentSummary(state);\n state.isEntityUpdated = true;\n }\n}\n```\n\nWhy this shape matters:\n\n- the code path is deterministic and reviewable\n- periodic work and event-driven work are both supported\n- all writes for one batch are staged before `save()`\n\n---\n\n### 14. Decode only after confirming that the message is a Sails event or message\n\nThe handler should first reject non-Sails messages, then decode once, then dispatch by service and method.\n\nFor `UserMessageSent` events (outbound from a program), use `isSailsEvent` to guard before decoding:\n\n```ts\nprivate async handleUserMessageSentEvent(\n state: TrackedProgramState,\n event: UserMessageSentEvent,\n common: { blockNumber: bigint; blockTimestamp: Date }\n) {\n if (!isSailsEvent(event)) {\n return;\n }\n\n const { service, method, payload } = this.decoder.decodeEvent(event);\n\n if (service === \"Domain\") {\n await this.handleDomainService(state, method, payload, common, event);\n }\n}\n```\n\nIf the indexer needs to track inbound messages (command inputs) or replies, use the corresponding `MessageQueued` events and a matching decoder path. The dispatch pattern is the same — guard first, decode once, route by service and method — but use `this.decoder.decodeInput` for inbound command payloads and `this.decoder.decodeOutput` for outbound reply payloads rather than `decodeEvent`.\n\nWhy this shape matters:\n\n- transport-level filtering happens before domain dispatch\n- handlers consume structured payloads, not raw SCALE hex\n- service routing is explicit and the same pattern applies to events, messages, and replies\n\n---\n\n### 15. Convert events into append-only activity records with deterministic IDs\n\nThe safe default is to use a chain-derived ID such as `message.id` when the event represents one logical action.\n\n```ts\nprivate createActivityRecord(\n state: TrackedProgramState,\n event: UserMessageSentEvent,\n common: { blockNumber: bigint; blockTimestamp: Date },\n type: string,\n additionalFields: Partial\u003cActivityRecord> = {}\n): ActivityRecord {\n if (!state.entity) {\n throw new Error(\"Primary entity is not initialized\");\n }\n\n return new ActivityRecord({\n id: event.args.message.id,\n type,\n blockNumber: common.blockNumber,\n timestamp: common.blockTimestamp,\n program: { id: state.entity.id } as TrackedProgram,\n ...additionalFields,\n });\n}\n```\n\nWhy this shape matters:\n\n- replay naturally overwrites or skips the same record instead of duplicating it\n- activity entities remain append-only and easy to inspect\n- the handler can compute aggregates from an explicit transaction log\n\n---\n\n### 16. Keep per-method projection logic small and explicit\n\nThe common pattern is: decode payload -> create activity record -> enrich -> update summary -> mark dirty.\n\n```ts\ninterface ActionStartedPayload {\n user_id: string;\n resource_id: string;\n amount: string;\n}\n\ninterface ActionCompletedPayload {\n user_id: string;\n output_id: string;\n total: string;\n}\n\nprivate async handleDomainService(\n state: TrackedProgramState,\n method: string,\n payload: unknown,\n common: { blockNumber: bigint; blockTimestamp: Date },\n event: UserMessageSentEvent\n) {\n switch (method) {\n case \"ActionStarted\": {\n const p = payload as ActionStartedPayload;\n const activity = this.createActivityRecord(\n state, event, common, \"ACTION_STARTED\",\n { user: p.user_id, resourceId: p.resource_id, amount: BigInt(p.amount) }\n );\n await this.processActivity(state, activity, common);\n break;\n }\n\n // Additional cases follow the same shape:\n // cast payload, create activity record, process it.\n\n default:\n this._ctx.log.debug({ method }, \"Unhandled domain event\");\n }\n}\n```\n\nWhy this shape matters:\n\n- every indexed event has one obvious projection path\n- review does not require reading a huge polymorphic event processor\n- adding a new event remains localized\n\n---\n\n### 17. Separate activity projection from current-state refresh\n\nWhen an event fires, it is often correct to:\n\n- create an append-only activity record from the event payload\n- refresh current state from an on-chain query\n\nThat gives one record of what happened and one canonical current summary.\n\n```ts\nprivate async processActivity(\n state: TrackedProgramState,\n activity: ActivityRecord,\n common: { blockNumber: bigint; blockTimestamp: Date }\n): Promise\u003cvoid> {\n await this.calculateDisplayFields(activity);\n await this.refreshCurrentState(state, common);\n this.updateMetricSnapshots(state, activity);\n\n state.activity.set(activity.id, activity);\n state.isEntityUpdated = true;\n state.isSnapshotsUpdated = true;\n}\n```\n\nA state refresh should update the current primary entity, not the append-only record.\n\n```ts\nprivate async refreshCurrentState(\n state: TrackedProgramState,\n common: { blockTimestamp: Date }\n): Promise\u003cvoid> {\n if (!state.entity) return;\n\n try {\n const current = await this.queryProgramState(state.info.address);\n\n state.entity.status = current.status;\n state.entity.totalCount = BigInt(current.totalCount);\n state.entity.owner = current.owner;\n } catch (error) {\n this._ctx.log.error(\n { error, programId: state.info.address },\n \"Failed to refresh state; keeping previous values\"\n );\n }\n\n state.entity.updatedAt = common.blockTimestamp;\n}\n```\n\nWhy this shape matters:\n\n- event payloads do not have to carry the full canonical state\n- the read model can preserve both activity history and current summary\n- on-chain queries remain explicit rather than hidden inside random helpers\n\n---\n\n### 18. Support periodic derived updates, not only event-driven updates\n\nThe reference pattern includes periodic updates for snapshots and derived metrics.\n\n```ts\nprivate shouldPerformPeriodicUpdates(\n state: TrackedProgramState,\n currentTime: Date\n): boolean {\n const oneHourMs = 60 * 60 * 1000;\n return (\n !state.lastPeriodicUpdate ||\n currentTime.getTime() - state.lastPeriodicUpdate.getTime() >= oneHourMs\n );\n}\n\nprivate async performPeriodicUpdates(\n state: TrackedProgramState,\n timestamp: Date,\n blockNumber: bigint\n): Promise\u003cvoid> {\n await this.updateResourcePrices(state, timestamp, blockNumber);\n\n if (!state.entity) {\n return;\n }\n\n const snapshot = MetricSnapshot.createEmptyHourly(\n state.entity.id,\n timestamp\n );\n\n state.snapshots.set(snapshot.id, snapshot);\n state.lastPeriodicUpdate = timestamp;\n state.isSnapshotsUpdated = true;\n state.isEntityUpdated = true;\n}\n```\n\nWhy this shape matters:\n\n- charts and rolling metrics keep moving even if the program is idle\n- periodic projections are modeled explicitly instead of being hidden in API queries\n- the same batch pipeline can handle events and time-based snapshots\n\n---\n\n### 19. Build rolling metrics from persisted and pending snapshots together\n\nWhen computing current windows, include both database rows and not-yet-saved in-memory snapshots.\n\n```ts\nprivate async calculateCurrentMetrics(\n state: TrackedProgramState,\n currentTime: Date\n): Promise\u003c{\n metric1h: number;\n metric24h: number;\n metric7d: number;\n}> {\n if (!state.entity) {\n return {\n metric1h: 0,\n metric24h: 0,\n metric7d: 0,\n };\n }\n\n const oneYearAgo = new Date(currentTime.getTime() - 365 * 24 * 60 * 60 * 1000);\n\n const dbSnapshots = await this._ctx.store.find(MetricSnapshot, {\n where: {\n program: { id: state.entity.id },\n timestamp: MoreThanOrEqual(oneYearAgo),\n },\n order: { timestamp: \"DESC\" },\n });\n\n const pendingSnapshots = Array.from(state.snapshots.values());\n const allSnapshots = [...dbSnapshots, ...pendingSnapshots];\n\n return MetricSnapshot.calculateWindows(allSnapshots, currentTime);\n}\n```\n\nWhy this shape matters:\n\n- the summary entity can be accurate before the current batch is saved\n- rolling windows work correctly during live processing\n- derived metrics do not depend on stale persisted data alone\n\n---\n\n### 20. Save in groups and only write what changed\n\nThe projection handler should persist accumulated writes in `save()`, grouped by entity type and guarded by dirty flags.\n\n```ts\npublic async save(): Promise\u003cvoid> {\n for (const state of this.programs.values()) {\n if (state.isSnapshotsUpdated) {\n const snapshots = Array.from(state.snapshots.values());\n if (snapshots.length > 0) {\n await this._ctx.store.save(snapshots);\n }\n }\n\n const activity = Array.from(state.activity.values());\n if (activity.length > 0) {\n await this._ctx.store.save(activity);\n }\n }\n\n if (this.resourceIdsToSave.size > 0) {\n const resources = Array.from(this.resourceIdsToSave)\n .map((id) => this.resources.get(id))\n .filter((value): value is Resource => value !== undefined);\n\n if (resources.length > 0) {\n await this._ctx.store.save(resources);\n }\n }\n\n if (this.priceSnapshots.size > 0) {\n await this._ctx.store.save(Array.from(this.priceSnapshots.values()));\n }\n}\n```\n\nWhy this shape matters:\n\n- multiple event-derived changes collapse into one batch write set\n- the handler owns its persistence strategy\n- the database is not hammered with tiny uncontrolled writes inside event loops\n\n---\n\n### 21. Keep the SQL schema read-model oriented\n\nDo not mirror contract storage blindly. Persist what the frontend or integration layer actually reads.\n\nA good generic pattern is:\n\n- one current summary entity per tracked program\n- one append-only activity entity\n- one snapshot entity for rolling windows or charts\n- optional supporting entities such as resources and price snapshots\n\n```graphql\n# schema.graphql\ntype Resource @entity {\n id: ID!\n symbol: String!\n name: String\n decimals: Int!\n totalSupply: BigInt\n createdAt: DateTime! @index\n updatedAt: DateTime! @index\n}\n\ntype ResourcePriceSnapshot @entity {\n id: ID!\n resource: Resource!\n priceUsd: Float!\n timestamp: DateTime! @index\n blockNumber: BigInt! @index\n}\n\ntype TrackedProgram @entity {\n id: ID!\n owner: String! @index\n title: String\n status: String! @index\n tags: [String!]!\n totalCount: BigInt!\n volume: Float\n metric1h: Float\n metric24h: Float\n metric7d: Float\n isActive: Boolean! @index\n createdAt: DateTime! @index\n updatedAt: DateTime! @index\n activity: [ActivityRecord!] @derivedFrom(field: \"program\")\n snapshots: [MetricSnapshot!] @derivedFrom(field: \"program\")\n}\n\ntype MetricSnapshot @entity {\n id: ID!\n program: TrackedProgram!\n value: Float!\n transactionCount: Int!\n timestamp: DateTime! @index\n createdAt: DateTime! @index\n}\n\ntype ActivityRecord @entity {\n id: ID!\n type: String! @index\n program: TrackedProgram!\n user: String! @index\n blockNumber: BigInt! @index\n timestamp: DateTime! @index\n resourceId: String\n outputId: String\n amount: BigInt\n total: BigInt\n valueUsd: Float\n}\n```\n\nWhy this shape matters:\n\n- the read model is optimized for queries, not for contract serialization symmetry\n- history and current summary are separated\n- charts and feed pages become cheap to serve\n\n---\n\n### 22. Expose a thin API layer on top of PostgreSQL\n\nA good default is to keep business logic in the processor and let the API expose already projected data.\n\nThis section intentionally targets the PostGraphile v4 bootstrap/config API (`postgraphile(...)`, `PostGraphileOptions`, `appendPlugins`). Treat the v4 dependency line as a compatibility constraint for this snippet and the bundled templates, not as a product requirement for every indexer. Keep `postgraphile` on the 4.x line and `postgraphile-plugin-connection-filter` on the 2.x line when following this template. Do not switch to v5 by changing package versions alone; update the API bootstrap and template config together.\n\n```ts\n// src/api.ts\nimport express from \"express\";\nimport { postgraphile, PostGraphileOptions } from \"postgraphile\";\nimport ConnectionFilterPlugin from \"postgraphile-plugin-connection-filter\";\nimport { createServer } from \"node:http\";\nimport { Pool } from \"pg\";\nimport cors from \"cors\";\nimport dotenv from \"dotenv\";\n\ndotenv.config();\n\nconst isDev = process.env.NODE_ENV === \"development\";\n\nasync function main() {\n const dbPool = new Pool({\n connectionString: process.env.DATABASE_URL || \"postgres://indexer\",\n });\n\n const frontendUrl = process.env.FRONTEND_URL || \"http://localhost:3000\";\n\n const options: PostGraphileOptions = {\n watchPg: isDev,\n graphiql: true,\n enhanceGraphiql: isDev,\n subscriptions: true,\n dynamicJson: true,\n disableDefaultMutations: true,\n ignoreRBAC: false,\n showErrorStack: isDev ? \"json\" : true,\n legacyRelations: \"omit\",\n appendPlugins: [ConnectionFilterPlugin],\n graphqlRoute: \"/graphql\",\n graphiqlRoute: \"/graphiql\",\n };\n\n const middleware = postgraphile(dbPool, \"public\", options);\n const app = express();\n\n app.use(cors({ origin: frontendUrl }));\n app.use(middleware);\n\n const server = createServer(app);\n const port = Number(process.env.GQL_PORT || 4350);\n\n server.listen({ host: \"0.0.0.0\", port }, () => {\n console.log(`GraphQL listening at http://0.0.0.0:${port}/graphql`);\n console.log(`GraphiQL listening at http://0.0.0.0:${port}/graphiql`);\n });\n}\n\nmain().catch((error) => {\n console.error(error);\n process.exit(1);\n});\n```\n\nWhy this shape matters:\n\n- the API layer stays thin\n- request-time logic does not re-derive chain meaning from scratch\n- frontend reads from projected tables instead of hot chain queries\n\n---\n\n## Frontend-Safe GraphQL Exposure\n\nA local browser frontend must be able to reach the GraphQL endpoint without origin-policy surprises. Name the local access strategy explicitly and implement it in code.\n\nUse one of these patterns:\n\n- enable CORS on the indexer API for the frontend origin\n- serve GraphQL through the frontend development proxy\n- serve frontend and GraphQL behind the same origin gateway\n\nDo not leave this implicit. If the frontend reads `http://127.0.0.1:4350` directly, the indexer must either allow that origin explicitly or the frontend must proxy requests through its own development server.\n\nA local verification step should always include a browser-origin request to the GraphQL endpoint, not only a curl request from the terminal.\n\n## Standard Project Layout\n\nA practical indexer layout should look like this:\n\n```text\nsrc/\n api.ts\n config.ts\n processor.ts\n sails-decoder.ts\n helpers/\n is.ts\n block.ts\n handlers/\n base.ts\n registry.ts\n programs.ts\n index.ts\n model/\n generated/*\n dataSource.ts\n index.ts\n services/\n query-client.ts\n calculators/*\n caches/*\n types/\n gear-events.ts\n tracked-program.ts\n block.ts\nschema.graphql\nassets/\n registry.idl\n program.idl\n```\n\nThis layout fits projects with multiple event types, separate handlers, and enrichment services. For programs with few events or simple business logic, a flatter structure (e.g., one or two handler files, no `services/` subdirectory) is equally valid. Adapt the layout to what the business logic actually requires rather than applying the full tree mechanically.\n\n---\n\n## Source-Of-Truth Rules\n\nEvery projection path should state one of these models explicitly.\n\n### Event-first\n\nUse this when the event payload already contains everything needed for the append-only record.\n\n```ts\nconst activity = this.createActivityRecord(state, event, common, \"ACTION\", {\n user: payload.user_id,\n amount: BigInt(payload.amount),\n});\n```\n\n### Event plus query confirmation\n\nUse this when the event tells you something happened, but the current summary should come from a query.\n\n```ts\nawait this.processActivity(state, activity, common);\nawait this.refreshCurrentState(state, common);\n```\n\n### Periodic derived updates\n\nUse this when snapshots or windows should continue to advance on time boundaries.\n\n```ts\nif (this.shouldPerformPeriodicUpdates(state, blockTimestamp)) {\n await this.performPeriodicUpdates(state, blockTimestamp, blockNumber);\n}\n```\n\nDo not mix these models implicitly.\n\n---\n\n## Idempotency And Restart Rules\n\nA production-ready indexer must survive replay and restart.\n\n### Deterministic IDs\n\nUse chain-derived IDs whenever possible.\n\n```ts\nconst record = new ActivityRecord({\n id: event.args.message.id,\n // ...\n});\n```\n\n### Rehydrate tracked programs on first run\n\n```ts\nif (!this.existingProgramsLoaded) {\n await this.loadExistingPrograms(ctx);\n this.existingProgramsLoaded = true;\n}\n```\n\n### Combine persisted and pending snapshots before computing windows\n\n```ts\nconst allSnapshots = [...dbSnapshots, ...Array.from(state.snapshots.values())];\n```\n\n### Remove inactive programs from memory only after they are marked inactive for persistence\n\n```ts\nif (!state.isActive) {\n state.entity!.isActive = false;\n // save phase will persist the inactive mark\n}\n```\n\nThese rules prevent duplicate records, stale rolling metrics, and lost tracked-program memory after restarts.\n\n---\n\n## What To Reuse Across Projects\n\nThese parts are usually reusable almost verbatim:\n\n- `config.ts` environment contract\n- `processor.ts` batch processor setup and exported derived types\n- `types/gear-events.ts` typed Gear boundary\n- `helpers/is.ts` chain event guards\n- `sails-decoder.ts` IDL decoder\n- `handlers/base.ts` lifecycle contract\n- the split between discovery handler and domain projection handler\n- the `process -> save` batch flow\n- the restart rehydration pattern\n- the periodic snapshot update pattern\n- the thin PostGraphile GraphQL API pattern\n- the explicit frontend-access policy for local GraphQL usage: CORS, proxy, or same-origin\n\nThese parts usually need domain adaptation:\n\n- payload interfaces per service event\n- current summary entity fields\n- append-only activity entity fields\n- snapshot entity fields\n- query enrichment methods\n- calculators for metrics, rankings, or prices\n\n---\n\n## Guardrails\n\n- Do not write a generic command-side backend in place of an indexer.\n- Do not let raw Subsquid `Event` objects leak through the whole codebase.\n- Do not decode payload prefixes inside handlers when a central decoder exists.\n- Do not mix discovery logic with domain projection logic.\n- Do not build the SQL schema as a blind mirror of contract storage.\n- Do not trust event payloads as canonical current state unless that choice is explicit.\n- Do not recompute core domain meaning in `api.ts`; project it earlier and serve it from the database.\n- Do not skip restart rehydration, duplicate safety, or replay behavior.\n- Do not default to listening to the entire chain unless the broad filter tradeoff is understood and accepted.\n- Do not describe the indexer as complete unless a real GraphQL endpoint is mounted at a known path such as `/graphql`.\n- Do not rely on terminal-only checks for API reachability; verify browser-facing frontend access as well.\n- Do not keep a null, placeholder, or no-op ingestion adapter once the indexer is claimed to ingest chain data.\n- Do not leave frontend access strategy implicit when GraphQL is served on a different local origin.\n---\n\n## Minimal Build Order\n\nWhen implementing a new indexer, follow this order:\n\n1. `config.ts`\n2. `processor.ts`\n3. typed Gear events in `src/types/`\n4. `sails-decoder.ts`\n5. `schema.graphql`\n6. `handlers/base.ts`\n7. discovery handler (skip this step for fixed-program topologies)\n8. projection handler with rehydration\n9. enrichment or calculator services\n10. `api.ts`\n11. replay and restart verification\n12. browser-origin GraphQL verification against the local frontend access path\n\nThat order matches the canonical flow above and keeps the project stable while it grows.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":45455,"content_sha256":"6d9eefcb86a9216b94ca21083c3122e419035d7df08916f1737e50adf16dded8"},{"filename":"references/sails-program-and-service-architecture.md","content":"# Sails Program And Service Architecture\n\n## Baseline Structure\n\n- `#[program]` should stay thin: constructors returning `Self`, wiring, and service exposure only.\n- `#[service]` should own business logic, stateful commands, read-only queries, and events.\n- One application has one `#[program]`, but it may expose multiple services by business boundary.\n\n## State Ownership Patterns\n\n- Name the state ownership pattern explicitly in the architecture note instead of leaving storage implied.\n- Program-owned state is the preferred default because ownership is explicit and test setup is simpler.\n- 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.\n- Use shallow `extends` composition when it improves reuse; do not build a maze of inherited service surfaces.\n\n## Routes And Public Contract\n\n- Treat exported service and method routes as a compatibility surface.\n- Use explicit route policy when compatibility matters across refactors.\n- Keep exported DTO names distinct from service names so generated clients stay readable.\n- Events should represent externally meaningful state transitions, not every internal branch.\n\n## Async And Failure Rules\n\n- Any state read before `await` may be stale after resume.\n- Revalidate or re-read shared mutable state after `await` before mutating it.\n- In stateful command paths, treat transport failure and error reply as fatal unless the design explicitly models compensation.\n- Panic is a valid transaction boundary for one message: state, replies, and events from that execution roll back together.\n\n## Recommended Defaults\n\n- Use program-owned state plus service wrappers.\n- Keep constructor shape, state ownership, and service exposure aligned so the program contract is obvious from the `#[program]` surface.\n- Keep access-control checks centralized in service-local guard helpers.\n- Emit events close to the successful state transition they describe.\n- Prefer fail-fast command semantics over soft-error partial commits.\n\n## Common Anti-Patterns\n\n- Fat `#[program]` methods with domain logic\n- Mixed route policy with no stability intent\n- Static state without deterministic seeding\n- Async stale-state bugs after `await`\n- Mutate-before-send logic that ignores rollback on failure\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2343,"content_sha256":"dedc84529b6f9c39d7d5b9526f88d8a43f15c78890929c542c4a591361747538"},{"filename":"references/sails-rs-imports.md","content":"# Sails RS Imports And Release Defaults\n\n## Current Baseline\n\n- Use `sails-rs 1.0.0` as the pack baseline unless the target repo already pins a different version.\n- Prefer teaching the current template defaults instead of older blog posts or copied snippets.\n\n## Cargo Defaults\n\n### App Or Wasm Crate\n\n```toml\n[dependencies]\nsails-rs = \"1.0.0\"\n\n[build-dependencies]\nsails-rs = { version = \"1.0.0\", features = [\"build\"] }\n\n[dev-dependencies]\nsails-rs = { version = \"1.0.0\", features = [\"gtest\"] }\n```\n\n- Add `gclient` features or dependencies only when the crate also runs local-node smoke or off-chain integration tests. For deployment and CLI interaction without a Rust test harness, use `vara-wallet` instead.\n- Do not add `sails-idl-gen` to the default app/wasm baseline unless the repo intentionally uses a more manual IDL pipeline.\n\n### Dedicated Client Crate\n\n```toml\n[dependencies]\nsails-rs = \"1.0.0\"\n\n[build-dependencies]\nsails-rs = { version = \"1.0.0\", features = [\"build\"] }\n```\n\n- In current official examples, dedicated Rust client crates commonly use sails-rs with features = [\"build\"].\n- Reach for direct sails-client-gen / sails-idl-gen only when the repo intentionally uses a manual generation pipeline.\n\n## Workspace Feature Conflict Warning\n\n`sails-rs` compiles `client/gstd_env.rs` even when only the `gtest` feature is enabled. That module references `::gstd` types. When Cargo feature unification enables both `gstd` and `gtest` on `sails-rs` in the same workspace, the build breaks.\n\nPrevention:\n\n- Use one workspace per program. Do not combine multiple programs and a shared test crate in a single top-level workspace.\n- Always bootstrap with `cargo sails new \u003cname>`, which generates the correct isolated layout.\n- If you must share types across programs, extract them into a standalone `no_std` types crate with no `sails-rs` dependency.\n\n## Canonical Workspace Layout\n\n`cargo sails new \u003cname>` generates:\n\n```\n\u003cname>/\n├── Cargo.toml # resolver = \"3\", edition = \"2024\", rust-version = \"1.91\"\n├── build.rs # sails_rs::build_wasm() + ClientBuilder::from_wasm_path().build_idl()\n├── src/\n│ └── lib.rs # wasm re-export + WASM_BINARY code module\n├── app/\n│ ├── Cargo.toml # sails-rs (no features) — business logic, no_std\n│ └── src/lib.rs\n├── client/\n│ ├── Cargo.toml # sails-rs with features = [\"build\"]\n│ ├── build.rs # sails_rs::build_client::\u003cProgram>()\n│ └── src/lib.rs\n├── tests/\n│ └── gtest.rs # sails-rs with features = [\"gtest\"] in dev-dependencies\n└── rust-toolchain.toml # channel = \"stable\", targets = [\"wasm32-unknown-unknown\", \"wasm32v1-none\"]\n```\n\nKey constraints:\n\n- `resolver = \"3\"`, `edition = \"2024\"`, and `rust-version = \"1.91\"` are the current template defaults.\n- `gtest` and `gclient` belong in `[dev-dependencies]` only, never in `[dependencies]`. `gclient` is for Rust-native test harnesses; for deployment and on-chain interaction, `vara-wallet` is the primary tool.\n- Each program is its own workspace root, not a member of a shared multi-program workspace.\n\n## Common Imports\n\n```rust\nuse core::cell::RefCell;\nuse sails_rs::collections::BTreeMap;\nuse sails_rs::prelude::*;\n```\n\n- Reach for `RefCell` when the program owns mutable state and services borrow it.\n- Use `sails_rs::collections::*` when you want `no_std`-friendly collections through the framework path. Note that `sails_rs::collections::BTreeMap` is a `no_std` re-export and may lack some `std` methods. In particular, `drain()` is not available. Use `keys().cloned().collect::\u003cVec\u003c_>>()` then iterate and remove as a workaround.\n- `gstd::prog` (program creation via `create_program_bytes`) is not re-exported through `sails_rs::gstd`. If a Sails program needs to create child programs, add `gstd` as a direct dependency: `gstd = \"1.10.0\"`.\n\n## Builder Defaults\n\n- For the root program crate, the default `build.rs` chains wasm build with IDL generation:\n ```rust\n fn main() {\n if let Some((_, wasm_path)) = sails_rs::build_wasm() {\n sails_rs::ClientBuilder::\u003capp::Program>::from_wasm_path(wasm_path).build_idl();\n }\n }\n ```\n- For a Rust client-generation crate, use `sails_rs::build_client::\u003cProgram>()` as the default shorthand.\n- Use the Wasm path when the crate’s job is to build the on-chain artifact.\n- Use the client path when the crate’s job is to generate a typed Rust client from a program interface.\n- Reach for direct `sails-idl-gen` / `sails-client-gen` only when the repo intentionally requires a manual pipeline.\n- If the repo needs custom client-generation control, prefer the official configurable builder path instead of dropping immediately to fully manual generators.\n\n## Export And Event Rules\n\n- Treat `#[export]` as mandatory for every service method that should be publicly callable.\n- Public Rust methods without `#[export]` are implementation details, not remote Sails routes.\n- `#[export(unwrap_result)]` allows internal use of `Result\u003cT, E>` and `?` while exposing the unwrapped success path to clients.\n- Use `self.emit_event(...)`, not `notify_on(...)`.\n\n```rust\n#[sails_rs::event]\n#[sails_rs::sails_type]\npub enum Event {\n Updated(u64),\n}\n\n#[service(events = Event)]\nimpl CounterService\u003c'_> {\n #[export]\n pub fn add(&mut self, by: u64) {\n self.value += by;\n self.emit_event(Event::Updated(self.value))\n .expect(\"Event error\");\n }\n}\n```\n\n## SCALE Derive Boilerplate\n\n- `#[sails_rs::sails_type]` handles standard Sails derives (`Encode`, `Decode`, `TypeInfo`, `ReflectHash`)\n- When shared DTOs or events derive SCALE traits in a `no_std` Sails crate, prefer:\n - `#[codec(crate = sails_rs::scale_codec)]`\n - `#[scale_info(crate = sails_rs::scale_info)]`\n- This avoids proc-macro confusion when the crate does not depend on `parity-scale-codec` or `scale-info` directly.\n\n## Agent Notes\n\n- If the repo already pins a specific version, follow the repo version instead of forcing the pack baseline.\n- Do not tell builders to hand-roll routes or event wiring when generated clients and `emit_event` already cover the standard path.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":6199,"content_sha256":"a8064ecf6fc6fdf800a784aa66a29d35a662c036529192e200b4b5959d288cac"},{"filename":"references/sails-syscall-mapping.md","content":"# Sails `Syscall` API Mapping\n\nUse `Syscall::*` for all runtime accessors in service code. Do not use raw `gcore::*`, `msg::*`, or `exec::*` calls.\n\n## Type Aliases\n\n- `ValueUnit = u128`\n- `GasUnit = u64`\n\n## Mapping\n\n### Message Context\n\n| `Syscall` method | Return type | Underlying call | `ethexe` gating |\n| --- | --- | --- | --- |\n| `Syscall::message_id()` | `MessageId` | `gcore::msg::id()` | |\n| `Syscall::message_size()` | `usize` | `gcore::msg::size()` | |\n| `Syscall::message_source()` | `ActorId` | `gcore::msg::source()` | |\n| `Syscall::message_value()` | `ValueUnit` | `gcore::msg::value()` | |\n| `Syscall::reply_to()` | `Result\u003cMessageId, Error>` | `gcore::msg::reply_to()` | |\n| `Syscall::reply_code()` | `Result\u003cReplyCode, Error>` | `gcore::msg::reply_code()` | |\n| `Syscall::signal_from()` | `Result\u003cMessageId, Error>` | `gcore::msg::signal_from()` | non-ethexe only |\n| `Syscall::signal_code()` | `Result\u003cOption\u003cSignalCode>, Error>` | `gcore::msg::signal_code()` | non-ethexe only |\n| `Syscall::read_bytes()` | `Result\u003cVec\u003cu8>, Error>` | alloc + `gcore::msg::read()` | |\n\n### Execution Context\n\n| `Syscall` method | Return type | Underlying call | `ethexe` gating |\n| --- | --- | --- | --- |\n| `Syscall::program_id()` | `ActorId` | `gcore::exec::program_id()` | |\n| `Syscall::block_height()` | `u32` | `gcore::exec::block_height()` | |\n| `Syscall::block_timestamp()` | `u64` | `gcore::exec::block_timestamp()` | |\n| `Syscall::value_available()` | `ValueUnit` | `gcore::exec::value_available()` | |\n| `Syscall::gas_available()` | `GasUnit` | `gcore::exec::gas_available()` | |\n| `Syscall::env_vars()` | `EnvVars` | `gcore::exec::env_vars()` | |\n| `Syscall::exit(inheritor_id)` | `!` | `gcore::exec::exit(inheritor_id)` | |\n| `Syscall::panic(data)` | `!` | `gcore::ext::panic(data)` | |\n| `Syscall::system_reserve_gas(amount)` | `Result\u003c(), Error>` | `gcore::exec::system_reserve_gas(amount)` | non-ethexe only |\n\n## Common Patterns\n\n- **Self-message guard**: `Syscall::message_source() == Syscall::program_id()`\n- **Gas-guarded loop**: check `Syscall::gas_available()` before expensive iterations\n- **Raw payload access**: `Syscall::read_bytes()` bypasses SCALE decode\n\n## Non-WASM Behavior\n\nOn non-`wasm32` + `std`: `Syscall` reads from thread-local mock state. Matching `with_*` setters exist for tests (`with_message_id`, `with_message_source`, `with_program_id`, etc.).\n\nOn non-`wasm32` without `std`: all methods are unimplemented and panic.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2464,"content_sha256":"7f7d6a66e567ba87d3c942cb926069227359fe05a9894e9d15a9b598298f4295"},{"filename":"references/scale-binary-decoding-guide.md","content":"# SCALE Binary Decoding Guide\n\nUse this guide when a builder has raw bytes, hex payloads, replies, event data, or state outputs and is not sure which decoding path is correct.\n\n## Default Rule\n\nStart by identifying the source of the bytes before choosing a decoder.\n\nDo not start with plain `Decode::\u003cT>` on arbitrary bytes when the source may be a routed Sails payload or a metadata-driven state read.\n\n## First Classify The Bytes\n\nBefore decoding, identify all of the following:\n\n- source:\n - constructor payload\n - service command payload\n - service query reply\n - event payload\n - full state output\n - state function output\n - raw mailbox or system message\n- framing:\n - Sails Header v1 plus SCALE body\n - plain SCALE\n - metadata-driven state output\n- artifact:\n - `.idl`\n - generated Rust or TypeScript client\n - `ProgramMetadata`\n - `state.meta.wasm`\n- version:\n - confirm the `.idl`, metadata, or state meta comes from the same build or release as the bytes you are decoding\n\n## Preferred Decode Order\n\nUse this order unless the task explicitly requires a lower-level path:\n\n1. Generated Sails client\n - Default for standard Sails constructors, service calls, replies, and events.\n - Prefer this when the workspace already has generated Rust or TypeScript client code.\n\n2. Runtime Sails IDL parsing\n - Use `parseIdl` only when dynamic runtime control is explicitly needed.\n - This is the dynamic fallback for Sails interface-driven decoding.\n\n3. `api.programState.read` with `ProgramMetadata`\n - Use for full state reads.\n\n4. `api.programState.readUsingWasm` with `state.meta.wasm`\n - Use for state-function outputs and state-conversion flows.\n\n5. Metadata-driven low-level decode\n - Use when the task is explicitly about raw Gear metadata or hex decoding.\n - Match the exact type and metadata artifact before decoding.\n\n6. Plain `Decode::\u003cT>`\n - Use only when the bytes are known to be a bare SCALE payload with no Sails routing layer and no metadata-driven transformation layer.\n\n## Sails Header Bytes\n\nFor a standard Sails app, do not assume constructor, service, reply, or event bytes are just a bare business DTO.\n\nSails 1.0 uses a 16-byte Sails Header for service and method routing, followed by the SCALE-encoded params or body. Generated clients handle this transparently. When debugging raw bytes, inspect the leading header bytes (`GM`, version `0x01`, header length `0x10`) before decoding the business DTO.\n\nIf the path involves a Sails route, decode with the generated client or another Sails-aware IDL path first.\n\nOnly decode as a bare struct after you have explicitly confirmed that:\n- the bytes are not carrying Sails routing framing, or\n- the routing layer has already been handled by the tool you are using\n\n## State Reads\n\nTreat these as different decode problems:\n\n- full state read:\n - `api.programState.read`\n - decode with `ProgramMetadata`\n\n- state function or state-conversion output:\n - `api.programState.readUsingWasm`\n - decode with `state.meta.wasm`\n\nDo not use `.idl` to decode full raw program state unless the task explicitly proves that the output type is coming from a Sails interface path rather than Gear metadata/state tooling.\n\n## Raw Gtest And Byte-Level Debugging\n\nWhen a test drops below generated clients:\n\n- identify whether the payload or reply is Sails Header framed or plain SCALE\n- inspect the source of the bytes before decoding logs\n- prefer Sails-aware helpers when the test is not intentionally validating raw framing\n- use plain SCALE decode only when the test is explicitly about a non-routed payload body\n\n## Metadata Fallback\n\n`gear-meta` can still be useful as a debugging fallback for manual hex encode or decode work.\n\nBut it is not the preferred long-term default.\nPrefer current metadata access paths first when the repo or toolchain already exposes them.\n\n## Never Do These By Default\n\n- Do not decode arbitrary Sails replies with plain `Decode::\u003cT>` before checking for routing framing.\n- Do not decode full state bytes with `.idl` when the correct path is metadata-based.\n- Do not decode state-function output with full-state metadata when the correct artifact is `state.meta.wasm`.\n- Do not mix artifacts from different builds or releases.\n- Do not switch to manual hex decoding when a generated client already exists and matches the deployed interface.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4363,"content_sha256":"6d68b8732b3c5f9f3ec81056cdc56d67c5bd7790ee9ce438745f558a3ae49e80"},{"filename":"references/vara-domain-overview.md","content":"# Vara Domain Overview\n\n## Core Model\n- Gear/Vara programs are isolated actors with private state.\n- State changes happen only while handling a message.\n- Messages are asynchronous; ordering is preserved between a given pair of actors.\n- The network maintains a global message queue and routes messages during block construction.\n\n## What A Skill Should Assume\n- Programs never share memory.\n- Program code is immutable Wasm plus persistent memory.\n- Async design is normal, not an edge case.\n- Cross-program work is a distributed transaction problem, not a single atomic call.\n\n## Reliability Patterns\n- Prefer explicit message flows, reply handling, and event emission.\n- For multi-step workflows, model failures up front with idempotency, retries, and compensation.\n- Use delayed messages and gas reservation only when the design truly needs deferred recovery.\n- Treat actor boundaries and execution ordering as architecture constraints, not implementation details.\n\n## First-Wave Skill Guidance\n- Specs should name actors, messages, replies, events, invariants, and recovery paths.\n- Architecture notes should explain ownership, message flow, and off-chain observation points.\n- Implementation skills should default to Gear/Vara semantics before considering Vara.eth extensions.\n\n## See Also\n- `references/gear-execution-model.md`\n- `references/gear-messaging-and-replies.md`\n- `references/gear-gas-reservations-and-waitlist.md`\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1433,"content_sha256":"4b4d10ff4208ac24b0447b841cf11f0e84db0a0313bf991bdfc8b25a08e16b64"},{"filename":"references/vara-eth-bridge-contracts.md","content":"# Vara-Ethereum Bridge Contract Addresses\n\nLast Verified: 2026-03-26\n\n**Testnet Warning:** Testnet addresses change when testnets are deprecated or bridge contracts are redeployed. Verify addresses against canonical sources before real deployments.\n\n## Canonical Sources\n\n- Vara wiki Bridge Developer Hub: `vara-wiki/content/docs/vara-network/bridge/developer_hub.mdx`\n- Vara wiki Contract Addresses: `vara-wiki/content/docs/vara-eth/reference/contract-addresses.mdx`\n- Source data: `vara-wiki/src/addresses/bridge-addresses.js`\n\n## Vara.eth Contracts (Hoodi Testnet)\n\nFor Hoodi testnet network configuration (chain ID, RPC, explorer, faucet), see `vara-network-endpoints.md` and `varaeth-extension-notes.md`.\n\n| Contract | Address |\n|----------|---------|\n| Router | `0xE549b0AfEdA978271FF7E712232B9F7f39A0b060` |\n| wVARA (ERC-20) | `0xE1ab85A8B4d5d5B6af0bbD0203EB322DF33d0464` |\n| Mirror (per program) | Deterministic: `CREATE2(Router, keccak256(codeId, salt))` |\n\nVara.eth mainnet contracts are not yet deployed.\n\n## Ethereum Bridge Contracts — Mainnet\n\n### Core Components\n\n| Component | Address |\n|-----------|---------|\n| Verifier | `0xDdAAC7F0814368D58D40216C6391a5e40A8cd47E` |\n| MessageQueue | `0x8E01Fbf136cA97627ca241dB9EFf1DFE3f2195F6` |\n| Bridging Payment | `0x03Dd51eeE793CE5523D28752f3019B0c9DfeE6C5` |\n\n### Token Contracts\n\n| Token | Address |\n|-------|---------|\n| USDC | `0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48` |\n| USDT | `0xdAC17F958D2ee523a2206206994597C13D831ec7` |\n| WETH | `0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2` |\n| WVARA | `0xB67010F2246814e5c39593ac23A925D9e9d7E5aD` |\n| WBTC | `0x2260fac5e5542a773aa44fbcfedf7c193bc2c599` |\n\n### ERC20 Manager\n\n| Component | Address |\n|-----------|---------|\n| ERC20Manager | `0x16fCff97822fcf3345Fa76D29c229b11C49EaE12` |\n\n### Bridge Governance\n\n| Component | Address |\n|-----------|---------|\n| GovernanceAdmin | `0x3681A3e25F5652389B8f52504D517E96352830C3` |\n| GovernancePauser | `0x257936C55518609E47eAab53f40a6e19437BEF47` |\n\n## Ethereum Bridge Contracts — Hoodi Testnet\n\n### Core Components\n\n| Component | Address |\n|-----------|---------|\n| Verifier | `0xc3ac0c364452acEE4366CD088F947965ec486e8F` |\n| MessageQueue | `0xAb8F315Cc80cf2368750fE5A33E259d6241b3dEB` |\n| MessageQueue Proxy | `0x55B97F9229dc310837A880c7898f7d411528cC6d` |\n| Bridging Payment | `0x512Ac5b5f0830d1C5CB44741a5c0a0E5B66696FD` |\n\n### Token Contracts\n\n| Token | Address |\n|-------|---------|\n| USDC | `0x263898d2f6f8E153F1e4DD4CAEF86C93784fCf33` |\n| USDT | `0x7728A33EBEBCfa852cf7f7Fc377BfC87C24a701A` |\n| WETH | `0xE0decAa66aED871ac9eb924443D1Bf333Fdb062E` |\n| WBTC | `0xa56a332d34b2db33ebc41dc0194afd28cb20d19b` |\n| WTVARA | `0xE1ab85A8B4d5d5B6af0bbD0203EB322DF33d0464` |\n\n### ERC20 Manager\n\n| Component | Address |\n|-----------|---------|\n| ERC20Manager | `0xA17187De490dB5F7160822dA197bcAc39d64baCb` |\n| ERC20Manager Proxy | `0xD2a0951DBCfdA8de36Ab315A57C30F0784d01342` |\n\n### Bridge Governance\n\n| Component | Address |\n|-----------|---------|\n| GovernanceAdmin | `0x3288b744ADbC25f07b636718d72ec07F8C77E5E9` |\n| GovernancePauser | `0x384B68Fc08c59Df51694f7F6A24A112B8331eEb0` |\n\n## Vara Bridge Programs — Mainnet\n\n| Component | Program ID |\n|-----------|-----------|\n| checkpoint-light-client | `0xf0a411ed5b8c28194d60781306de2ef131ec7df28658f7a857fc95fd86db7e8b` |\n| historical-proxy | `0x21b82d3ee72e0a5ac81db100c0a703050593b551c50e86b18c9da0be793660fb` |\n| vft-manager | `0xe01ddc667f80cf57704352b557668b710c345395abcac0752c01402d16e3e81b` |\n| bridging-payment | `0xcc1901de1f8134ed2c2d30775e4840084ad5d527cfcbf63c3247df24a2e3075e` |\n\n### Bridged Tokens on Vara Mainnet\n\n| Token | Program ID |\n|-------|-----------|\n| WUSDC | `0xd1de816d7dce6439504552686ab333e5b7302b1549763656b30af1f8a5871b6a` |\n| WUSDT | `0x4255ff4a87a4c13dc39f74ace8c4948bbef2f75fb639d66639a1cfcc99e6243e` |\n| WETH | `0xde45bdbb0345919a11561d43a5082e0b25061d4a2c6eb80009c1cfbccb80d0de` |\n| WBTC | `0x4984671804477d0689eabcd5418eb751207f20f251eaf7884a25b98645f342b1` |\n| Tokenized VARA | `0xdbf80fe5bd78b44510762770a14dc2a5b13a6bb167ff12c2edcc7ca3deadc16d` |\n\n## Vara Bridge Programs — Testnet\n\n| Component | Program ID |\n|-----------|-----------|\n| checkpoint-light-client | `0xdb7bbcaff8caa131a94d73f63c8f0dd1fec60e0d263e551d138a9dfb500134ca` |\n| eth-events-electra | `0x0e81a4201bdb78fb313aa01a7764aa2c6bd254463026dc700db9f657c54d47b1` |\n| historical-proxy | `0x5d2a0dcfc30301ad5eda002481e6d0b283f81a1221bef8ba2a3fa65fd56c8e0f` |\n| vft-manager | `0x39514728b5828eccad02b8f1183e7ccea43849157ed6504c38b28420a85f0d12` |\n| bridging-payment | `0x8b86f8cb74f2a0050bd2207efa22cdb59ce20051126618e2ebcacfe49c7e1e4c` |\n\n### Bridged Tokens on Vara Testnet\n\n| Token | Program ID |\n|-------|-----------|\n| WUSDC | `0x9f332e61589e0850dce6d8e6070ea5618de33d9f134a4a35d6d1164dc9002f48` |\n| WUSDT | `0x464511231a1afe9108a689ed3dbbb047ca308d6f5dfb86453e4df5612a2d668a` |\n| WETH | `0xba764e2836b28806be10fe6f674d89d1e0c86898d25728f776588f03bddc6f58` |\n| WBTC | `0xc1ec06d99efcffd863f9c2ad2bc76f656aff861acf06f438046c64e5b41e3fd9` |\n| Tokenized VARA | `0xa1a37e5a36e8a53921f6bedefadec91dc510636079a22238e9edf8233aaa494e` |\n\n## Bridge UI\n\n- Bridge interface: `https://bridge.vara.network`\n- Supported assets: VARA/wVARA, ETH/wETH, USDT/wUSDT, USDC/wUSDC, WBTC\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":5275,"content_sha256":"ffddb314081b4cebf83ce642517c5a94389cde5d2fcf690d768833c009597878"},{"filename":"references/vara-eth-bridge-flows.md","content":"# Vara-Ethereum Bridge Flows\n\n## Overview\n\nThe Vara-Ethereum bridge uses different trust models in each direction. Both directions are trustless — no centralized intermediary holds funds or controls message passing.\n\nPermissionless relayers carry data between chains. They earn fees but cannot forge messages.\n\n## Vara to Ethereum\n\nUses zero-knowledge proofs for cross-chain message verification.\n\n1. User locks tokens via the VFT Manager program on Vara.\n2. The `gear_eth_bridge` pallet collects messages into an outbound queue.\n3. At each finalized block, a Merkle root of the message queue is calculated.\n4. A relayer generates a ZK proof (Plonky2) that the Merkle root was signed by Vara's active validator set.\n5. The relayer submits the proof to the Proof Verification Contract on Ethereum (verified with GNARK).\n6. Ethereum records the Merkle root as an approved source of truth.\n7. The relayer submits the individual message with a Merkle inclusion proof to the Bridge Message Contract.\n8. The contract verifies inclusion, marks the message as used, and triggers the action (e.g., minting wVARA via the ERC-20 Manager).\n\nTypical transfer time: ~30 minutes.\n\n## Ethereum to Vara\n\nUses Ethereum's native finality via Beacon Chain light client verification. No ZK proofs needed in this direction.\n\n1. User burns or locks tokens on Ethereum (e.g., burns wVARA in the ERC-20 Manager).\n2. Relayers monitor the Ethereum Beacon Chain and its Sync Committee.\n3. Once a block is finalized, a relayer submits the block header and aggregated BLS signatures to Vara.\n4. The `checkpoint-light-client` program on Vara verifies the BLS signatures against the known Sync Committee.\n5. Once Vara accepts the block as finalized, the relevant Ethereum event (e.g., Burn event) is proven via Merkle proof.\n6. The `vft-manager` program on Vara processes the proof and executes the corresponding action (e.g., minting native VARA or releasing locked tokens).\n\nTypical transfer time: ~30 minutes.\n\n## Fee Model\n\nGas fees depend on the source network:\n\n- **Vara to Ethereum:** only VARA tokens needed. No ETH required for the initial transfer.\n- **Ethereum to Vara:** ETH is required regardless of the asset being bridged (VARA, ETH, or stablecoins).\n\nKeep a small balance of the native gas token on the source chain to avoid transaction failures.\n\n## Programmatic Integration\n\nFor Sails programs that need to interact with the bridge:\n\n- The `vft-manager` program is the key integration point. It mediates between the bridge infrastructure and VFT token contracts.\n- For VFT token patterns and bridge-aware token design, see `awesome-sails-token-patterns.md`.\n- For deployed bridge program addresses and token contract addresses, see `vara-eth-bridge-contracts.md`.\n\n## Builder Warnings\n\n- **Pre-confirmation vs finality:** Vara.eth pre-confirmed state may be newer than finalized state on Ethereum. Bridge transfers require full finality, not pre-confirmations.\n- **Relay delays:** Relayers are permissionless and incentivized but not instant. Transfers take ~30 minutes under normal conditions; network congestion can increase this.\n- **Testnet instability:** Ethereum testnets have a history of deprecation (Ropsten, Rinkeby, Goerli, Sepolia, Holesky, Hoodi). Bridge testnet addresses will change when the underlying testnet changes.\n- **Address format mismatch:** Ethereum uses 0x-prefixed 20-byte hex addresses. Vara uses 0x-prefixed 32-byte hex ActorIds. Bridge programs handle the mapping internally; do not convert between formats manually.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3535,"content_sha256":"2f6394a199704b89a77ea973673ef1b795ef2f38017c9bd5546405f8bad0542e"},{"filename":"references/vara-network-endpoints.md","content":"# Vara Network Endpoints\n\n## Vara Network (Substrate)\n\n| Network | WebSocket RPC | Purpose |\n|---------|--------------|---------|\n| Mainnet | `wss://rpc.vara.network` | Production |\n| Testnet | `wss://testnet.vara.network` | Development and testing |\n\n## Explorers\n\n| Tool | URL | Use |\n|------|-----|-----|\n| Gear IDEA | `https://idea.gear-tech.io` | Program upload, message sending, state inspection |\n| Vara Subscan | `https://vara.subscan.io` | Block and extrinsic browsing (mainnet) |\n| Vara Testnet Subscan | `https://vara-testnet.subscan.io` | Block and extrinsic browsing (testnet) |\n\n## Testnet Faucet\n\nGet testnet VARA tokens from the Gear IDEA portal at `https://idea.gear-tech.io`. Connect a Substrate wallet and request tokens through the portal interface.\n\n## Account And Address Format\n\n- **SS58 prefix:** 137 (Vara-specific)\n- **Token decimals:** 1 VARA = 10^12 minimal units\n- **Existential deposit:** ~1 VARA on mainnet\n- **Address converter:** `https://ss58.org`\n\n### ActorId vs SS58\n\n- **ActorId** is a 256-bit hex value (`0x...`, 64 hex characters). Programs and on-chain logic use ActorId exclusively.\n- **SS58** is a base-58 encoded display format with a network prefix. Wallets, explorers, and UIs show SS58.\n- When calling Sails methods or sending messages on-chain, use hex ActorId.\n- When displaying addresses in a frontend, use SS58.\n- For bridge interactions on the Ethereum side, use `0x`-prefixed hex Ethereum addresses. Vara-side bridge programs use hex H256 ActorIds, not SS58.\n\n## Program Lifecycle\n\nPrograms must be uploaded, initialized, and funded before they accept messages. For the full lifecycle (upload, create, active, exit) and rent system details, see `gear-execution-model.md`.\n\n## Vara.eth Network\n\nFor Vara.eth (Ethereum-anchored) endpoints, chain ID, and RPC configuration, see `varaeth-extension-notes.md`.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1857,"content_sha256":"608b377e1264ee4c1379a97298ef18a065f4803e0c1415e05edfddaf7f2b230c"},{"filename":"references/vara-token-exchanges.md","content":"# Vara Token Exchanges\n\nWhere the VARA token (native asset of Vara Network) is listed for trading on centralized exchanges. Lookup-only — for on-chain interaction patterns see `vara-network-endpoints.md`, and for the Ethereum-bridged form see `vara-eth-bridge-flows.md`.\n\n## Centralized Exchanges\n\n| Exchange | Pair(s) | Listing / Price Page |\n|----------|---------|----------------------|\n| Coinbase | VARA-USD, VARA-USDT, VARA-BTC | `https://www.coinbase.com/price/vara` |\n| Gate | VARA/USDT | `https://www.gate.com/trade/VARA_USDT` |\n| Crypto.com | VARA spot | `https://crypto.com/en/price/vara-network` |\n\nCoinbase added VARA support in 2024 with the VARA-USD pair launched in phases. Gate.io was the launch venue (January 2024) and historically the deepest VARA/USDT market. Crypto.com lists VARA on its centralized exchange and price index.\n\n## Verification\n\nExchange listings, available pairs, and per-region availability change. Before quoting any pair or URL to a user — especially inside generated frontend copy or `vara-brand-system` artifacts — open the listing page above and confirm the pair is still active. Treat this file as a starting point, not a source of truth.\n\nFor aggregate views across all listing venues (including DEXes and ones not covered here), CoinGecko's `vara-network` page and CoinMarketCap's `vara-network` page maintain a markets list.\n\n## Related References\n\n- `vara-network-endpoints.md` — RPC, explorers, SS58 format, **testnet faucet** (note: faucet gives free testnet VARA, not mainnet — \"buy VARA\" and \"get testnet VARA\" are distinct flows)\n- `vara-eth-bridge-flows.md` — bridge between Vara mainnet and Ethereum for users holding the wrapped form\n- `vara-eth-bridge-contracts.md` — bridge contract addresses and bridged token program IDs\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1795,"content_sha256":"61aac0daa093e73b704b8e99fcf76e670ad7b907951078da469a9c505b6003a5"},{"filename":"references/varaeth-extension-notes.md","content":"# Vara.eth Extension Notes\n\n## Scope\nThis is an extension layer for the first release, not the default execution path.\n\n## Mental Model Differences\n- Programs have both a Gear-style actor identity and an Ethereum-facing Mirror address.\n- Router validates code, creates programs, and emits lifecycle events.\n- Each program Mirror needs executable balance to pay for execution.\n- Pre-confirmed state can be newer than the last committed on-chain state.\n\n## Network Config (Hoodi Testnet)\n\n- Chain ID: `560048` (hex `0x88bb0`)\n- RPC (HTTPS): `https://hoodi-reth-rpc.gear-tech.io`\n- RPC (WebSocket): `wss://hoodi-reth-rpc.gear-tech.io/ws`\n- Block explorer: `https://hoodi.etherscan.io`\n- Faucet: `https://eth.vara.network/faucet`\n- Beacon RPC: `https://hoodi-lighthouse-rpc.gear-tech.io`\n- Validator endpoints (pre-confirmations): `wss://vara-eth-validator-{1..4}.gear-tech.io`\n\nFor Vara Network (Substrate) endpoints and account format, see `vara-network-endpoints.md`.\n\nVara.eth mainnet endpoints are not yet published.\n\n## Design Warnings\n- Do not assume a plain Gear deploy flow maps directly to Vara.eth.\n- Message execution depends on executable balance, not only the sender account.\n- `callReply` and async reply handling matter for user-visible behavior.\n- ABI, IDL, and deployment mode must stay aligned or queries and messages fail at the edge.\n\n## First-Wave Usage\n- Mention Vara.eth only when the target project explicitly needs it.\n- Use this note to flag Router/Mirror/executable-balance concerns during planning.\n- Defer full Vara.eth-specific skills to the next milestone.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1585,"content_sha256":"9605b7d2ac2fe1e3aa8d1bda286aef34bea25c8671af9b8284d9ec71f48bffd3"},{"filename":"references/voucher-and-signless-flows.md","content":"# Voucher And Signless Flows\n\n## When To Use Vouchers\n\n- Does the dApp need users to interact without holding VARA? Use vouchers — a sponsor pays gas on behalf of users.\n- Need seamless UX without per-action signing? Add signless sessions on top of vouchers.\n- Need both gasless and signless? Use the EZ-Transactions package.\n- A voucher is a bounded gas budget tied to a spender, duration, and optional program whitelist. It is not free chain access.\n- Choosing vouchers, signless, or both is an explicit product decision. Name it in the spec and architecture, not as a hidden implementation detail.\n- For general message sending patterns, see `references/gear-messaging-and-replies.md`. This doc covers only the voucher-specific wrapping and sponsored UX flows.\n\n## Builder Recipe: Voucher-Only Flow\n\n1. Sponsor issues a voucher: call `api.voucher.issue(spender, balance, duration, programs, codeUploading)`. This returns `{ extrinsic }`. Sign and send the extrinsic, then extract the `VoucherId` from the `VoucherIssued` event in the transaction result.\n2. Frontend checks a voucher exists for the user and program: `api.voucher.exists(accountId, programId)`. This is a UX hint only — expiry, revocation, and exhaustion can still cause failure after this check.\n3. Frontend wraps the transaction with the voucher: `api.voucher.call(voucherId, { SendMessage: tx })`.\n4. User sends the wrapped transaction. Gas is paid from the voucher balance, not the user wallet.\n5. Monitor voucher health: check remaining balance via `api.voucher.details(spender, voucherId)`, extend duration or top up balance via `api.voucher.update(spender, voucherId, opts)` if needed.\n6. Cleanup: after the voucher expires, sponsor revokes it via `api.voucher.revoke(spender, voucherId)` to reclaim unspent funds.\n\n## Builder Recipe: Signless Session Flow\n\n1. User creates a temporary session keypair (sub-account) in the frontend.\n2. User signs a one-time transaction to register the session in the program: session key, expiry block, and allowed actions.\n3. Sponsor issues a voucher to the temporary session account (not the user's main account).\n4. App signs all subsequent transactions with the session key — no user signature prompts per action.\n5. Session expires by block number or user explicitly revokes it. The temporary key becomes inert.\n\nGuardrails:\n- Sessions are bounded by duration, program scope, and allowed actions. The program must enforce these constraints.\n- Session creation itself requires a real user signature. Only subsequent actions use the session key.\n- One active session per user per program. Multi-session collision must be handled in program logic.\n- Session expiry, revocation, and replay protection are program-level responsibilities. Design them explicitly.\n\n## Builder Recipe: EZ-Transactions (Full Gasless + Signless)\n\n- **Prerequisite:** a backend sponsor service that issues and manages vouchers on behalf of users. EZ-transactions does not create vouchers from thin air — a funded backend must issue them.\n- Wrap the app root with `GaslessTransactionsProvider` and `SignlessTransactionsProvider`.\n- Use `useGaslessTransaction` and `useSignlessTransaction` hooks in components.\n- The EZ-transactions package handles session management and transaction wrapping on the frontend. Voucher issuance and lifecycle management happen on the backend.\n- This is a product-level integration. Document the backend sponsor service in the feature spec. Plan for backend outage and voucher budget exhaustion as first-class failure modes.\n\n## Voucher Lifecycle\n\n- **Issue**: sponsor creates a voucher for a spender with a gas budget, duration, optional program whitelist, and code-uploading flag.\n- **Use**: spender wraps a `SendMessage`, `SendReply`, or `UploadCode` call via `api.voucher.call()`. Gas is deducted from the voucher.\n- **Update**: sponsor can top up balance, extend duration, add programs to whitelist, or transfer ownership via `api.voucher.update()`.\n- **Decline**: spender voluntarily returns a voucher before expiry, marking it as expired so the sponsor can revoke.\n- **Expire**: voucher reaches its block-number deadline and becomes unusable.\n- **Revoke**: sponsor reclaims unused funds from an expired or declined voucher.\n\nDuration is block-based and bounded by on-chain constants (`minDuration`, `maxDuration`). The program whitelist scopes which programs the voucher can interact with. If the whitelist is `None`, the voucher works with any program. The `code_uploading` flag controls whether `UploadCode` is permitted through this voucher.\n\n## Voucher Parameters\n\n| Parameter | Type | Purpose |\n|-----------|------|---------|\n| spender | AccountId | Who can use the voucher |\n| balance | u128 | Gas allocation in smallest unit (use `BigInt` or string in JS to avoid precision loss above 2^53) |\n| duration | BlockNumber | How many blocks the voucher is valid |\n| programs | Option\\\u003cBTreeSet\\\u003cProgramId\\>\\> | Authorized programs (None = any) |\n| code_uploading | bool | Allow UploadCode via voucher |\n\n## JS API Surface\n\n| Intent | Method | Returns |\n|--------|--------|---------|\n| Issue | `api.voucher.issue(spender, balance, duration, programs?, codeUploading?)` | `{ extrinsic }` — extract VoucherId from `VoucherIssued` event after signAndSend |\n| Check exists | `api.voucher.exists(accountId, programId)` | boolean (checks if any voucher exists for this user+program pair) |\n| List all | `api.voucher.getAllForAccount(accountId)` | Record\\\u003cVoucherId, ProgramId[]\\> |\n| Details | `api.voucher.details(spender, voucherId)` | VoucherDetails (owner, programs, expiry, codeUploading) |\n| Execute | `api.voucher.call(voucherId, { SendMessage \\| SendReply \\| UploadCode })` | extrinsic |\n| Update | `api.voucher.update(spender, voucherId, opts)` | extrinsic |\n| Decline | `api.voucher.decline(voucherId)` | extrinsic |\n| Revoke | `api.voucher.revoke(spender, voucherId)` | extrinsic |\n| Min duration | `api.voucher.minDuration()` | blocks |\n| Max duration | `api.voucher.maxDuration()` | blocks |\n| Max programs | `api.voucher.maxProgramsAmount()` | number |\n\nUpdate options: `moveOwnership`, `balanceTopUp`, `appendPrograms`, `prolongValidity`. At least one is required.\n\n## On-Chain Extrinsics\n\n| Extrinsic | Signer | Purpose |\n|-----------|--------|---------|\n| `GearVoucher::issue` | Sponsor | Create a voucher for a spender |\n| `GearVoucher::call` | Spender | Execute a prepaid action (PrepaidCall: SendMessage, SendReply, or UploadCode) |\n| `GearVoucher::update` | Sponsor | Change balance, duration, programs, or code_uploading |\n| `GearVoucher::revoke` | Sponsor | Reclaim unspent funds after expiry |\n| `GearVoucher::decline` | Spender | Voluntarily return a voucher |\n\n## Hooks And Transaction Builder Integration\n\n- `useSendProgramTransaction` and `usePrepareProgramTransaction` both accept a `voucherId` option for voucher-backed sends.\n- Transaction builder: pass `voucherId` in the options object to wrap the call automatically.\n- Voucher existence check before sending is the caller's responsibility. The hooks do not auto-check.\n- Fee preview via `usePrepareProgramTransaction` works with vouchers — the fee is still calculated but paid from the voucher.\n\n## Testing Guidance\n\n- Do not invent voucher IDs in tests. Issue a real voucher and use the returned ID.\n- In `gtest`: voucher issuance is not natively supported. Test the program logic that receives voucher-backed messages, not the voucher issuance itself.\n- In local-node smoke tests: issue a voucher via the JS API or CLI, then use it in the typed client flow.\n- For signless flows: test session creation, action execution with the session key, and session expiry behavior at the program level.\n\n## Failure Modes\n\n- Voucher expired: duration exceeded, transaction rejected.\n- Voucher balance exhausted: insufficient gas, transaction rejected.\n- Program not in voucher whitelist: destination program not authorized, transaction rejected.\n- `code_uploading` not enabled: `UploadCode` attempted through a voucher that does not permit it.\n- Voucher already revoked or declined: voucher no longer usable.\n- Spender mismatch: wrong account attempting to use the voucher.\n- Duration out of bounds: below `minDuration` or above `maxDuration` at issuance time.\n- Session expired (signless): session block deadline passed, session key rejected by program.\n- Session action not allowed (signless): action type not in the session's allowed set, rejected by program.\n- Backend sponsor service down (EZ-transactions): no new vouchers issued, existing vouchers still work until exhausted.\n- Sponsor budget exhausted: backend has no funds to issue new vouchers.\n\n## See Also\n\n- `references/sails-frontend-and-gear-js.md`\n- `references/gear-gas-reservations-and-waitlist.md`\n- `references/gear-execution-model.md`\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":8809,"content_sha256":"2653fe8258de02a195d2e9e3d78a3fcda9e9a22612744d93aba9cc9977c015d5"},{"filename":"scripts/install-codex-skills.sh","content":"#!/usr/bin/env bash\nset -euo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nREPO_ROOT=\"$(cd \"${SCRIPT_DIR}/..\" && pwd)\"\nCODEX_HOME=\"${CODEX_HOME:-${HOME}/.codex}\"\nTARGET_DIR=\"${CODEX_HOME}/skills\"\nPACK_NAME=\"vara-skills\"\n\nmkdir -p \"${TARGET_DIR}\"\n\npack_target=\"${TARGET_DIR}/${PACK_NAME}\"\nln -sfn \"${REPO_ROOT}\" \"${pack_target}\"\nprintf 'INSTALLED_PACK=%s\\n' \"${PACK_NAME}\"\nprintf 'TARGET=%s\\n' \"${pack_target}\"\n\nfind \"${REPO_ROOT}/skills\" -mindepth 1 -maxdepth 1 -type d | LC_ALL=C sort | while IFS= read -r skill_dir; do\n skill_name=\"$(basename \"${skill_dir}\")\"\n target_path=\"${TARGET_DIR}/${skill_name}\"\n ln -sfn \"${skill_dir}\" \"${target_path}\"\n printf 'INSTALLED_SKILL=%s\\n' \"${skill_name}\"\n printf 'TARGET=%s\\n' \"${target_path}\"\ndone\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":765,"content_sha256":"fefb573cb08baba402cc4519bc868df91c3c1a924d9864c9a4fc556e63a0d418"},{"filename":"scripts/parse_test_output.py","content":"#!/usr/bin/env python3\n\nfrom __future__ import annotations\n\nfrom pathlib import Path\nimport json\nimport re\nimport sys\n\n\nFAILED_TEST_RE = re.compile(r\"^test\\s+(\\S+)\\s+\\.\\.\\.\\s+FAILED$\", re.MULTILINE)\nPANIC_SUMMARY_RE = re.compile(r\"panicked at .*:\\n(?P\u003cmessage>.+)\", re.MULTILINE)\nERROR_SUMMARY_RE = re.compile(r\"^error:\\s+(?P\u003cmessage>.+)$\", re.MULTILINE)\n\n\ndef parse_log(text: str) -> dict[str, object]:\n failed_tests = FAILED_TEST_RE.findall(text)\n summary_match = PANIC_SUMMARY_RE.search(text)\n error_match = ERROR_SUMMARY_RE.search(text)\n if error_match:\n return {\n \"status\": \"error\",\n \"failed_tests\": [],\n \"summary\": error_match.group(\"message\").strip(),\n }\n if failed_tests:\n summary = summary_match.group(\"message\").strip() if summary_match else \"gtest failed\"\n return {\n \"status\": \"failed\",\n \"failed_tests\": failed_tests,\n \"summary\": summary,\n }\n return {\n \"status\": \"passed\",\n \"failed_tests\": [],\n \"summary\": \"gtest passed\",\n }\n\n\ndef main() -> int:\n if len(sys.argv) != 2:\n print(\"usage: parse_test_output.py \u003clogfile>\", file=sys.stderr)\n return 1\n path = Path(sys.argv[1]).resolve()\n if not path.exists():\n print(f\"missing logfile: {path}\", file=sys.stderr)\n return 1\n payload = parse_log(path.read_text(encoding=\"utf-8\"))\n print(json.dumps(payload))\n return 0\n\n\nif __name__ == \"__main__\":\n raise SystemExit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":1517,"content_sha256":"85cd7eb289fd76bfdb61f56c1f74a2094909f4850b4b87c24f80832769d0bf47"},{"filename":"scripts/run_gtest.sh","content":"#!/usr/bin/env bash\nset -euo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nROOT=\"$(cd \"${SCRIPT_DIR}/..\" && pwd)\"\n\nif [[ $# -lt 1 ]]; then\n echo \"usage: run_gtest.sh \u003cworkspace> [cargo args...]\" >&2\n exit 1\nfi\n\nWORKSPACE=\"$(cd \"$1\" && pwd)\"\nshift || true\n\nif [[ ! -d \"${WORKSPACE}\" ]]; then\n echo \"workspace not found: ${WORKSPACE}\" >&2\n exit 1\nfi\n\nif [[ $# -gt 0 ]]; then\n CMD=(cargo test \"$@\")\nelse\n CMD=(cargo test --test gtest)\nfi\n\nLOGFILE=\"$(mktemp)\"\nset +e\n(\n cd \"${WORKSPACE}\"\n \"${CMD[@]}\" -- --nocapture\n) 2>&1 | tee \"${LOGFILE}\"\nSTATUS=${PIPESTATUS[0]}\nset -e\n\npython3 \"${ROOT}/scripts/parse_test_output.py\" \"${LOGFILE}\"\nrm -f \"${LOGFILE}\"\nexit \"${STATUS}\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":697,"content_sha256":"c1f133d8608432f74f23704f84dda0c2c381dd62002e1451ca6b010bc1a5d699"},{"filename":"scripts/validate-repo.sh","content":"#!/usr/bin/env bash\nset -euo pipefail\n\nROOT=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")/..\" && pwd)\"\n\npython3 \"${ROOT}/tests/test_repo_layout.py\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":140,"content_sha256":"847760f9b96c3bc9472d5e306aa1fa7da377da52737986b51adecdf1c6729d41"},{"filename":"scripts/validate-skill.py","content":"#!/usr/bin/env python3\n\nfrom __future__ import annotations\n\nfrom pathlib import Path\nimport re\nimport sys\n\n\nNAME_RE = re.compile(r\"^[a-z0-9]+(?:-[a-z0-9]+)*$\")\nNEGATIVE_TRIGGER_PATTERNS = (\"Do not use\", \"Don't use\")\nALLOWED_FRONTMATTER_FIELDS = {\"name\", \"description\"}\n\n\ndef fail(message: str) -> int:\n print(message, file=sys.stderr)\n return 1\n\n\ndef parse_frontmatter(skill_md: Path) -> tuple[str, str]:\n lines = skill_md.read_text(encoding=\"utf-8\").splitlines()\n if len(lines) > 500:\n raise ValueError(f\"SKILL.md exceeds 500 lines: {len(lines)}\")\n if len(lines) \u003c 4 or lines[0] != \"---\":\n raise ValueError(\"SKILL.md must start with YAML frontmatter\")\n try:\n closing = lines[1:].index(\"---\") + 1\n except ValueError as err:\n raise ValueError(\"SKILL.md frontmatter is not closed\") from err\n fields: dict[str, str] = {}\n for line in lines[1:closing]:\n if \":\" not in line:\n raise ValueError(f\"malformed frontmatter line: {line}\")\n key, value = line.split(\":\", 1)\n fields[key.strip()] = value.strip().strip('\"')\n extra_fields = sorted(set(fields) - ALLOWED_FRONTMATTER_FIELDS)\n if extra_fields:\n raise ValueError(\n \"frontmatter must contain only name and description; \"\n f\"found extra fields: {', '.join(extra_fields)}\"\n )\n name = fields.get(\"name\")\n description = fields.get(\"description\")\n if not name or not description:\n raise ValueError(\"frontmatter must contain name and description\")\n if not NAME_RE.match(name):\n raise ValueError(\"name must use lowercase letters, numbers, and single hyphens only\")\n if len(name) > 64:\n raise ValueError(\"name must be at most 64 characters\")\n if len(name) + len(description) > 1024:\n raise ValueError(\"frontmatter exceeds 1024 combined characters\")\n if not description.startswith(\"Use when\"):\n raise ValueError(\"description must start with 'Use when'\")\n if not any(pattern in description for pattern in NEGATIVE_TRIGGER_PATTERNS):\n raise ValueError(\"description must contain a negative trigger such as 'Do not use'\")\n return name, description\n\n\ndef validate_support_directory(path: Path, dirname: str) -> None:\n support_dir = path / dirname\n if not support_dir.is_dir():\n raise ValueError(f\"missing required directory: {dirname}\")\n for child in support_dir.iterdir():\n if child.name.startswith(\".\"):\n continue\n if child.is_dir():\n raise ValueError(\n f\"{dirname} must stay one level deep; found nested directory: {child.relative_to(path)}\"\n )\n\n\ndef validate(path: Path) -> None:\n if not path.is_dir():\n raise ValueError(f\"skill path is not a directory: {path}\")\n skill_md = path / \"SKILL.md\"\n if not skill_md.exists():\n raise ValueError(f\"missing SKILL.md in {path}\")\n name, _ = parse_frontmatter(skill_md)\n if path.name != name:\n raise ValueError(f\"directory name '{path.name}' must match skill name '{name}'\")\n for dirname in (\"scripts\", \"references\", \"assets\"):\n validate_support_directory(path, dirname)\n\n\ndef main() -> int:\n if len(sys.argv) != 2:\n return fail(\"usage: validate-skill.py \u003cskill-dir>\")\n try:\n validate(Path(sys.argv[1]).resolve())\n except ValueError as err:\n return fail(str(err))\n print(\"ok\")\n return 0\n\n\nif __name__ == \"__main__\":\n raise SystemExit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":3476,"content_sha256":"b38facf066107fe7652ef78653bbf5ff591800ddaf2661af70386ef8122ab38b"},{"filename":"scripts/verify-real-sails-program.sh","content":"#!/usr/bin/env bash\nset -euo pipefail\n\nROOT=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")/..\" && pwd)\"\nSAILS_RS_PATH=\"${SAILS_RS_PATH:-\"$ROOT/../sails/rs\"}\"\n\nif [ ! -f \"$SAILS_RS_PATH/Cargo.toml\" ]; then\n echo \"missing Sails workspace at $SAILS_RS_PATH\" >&2\n echo \"set SAILS_RS_PATH=/path/to/sails/rs if ../sails/rs is not available\" >&2\n exit 1\nfi\n\nif ! cargo sails --help >/dev/null 2>&1; then\n echo \"missing cargo-sails; install with: cargo install sails-cli\" >&2\n exit 1\nfi\n\nif ! rustup target list --installed | grep -qx \"wasm32v1-none\"; then\n echo \"missing Rust target wasm32v1-none; install with: rustup target add wasm32v1-none\" >&2\n exit 1\nfi\n\nTMPDIR=\"$(mktemp -d)\"\ntrap 'rm -rf \"$TMPDIR\"' EXIT\n\nAPP_DIR=\"$TMPDIR/real-check\"\ncargo sails new \"$APP_DIR\" --name real-check --sails-path \"$SAILS_RS_PATH\" --offline\n\ncat > \"$APP_DIR/tests/header_payload.rs\" \u003c\u003c'RS'\nuse real_check_client::{RealCheckClientProgram, real_check::io::DoSomething};\n\n#[test]\nfn generated_service_call_uses_sails_header_v1() {\n let payload = DoSomething::encode_call(RealCheckClientProgram::ROUTE_ID_REAL_CHECK);\n\n assert!(payload.len() >= 16);\n assert_eq!(&payload[0..2], b\"GM\");\n assert_eq!(payload[2], 1);\n assert_eq!(payload[3], 0x10);\n}\nRS\n\nexport CARGO_TARGET_DIR=\"${CARGO_TARGET_DIR:-\"$ROOT/target/verify-real-sails-program\"}\"\n\ncargo build --manifest-path \"$APP_DIR/Cargo.toml\" --release --locked\ncargo test --manifest-path \"$APP_DIR/Cargo.toml\" --workspace --locked\n\ntest -s \"$APP_DIR/client/real_check_client.idl\"\ntest -s \"$APP_DIR/client/src/real_check_client.rs\"\ngrep -q \"ROUTE_ID_REAL_CHECK\" \"$APP_DIR/client/src/real_check_client.rs\"\ngrep -q \"io_struct_impl!(DoSomething\" \"$APP_DIR/client/src/real_check_client.rs\"\n\necho \"real Sails program verification ok\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":1764,"content_sha256":"493ef72e1ca227305e40c333382a41e051f671661f57658aa61f3bd54ce63495"},{"filename":"tests/test_gstd_api_map_skill.py","content":"#!/usr/bin/env python3\n\nfrom pathlib import Path\nimport subprocess\nimport sys\n\n\nROOT = Path(__file__).resolve().parents[1]\nVALIDATOR = ROOT / \"scripts\" / \"validate-skill.py\"\n\n\ndef require(path: Path) -> None:\n assert path.exists(), f\"missing expected path: {path.relative_to(ROOT)}\"\n\n\ndef validate(skill_dir: Path) -> None:\n result = subprocess.run(\n [sys.executable, str(VALIDATOR), str(skill_dir)],\n text=True,\n capture_output=True,\n check=False,\n )\n assert result.returncode == 0, result.stderr\n\n\ndef read(relative: str) -> str:\n return (ROOT / relative).read_text(encoding=\"utf-8\")\n\n\ndef main() -> int:\n skill_dir = ROOT / \"skills\" / \"gear-gstd-api-map\"\n require(skill_dir)\n require(skill_dir / \"SKILL.md\")\n require(skill_dir / \"assets\" / \"pressure-scenarios.md\")\n require(skill_dir / \"references\")\n require(skill_dir / \"scripts\")\n require(ROOT / \"references\" / \"gear-gstd-api-and-syscalls.md\")\n validate(skill_dir)\n\n skill = read(\"skills/gear-gstd-api-map/SKILL.md\")\n skill_lower = skill.lower()\n assert \"../../references/gear-gstd-api-and-syscalls.md\" in skill\n assert \"gstd\" in skill and \"gcore\" in skill and \"gsys\" in skill\n assert \"design\" in skill_lower and \"debug\" in skill_lower\n assert \"msg\" in skill and \"exec\" in skill and \"prog\" in skill\n assert \"intent\" in skill_lower and \"syscall\" in skill_lower\n\n reference = read(\"references/gear-gstd-api-and-syscalls.md\")\n reference_lower = reference.lower()\n assert \"gstd\" in reference and \"gcore\" in reference and \"gsys\" in reference\n assert \"msg\" in reference and \"exec\" in reference and \"prog\" in reference\n assert \"gr_send\" in reference\n assert \"gr_reply\" in reference\n assert \"gr_reserve_gas\" in reference\n assert \"gr_wait\" in reference\n assert \"design first\" in reference_lower or \"design-first\" in reference_lower\n assert \"debugging second\" in reference_lower or \"debugging\" in reference_lower\n\n planner = read(\"skills/gear-architecture-planner/SKILL.md\")\n sails_architecture = read(\"skills/sails-architecture/SKILL.md\")\n feature_workflow = read(\"skills/sails-feature-workflow/SKILL.md\")\n assert \"gear-gstd-api-map\" in planner\n assert \"gear-gstd-api-map\" in sails_architecture\n assert \"gear-gstd-api-map\" in feature_workflow\n\n router = read(\"SKILL.md\")\n assert \"gear-gstd-api-map\" not in router\n\n print(\"gstd api map skill ok\")\n return 0\n\n\nif __name__ == \"__main__\":\n try:\n raise SystemExit(main())\n except AssertionError as err:\n print(err, file=sys.stderr)\n raise SystemExit(1)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":2619,"content_sha256":"383da904731a7bd4b2f1a3192d423ecea4ed2d41b855ba352a3c4fb09cb2bd22"},{"filename":"tests/test_install_codex_skills.py","content":"#!/usr/bin/env python3\n\nfrom pathlib import Path\nimport os\nimport subprocess\nimport sys\nimport tempfile\n\n\nROOT = Path(__file__).resolve().parents[1]\nSCRIPT = ROOT / \"scripts\" / \"install-codex-skills.sh\"\nPACK_NAME = \"vara-skills\"\nEXPECTED_SKILLS = tuple(\n sorted(path.name for path in (ROOT / \"skills\").iterdir() if path.is_dir())\n)\n\n\ndef main() -> int:\n assert SCRIPT.exists(), \"missing expected path: scripts/install-codex-skills.sh\"\n with tempfile.TemporaryDirectory() as tmpdir:\n env = os.environ.copy()\n env[\"CODEX_HOME\"] = tmpdir\n result = subprocess.run(\n [\"bash\", str(SCRIPT)],\n cwd=ROOT,\n text=True,\n capture_output=True,\n check=False,\n env=env,\n )\n assert result.returncode == 0, result.stderr\n target = Path(tmpdir) / \"skills\"\n pack_link = target / PACK_NAME\n assert pack_link.is_symlink(), f\"missing symlink for {PACK_NAME}\"\n assert pack_link.resolve() == ROOT.resolve(), (\n f\"wrong target for {PACK_NAME}: {pack_link.resolve()}\"\n )\n assert (pack_link / \"SKILL.md\").exists(), \"root pack should expose SKILL.md\"\n assert (pack_link / \"assets\").is_dir(), \"root pack should expose assets/\"\n assert (pack_link / \"references\").is_dir(), \"root pack should expose references/\"\n for skill in EXPECTED_SKILLS:\n link = target / skill\n assert link.is_symlink(), f\"missing symlink for {skill}\"\n assert link.resolve() == (ROOT / \"skills\" / skill).resolve(), (\n f\"wrong target for {skill}: {link.resolve()}\"\n )\n print(\"install script ok\")\n return 0\n\n\nif __name__ == \"__main__\":\n try:\n raise SystemExit(main())\n except AssertionError as err:\n print(err, file=sys.stderr)\n raise SystemExit(1)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":1863,"content_sha256":"f6bc8afe462a3b0be3f27f36a52cf0f03adaed04dec371e2f8b0c6531fc9353c"},{"filename":"tests/test_packaging_metadata.py","content":"#!/usr/bin/env python3\n\nfrom __future__ import annotations\n\nfrom pathlib import Path\nimport json\nimport re\nimport sys\n\n\nROOT = Path(__file__).resolve().parents[1]\n\n\ndef require(path: Path) -> None:\n assert path.exists(), f\"missing expected path: {path.relative_to(ROOT)}\"\n\n\ndef load_json(path: Path) -> dict[str, object]:\n return json.loads(path.read_text(encoding=\"utf-8\"))\n\n\ndef assert_nonempty_string(value: object, label: str) -> None:\n assert isinstance(value, str) and value.strip(), label\n\n\ndef main() -> int:\n plugin_path = ROOT / \".claude-plugin\" / \"plugin.json\"\n marketplace_path = ROOT / \".claude-plugin\" / \"marketplace.json\"\n openclaw_skill_path = ROOT / \"openclaw-skill\" / \"SKILL.md\"\n openclaw_readme_path = ROOT / \"openclaw-skill\" / \"README.md\"\n readme_path = ROOT / \"README.md\"\n claude_path = ROOT / \"CLAUDE.md\"\n skill_count = len(list((ROOT / \"skills\").glob(\"*/SKILL.md\")))\n\n require(plugin_path)\n require(marketplace_path)\n require(openclaw_skill_path)\n require(openclaw_readme_path)\n\n plugin = load_json(plugin_path)\n for field in (\"name\", \"description\"):\n assert plugin.get(field), f\"plugin.json missing field: {field}\"\n assert plugin[\"name\"] == \"vara-skills\", \"plugin.json should publish the pack as vara-skills\"\n # Version is managed solely in marketplace.json for relative-path plugins\n # (per Claude Code docs: plugin.json version silently overrides marketplace version)\n assert \"version\" not in plugin, \"plugin.json should not set version for relative-path plugins (set in marketplace.json only)\"\n for field in (\"homepage\", \"repository\"):\n value = plugin.get(field)\n if value is not None:\n assert_nonempty_string(value, f\"plugin.json field should be a non-empty string when present: {field}\")\n assert_nonempty_string(plugin.get(\"license\"), \"plugin.json should include an SPDX license identifier\")\n plugin_author = plugin.get(\"author\")\n assert isinstance(plugin_author, dict), \"plugin author should use object form\"\n assert_nonempty_string(plugin_author.get(\"name\"), \"plugin author should include a non-empty name\")\n keywords = plugin.get(\"keywords\", [])\n assert isinstance(keywords, list), \"plugin keywords must be an array\"\n assert \"vara\" in keywords and \"sails\" in keywords, \"plugin keywords should expose Vara and Sails\"\n assert f\"{skill_count} skills\" in plugin[\"description\"], \"plugin description should match installable skill count\"\n\n marketplace = load_json(marketplace_path)\n assert marketplace.get(\"name\") == \"vara-skills\", \"marketplace should publish a public vara-skills name\"\n owner = marketplace.get(\"owner\")\n assert isinstance(owner, dict), \"marketplace owner should use the object form expected by Claude Code\"\n assert_nonempty_string(owner.get(\"name\"), \"marketplace owner should include a non-empty name\")\n # owner only supports name and email per Claude Code docs\n for key in owner:\n assert key in (\"name\", \"email\"), f\"marketplace owner has unsupported field: {key}\"\n metadata = marketplace.get(\"metadata\")\n assert isinstance(metadata, dict), \"marketplace.json missing metadata block\"\n assert re.match(r\"^\\d+\\.\\d+\\.\\d+$\", str(metadata.get(\"version\", \"\"))), \"marketplace metadata version must use semver\"\n assert metadata.get(\"description\"), \"marketplace metadata should describe the pack\"\n plugins = marketplace.get(\"plugins\")\n assert isinstance(plugins, list) and plugins, \"marketplace.json must list at least one plugin\"\n first_plugin = plugins[0]\n assert first_plugin.get(\"name\") == \"vara-skills\", \"marketplace should expose vara-skills\"\n assert first_plugin.get(\"source\") == \"./\", \"marketplace should point local testing at the repo root\"\n assert re.match(r\"^\\d+\\.\\d+\\.\\d+$\", str(first_plugin.get(\"version\", \"\"))), \"marketplace plugin version must use semver\"\n assert first_plugin.get(\"version\") == metadata.get(\"version\"), \"marketplace plugin version should match metadata version\"\n assert first_plugin.get(\"description\") == plugin[\"description\"], \"marketplace plugin description should match plugin.json\"\n assert f\"{skill_count} skills\" in first_plugin.get(\"description\", \"\"), (\n \"marketplace description should match installable skill count\"\n )\n assert_nonempty_string(first_plugin.get(\"license\"), \"marketplace plugin entry should include an SPDX license identifier\")\n author = first_plugin.get(\"author\")\n assert isinstance(author, dict), \"marketplace plugin author should use object form\"\n assert_nonempty_string(author.get(\"name\"), \"marketplace plugin author should include a non-empty name\")\n\n version_path = ROOT / \"VERSION\"\n require(version_path)\n file_version = version_path.read_text(encoding=\"utf-8\").strip()\n assert file_version == metadata.get(\"version\"), (\n f\"VERSION file ({file_version}) must match marketplace.json metadata version ({metadata.get('version')})\"\n )\n\n openclaw_skill = openclaw_skill_path.read_text(encoding=\"utf-8\")\n assert \"ship-sails-app\" in openclaw_skill, \"OpenClaw wrapper should route through ship-sails-app\"\n assert \"standard Gear/Vara Sails\" in openclaw_skill, \"OpenClaw wrapper should constrain scope\"\n\n openclaw_readme = openclaw_readme_path.read_text(encoding=\"utf-8\")\n assert \"OpenClaw\" in openclaw_readme\n assert \"SKILL.md\" in openclaw_readme\n\n readme = readme_path.read_text(encoding=\"utf-8\")\n claude = claude_path.read_text(encoding=\"utf-8\")\n assert f\"{skill_count} skills\" in readme, \"README should match installable skill count\"\n assert f\"({skill_count} total)\" in claude, \"CLAUDE.md should match installable skill count\"\n\n print(\"packaging metadata ok\")\n return 0\n\n\nif __name__ == \"__main__\":\n try:\n raise SystemExit(main())\n except AssertionError as err:\n print(err, file=sys.stderr)\n raise SystemExit(1)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":5895,"content_sha256":"5053decd678a5fbca1a39bdb4aec0f5f710d48e39d26d605650a152eb3c24bb3"},{"filename":"tests/test_parse_test_output.py","content":"#!/usr/bin/env python3\n\nfrom pathlib import Path\nimport json\nimport subprocess\nimport sys\n\n\nROOT = Path(__file__).resolve().parents[1]\nSCRIPT = ROOT / \"scripts\" / \"parse_test_output.py\"\nFIXTURE = ROOT / \"tests\" / \"fixtures\" / \"gtest-failure.log\"\nERROR_FIXTURE = ROOT / \"tests\" / \"fixtures\" / \"gtest-workspace-error.log\"\n\n\ndef run_fixture(path: Path) -> dict[str, object]:\n result = subprocess.run(\n [sys.executable, str(SCRIPT), str(path)],\n text=True,\n capture_output=True,\n check=False,\n )\n assert result.returncode == 0, result.stderr\n return json.loads(result.stdout)\n\n\ndef main() -> int:\n assert SCRIPT.exists(), \"missing expected path: scripts/parse_test_output.py\"\n payload = run_fixture(FIXTURE)\n assert payload[\"status\"] == \"failed\", payload\n assert payload[\"failed_tests\"] == [\"tests::fails_on_missing_reply\"], payload\n assert \"expected reply event\" in payload[\"summary\"], payload\n\n error_payload = run_fixture(ERROR_FIXTURE)\n assert error_payload[\"status\"] == \"error\", error_payload\n assert error_payload[\"failed_tests\"] == [], error_payload\n assert \"failed to load manifest\" in error_payload[\"summary\"], error_payload\n print(\"gtest parser ok\")\n return 0\n\n\nif __name__ == \"__main__\":\n try:\n raise SystemExit(main())\n except AssertionError as err:\n print(err, file=sys.stderr)\n raise SystemExit(1)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":1408,"content_sha256":"ed82f298de93b282410eba6952d09a3020e0ac336870cf47739cd34e09d069f9"},{"filename":"tests/test_repo_layout.py","content":"#!/usr/bin/env python3\n\nfrom pathlib import Path\nimport sys\n\n\nROOT = Path(__file__).resolve().parents[1]\nLOCAL_PATTERNS = (\n \"/\" + \"Users/\",\n \"~/\" + \".codex/skills\",\n \"..\" + \"/gear-skills\",\n \"Documents\" + \"/projects/\",\n)\n\n\ndef require(path: Path) -> None:\n assert path.exists(), f\"missing expected path: {path.relative_to(ROOT)}\"\n\n\ndef sanitized_files() -> list[Path]:\n return [\n ROOT / \"README.md\",\n ROOT / \"SKILL.md\",\n ROOT / \"openclaw-skill\" / \"README.md\",\n ROOT / \"openclaw-skill\" / \"SKILL.md\",\n ROOT / \"references\" / \"gear-gstd-api-and-syscalls.md\",\n ROOT / \"references\" / \"gear-sails-production-patterns.md\",\n ROOT / \"skills\" / \"gear-gstd-api-map\" / \"SKILL.md\",\n ROOT / \"skills\" / \"gear-gstd-api-map\" / \"assets\" / \"pressure-scenarios.md\",\n ROOT / \"tests\" / \"fixtures\" / \"gtest-workspace-error.log\",\n ]\n\n\ndef main() -> int:\n require(ROOT / \"AGENTS.md\")\n require(ROOT / \"README.md\")\n require(ROOT / \"SKILL.md\")\n require(ROOT / \"Makefile\")\n require(ROOT / \".gitignore\")\n require(ROOT / \"assets\" / \"spec-template.md\")\n require(ROOT / \"assets\" / \"architecture-template.md\")\n require(ROOT / \"assets\" / \"task-plan-template.md\")\n require(ROOT / \"assets\" / \"gtest-report-template.md\")\n require(ROOT / \".claude-plugin\" / \"plugin.json\")\n require(ROOT / \".claude-plugin\" / \"marketplace.json\")\n require(ROOT / \"openclaw-skill\" / \"SKILL.md\")\n require(ROOT / \"openclaw-skill\" / \"README.md\")\n require(ROOT / \"references\" / \"vara-domain-overview.md\")\n require(ROOT / \"references\" / \"sails-cheatsheet.md\")\n require(ROOT / \"references\" / \"sails-rs-imports.md\")\n require(ROOT / \"references\" / \"gtest-cheatsheet.md\")\n require(ROOT / \"references\" / \"gtest-patterns.md\")\n require(ROOT / \"references\" / \"varaeth-extension-notes.md\")\n require(ROOT / \"references\" / \"gear-execution-model.md\")\n require(ROOT / \"references\" / \"gear-gstd-api-and-syscalls.md\")\n require(ROOT / \"references\" / \"gear-messaging-and-replies.md\")\n require(ROOT / \"references\" / \"gear-gas-reservations-and-waitlist.md\")\n require(ROOT / \"references\" / \"gear-sails-production-patterns.md\")\n require(ROOT / \"references\" / \"delayed-message-pattern.md\")\n require(ROOT / \"references\" / \"sails-program-and-service-architecture.md\")\n require(ROOT / \"references\" / \"sails-idl-client-pipeline.md\")\n require(ROOT / \"references\" / \"sails-gtest-and-local-validation.md\")\n require(ROOT / \"references\" / \"vara-network-endpoints.md\")\n require(ROOT / \"references\" / \"vara-eth-bridge-contracts.md\")\n require(ROOT / \"references\" / \"vara-eth-bridge-flows.md\")\n require(ROOT / \"references\" / \"voucher-and-signless-flows.md\")\n require(ROOT / \"skills\" / \"gear-gstd-api-map\" / \"SKILL.md\")\n require(ROOT / \"skills\" / \"gear-message-execution\" / \"SKILL.md\")\n require(ROOT / \"scripts\" / \"install-codex-skills.sh\")\n require(ROOT / \"scripts\" / \"validate-repo.sh\")\n require(ROOT / \"tests\" / \"test_install_codex_skills.py\")\n require(ROOT / \"VERSION\")\n require(ROOT / \"bin\" / \"vara-skills-update-check\")\n require(ROOT / \"tests\")\n\n for path in sanitized_files():\n text = path.read_text(encoding=\"utf-8\")\n for pattern in LOCAL_PATTERNS:\n assert pattern not in text, (\n f\"{path.relative_to(ROOT)} should not contain machine-specific path pattern: {pattern}\"\n )\n print(\"repo layout ok\")\n return 0\n\n\nif __name__ == \"__main__\":\n try:\n raise SystemExit(main())\n except AssertionError as err:\n print(err, file=sys.stderr)\n raise SystemExit(1)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":3646,"content_sha256":"5b84692248eddc5477af89ecf4c2a68e17dc985beeb30315224f5d85a2307654"},{"filename":"tests/test_skill_catalog.py","content":"#!/usr/bin/env python3\n\nfrom pathlib import Path\nimport subprocess\nimport sys\n\n\nROOT = Path(__file__).resolve().parents[1]\nVALIDATOR = ROOT / \"scripts\" / \"validate-skill.py\"\nSTARTER_SKILLS = {\n \"gear-message-execution\": [\n \"SKILL.md\",\n \"assets\",\n \"references\",\n \"scripts\",\n ],\n \"sails-dev-env\": [\n \"SKILL.md\",\n \"assets\",\n \"references\",\n \"scripts\",\n ],\n \"ship-sails-app\": [\n \"SKILL.md\",\n \"assets\",\n \"references\",\n \"scripts\",\n ],\n \"sails-new-app\": [\n \"SKILL.md\",\n \"assets\",\n \"references\",\n \"scripts\",\n ],\n \"sails-feature-workflow\": [\n \"SKILL.md\",\n \"assets\",\n \"references\",\n \"scripts\",\n ],\n \"sails-architecture\": [\n \"SKILL.md\",\n \"assets\",\n \"references\",\n \"scripts\",\n ],\n \"sails-idl-client\": [\n \"SKILL.md\",\n \"assets\",\n \"references\",\n \"scripts\",\n ],\n \"sails-gtest\": [\n \"SKILL.md\",\n \"assets\",\n \"references\",\n \"scripts\",\n ],\n \"sails-local-smoke\": [\n \"SKILL.md\",\n \"assets\",\n \"references\",\n \"scripts\",\n ],\n \"awesome-sails-vft\": [\n \"SKILL.md\",\n \"assets\",\n \"references\",\n \"scripts\",\n ],\n \"sails-frontend\": [\n \"SKILL.md\",\n \"assets\",\n \"references\",\n \"scripts\",\n ],\n \"sails-indexer\": [\n \"SKILL.md\",\n \"assets\",\n \"references\",\n \"scripts\",\n ],\n \"gear-builtin-actors\": [\n \"SKILL.md\",\n \"assets\",\n \"references\",\n \"scripts\",\n ],\n \"sails-ethexe-architecture\": [\n \"SKILL.md\",\n \"assets\",\n \"references\",\n \"scripts\",\n ],\n \"sails-ethexe-implementer\": [\n \"SKILL.md\",\n \"assets\",\n \"references\",\n \"scripts\",\n ],\n \"vara-wallet\": [\n \"SKILL.md\",\n \"assets\",\n \"references\",\n \"scripts\",\n ],\n}\n\n\ndef require(path: Path) -> None:\n assert path.exists(), f\"missing expected path: {path.relative_to(ROOT)}\"\n\n\ndef validate(skill_dir: Path) -> None:\n result = subprocess.run(\n [sys.executable, str(VALIDATOR), str(skill_dir)],\n text=True,\n capture_output=True,\n check=False,\n )\n assert result.returncode == 0, result.stderr\n\n\ndef read(relative: str) -> str:\n return (ROOT / relative).read_text(encoding=\"utf-8\")\n\n\ndef main() -> int:\n require(VALIDATOR)\n require(ROOT / \"SKILL.md\")\n\n for skill_name, expected_paths in STARTER_SKILLS.items():\n skill_dir = ROOT / \"skills\" / skill_name\n require(skill_dir)\n for relative in expected_paths:\n require(skill_dir / relative)\n validate(skill_dir)\n\n router = read(\"SKILL.md\")\n assert \"ship-sails-app\" in router\n assert \"sails-dev-env\" in router\n assert \"Codex\" in router and \"Claude\" in router and \"OpenClaw\" in router\n\n dev_env = read(\"skills/sails-dev-env/SKILL.md\")\n dev_env_lower = dev_env.lower()\n assert \"rustup\" in dev_env_lower\n assert \"cargo install sails-cli\" in dev_env_lower\n assert \"cargo-sails\" in dev_env_lower\n assert \"cargo sails new\" in dev_env_lower\n assert \"wasm32-unknown-unknown\" in dev_env\n assert \"wasm32v1-none\" in dev_env\n assert \"install-gear.sh\" in dev_env or \"install-gear.ps1\" in dev_env\n assert \"../gear\" not in dev_env\n\n ship = read(\"skills/ship-sails-app/SKILL.md\")\n assert \"../../references/gear-execution-model.md\" in ship\n assert \"../../references/gear-messaging-and-replies.md\" in ship\n assert \"../../references/gear-gas-reservations-and-waitlist.md\" in ship\n assert \"../../references/sails-program-and-service-architecture.md\" in ship\n assert \"../../references/sails-idl-client-pipeline.md\" in ship\n assert \"../../references/sails-gtest-and-local-validation.md\" in ship\n assert \"../../references/sails-rs-imports.md\" in ship\n assert \"../../references/delayed-message-pattern.md\" in ship\n assert \"../../references/scale-binary-decoding-guide.md\" in ship\n assert \"../../references/voucher-and-signless-flows.md\" in ship\n assert \"gear-message-execution\" in ship\n assert \"sails-dev-env\" in ship\n assert \"cargo sails new\" in ship\n assert \"1.0.0\" in ship\n assert \"build.rs\" in ship\n\n\n new_app = read(\"skills/sails-new-app/SKILL.md\")\n assert \"Sails\" in new_app\n assert \".idl\" in new_app and \".opt.wasm\" in new_app\n assert \"build.rs\" in new_app\n assert \"cargo sails new\" in new_app\n assert \"../../references/sails-idl-client-pipeline.md\" in new_app\n assert \"../../references/sails-program-and-service-architecture.md\" in new_app\n assert \"sails-dev-env\" in new_app\n assert \"sails-dev-env\" in new_app\n\n feature = read(\"skills/sails-feature-workflow/SKILL.md\")\n feature_lower = feature.lower()\n assert \"gtest\" in feature_lower\n assert \"gear-gstd-api-map\" in feature\n assert \"../../references/gear-execution-model.md\" in feature\n assert \"../../references/gear-messaging-and-replies.md\" in feature\n assert \"../../references/sails-gtest-and-local-validation.md\" in feature\n assert \"../../references/voucher-and-signless-flows.md\" in feature\n\n architecture = read(\"skills/sails-architecture/SKILL.md\")\n architecture_lower = architecture.lower()\n assert \"state\" in architecture_lower\n assert \"reservation\" in architecture_lower or \"waitlist\" in architecture_lower\n assert \"../../references/gear-sails-production-patterns.md\" in architecture\n assert \"../../references/sails-program-and-service-architecture.md\" in architecture\n assert \"../../references/gear-messaging-and-replies.md\" in architecture\n assert \"../../references/gear-gas-reservations-and-waitlist.md\" in architecture\n\n idl_client = read(\"skills/sails-idl-client/SKILL.md\")\n idl_client_lower = idl_client.lower()\n assert \"sails-js\" in idl_client_lower and \"parseidl\" in idl_client_lower\n assert \"build.rs\" in idl_client\n assert \"../../references/sails-idl-client-pipeline.md\" in idl_client\n\n frontend = read(\"skills/sails-frontend/SKILL.md\")\n frontend_lower = frontend.lower()\n assert \"useprogram\" in frontend_lower\n assert \"usesendprogramtransaction\" in frontend_lower\n assert \"useprepareprogramtransaction\" in frontend_lower\n assert \"voucherid\" in frontend_lower\n assert \"@gear-js/api\" in frontend\n assert \"../../references/sails-frontend-and-gear-js.md\" in frontend\n assert \"../../references/scale-binary-decoding-guide.md\" in frontend\n\n indexer = read(\"skills/sails-indexer/SKILL.md\")\n indexer_ref = read(\"references/sails-indexer-patterns.md\")\n indexer_env = read(\"skills/sails-indexer/assets/.env.example\")\n indexer_package = read(\"skills/sails-indexer/assets/package.json\")\n assert \"v4 bootstrap/config API\" in indexer\n assert \"v4 bootstrap/config API\" in indexer_ref\n assert \"Do not switch to v5 by changing package versions alone\" in indexer\n assert \"VARA_RPC_URL=wss://testnet.vara.network\" not in indexer_env\n assert \"archive RPC endpoint\" in indexer_env\n assert '\"postgraphile\": \"^4.' in indexer_package\n assert '\"postgraphile-plugin-connection-filter\": \"^2.' in indexer_package\n\n gtest_loop = read(\"skills/sails-gtest/SKILL.md\")\n assert \"../../references/sails-gtest-and-local-validation.md\" in gtest_loop\n assert \"../../references/gear-gas-reservations-and-waitlist.md\" in gtest_loop\n assert \"../../references/gtest-patterns.md\" in gtest_loop\n assert \"../../references/scale-binary-decoding-guide.md\" in gtest_loop\n assert \"MessageId\" in gtest_loop\n assert \"run_next_block\" in gtest_loop\n\n smoke = read(\"skills/sails-local-smoke/SKILL.md\")\n smoke_lower = smoke.lower()\n assert \"program id\" in smoke_lower\n assert \"../../references/sails-gtest-and-local-validation.md\" in smoke\n assert \"../../references/sails-idl-client-pipeline.md\" in smoke\n\n execution = read(\"skills/gear-message-execution/SKILL.md\")\n execution_lower = execution.lower()\n assert \"reply\" in execution_lower\n assert \"reservation\" in execution_lower or \"waitlist\" in execution_lower\n assert \"../../references/gear-sails-production-patterns.md\" in execution\n assert \"../../references/gear-execution-model.md\" in execution\n assert \"../../references/gear-messaging-and-replies.md\" in execution\n assert \"../../references/gear-gas-reservations-and-waitlist.md\" in execution\n assert \"../../references/scale-binary-decoding-guide.md\" in execution\n\n gear_execution = read(\"references/gear-execution-model.md\")\n gear_execution_lower = gear_execution.lower()\n assert \"block\" in gear_execution_lower and \"rollback\" in gear_execution_lower\n assert \"message queue\" in gear_execution_lower or \"queue\" in gear_execution_lower\n\n gear_messaging = read(\"references/gear-messaging-and-replies.md\")\n gear_messaging_lower = gear_messaging.lower()\n assert \"for_reply\" in gear_messaging\n assert \"reply\" in gear_messaging_lower\n assert \"delayed\" in gear_messaging_lower or \"reservation\" in gear_messaging_lower\n\n gas_waitlist = read(\"references/gear-gas-reservations-and-waitlist.md\")\n gas_waitlist_lower = gas_waitlist.lower()\n assert \"reservation\" in gas_waitlist_lower and \"waitlist\" in gas_waitlist_lower\n assert \"rent\" in gas_waitlist_lower or \"expiry\" in gas_waitlist_lower\n\n production_patterns = read(\"references/gear-sails-production-patterns.md\")\n production_patterns_lower = production_patterns.lower()\n assert \"awesome-sails\" in production_patterns_lower\n assert \"reservationid\" in production_patterns_lower\n assert \"generated client\" in production_patterns_lower\n assert \"voucher\" in production_patterns_lower and \"signless\" in production_patterns_lower\n assert \"examples/demo/app/src\" not in production_patterns\n assert \"examples/rmrk/\" not in production_patterns\n assert \"dapps/contracts/\" not in production_patterns\n assert \"awesome-sails/tests/\" not in production_patterns\n\n sails_arch_ref = read(\"references/sails-program-and-service-architecture.md\")\n sails_arch_ref_lower = sails_arch_ref.lower()\n assert \"#[program]\" in sails_arch_ref and \"#[service]\" in sails_arch_ref\n assert \"await\" in sails_arch_ref_lower and \"revalidate\" in sails_arch_ref_lower\n assert \"returning `self`\" in sails_arch_ref_lower or \"returning self\" in sails_arch_ref_lower\n assert \"program-owned state\" in sails_arch_ref_lower\n assert \"static service state\" in sails_arch_ref_lower\n\n sails_cheatsheet = read(\"references/sails-cheatsheet.md\")\n sails_cheatsheet_lower = sails_cheatsheet.lower()\n assert \"constructors\" in sails_cheatsheet_lower and \"return `self`\" in sails_cheatsheet_lower\n assert \"generated clients\" in sails_cheatsheet_lower\n assert \"sails header\" in sails_cheatsheet_lower\n assert \"binary header\" in sails_cheatsheet_lower\n assert \"#[export]\" in sails_cheatsheet\n assert \"emit_event\" in sails_cheatsheet\n assert \"1.0.0\" in sails_cheatsheet\n assert \"../sails\" not in sails_cheatsheet\n assert \"Troubleshooting\" in sails_cheatsheet\n assert \"parity-scale-codec\" in sails_cheatsheet\n\n idl_pipeline = read(\"references/sails-idl-client-pipeline.md\")\n idl_pipeline_lower = idl_pipeline.lower()\n assert \"build.rs\" in idl_pipeline\n assert \"build_client\" in idl_pipeline_lower or \"clientbuilder\" in idl_pipeline_lower\n assert \".idl\" in idl_pipeline\n assert \"build.rs\" in idl_pipeline\n assert 'features = [\"build\"]' in idl_pipeline\n\n frontend_ref = read(\"references/sails-frontend-and-gear-js.md\")\n frontend_ref_lower = frontend_ref.lower()\n assert \"useprogramquery\" in frontend_ref_lower\n assert \"useprogramevent\" in frontend_ref_lower\n assert \"signandsend\" in frontend_ref_lower\n assert \"getprogrammetadata\" in frontend_ref_lower\n assert \"api.message.send\" in frontend_ref\n assert \"wallet\" in frontend_ref_lower\n\n validation_ref = read(\"references/sails-gtest-and-local-validation.md\")\n validation_ref_lower = validation_ref.lower()\n assert \"gtestenv\" in validation_ref_lower\n assert \"blockrunmode\" in validation_ref_lower\n assert \"program id\" in validation_ref_lower\n assert \"local node\" in validation_ref_lower\n assert \"messageid\" in validation_ref_lower\n assert \"blockrunresult\" in validation_ref_lower\n assert \"run_to_block\" in validation_ref_lower\n\n sails_imports = read(\"references/sails-rs-imports.md\")\n assert \"1.0.0\" in sails_imports\n assert \"build.rs\" in sails_imports\n assert \"#[codec(crate = sails_rs::scale_codec)]\" in sails_imports\n assert \"#[scale_info(crate = sails_rs::scale_info)]\" in sails_imports\n assert \"#[export]\" in sails_imports\n assert \"emit_event\" in sails_imports\n assert \"../sails\" not in sails_imports\n assert \"../awesome-sails\" not in sails_imports\n assert \"../dapps\" not in sails_imports\n\n idl_client = read(\"skills/sails-idl-client/SKILL.md\")\n assert \"Generated Client Pitfalls\" in idl_client\n\n gtest_skill = read(\"skills/sails-gtest/SKILL.md\")\n assert \"Common Pitfalls\" in gtest_skill\n assert \"Rust 2024 listener lifetime\" in gtest_skill\n assert \"Sails 1.0\" in gtest_skill\n assert \"client.listen().await\" in gtest_skill\n\n gtest_patterns = read(\"references/gtest-patterns.md\")\n gtest_patterns_lower = gtest_patterns.lower()\n assert \"messageid\" in gtest_patterns_lower\n assert \"blockrunresult\" in gtest_patterns_lower\n assert \"r.log()\" in gtest_patterns\n assert \"run_to_block\" in gtest_patterns_lower\n assert \"failed.contains\" in gtest_patterns\n assert \"Program\u003c'_\" in gtest_patterns\n\n delayed_message = read(\"references/delayed-message-pattern.md\")\n delayed_message_lower = delayed_message.lower()\n assert \"send_bytes_with_gas_delayed\" in delayed_message\n assert \"Syscall::program_id()\" in delayed_message\n assert \"Syscall::gas_available()\" in delayed_message\n assert \"encode_call\" in delayed_message\n assert \"ROUTE_ID\" in delayed_message\n assert \"legacy\" in delayed_message_lower\n assert \"Syscall::message_source() == Syscall::program_id()\" in delayed_message\n assert \"idempot\" in delayed_message_lower\n assert \"../sails\" not in delayed_message\n assert \"../awesome-sails\" not in delayed_message\n assert \"../dapps\" not in delayed_message\n ship_no_sibling_paths = read(\"skills/ship-sails-app/SKILL.md\")\n assert \"../sails`\" not in ship_no_sibling_paths\n\n idl_v2 = read(\"references/sails-idl-v2-syntax.md\")\n assert \"alias map\u003cK,V> = [(K,V)];\" in idl_v2\n assert \"alias set\u003cT> = [T];\" in idl_v2\n assert \"map\u003cK,V>` and `set\u003cT>` are not built in\" in idl_v2\n\n header_wire = read(\"references/sails-header-wire-format.md\")\n header_wire_lower = header_wire.lower()\n assert \"renaming public methods **changes** the interface id\" in header_wire_lower\n assert \"header length: must equal `0x10` for v1\" in header_wire_lower\n assert \"extension-sized headers are reserved for future versions\" in header_wire_lower\n\n scale_decode = read(\"references/scale-binary-decoding-guide.md\")\n scale_decode_lower = scale_decode.lower()\n assert \"sails header\" in scale_decode_lower\n assert \"scale-encoded service and method name strings\" not in scale_decode_lower\n\n planner = read(\"skills/gear-architecture-planner/SKILL.md\")\n assert \"../../references/sails-program-and-service-architecture.md\" in planner\n assert \"../../references/gear-messaging-and-replies.md\" in planner\n assert \"../../references/sails-idl-client-pipeline.md\" in planner\n assert \"gear-gstd-api-map\" in planner\n\n gstd_capability = read(\"skills/gear-gstd-api-map/SKILL.md\")\n gstd_capability_lower = gstd_capability.lower()\n assert \"../../references/gear-gstd-api-and-syscalls.md\" in gstd_capability\n assert \"gstd\" in gstd_capability and \"gcore\" in gstd_capability and \"gsys\" in gstd_capability\n assert \"design\" in gstd_capability_lower and \"debug\" in gstd_capability_lower\n\n gstd_reference = read(\"references/gear-gstd-api-and-syscalls.md\")\n assert \"gr_send\" in gstd_reference\n assert \"gr_reply\" in gstd_reference\n assert \"gr_reserve_gas\" in gstd_reference\n assert \"gr_wait\" in gstd_reference\n\n builtin_actors = read(\"skills/gear-builtin-actors/SKILL.md\")\n builtin_actors_lower = builtin_actors.lower()\n assert \"../../references/gear-builtin-actors.md\" in builtin_actors\n assert \"../../references/gear-messaging-and-replies.md\" in builtin_actors\n assert \"../../references/gear-gas-reservations-and-waitlist.md\" in builtin_actors\n assert \"../../references/gear-gstd-api-and-syscalls.md\" in builtin_actors\n assert \"send_for_reply\" in builtin_actors_lower\n assert \"staking\" in builtin_actors_lower and \"proxy\" in builtin_actors_lower\n assert \"bls\" in builtin_actors_lower\n assert \"eth bridge\" in builtin_actors_lower or \"eth-bridge\" in builtin_actors_lower\n\n builtin_ref = read(\"references/gear-builtin-actors.md\")\n builtin_ref_lower = builtin_ref.lower()\n assert \"send_for_reply\" in builtin_ref or \"send_bytes_for_reply\" in builtin_ref\n assert \"ActorId\" in builtin_ref\n assert \"gbuiltin\" in builtin_ref_lower\n assert \"Bond\" in builtin_ref and \"AddProxy\" in builtin_ref\n assert \"MultiMillerLoop\" in builtin_ref\n assert \"pallet-gear-builtin\" in builtin_ref_lower or \"pallet_gear_builtin\" in builtin_ref_lower\n assert \"built/in\" in builtin_ref\n assert \"runtime/vara/src/lib.rs\" in builtin_ref\n\n router = read(\"SKILL.md\")\n assert \"gear-builtin-actors\" in router\n assert \"sails-ethexe-architecture\" in router\n assert \"sails-ethexe-implementer\" in router\n\n ethexe_arch = read(\"skills/sails-ethexe-architecture/SKILL.md\")\n assert \"../../references/sails-ethexe-patterns.md\" in ethexe_arch\n assert \"emit_eth_event\" in ethexe_arch\n assert \"sails-sol-gen\" in ethexe_arch\n\n ethexe_impl = read(\"skills/sails-ethexe-implementer/SKILL.md\")\n assert \"../../references/sails-ethexe-patterns.md\" in ethexe_impl\n assert \"#[export(ethabi\" in ethexe_impl\n assert \"emit_eth_event\" in ethexe_impl\n\n implementer = read(\"skills/sails-rust-implementer/SKILL.md\")\n implementer_lower = implementer.lower()\n assert \"../../references/gear-sails-production-patterns.md\" in implementer\n assert \"../../references/gear-messaging-and-replies.md\" in implementer\n assert \"../../references/gear-gas-reservations-and-waitlist.md\" in implementer\n assert \"../../references/sails-rs-imports.md\" in implementer\n assert \"../../references/delayed-message-pattern.md\" in implementer\n assert \"state ownership\" in implementer_lower\n assert \"generated clients\" in implementer_lower or \"generated client\" in implementer_lower\n assert \"#[export]\" in implementer\n assert \"emit_event\" in implementer\n gtest_tdd = read(\"skills/gtest-tdd-loop/SKILL.md\")\n assert \"../../references/sails-gtest-and-local-validation.md\" in gtest_tdd\n assert \"../../references/gear-gas-reservations-and-waitlist.md\" in gtest_tdd\n\n token_skill = read(\"skills/awesome-sails-vft/SKILL.md\")\n assert \"../../references/awesome-sails-token-patterns.md\" in token_skill\n assert \"token-scope-checklist.md\" in token_skill\n assert \"token-crate-chooser.md\" in token_skill\n assert \"token-gtest-matrix.md\" in token_skill\n\n scale_decode = read(\"references/scale-binary-decoding-guide.md\")\n scale_decode_lower = scale_decode.lower()\n assert \"decode::\u003cT>\" in scale_decode_lower or \"bare scale payload\" in scale_decode_lower\n assert \"state.meta.wasm\" in scale_decode\n assert \"programmetadata\" in scale_decode_lower\n assert \"gear-meta\" in scale_decode_lower\n assert \"sails header\" in scale_decode_lower\n\n endpoints = read(\"references/vara-network-endpoints.md\")\n assert \"wss://rpc.vara.network\" in endpoints\n assert \"SS58\" in endpoints\n assert \"137\" in endpoints\n assert \"10^12\" in endpoints\n\n bridge_contracts = read(\"references/vara-eth-bridge-contracts.md\")\n assert \"Last Verified\" in bridge_contracts\n assert \"Verifier\" in bridge_contracts\n assert \"MessageQueue\" in bridge_contracts\n assert \"vft-manager\" in bridge_contracts\n\n bridge_flows = read(\"references/vara-eth-bridge-flows.md\")\n assert \"Vara\" in bridge_flows\n assert \"Ethereum\" in bridge_flows\n assert \"ZK\" in bridge_flows or \"zero-knowledge\" in bridge_flows\n assert \"fee\" in bridge_flows.lower()\n\n voucher_ref = read(\"references/voucher-and-signless-flows.md\")\n voucher_ref_lower = voucher_ref.lower()\n assert \"when to use\" in voucher_ref_lower\n assert \"builder recipe\" in voucher_ref_lower\n assert \"api.voucher.issue\" in voucher_ref\n assert \"GearVoucher\" in voucher_ref\n assert \"signless\" in voucher_ref_lower\n assert \"failure\" in voucher_ref_lower\n assert \"lifecycle\" in voucher_ref_lower\n assert \"vara-wiki\" not in voucher_ref_lower\n assert \"../../references/voucher-and-signless-flows.md\" in frontend\n assert \"../../references/voucher-and-signless-flows.md\" in smoke\n\n wallet = read(\"skills/vara-wallet/SKILL.md\")\n assert \"voucher issue\" in wallet.lower()\n assert \"voucher revoke\" in wallet.lower()\n assert \"../../references/voucher-and-signless-flows.md\" in wallet\n\n frontend_ref = read(\"references/sails-frontend-and-gear-js.md\")\n assert \"voucher-and-signless-flows.md\" in frontend_ref\n upgrade_skill = ROOT / \"skills\" / \"vara-skills-upgrade\"\n require(upgrade_skill)\n validate(upgrade_skill)\n\n upgrade_md = read(\"skills/vara-skills-upgrade/SKILL.md\")\n assert \"Inline upgrade flow\" in upgrade_md or \"Inline Upgrade Flow\" in upgrade_md\n assert \"UPGRADE_AVAILABLE\" in upgrade_md\n assert \"JUST_UPGRADED\" in upgrade_md or \"just-upgraded-from\" in upgrade_md\n\n print(\"starter skills ok\")\n return 0\n\n\nif __name__ == \"__main__\":\n try:\n raise SystemExit(main())\n except AssertionError as err:\n print(err, file=sys.stderr)\n raise SystemExit(1)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":21980,"content_sha256":"71ca3b72d3fd6641c2c5afda8d805ff4ef685f976d8a36c20d1017c1a61c43a0"},{"filename":"tests/test_skill_validation.py","content":"#!/usr/bin/env python3\n\nfrom pathlib import Path\nimport subprocess\nimport sys\nimport tempfile\n\n\nROOT = Path(__file__).resolve().parents[1]\nVALIDATOR = ROOT / \"scripts\" / \"validate-skill.py\"\n\n\ndef run_validator(path: Path) -> subprocess.CompletedProcess[str]:\n return subprocess.run(\n [sys.executable, str(VALIDATOR), str(path)],\n text=True,\n capture_output=True,\n check=False,\n )\n\n\ndef write(path: Path, content: str) -> None:\n path.parent.mkdir(parents=True, exist_ok=True)\n path.write_text(content, encoding=\"utf-8\")\n\n\ndef main() -> int:\n assert VALIDATOR.exists(), \"missing expected path: scripts/validate-skill.py\"\n\n with tempfile.TemporaryDirectory() as tmpdir:\n tmp = Path(tmpdir)\n\n missing = tmp / \"missing-skill\"\n missing.mkdir()\n result = run_validator(missing)\n assert result.returncode != 0, \"validator should reject missing SKILL.md\"\n\n malformed = tmp / \"malformed-skill\"\n malformed.mkdir()\n write(malformed / \"SKILL.md\", \"# no frontmatter\\n\")\n result = run_validator(malformed)\n assert result.returncode != 0, \"validator should reject malformed frontmatter\"\n\n valid = tmp / \"valid-skill\"\n (valid / \"scripts\").mkdir(parents=True)\n (valid / \"references\").mkdir()\n (valid / \"assets\").mkdir()\n write(\n valid / \"SKILL.md\",\n \"---\\nname: valid-skill\\ndescription: Use when validating starter skills for Gear and Vara. Do not use this skill for production work.\\n---\\n\\n# Valid Skill\\n\",\n )\n result = run_validator(valid)\n assert result.returncode == 0, result.stderr\n\n skill_dirs = sorted(path for path in (ROOT / \"skills\").iterdir() if (path / \"SKILL.md\").exists())\n assert skill_dirs, \"expected at least one installable skill\"\n for skill_dir in skill_dirs:\n result = run_validator(skill_dir)\n assert result.returncode == 0, f\"{skill_dir.relative_to(ROOT)}: {result.stderr}\"\n\n print(\"skill validation ok\")\n return 0\n\n\nif __name__ == \"__main__\":\n try:\n raise SystemExit(main())\n except AssertionError as err:\n print(err, file=sys.stderr)\n raise SystemExit(1)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":2215,"content_sha256":"85c09bdc952ee813171f295cc3ae0f490a10906b4e88b93284ea764a68158cb8"},{"filename":"tests/test_update_check.py","content":"#!/usr/bin/env python3\n\nfrom __future__ import annotations\n\nimport os\nimport subprocess\nimport sys\nimport tempfile\nimport time\nfrom pathlib import Path\n\n\nROOT = Path(__file__).resolve().parents[1]\nSCRIPT = ROOT / \"bin\" / \"vara-skills-update-check\"\n\n\ndef run_check(\n skills_dir: str,\n state_dir: str,\n remote_url: str = \"\",\n force: bool = False,\n update_check: str | None = None,\n) -> subprocess.CompletedProcess[str]:\n env = {\n \"PATH\": os.environ[\"PATH\"],\n \"HOME\": os.environ.get(\"HOME\", \"/tmp\"),\n \"VARA_SKILLS_DIR\": skills_dir,\n \"VARA_SKILLS_STATE_DIR\": state_dir,\n }\n if remote_url:\n env[\"VARA_SKILLS_REMOTE_URL\"] = remote_url\n if update_check is not None:\n env[\"VARA_SKILLS_UPDATE_CHECK\"] = update_check\n cmd = [str(SCRIPT)]\n if force:\n cmd.append(\"--force\")\n return subprocess.run(cmd, capture_output=True, text=True, env=env, check=False)\n\n\ndef test_script_exists_and_is_executable() -> None:\n assert SCRIPT.exists(), \"bin/vara-skills-update-check must exist\"\n assert os.access(SCRIPT, os.X_OK), \"bin/vara-skills-update-check must be executable\"\n\n\ndef test_no_version_file_silent_exit() -> None:\n with tempfile.TemporaryDirectory() as skills_dir, tempfile.TemporaryDirectory() as state_dir:\n result = run_check(skills_dir, state_dir)\n assert result.returncode == 0\n assert result.stdout.strip() == \"\"\n\n\ndef test_disabled_via_env() -> None:\n with tempfile.TemporaryDirectory() as skills_dir, tempfile.TemporaryDirectory() as state_dir:\n Path(skills_dir, \"VERSION\").write_text(\"1.0.0\\n\")\n result = run_check(skills_dir, state_dir, update_check=\"false\")\n assert result.returncode == 0\n assert result.stdout.strip() == \"\"\n\n\ndef test_just_upgraded_marker() -> None:\n with tempfile.TemporaryDirectory() as skills_dir, tempfile.TemporaryDirectory() as state_dir:\n Path(skills_dir, \"VERSION\").write_text(\"2.0.0\\n\")\n Path(state_dir, \"just-upgraded-from\").write_text(\"1.0.0\\n\")\n result = run_check(skills_dir, state_dir)\n assert result.returncode == 0\n assert \"JUST_UPGRADED 1.0.0 2.0.0\" in result.stdout\n # Marker should be consumed\n assert not Path(state_dir, \"just-upgraded-from\").exists()\n # Cache should be written\n cache = Path(state_dir, \"last-update-check\").read_text()\n assert \"UP_TO_DATE 2.0.0\" in cache\n\n\ndef test_just_upgraded_marker_empty() -> None:\n with tempfile.TemporaryDirectory() as skills_dir, tempfile.TemporaryDirectory() as state_dir:\n Path(skills_dir, \"VERSION\").write_text(\"2.0.0\\n\")\n Path(state_dir, \"just-upgraded-from\").write_text(\"\\n\")\n result = run_check(skills_dir, state_dir)\n assert result.returncode == 0\n # Empty marker → no JUST_UPGRADED output\n assert \"JUST_UPGRADED\" not in result.stdout\n assert not Path(state_dir, \"just-upgraded-from\").exists()\n\n\ndef test_cache_up_to_date() -> None:\n with tempfile.TemporaryDirectory() as skills_dir, tempfile.TemporaryDirectory() as state_dir:\n Path(skills_dir, \"VERSION\").write_text(\"1.0.0\\n\")\n Path(state_dir, \"last-update-check\").write_text(\"UP_TO_DATE 1.0.0\\n\")\n # Use a bogus URL that would fail if actually fetched\n result = run_check(skills_dir, state_dir, remote_url=\"http://localhost:1/nope\")\n assert result.returncode == 0\n assert result.stdout.strip() == \"\"\n\n\ndef test_cache_upgrade_available() -> None:\n with tempfile.TemporaryDirectory() as skills_dir, tempfile.TemporaryDirectory() as state_dir:\n Path(skills_dir, \"VERSION\").write_text(\"1.0.0\\n\")\n Path(state_dir, \"last-update-check\").write_text(\"UPGRADE_AVAILABLE 1.0.0 2.0.0\\n\")\n result = run_check(skills_dir, state_dir, remote_url=\"http://localhost:1/nope\")\n assert result.returncode == 0\n assert \"UPGRADE_AVAILABLE 1.0.0 2.0.0\" in result.stdout\n\n\ndef test_snooze_active() -> None:\n with tempfile.TemporaryDirectory() as skills_dir, tempfile.TemporaryDirectory() as state_dir:\n Path(skills_dir, \"VERSION\").write_text(\"1.0.0\\n\")\n Path(state_dir, \"last-update-check\").write_text(\"UPGRADE_AVAILABLE 1.0.0 2.0.0\\n\")\n now = int(time.time())\n Path(state_dir, \"update-snoozed\").write_text(f\"2.0.0 1 {now}\\n\")\n result = run_check(skills_dir, state_dir, remote_url=\"http://localhost:1/nope\")\n assert result.returncode == 0\n assert result.stdout.strip() == \"\" # snoozed\n\n\ndef test_snooze_expired() -> None:\n with tempfile.TemporaryDirectory() as skills_dir, tempfile.TemporaryDirectory() as state_dir:\n Path(skills_dir, \"VERSION\").write_text(\"1.0.0\\n\")\n Path(state_dir, \"last-update-check\").write_text(\"UPGRADE_AVAILABLE 1.0.0 2.0.0\\n\")\n old = int(time.time()) - 90000 # >24h ago\n Path(state_dir, \"update-snoozed\").write_text(f\"2.0.0 1 {old}\\n\")\n result = run_check(skills_dir, state_dir, remote_url=\"http://localhost:1/nope\")\n assert result.returncode == 0\n assert \"UPGRADE_AVAILABLE 1.0.0 2.0.0\" in result.stdout\n\n\ndef test_snooze_new_version_ignores_snooze() -> None:\n with tempfile.TemporaryDirectory() as skills_dir, tempfile.TemporaryDirectory() as state_dir:\n Path(skills_dir, \"VERSION\").write_text(\"1.0.0\\n\")\n Path(state_dir, \"last-update-check\").write_text(\"UPGRADE_AVAILABLE 1.0.0 3.0.0\\n\")\n now = int(time.time())\n # Snoozed for 2.0.0 but 3.0.0 is available now\n Path(state_dir, \"update-snoozed\").write_text(f\"2.0.0 1 {now}\\n\")\n result = run_check(skills_dir, state_dir, remote_url=\"http://localhost:1/nope\")\n assert result.returncode == 0\n assert \"UPGRADE_AVAILABLE 1.0.0 3.0.0\" in result.stdout\n\n\ndef test_force_clears_cache_and_snooze() -> None:\n with tempfile.TemporaryDirectory() as skills_dir, tempfile.TemporaryDirectory() as state_dir:\n Path(skills_dir, \"VERSION\").write_text(\"1.0.0\\n\")\n Path(state_dir, \"last-update-check\").write_text(\"UP_TO_DATE 1.0.0\\n\")\n now = int(time.time())\n Path(state_dir, \"update-snoozed\").write_text(f\"2.0.0 1 {now}\\n\")\n # Force should clear cache and snooze, then hit network (which fails → UP_TO_DATE)\n result = run_check(skills_dir, state_dir, remote_url=\"http://localhost:1/nope\", force=True)\n assert result.returncode == 0\n assert not Path(state_dir, \"update-snoozed\").exists()\n # Cache gets rewritten as UP_TO_DATE since network fails\n cache = Path(state_dir, \"last-update-check\").read_text()\n assert \"UP_TO_DATE\" in cache\n\n\ndef test_network_failure_graceful() -> None:\n with tempfile.TemporaryDirectory() as skills_dir, tempfile.TemporaryDirectory() as state_dir:\n Path(skills_dir, \"VERSION\").write_text(\"1.0.0\\n\")\n # No cache, bogus URL → should fail gracefully\n result = run_check(skills_dir, state_dir, remote_url=\"http://localhost:1/nope\")\n assert result.returncode == 0\n assert result.stdout.strip() == \"\"\n cache = Path(state_dir, \"last-update-check\").read_text()\n assert \"UP_TO_DATE 1.0.0\" in cache\n\n\ndef test_corrupt_snooze_file() -> None:\n with tempfile.TemporaryDirectory() as skills_dir, tempfile.TemporaryDirectory() as state_dir:\n Path(skills_dir, \"VERSION\").write_text(\"1.0.0\\n\")\n Path(state_dir, \"last-update-check\").write_text(\"UPGRADE_AVAILABLE 1.0.0 2.0.0\\n\")\n Path(state_dir, \"update-snoozed\").write_text(\"garbage\\n\")\n result = run_check(skills_dir, state_dir, remote_url=\"http://localhost:1/nope\")\n assert result.returncode == 0\n assert \"UPGRADE_AVAILABLE 1.0.0 2.0.0\" in result.stdout\n\n\ndef test_corrupt_cache_file() -> None:\n with tempfile.TemporaryDirectory() as skills_dir, tempfile.TemporaryDirectory() as state_dir:\n Path(skills_dir, \"VERSION\").write_text(\"1.0.0\\n\")\n Path(state_dir, \"last-update-check\").write_text(\"BOGUS DATA\\n\")\n # Corrupt cache → TTL=0 → re-fetch (fails → UP_TO_DATE)\n result = run_check(skills_dir, state_dir, remote_url=\"http://localhost:1/nope\")\n assert result.returncode == 0\n assert result.stdout.strip() == \"\"\n\n\ndef test_plugin_cache_path_discovery() -> None:\n \"\"\"Verify update-check works when VARA_SKILLS_DIR points to a plugin cache path.\"\"\"\n with tempfile.TemporaryDirectory() as base:\n # Simulate ~/.claude/plugins/cache/vara-skills/vara-skills/1.3.1/\n plugin_dir = Path(base, \"plugins\", \"cache\", \"vara-skills\", \"vara-skills\", \"1.3.1\")\n plugin_dir.mkdir(parents=True)\n Path(plugin_dir, \"VERSION\").write_text(\"1.3.1\\n\")\n bin_dir = plugin_dir / \"bin\"\n bin_dir.mkdir()\n import shutil\n shutil.copy2(str(SCRIPT), str(bin_dir / \"vara-skills-update-check\"))\n\n state_dir = Path(base, \"state\")\n state_dir.mkdir()\n\n # No cache, bogus URL → should fall back to UP_TO_DATE gracefully\n result = run_check(str(plugin_dir), str(state_dir), remote_url=\"http://localhost:1/nope\")\n assert result.returncode == 0\n cache = Path(state_dir, \"last-update-check\").read_text()\n assert \"UP_TO_DATE 1.3.1\" in cache\n\n\ndef test_cache_version_mismatch_refetches() -> None:\n with tempfile.TemporaryDirectory() as skills_dir, tempfile.TemporaryDirectory() as state_dir:\n Path(skills_dir, \"VERSION\").write_text(\"2.0.0\\n\")\n # Cache says 1.0.0 is up to date, but local is now 2.0.0 → refetch\n Path(state_dir, \"last-update-check\").write_text(\"UP_TO_DATE 1.0.0\\n\")\n result = run_check(skills_dir, state_dir, remote_url=\"http://localhost:1/nope\")\n assert result.returncode == 0\n # Network fails → UP_TO_DATE for 2.0.0\n cache = Path(state_dir, \"last-update-check\").read_text()\n assert \"UP_TO_DATE 2.0.0\" in cache\n\n\ndef main() -> int:\n tests = [\n test_script_exists_and_is_executable,\n test_no_version_file_silent_exit,\n test_disabled_via_env,\n test_just_upgraded_marker,\n test_just_upgraded_marker_empty,\n test_cache_up_to_date,\n test_cache_upgrade_available,\n test_snooze_active,\n test_snooze_expired,\n test_snooze_new_version_ignores_snooze,\n test_force_clears_cache_and_snooze,\n test_plugin_cache_path_discovery,\n test_network_failure_graceful,\n test_corrupt_snooze_file,\n test_corrupt_cache_file,\n test_cache_version_mismatch_refetches,\n ]\n failed = 0\n for test in tests:\n try:\n test()\n print(f\" ok: {test.__name__}\")\n except Exception as err:\n print(f\"FAIL: {test.__name__}: {err}\", file=sys.stderr)\n failed += 1\n if failed:\n print(f\"\\n{failed}/{len(tests)} tests failed\", file=sys.stderr)\n return 1\n print(f\"\\nupdate check ok ({len(tests)} tests)\")\n return 0\n\n\nif __name__ == \"__main__\":\n raise SystemExit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":10958,"content_sha256":"e415b2404ea69e0fb75da69c30713a5318b6dee67101bdd0841d530caf73fa73"},{"filename":"TODOS.md","content":"# TODOS\n\n## Skill Effectiveness Telemetry\n\n**Priority:** P3\n**Effort:** M (human) / S (CC)\n**Depends on:** Nothing\n\nCurrently there is no structured way to know whether documentation updates actually prevent errors for future agents. A lightweight mechanism (e.g., structured error reporting in skill output, or agent session error logging) could close the feedback loop between field usage and skill improvement.\n\nThis is a separate feature, not a documentation fix.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":468,"content_sha256":"f2bb9021335d3593a956b0b4c04666ad917be626f74fbd6cca734dc637552b40"},{"filename":"VERSION","content":"3.0.0\n","content_type":"text/plain; charset=utf-8","language":null,"size":6,"content_sha256":"2985be8b28d3ade858e8d8fb4bc22f565b1bf6020dff982dce141f7721b9999c"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":2},"content":[{"text":"Preamble (run first)","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"_VS_DIR=\"\"\nfor _d in \\\n \"${VARA_SKILLS_DIR:-}\" \\\n \"$HOME/.claude/skills/vara-skills\" \\\n \".claude/skills/vara-skills\" \\\n \"$HOME\"/.claude/plugins/cache/vara-skills/vara-skills/*; do\n if [ -n \"$_d\" ] && [ -f \"$_d/bin/vara-skills-update-check\" ]; then\n _VS_DIR=\"$_d\"; break\n fi\ndone\nif [ -n \"$_VS_DIR\" ]; then\n export VARA_SKILLS_DIR=\"$_VS_DIR\"\n _UPD=$(\"$_VS_DIR/bin/vara-skills-update-check\" 2>/dev/null || true)\n [ -n \"$_UPD\" ] && echo \"$_UPD\" || true\nfi","type":"text"}]},{"type":"paragraph","content":[{"text":"If output shows ","type":"text"},{"text":"UPGRADE_AVAILABLE \u003cold> \u003cnew>","type":"text","marks":[{"type":"code_inline"}]},{"text":": read ","type":"text"},{"text":"skills/vara-skills-upgrade/SKILL.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" and follow the \"Inline upgrade flow\" (auto-upgrade if configured, otherwise ask user with 3 options, write snooze if declined). If ","type":"text"},{"text":"JUST_UPGRADED \u003cfrom> \u003cto>","type":"text","marks":[{"type":"code_inline"}]},{"text":": tell user \"Running vara-skills v{to} (upgraded from v{from})!\" and continue.","type":"text"}]},{"type":"heading","attrs":{"level":1},"content":[{"text":"Vara Skills","type":"text"}]},{"type":"paragraph","content":[{"text":"This repository is the portable router for the provisional ","type":"text"},{"text":"vara-skills","type":"text","marks":[{"type":"code_inline"}]},{"text":" pack.","type":"text"}]},{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"skills/ship-sails-app/SKILL.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" first when the task is about standard greenfield or unreleased Gear/Vara Sails application work.","type":"text"}]},{"type":"paragraph","content":[{"text":"The repo is intended to be self-contained: use local ","type":"text"},{"text":"references/","type":"text","marks":[{"type":"code_inline"}]},{"text":" handbook files before depending on sibling repositories or machine-local skill directories.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"What This Router Covers","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Codex","type":"text","marks":[{"type":"code_inline"}]},{"text":": install the local skill directories with ","type":"text"},{"text":"bash scripts/install-codex-skills.sh","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Claude","type":"text","marks":[{"type":"code_inline"}]},{"text":": use the same repo content through plugin metadata","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"OpenClaw","type":"text","marks":[{"type":"code_inline"}]},{"text":": use the same markdown surface through the wrapper skill","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Route By Builder Intent","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Prepare or repair the local Rust or Gear toolchain first: ","type":"text"},{"text":"skills/sails-dev-env/SKILL.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Start a new Sails workspace: ","type":"text"},{"text":"skills/sails-new-app/SKILL.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Add or change a feature in an existing Sails repo: ","type":"text"},{"text":"skills/sails-feature-workflow/SKILL.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Rework service or program boundaries: ","type":"text"},{"text":"skills/sails-architecture/SKILL.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Debug message flow, replies, delays, or reservations: ","type":"text"},{"text":"skills/gear-message-execution/SKILL.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Design or debug calls into runtime builtin actors (staking, proxy, BLS12-381, ETH bridge): ","type":"text"},{"text":"skills/gear-builtin-actors/SKILL.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Fix ","type":"text"},{"text":"build.rs","type":"text","marks":[{"type":"code_inline"}]},{"text":", IDL, or generated clients: ","type":"text"},{"text":"skills/sails-idl-client/SKILL.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Write or debug ","type":"text"},{"text":"gtest","type":"text","marks":[{"type":"code_inline"}]},{"text":": ","type":"text"},{"text":"skills/sails-gtest/SKILL.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Run typed local-node smoke after green ","type":"text"},{"text":"gtest","type":"text","marks":[{"type":"code_inline"}]},{"text":": ","type":"text"},{"text":"skills/sails-local-smoke/SKILL.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Add a fungible token layer with awesome-sails: ","type":"text"},{"text":"skills/awesome-sails-vft/SKILL.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Interact with Vara Network on-chain (deploy, call, transfer, monitor): ","type":"text"},{"text":"skills/vara-wallet/SKILL.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Build or extend a React frontend for a Sails app: ","type":"text"},{"text":"skills/sails-frontend/SKILL.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Build or extend a read-side indexer and query API for a Sails app: ","type":"text"},{"text":"skills/sails-indexer/SKILL.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Evolve a released Sails contract, prepare a new deployed version, plan cutover, or design V1->V2 migration: ","type":"text"},{"text":"skills/sails-program-evolution/SKILL.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Plan or scope an ethexe (EVM-compatible Gear) program: ","type":"text"},{"text":"skills/sails-ethexe-architecture/SKILL.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Implement Rust code for an ethexe program (Syscall gating, ","type":"text"},{"text":"#[export(ethabi|payable)]","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"emit_eth_event","type":"text","marks":[{"type":"code_inline"}]},{"text":"): ","type":"text"},{"text":"skills/sails-ethexe-implementer/SKILL.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Look up network endpoints, testnet/mainnet RPC, account format, or program lifecycle: ","type":"text"},{"text":"references/vara-network-endpoints.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Look up where to buy VARA on centralized exchanges (Coinbase, Gate, Crypto.com): ","type":"text"},{"text":"references/vara-token-exchanges.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Look up bridge contract addresses or bridged token program IDs: ","type":"text"},{"text":"references/vara-eth-bridge-contracts.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Understand Vara-Ethereum bridge flows, fee model, or integration patterns: ","type":"text"},{"text":"references/vara-eth-bridge-flows.md","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Guardrails","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"This catalog is still provisional and is expected to change as ","type":"text"},{"text":"vara-skills-evals","type":"text","marks":[{"type":"code_inline"}]},{"text":" measures uplift.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Stay on the standard Gear/Vara Sails path for v1.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"For ethexe-specific work, use ","type":"text"},{"text":"skills/sails-ethexe-architecture/SKILL.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"skills/sails-ethexe-implementer/SKILL.md","type":"text","marks":[{"type":"code_inline"}]},{"text":"; do not blend ethexe and standard Sails patterns in the same skill invocation.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Bridge reference knowledge (addresses, flows, fees) is in scope as a lookup resource. Full Vara.eth workflow routing is still deferred to dedicated ethexe skills.","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}}]},"metadata":{"date":"2026-06-05","name":"vara-skills","author":"@skillopedia","source":{"stars":17,"repo_name":"vara-skills","origin_url":"https://github.com/gear-foundation/vara-skills/blob/HEAD/SKILL.md","repo_owner":"gear-foundation","body_sha256":"56148b1b855add78a06d99adc6327e9a3369cc8f0ddd74efdd46da996c1e7604","cluster_key":"5f2b79bb98475e0ae64f7054188f6c8300c829ca1eca4bec32fd3b7efdc44298","clean_bundle":{"format":"clean-skill-bundle-v1","source":"gear-foundation/vara-skills/SKILL.md","attachments":[{"id":"e1e91a04-227a-5363-baaa-840af473a723","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e1e91a04-227a-5363-baaa-840af473a723/attachment.json","path":".claude-plugin/marketplace.json","size":885,"sha256":"9e4c18aec8c6870fb8871535aed218b3b6dd261fee5d0660be869ac7f6cede0f","contentType":"application/json; charset=utf-8"},{"id":"59e03837-84af-58a3-9fed-10cb65a95c0f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/59e03837-84af-58a3-9fed-10cb65a95c0f/attachment.json","path":".claude-plugin/plugin.json","size":444,"sha256":"e16cf8bb4e7c96d27faac2f06c58e196b1aa55ca3175330c6c39ac137a779336","contentType":"application/json; charset=utf-8"},{"id":"7ca23f4c-c743-5c70-a65c-8559882b0a53","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/7ca23f4c-c743-5c70-a65c-8559882b0a53/attachment.yml","path":".github/workflows/ci.yml","size":335,"sha256":"4a10f943712ed9fd98dd510aac4c648b9e9259a82d35085c8aa830fffd4598e6","contentType":"application/yaml; charset=utf-8"},{"id":"806b64fa-4677-5a19-b866-b7c0019279f6","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/806b64fa-4677-5a19-b866-b7c0019279f6/attachment.yml","path":".github/workflows/release.yml","size":1487,"sha256":"1181400ed9ec9ecad1ec5e90c0afcc54d55067c3809f70952b4b7c640e7aeeff","contentType":"application/yaml; charset=utf-8"},{"id":"75aabc5a-0521-5b1c-865d-03428f420e76","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/75aabc5a-0521-5b1c-865d-03428f420e76/attachment","path":".gitignore","size":34,"sha256":"292ccbc699f895099528352cb8ed37bd96d2317da391bbd762493df0685a5227","contentType":"text/plain; charset=utf-8"},{"id":"1439e987-b71c-5cbd-850a-8c0d54ae576a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1439e987-b71c-5cbd-850a-8c0d54ae576a/attachment.md","path":"AGENTS.md","size":1386,"sha256":"4d883bf6dba93a767369bc50cc5b54d8f46adf7223e8f2e7e6601b5d05ff234a","contentType":"text/markdown; charset=utf-8"},{"id":"a0ee7c73-649d-50bb-a536-33902ff79e4d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a0ee7c73-649d-50bb-a536-33902ff79e4d/attachment.md","path":"CHANGELOG.md","size":8783,"sha256":"562df568a39a873ca3cb2d15d04c29894a8b50f6b6627ad17e0c48a627f69880","contentType":"text/markdown; charset=utf-8"},{"id":"24d8c248-ba3d-549f-aa38-a70f866249d1","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/24d8c248-ba3d-549f-aa38-a70f866249d1/attachment.md","path":"CLAUDE.md","size":3696,"sha256":"93a83b944698ab45a1adffdab6fb09fd79642e7b9cdf1ce22608e676a5383874","contentType":"text/markdown; charset=utf-8"},{"id":"1d026bd2-7712-5ba3-8934-8c1d9c44ce94","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1d026bd2-7712-5ba3-8934-8c1d9c44ce94/attachment","path":"Makefile","size":693,"sha256":"2e4e380d96e6f26c727f954c7abc92975d8f45e70dd7ffd52b28162e66897dcb","contentType":"text/plain; charset=utf-8"},{"id":"a925c7a0-ebbe-53f7-ab53-f475b06718c6","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a925c7a0-ebbe-53f7-ab53-f475b06718c6/attachment.md","path":"README.md","size":9927,"sha256":"e95ebd90b26c1b0b55b40002a820566d766c4f47000041e7743044e884b9a361","contentType":"text/markdown; charset=utf-8"},{"id":"37ffb380-e95d-544e-924b-7f553e15cf4f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/37ffb380-e95d-544e-924b-7f553e15cf4f/attachment.md","path":"TODOS.md","size":468,"sha256":"f2bb9021335d3593a956b0b4c04666ad917be626f74fbd6cca734dc637552b40","contentType":"text/markdown; charset=utf-8"},{"id":"84818f38-394d-594c-8661-f845667e1390","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/84818f38-394d-594c-8661-f845667e1390/attachment","path":"VERSION","size":6,"sha256":"2985be8b28d3ade858e8d8fb4bc22f565b1bf6020dff982dce141f7721b9999c","contentType":"text/plain; charset=utf-8"},{"id":"7e26f2c5-7cd4-5886-9555-18d25a33df7f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/7e26f2c5-7cd4-5886-9555-18d25a33df7f/attachment.md","path":"assets/architecture-template.md","size":1509,"sha256":"0f984154bd63bd6afb645fa9fd22459f462ae6b9c7615b490c9d89608d89e16d","contentType":"text/markdown; charset=utf-8"},{"id":"835bd3ca-f9c6-557a-a6cd-41d1f410115a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/835bd3ca-f9c6-557a-a6cd-41d1f410115a/attachment.md","path":"assets/gtest-report-template.md","size":123,"sha256":"fd7c5571e8780db7e52a22ceb7b23f5f193f6a21db4f8b06d4534fcfacc5e32c","contentType":"text/markdown; charset=utf-8"},{"id":"b8c04043-c33a-5fad-bfc9-1e2e2f31ed4a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b8c04043-c33a-5fad-bfc9-1e2e2f31ed4a/attachment.md","path":"assets/spec-template.md","size":180,"sha256":"00d2d9c4b958b8e839d7dde9745f78d0803061293b911c55f9b8097b931e921f","contentType":"text/markdown; charset=utf-8"},{"id":"7ee95495-fe18-5271-ba9a-006757f18e68","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/7ee95495-fe18-5271-ba9a-006757f18e68/attachment.md","path":"assets/task-plan-template.md","size":133,"sha256":"f8044888d8629bcd0c2d87ef1ddf3090919c99847f90bfd241af4339b0cea6c4","contentType":"text/markdown; charset=utf-8"},{"id":"20b7b8b2-34d6-53a7-b06f-5d7fd683ec51","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/20b7b8b2-34d6-53a7-b06f-5d7fd683ec51/attachment","path":"bin/vara-skills-update-check","size":6273,"sha256":"59a858a2e9562f2fb8bd2d8d6a76a3bef2095730b7133321d6ed92768ea2ccf9","contentType":"text/plain; charset=utf-8"},{"id":"980bd9d9-b1f7-504d-8b0b-eb3f0684ccb6","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/980bd9d9-b1f7-504d-8b0b-eb3f0684ccb6/attachment.docx","path":"docs/agentic_gear_devkit_blueprint.docx","size":48136,"sha256":"9afee180719c2f78669d30d848e1be741722a67cb1c363b4fe56e9f1137ede10","contentType":"application/vnd.openxmlformats-officedocument.wordprocessingml.document"},{"id":"be24422d-222d-585d-96e6-baaf7618ac7f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/be24422d-222d-585d-96e6-baaf7618ac7f/attachment.md","path":"references/awesome-sails-token-patterns.md","size":12029,"sha256":"e6a469f4f1779c4e758ea399f39879a68a9fe4b724282fc8a71da969e621d85e","contentType":"text/markdown; charset=utf-8"},{"id":"84829183-9284-5328-abd6-6cc293a37301","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/84829183-9284-5328-abd6-6cc293a37301/attachment.md","path":"references/contract-interface-evolution.md","size":10751,"sha256":"097f2aefb38e98d0b1d2026efd570406585715fa9b0a488ae2851d46e92374d0","contentType":"text/markdown; charset=utf-8"},{"id":"d300ec46-7496-5304-b9a4-a8724186b207","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d300ec46-7496-5304-b9a4-a8724186b207/attachment.md","path":"references/delayed-message-pattern.md","size":4001,"sha256":"3bef40e8ba4096ba3881f20a34f33f8bb53ffd36f5b03a5e13d08c4a0511ec18","contentType":"text/markdown; charset=utf-8"},{"id":"86dba31e-23aa-5d4a-a881-598f8bd9c3bc","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/86dba31e-23aa-5d4a-a881-598f8bd9c3bc/attachment.md","path":"references/gear-builtin-actors.md","size":10879,"sha256":"08fd4a9c9c7e48db10f94d12e86ca219ac32b72e532543337a5e5a86cc8743a3","contentType":"text/markdown; charset=utf-8"},{"id":"b2df73ff-5df2-511c-839c-99a7f70ffd7d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b2df73ff-5df2-511c-839c-99a7f70ffd7d/attachment.md","path":"references/gear-execution-model.md","size":3549,"sha256":"46eef1337accf1d1ce28a185f7af928fb6df09d9802e901e417a3de75ff5d2f8","contentType":"text/markdown; charset=utf-8"},{"id":"1d72b1a6-f6b0-570c-bf23-8302ac95c29e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1d72b1a6-f6b0-570c-bf23-8302ac95c29e/attachment.md","path":"references/gear-gas-reservations-and-waitlist.md","size":2273,"sha256":"f743cf9063f0c041c695766e1ec3003c00d1b62a1c53330094f11491806ca975","contentType":"text/markdown; charset=utf-8"},{"id":"56138aed-7e27-5476-a8d2-175e665427ac","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/56138aed-7e27-5476-a8d2-175e665427ac/attachment.md","path":"references/gear-gstd-api-and-syscalls.md","size":6846,"sha256":"27ffd18b6f878b569cd34ecacc179fd1f440588493e77213c369baf5f9cb8508","contentType":"text/markdown; charset=utf-8"},{"id":"1feab5d4-9d58-5c17-8d00-774ca7a27686","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1feab5d4-9d58-5c17-8d00-774ca7a27686/attachment.md","path":"references/gear-messaging-and-replies.md","size":2693,"sha256":"17fe92903b839b43f618cf408e43452c1082f2558afeae3d9292dee38a72d49f","contentType":"text/markdown; charset=utf-8"},{"id":"6296cea0-1af4-54f8-9738-42e287bdd339","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/6296cea0-1af4-54f8-9738-42e287bdd339/attachment.md","path":"references/gear-sails-production-patterns.md","size":23727,"sha256":"86645e222f0fe1c5c954b44aec3c4445e162a1e72d24ae74908ebe551616b666","contentType":"text/markdown; charset=utf-8"},{"id":"34ff9b4a-562c-52d2-bd7c-6488e697e91f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/34ff9b4a-562c-52d2-bd7c-6488e697e91f/attachment.md","path":"references/gtest-cheatsheet.md","size":2067,"sha256":"65380582b341ed6b22c24baa6475a9cd13c8c1f2e2a0ec0ea1da41af7b3b3a2d","contentType":"text/markdown; charset=utf-8"},{"id":"f3f27451-7a90-50c3-9bd8-af3460494675","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f3f27451-7a90-50c3-9bd8-af3460494675/attachment.md","path":"references/gtest-patterns.md","size":4588,"sha256":"e670983878293dc242427cf586aaa4e8c9f102eb5e3ec0c8d1c23a5ef3ac3c68","contentType":"text/markdown; charset=utf-8"},{"id":"aaab8c03-421b-5628-bb4e-84952988cfd2","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/aaab8c03-421b-5628-bb4e-84952988cfd2/attachment.md","path":"references/sails-cheatsheet.md","size":5601,"sha256":"e04548ad8b8e6b6f3e703f34db617cf55f5d026007c5b1c9c3ce60603724686e","contentType":"text/markdown; charset=utf-8"},{"id":"3becfb34-c336-564f-a11d-b4902a79b564","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/3becfb34-c336-564f-a11d-b4902a79b564/attachment.md","path":"references/sails-ethexe-patterns.md","size":5603,"sha256":"ab69bc6b32feca250e087688f13281f2ddcfc4071b7e38017a3b3a44009ed6a7","contentType":"text/markdown; charset=utf-8"},{"id":"6e814719-2ca7-5b8c-8268-d26a96dfd2da","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/6e814719-2ca7-5b8c-8268-d26a96dfd2da/attachment.md","path":"references/sails-frontend-and-gear-js.md","size":22717,"sha256":"0bb587f1d560755c6b4105d7e4b1ae15a83c90a7ce64e22aede453349b2324c0","contentType":"text/markdown; charset=utf-8"},{"id":"91f05656-8664-5f45-8209-c63cab099c0f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/91f05656-8664-5f45-8209-c63cab099c0f/attachment.md","path":"references/sails-gtest-and-local-validation.md","size":7308,"sha256":"b1a8fcad8ba049bd1366698ee62f9c3a1396676f03c4889d0b2d5427cd25d030","contentType":"text/markdown; charset=utf-8"},{"id":"d0a5af9d-14b2-579b-922e-1cc22bf9dd4d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d0a5af9d-14b2-579b-922e-1cc22bf9dd4d/attachment.md","path":"references/sails-header-wire-format.md","size":3067,"sha256":"9061af6674461b14822e4e317fb8b8a7502ba699d3fdeea81dc4a46e08061c30","contentType":"text/markdown; charset=utf-8"},{"id":"bbf8fee8-8305-5951-8a06-e8d14cba492b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/bbf8fee8-8305-5951-8a06-e8d14cba492b/attachment.md","path":"references/sails-idl-client-pipeline.md","size":3513,"sha256":"1196de45156b4b1943a07e3405e7e436ba674d9a56602f5bfb53e252263e6967","contentType":"text/markdown; charset=utf-8"},{"id":"a170975c-cca5-5820-b745-ab1a3b0195bc","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a170975c-cca5-5820-b745-ab1a3b0195bc/attachment.md","path":"references/sails-idl-v2-syntax.md","size":4111,"sha256":"8838a6e84fd67c79c98cd06dc5cccaa3b5b24d5e9399774b2643f472b5d48842","contentType":"text/markdown; charset=utf-8"},{"id":"a4d70638-10cb-5534-86b2-4341c3512a28","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a4d70638-10cb-5534-86b2-4341c3512a28/attachment.md","path":"references/sails-indexer-patterns.md","size":45455,"sha256":"6d9eefcb86a9216b94ca21083c3122e419035d7df08916f1737e50adf16dded8","contentType":"text/markdown; charset=utf-8"},{"id":"dbc7a7d5-5558-5071-a7b7-9c19084b394a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/dbc7a7d5-5558-5071-a7b7-9c19084b394a/attachment.md","path":"references/sails-program-and-service-architecture.md","size":2343,"sha256":"dedc84529b6f9c39d7d5b9526f88d8a43f15c78890929c542c4a591361747538","contentType":"text/markdown; charset=utf-8"},{"id":"eb79234f-cc27-5acc-af73-e124b6d15559","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/eb79234f-cc27-5acc-af73-e124b6d15559/attachment.md","path":"references/sails-rs-imports.md","size":6199,"sha256":"a8064ecf6fc6fdf800a784aa66a29d35a662c036529192e200b4b5959d288cac","contentType":"text/markdown; charset=utf-8"},{"id":"0dc9fb21-3f3a-50e6-8443-ac4166cf7de1","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/0dc9fb21-3f3a-50e6-8443-ac4166cf7de1/attachment.md","path":"references/sails-syscall-mapping.md","size":2464,"sha256":"7f7d6a66e567ba87d3c942cb926069227359fe05a9894e9d15a9b598298f4295","contentType":"text/markdown; charset=utf-8"},{"id":"aedaa205-f6a5-5625-a2b0-026e301320b7","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/aedaa205-f6a5-5625-a2b0-026e301320b7/attachment.md","path":"references/scale-binary-decoding-guide.md","size":4363,"sha256":"6d68b8732b3c5f9f3ec81056cdc56d67c5bd7790ee9ce438745f558a3ae49e80","contentType":"text/markdown; charset=utf-8"},{"id":"cd0340c2-25cd-598c-ad76-839f5388dadd","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/cd0340c2-25cd-598c-ad76-839f5388dadd/attachment.md","path":"references/vara-domain-overview.md","size":1433,"sha256":"4b4d10ff4208ac24b0447b841cf11f0e84db0a0313bf991bdfc8b25a08e16b64","contentType":"text/markdown; charset=utf-8"},{"id":"baea4a03-c6eb-5cbb-af88-36fad9188acb","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/baea4a03-c6eb-5cbb-af88-36fad9188acb/attachment.md","path":"references/vara-eth-bridge-contracts.md","size":5275,"sha256":"ffddb314081b4cebf83ce642517c5a94389cde5d2fcf690d768833c009597878","contentType":"text/markdown; charset=utf-8"},{"id":"d28e7d5a-cfc9-50b3-865a-a134725da237","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d28e7d5a-cfc9-50b3-865a-a134725da237/attachment.md","path":"references/vara-eth-bridge-flows.md","size":3535,"sha256":"2f6394a199704b89a77ea973673ef1b795ef2f38017c9bd5546405f8bad0542e","contentType":"text/markdown; charset=utf-8"},{"id":"1acc8727-c349-584b-ad9e-1603a3ad313f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1acc8727-c349-584b-ad9e-1603a3ad313f/attachment.md","path":"references/vara-network-endpoints.md","size":1857,"sha256":"608b377e1264ee4c1379a97298ef18a065f4803e0c1415e05edfddaf7f2b230c","contentType":"text/markdown; charset=utf-8"},{"id":"9dfb3eef-7f36-57cf-a52c-02bb1f54673e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/9dfb3eef-7f36-57cf-a52c-02bb1f54673e/attachment.md","path":"references/vara-token-exchanges.md","size":1795,"sha256":"61aac0daa093e73b704b8e99fcf76e670ad7b907951078da469a9c505b6003a5","contentType":"text/markdown; charset=utf-8"},{"id":"d20b5b75-a8fd-5a10-a4ac-17a6edb29999","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d20b5b75-a8fd-5a10-a4ac-17a6edb29999/attachment.md","path":"references/varaeth-extension-notes.md","size":1585,"sha256":"9605b7d2ac2fe1e3aa8d1bda286aef34bea25c8671af9b8284d9ec71f48bffd3","contentType":"text/markdown; charset=utf-8"},{"id":"5fba1095-8a78-5446-8f25-1a279d689037","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5fba1095-8a78-5446-8f25-1a279d689037/attachment.md","path":"references/voucher-and-signless-flows.md","size":8809,"sha256":"2653fe8258de02a195d2e9e3d78a3fcda9e9a22612744d93aba9cc9977c015d5","contentType":"text/markdown; charset=utf-8"},{"id":"5a1f0f56-8c0d-52eb-b931-b190cfa583a2","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5a1f0f56-8c0d-52eb-b931-b190cfa583a2/attachment.sh","path":"scripts/install-codex-skills.sh","size":765,"sha256":"fefb573cb08baba402cc4519bc868df91c3c1a924d9864c9a4fc556e63a0d418","contentType":"application/x-sh; charset=utf-8"},{"id":"322acd1e-7ed4-5c5e-ae50-a76403d18f37","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/322acd1e-7ed4-5c5e-ae50-a76403d18f37/attachment.py","path":"scripts/parse_test_output.py","size":1517,"sha256":"85cd7eb289fd76bfdb61f56c1f74a2094909f4850b4b87c24f80832769d0bf47","contentType":"text/x-python; charset=utf-8"},{"id":"055f24f6-4851-5aa8-b94e-d4b6b323cf1e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/055f24f6-4851-5aa8-b94e-d4b6b323cf1e/attachment.sh","path":"scripts/run_gtest.sh","size":697,"sha256":"c1f133d8608432f74f23704f84dda0c2c381dd62002e1451ca6b010bc1a5d699","contentType":"application/x-sh; charset=utf-8"},{"id":"539cdade-fe08-5dc8-b14f-0d0b468694b1","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/539cdade-fe08-5dc8-b14f-0d0b468694b1/attachment.sh","path":"scripts/validate-repo.sh","size":140,"sha256":"847760f9b96c3bc9472d5e306aa1fa7da377da52737986b51adecdf1c6729d41","contentType":"application/x-sh; charset=utf-8"},{"id":"2c43ef33-1da8-5ea1-9092-b9633dfc1ec9","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/2c43ef33-1da8-5ea1-9092-b9633dfc1ec9/attachment.py","path":"scripts/validate-skill.py","size":3476,"sha256":"b38facf066107fe7652ef78653bbf5ff591800ddaf2661af70386ef8122ab38b","contentType":"text/x-python; charset=utf-8"},{"id":"1a3c536f-4fe5-5623-ac4f-2ed69acc9585","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1a3c536f-4fe5-5623-ac4f-2ed69acc9585/attachment.sh","path":"scripts/verify-real-sails-program.sh","size":1764,"sha256":"493ef72e1ca227305e40c333382a41e051f671661f57658aa61f3bd54ce63495","contentType":"application/x-sh; charset=utf-8"},{"id":"b0a45fcd-104d-58a7-ab2b-bbe6d59c219c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b0a45fcd-104d-58a7-ab2b-bbe6d59c219c/attachment.log","path":"tests/fixtures/gtest-failure.log","size":391,"sha256":"9ea3bae241b9cd44afd7d716f089a347744d93773e2b6d5aee54af64c8d4ee52","contentType":"text/plain; charset=utf-8"},{"id":"3bbecab5-46e3-5c3a-abc4-fdc86589593a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/3bbecab5-46e3-5c3a-abc4-fdc86589593a/attachment.log","path":"tests/fixtures/gtest-workspace-error.log","size":276,"sha256":"30403fe8e4a162a6f51852f992d5a3046c59dcc3114658b3c5d6c297b2b67bcb","contentType":"text/plain; charset=utf-8"},{"id":"648c71a0-6fe1-5ee6-8989-78478fd18433","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/648c71a0-6fe1-5ee6-8989-78478fd18433/attachment.py","path":"tests/test_gstd_api_map_skill.py","size":2619,"sha256":"383da904731a7bd4b2f1a3192d423ecea4ed2d41b855ba352a3c4fb09cb2bd22","contentType":"text/x-python; charset=utf-8"},{"id":"2fbe6a0f-0bf8-5669-a1eb-c114cee7c112","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/2fbe6a0f-0bf8-5669-a1eb-c114cee7c112/attachment.py","path":"tests/test_install_codex_skills.py","size":1863,"sha256":"f6bc8afe462a3b0be3f27f36a52cf0f03adaed04dec371e2f8b0c6531fc9353c","contentType":"text/x-python; charset=utf-8"},{"id":"2cb66e65-4485-572c-8279-8c4ffb185a7a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/2cb66e65-4485-572c-8279-8c4ffb185a7a/attachment.py","path":"tests/test_packaging_metadata.py","size":5895,"sha256":"5053decd678a5fbca1a39bdb4aec0f5f710d48e39d26d605650a152eb3c24bb3","contentType":"text/x-python; charset=utf-8"},{"id":"5994acaf-060e-5dc7-b487-aee6308bc5b8","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5994acaf-060e-5dc7-b487-aee6308bc5b8/attachment.py","path":"tests/test_parse_test_output.py","size":1408,"sha256":"ed82f298de93b282410eba6952d09a3020e0ac336870cf47739cd34e09d069f9","contentType":"text/x-python; charset=utf-8"},{"id":"e4b8f6eb-dff1-5f01-866a-76db50c4c339","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e4b8f6eb-dff1-5f01-866a-76db50c4c339/attachment.py","path":"tests/test_repo_layout.py","size":3646,"sha256":"5b84692248eddc5477af89ecf4c2a68e17dc985beeb30315224f5d85a2307654","contentType":"text/x-python; charset=utf-8"},{"id":"b3fb818b-343a-5741-b34f-a25e4b835be7","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b3fb818b-343a-5741-b34f-a25e4b835be7/attachment.py","path":"tests/test_skill_catalog.py","size":21980,"sha256":"71ca3b72d3fd6641c2c5afda8d805ff4ef685f976d8a36c20d1017c1a61c43a0","contentType":"text/x-python; charset=utf-8"},{"id":"2dda3f6f-8917-5a95-831a-e1b2c97440cb","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/2dda3f6f-8917-5a95-831a-e1b2c97440cb/attachment.py","path":"tests/test_skill_validation.py","size":2215,"sha256":"85c09bdc952ee813171f295cc3ae0f490a10906b4e88b93284ea764a68158cb8","contentType":"text/x-python; charset=utf-8"},{"id":"2fceae22-b5dc-51b2-b65e-fd72d8257f32","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/2fceae22-b5dc-51b2-b65e-fd72d8257f32/attachment.py","path":"tests/test_update_check.py","size":10958,"sha256":"e415b2404ea69e0fb75da69c30713a5318b6dee67101bdd0841d530caf73fa73","contentType":"text/x-python; charset=utf-8"}],"bundle_sha256":"5d3b2d3f75e1ca02fb1448f7a6775747f091d8091aa47e5a8584615cff8755a7","attachment_count":64,"text_attachments":60,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":4,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"web-development","category_label":"Web"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"web-development","import_tag":"clean-skills-v1","description":"Use when a builder needs the top-level router for the provisional standard Gear/Vara Sails skill pack across Codex, Claude, or OpenClaw. Covers standard Sails apps and ethexe-specific work via dedicated skills. Do not use for non-Sails programs or broad protocol research."}},"renderedAt":1782981547398}

Preamble (run first) If output shows : read and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise ask user with 3 options, write snooze if declined). If : tell user "Running vara-skills v{to} (upgraded from v{from})!" and continue. Vara Skills This repository is the portable router for the provisional pack. Use first when the task is about standard greenfield or unreleased Gear/Vara Sails application work. The repo is intended to be self-contained: use local handbook files before depending on sibling repositories or machine-local skill directories. What This Router Cover…