Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Deployment specs and state patches

A market is described by one spec file and deployed in two commands. There is no shell script: the spec is the source of truth, and everything that used to live in env.sh, market-args.json and proxy-*.json is derived from it.

deployments/alpha targets a mainnet registry, so both commands need --network mainnet — the CLI defaults to testnet. NETWORK and SIGNER_ID work in place of the flags.

tmplrmgr market plan  deployments/alpha/<market>.toml --out plan.json \
    --network mainnet --signer-id "$REGISTRY_OWNER" --public-key ed25519:…
tmplrmgr market apply --plan plan.json \
    --network mainnet --signer-id "$REGISTRY_OWNER" --sign-with keychain

The signer is not a personal account: registry.deploy asserts the registry's owner, and a proxy spec additionally requires it to equal governance.admin, which the mainnet profiles set to the registry itself.

plan reads the chain and writes a file; it sends nothing and takes no credential. apply sends what the file says.

Why two steps

The plan is a reviewable artifact. It lists every transaction, decodes the market configuration for reading, names the keys each new account will grant, and carries the results of every preflight check. Reviewing a deployment no longer means reading a shell script and trusting it matches four JSON files.

It is a record of a derivation, not an input: the file carries the spec it came from, and apply re-derives the steps and refuses anything that does not match. Editing the plan is therefore not a way to change a deployment — change the spec and re-plan. For something no spec can express, run the transaction yourself with the command that performs it (registry deploy, proxy-oracle governance create-proposal, storage deposit); each is typed and validated on its own.

Writing a spec

Shared values live in deployments/profiles/. A market file names the profiles it extends and states only what differs. Abbreviated — a market also needs a [borrow] leg and the [market] parameters the profiles above do not set; see any file under deployments/ for a complete one:

extends = ["../profiles/alpha.toml", "../profiles/irs-standard.toml"]
name = "my-market"

[oracle.direct]                    # reads an oracle that already exists
account_id = "pyth-oracle.near"

[collateral]
asset = "nep141:usdc.near"
price_id = "eaa020c6…"             # the oracle's own identifier
decimals = 6

Omit [oracle.direct] to deploy a dedicated proxy oracle instead. A proxy market names sources per asset and the deployment creates a governance contract, the oracle it owns, and the market — seven transactions, plus one storage registration per NEP-141 asset, rather than one.

Amounts

Every amount states its unit, and the tool does the scaling:

[market]
borrow_range            = { minimum = "1 atom" }
supply_range            = { minimum = "0.04 tokens" }
supply_withdrawal_range = { minimum = "0.04 tokens", maximum = "1000 tokens" }
origination_fee         = { Flat = "0 atoms" }

tokens counts whole units of the borrow asset, scaled by its decimals when the plan is built — "0.04 tokens" is four cents of a stablecoin whether it carries 6 decimals or 7. atoms counts the indivisible base units the chain stores, so "1 atom" says "no real floor" in a way "0.0000001 tokens" does not. All three ranges and both fees are denominated in the borrow asset; nothing is stated in collateral.

The unit is mandatory. A bare number is refused rather than guessed at, as is a tokens value with more decimal places than the asset can hold, or a fractional atom. Both spellings parse (1 atom, 1 atoms); the tool writes the plural.

This is what schema 5 changed. A schema 4 file wrote the same amounts as bare base-unit integers, which are still well-formed numbers — read as whole units they would be 10^decimals too large — so such a file is refused by version and must be re-authored, not renumbered.

Checking before and after

tmplrmgr spec check   deployments/alpha/<market>.toml --network mainnet
tmplrmgr market verify <account-id> --network mainnet \
    --governance-admin <account-id> \
    --against deployments/alpha/<market>.toml

Both modes verify. A direct market reconstructs without proxies or governance, so the two governance checks are skipped and everything else runs; --governance-admin is still required and means nothing there.

verify re-runs the preflight against what is actually on chain and exits non-zero on failure, so it can run on a schedule. That matters because the governance call that configures a price feed is dispatched detached: it reports success even when the oracle rejected the proxy, so deployed state is the only witness that a market can price anything.

Reading the report

Checks are printed to stderr as they run, grouped by what is being read, then summarized. The summary leads with the failures, in full, and lists what was skipped separately — a check that did not run proves nothing, and must never be counted as one that passed.

→ registry versions
  ok   registry.version.market          v1.3.0
  FAIL registry.version.oracle          `0.5.9` is not registered in v1.tmplr.near; the depl…

5 check(s): 3 passed, 1 skipped, 1 FAILED

FAILED
  registry.version.oracle
    `0.5.9` is not registered in v1.tmplr.near; the deploy would fail partway

Colour is used only on a terminal, and NO_COLOR turns it off. -q silences the report. stdout stays the machine-readable channel throughout, so spec check … >/dev/null leaves the report alone and … 2>/dev/null | jq leaves the JSON alone.

--skip-check <id> suppresses one verdict — every other check still runs, and the report records what the skip suppressed, so an override stays reviewable rather than reading as a pass. An id that matches no check is an error, since a typo would otherwise silently suppress nothing. Available on spec check, market plan and market apply.

Resuming

apply journals each step beside the plan as it lands. If a run is interrupted, re-running it skips what completed and continues from the first incomplete step. A plan truncated to its completed prefix is refused rather than reported complete: the re-derivation runs before the journal is consulted.

Patching contract storage

tmplrmgr patch builds one atomic transaction for a contract whose full-access key is still held: deploy the pinned PatchState WASM, apply guarded storage operations, then restore the exact local code or global-contract linkage.

Export complete, block-pinned contract storage before authoring guards:

tmplrmgr patch export <account> --out deployments/patches/<account>/<date>-<slug>.toml \
  --network mainnet

The export writes every contract-storage entry as schema-3 TOML and a sibling <stem>.blobs/ directory. Values are stored as deterministic files named from the SHA-256 of their raw keys. Planning separately re-fetches the pinned account metadata, access keys, code, and contract linkage. Existing spec or blob paths are never overwritten. A patch.state_complete check reports whether the trie fit in one request or required widening one-byte prefixes; incomplete or conservatively unaccountable state aborts without creating output.

Review and build the plan:

tmplrmgr patch plan deployments/patches/<account>/<date>-<slug>.toml \
  --out patch-plan.json --network mainnet \
  --signer-id <account> --public-key ed25519:…
tmplrmgr patch dry-run --plan patch-plan.json --network mainnet

patch dry-run reconstructs the target under its literal account ID in a fresh sandbox, installs the fetched code and complete state, and executes the exact reviewed tx.batch. Each view [[check]] is called before and after. An expect compares only the after JSON; a check without expect passes after a successful call and still reports its observed value. Before failures and before/after differences are diagnostic; after failures and expectation mismatches fail the check. JSON Patch diffs are present when both calls return JSON.

The dry-run prints one machine-readable JSON report on stdout, including the transaction, target code hash, every before/after view result, JSON diff, and check verdict. Reporter output, progress, diagnostics, and the digest go to stderr. Keep stdout dedicated to the report when piping it to review tooling. The completed replay is stamped into the same plan when no replay check fails. An apply-valid stamp requires every replay check to be non-skipped and passed; apply rejects stamps with skipped checks. The stamp binds the plan digest, semantic complete-state digest, target code hash, and verdicts; it records the sandbox chain ID for review context.

Apply only after reviewing both the plan and stamped replay:

tmplrmgr patch apply --plan patch-plan.json \
  --network mainnet --signer-id <account> --sign-with keychain

Apply re-derives the spec, live code/linkage, complete-state digest, and batch; state drift invalidates the plan. Without an explicit override it refuses a missing, stale, digest-mismatched, or failed patch.dry_run stamp. --skip-check patch.dry_run and --skip-check patch.state_complete are rejected. If local replay is unavailable and the operator accepts that risk, use the dedicated override:

tmplrmgr patch apply --plan patch-plan.json --no-dry-run \
  --network mainnet --signer-id <account> --sign-with keychain

--no-dry-run records patch.dry_run as explicitly skipped; all other preflight checks still run. Prefix deletes are expanded from one verified full snapshot and retain an in-receipt expectation for every concrete removal. Accounts containing record kinds the accounting reader cannot enumerate are rejected rather than silently treated as complete.

The plan is not self-contained: apply re-reads its canonical source spec and referenced files at their original paths. Dry-run views are sandbox evidence; check the live account separately after apply.

Schema-2 storage-only specs are not accepted. Review the authored operations and checks, set schema = 3, then rerun tmplrmgr patch plan; alternatively, export a fresh schema-3 spec with the command above.

Authored set and single-key remove operations should state expect. Use expect = "absent" for a fresh key; it compiles to an in-receipt absence guard. Keys and values use utf8, hex, base64, file, concat, sha256, json, or borsh byte expressions. file is relative to the declaring spec.

This is a privileged authorization checklist:

  • Confirm the target account, full-access signer, and plan public key.
  • Inspect the released PatchState 0.1.0 artifact and pinned SHA-256.
  • Confirm the batch receiver, spec target, and PatchState payload account match.
  • Inspect all view before/after values and diffs, including no-expect checks.
  • Verify the apply-time stamp binding and resolved-state re-derivation checks.
  • Authorize only after the complete arbitrary-storage write is understood.