Nix flake providing development environments and build tasks for the Rain Protocol ecosystem.
Rainix is shared infrastructure consumed by other Rain repos — the actual project code lives in downstream consumers.
Add Rainix as a flake input:
{inputs.rainix.url="github:rainlanguage/rainix";}Requires Nix with flakes enabled.
nix develop # default shell (Solidity + Rust + Node + subgraph tools)
nix develop .#sol-shell # slim Solidity-only shell — no rust, node, subgraph
nix develop .#rust-shell # slim Rust-only shell — no sol, nodeThe default shell auto-sources .env if present and runs
npm ci --ignore-scripts if package.json exists. sol-shell skips both.
lib/update-rainix.sh bumps a consuming repo to the latest rainix and re-locks
Soldeer. Run it from the repo root (it makes local changes only — review and
commit yourself):
/path/to/rainix/lib/update-rainix.shIt bumps the rainix flake input to the latest default branch and — for
Solidity repos — re-locks Soldeer and runs a sanity forge build. Soldeer
dependency version bumps are left to the developer (edit foundry.toml, run
forge soldeer update, fix the version-suffixed imports), since bumping blindly
can break builds when a transitive dependency pins an older version.
All tasks are Nix packages run via nix run. From a consuming repo:
nix run ..#rainix-sol-test— forge testnix run ..#rainix-sol-static— slither + forge fmt checknix run ..#rainix-sol-legal— REUSE/DCL-1.0 license compliancenix run ..#rainix-sol-artifacts— deploy to testnet
nix run ..#rainix-rs-test— cargo testnix run ..#rainix-rs-static— cargo fmt + clippy
Downstream flakes can compose their own tasks and shells using:
pkgs— nixpkgs with all overlays appliedrust-toolchain— pinned Rust toolchainrust-build-inputs,sol-build-inputs,node-build-inputs— dependency listsmkTask— create Nix derivations wrapping shell scripts with dependencies on PATH
.github/workflows/rainix-sol-static.yaml runs rainix-sol-static (slither) on
Linux. Wrapper in the consumer repo:
name: rainix-sol-staticon: [push]jobs:
static:
uses: rainlanguage/rainix/.github/workflows/rainix-sol-static.yaml@mainRuns forge soldeer install automatically when a soldeer.lock is present.
.github/workflows/rainix-sol-legal.yaml runs rainix-sol-legal (reuse lint)
on Linux. Same wrapper shape as the static one:
name: rainix-sol-legalon: [push]jobs:
legal:
uses: rainlanguage/rainix/.github/workflows/rainix-sol-legal.yaml@main.github/workflows/rainix-sol-test.yaml runs rainix-sol-test (forge test)
on Linux. Wrapper:
name: rainix-sol-teston: [push]jobs:
test:
uses: rainlanguage/rainix/.github/workflows/rainix-sol-test.yaml@mainsecrets: inheritsecrets: inherit is required because the reusable wires the standard fork RPC
env vars (ARBITRUM_RPC_URL, BASE_RPC_URL, BASE_SEPOLIA_RPC_URL,
ETHEREUM_RPC_URL, FLARE_RPC_URL, HYPEREVM_RPC_URL, POLYGON_RPC_URL,
SEPOLIA_RPC_URL, CI_DEPLOY_SEPOLIA_RPC_URL) plus ETHERSCAN_API_KEY and
DEPLOYMENT_KEY from the consumer org's secrets/vars. Repos that do no fork
tests can ignore — empty values are harmless.
CI_DEPLOY_SEPOLIA_RPC_URL and the ETH_RPC_URL it is bound to are LEGACY and
scheduled for removal (#340). ETH_RPC_URL reads as though
it means Ethereum mainnet but resolves to the Sepolia-era deploy secret, so a
test trusting the name forks the wrong network. New code wants
ETHEREUM_RPC_URL or SEPOLIA_RPC_URL — whichever it actually means — both of
which the preflight health-checks before binding.
.github/workflows/rainix-sol.yaml fans out static, legal, and test in parallel
— each on its own runner. Single wrapper for sol-only repos that want all three:
name: rainixon: [push]jobs:
rainix:
uses: rainlanguage/rainix/.github/workflows/rainix-sol.yaml@mainsecrets: inheritConsumers needing only one of the three should call the individual reusable directly rather than this composite.
.github/workflows/rainix-copy-artifacts.yaml regenerates committed generated
Solidity artifacts from source and asserts git diff --exit-code — failing the
PR if a maintainer changed source without committing the regenerated files. In a
single job it runs whichever of these the repo has:
./script/Build.sol→src/generated/(the rollingcandidate/deploy pins, plus the alias and released-suites libs generated from the record). The same regenerationrainix-tag-releasere-runs at publish time to prove the tagged commit's frozen snapshot is a fresh one.forge build+./script/CopyArtifacts.sol --ffi→ committed ABI JSON
then forge fmt and the git diff assert.
name: copy-artifactson: [push]jobs:
copy-artifacts:
uses: rainlanguage/rainix/.github/workflows/rainix-copy-artifacts.yaml@mainsecrets: inheritThis replaces the former rainix-build-pointers reusable — a pointer-only repo
just omits CopyArtifacts.sol (the copy step is skipped via hashFiles).
Always runs through rainix's sol-shell (slim), regardless of the consumer's
default devShell. secrets: inherit carries CACHIX_AUTH_TOKEN.
.github/workflows/rainix-rs-static.yaml runs rainix-rs-static (cargo fmt
check + clippy with -D clippy::all) on Linux. Wrapper:
name: rainix-rs-staticon: [push]jobs:
rs-static:
uses: rainlanguage/rainix/.github/workflows/rainix-rs-static.yaml@mainAlways runs through rainix's rust-shell (rust toolchain only — no sol/node),
regardless of the consumer's default devShell.
.github/workflows/rainix-rs-test.yaml runs cargo test on Linux and macOS.
Wrapper:
name: rainix-rs-teston: [push]jobs:
rs-test:
uses: rainlanguage/rainix/.github/workflows/rainix-rs-test.yaml@mainSame shape as rs-static — runs through rust-shell. Consumers whose rust crate
compiles standalone (no live forge artifacts at compile time) can drop their
bespoke rs-test matrix in favour of this.
.github/workflows/rainix-rs-wasm.yaml cross-compiles the workspace to
wasm32-unknown-unknown (release, library targets only). For consumers that
ship rust crates downstream as WASM (e.g. via wasm-bindgen for JS/TS), this
catches WASM-incompatible dependencies before they reach the JS build. Wrapper:
name: rainix-rs-wasmon: [push]jobs:
rs-wasm:
uses: rainlanguage/rainix/.github/workflows/rainix-rs-wasm.yaml@mainrust-shell's toolchain already includes the wasm32-unknown-unknown target,
so no extra setup is required.
.github/workflows/rainix-rs.yaml fans out static, test, and wasm in parallel —
each on its own runner. Single wrapper for rust-shipping repos that want all
three:
name: rainix-rson: [push]jobs:
rainix-rs:
uses: rainlanguage/rainix/.github/workflows/rainix-rs.yaml@mainConsumers needing only one of the three should call the individual reusable directly rather than this composite.
Each <NETWORK>_RPC_URL is chosen at job start by the rpc-preflight composite
action, not bound to a single configured URL. Foundry maps one [rpc_endpoints]
alias to exactly one URL and --fork-retries only retries that same URL, so a
dead upstream — plan quota exhausted, pruning node, host gone — cannot be
recovered inside forge. The preflight recovers it one layer up.
Candidates are a merged pool, not a fallback chain. For each network:
| source | holds | order |
|---|---|---|
secret RPC_URL_<NETWORK>_FORK | keyed/paid URLs (masked) | first |
variable RPC_URL_<NETWORK>_FORK | public keyless URLs (visible) | next |
| hardcoded public archive defaults | measured keyless archive endpoints | last |
Both the secret and the variable hold a newline-separated list; a single
bare URL is a one-element list, which is what they contain today. Every entry in
every source is a real candidate — the variable's URLs are tried even when the
secret is set. The order only expresses preference: the paid endpoint first, the
org's curated public list next, the hardcoded safety net when both are
exhausted. Keeping keyed URLs in the secret and keyless ones in the variable is
the point of merging: a public archive endpoint can back up a keyed one without
putting a non-secret into a secret (where masking makes logs unreadable for no
security benefit). # starts a comment, so a candidate can be parked with a
note.
Health is archive-aware. A candidate must report the right chain id, then
serve historical account state and a historical eth_call at the deepest block
any repo in the org pins for that network, three times consecutively. An
eth_blockNumber check would happily select a pruning node that then fails the
suite with trying to fork from an older block with a non-archive node; a
code-only check would select a host that answers no eth_call at all; and a
single sample would qualify a load balancer that round-robins over a mix of
archive and pruning backends. Ethereum and HyperEVM are latest-only in every
consumer, so they are not held to the archive bar, and neither are
deploy/broadcast paths.
Health also covers load, not just correctness. Chain id, historical state
and historical eth_call are all correctness questions, and an endpoint that is
throttled rather than broken answers every one of them perfectly — then returns
408 Request timeout on the free plan the moment forge opens real fork
traffic (#340). Each candidate is therefore also hit with
--burst simultaneous eth_calls (16 by default), repeated for as many rounds
as there are samples, and is rejected when a majority of any one round comes
back throttled.
Rounds rather than a single burst, because these endpoints meter a token bucket:
the first burst after an idle period is served out of a full bucket and passes
even on an endpoint that then collapses. A majority rather than any single
failure, because every healthy public endpoint sheds the occasional request
under load, and rejecting on one would make the preflight flakier than the
outage it exists to prevent. --burst 0 disables the check.
Because public rate limits are per-IP, the hardcoded default order is only a preference and cannot be right for every runner — the burst is what makes the selection safe, by rejecting whichever candidate is throttled for the runner running right now.
No candidate URL is ever printed. Logs name the source (secret[0],
variable[1], default[0]) and a typed reason, never a URL:
rpc-preflight: arbitrum: secret[0] rejected: quota exhausted / rate limited (rpc error -32001)
rpc-preflight: arbitrum: SELECTED variable[0] (chain 42161, archive at block 280000000, 3/3 samples)
Only networks the repo actually references are probed, and a network with no candidates at all is left exactly as it is today.
- Rust: 1.94.0
- Solidity: solc 0.8.25
- Foundry: via foundry.nix
- Graph CLI: 0.69.2
- Goldsky CLI: 13.3.4
DecentraLicense 1.0 — enforced via reuse lint.