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-static
on: [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-legal
on: [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-test
on: [push]
jobs:
test:
uses: rainlanguage/rainix/.github/workflows/rainix-sol-test.yaml@main
secrets: 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,
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.
.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: rainix
on: [push]
jobs:
rainix:
uses: rainlanguage/rainix/.github/workflows/rainix-sol.yaml@main
secrets: 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/BuildPointers.sol→src/generated/*.pointers.solforge build+./script/CopyArtifacts.sol --ffi→ committed ABI JSON
then forge fmt and the git diff assert.
name: copy-artifacts
on: [push]
jobs:
copy-artifacts:
uses: rainlanguage/rainix/.github/workflows/rainix-copy-artifacts.yaml@main
secrets: 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-static
on: [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-test
on: [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-wasm
on: [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-rs
on: [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.
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.