@gatedpath/preflight 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +189 -0
  3. package/dist/bip39-words.d.ts +1 -0
  4. package/dist/bip39-words.js +132 -0
  5. package/dist/checks/admin.d.ts +24 -0
  6. package/dist/checks/admin.js +81 -0
  7. package/dist/checks/balance.d.ts +3 -0
  8. package/dist/checks/balance.js +39 -0
  9. package/dist/checks/chain-id.d.ts +2 -0
  10. package/dist/checks/chain-id.js +11 -0
  11. package/dist/checks/compiler.d.ts +5 -0
  12. package/dist/checks/compiler.js +20 -0
  13. package/dist/checks/deployer.d.ts +14 -0
  14. package/dist/checks/deployer.js +53 -0
  15. package/dist/checks/git-secrets.d.ts +14 -0
  16. package/dist/checks/git-secrets.js +91 -0
  17. package/dist/checks/slither.d.ts +11 -0
  18. package/dist/checks/slither.js +119 -0
  19. package/dist/cli.d.ts +8 -0
  20. package/dist/cli.js +131 -0
  21. package/dist/config.d.ts +25 -0
  22. package/dist/config.js +197 -0
  23. package/dist/doctor-cli.d.ts +9 -0
  24. package/dist/doctor-cli.js +76 -0
  25. package/dist/doctor.d.ts +60 -0
  26. package/dist/doctor.js +251 -0
  27. package/dist/dotenv.d.ts +7 -0
  28. package/dist/dotenv.js +28 -0
  29. package/dist/gas-cli.d.ts +8 -0
  30. package/dist/gas-cli.js +100 -0
  31. package/dist/gas.d.ts +110 -0
  32. package/dist/gas.js +221 -0
  33. package/dist/index.d.ts +28 -0
  34. package/dist/index.js +20 -0
  35. package/dist/proc.d.ts +14 -0
  36. package/dist/proc.js +28 -0
  37. package/dist/redbelly.d.ts +2 -0
  38. package/dist/redbelly.js +40 -0
  39. package/dist/run.d.ts +9 -0
  40. package/dist/run.js +106 -0
  41. package/dist/secrets.d.ts +18 -0
  42. package/dist/secrets.js +89 -0
  43. package/dist/ship-cli.d.ts +8 -0
  44. package/dist/ship-cli.js +128 -0
  45. package/dist/ship.d.ts +108 -0
  46. package/dist/ship.js +383 -0
  47. package/dist/suites.d.ts +26 -0
  48. package/dist/suites.js +53 -0
  49. package/dist/types.d.ts +58 -0
  50. package/dist/types.js +3 -0
  51. package/dist/well-known.d.ts +2 -0
  52. package/dist/well-known.js +21 -0
  53. package/package.json +50 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 gatedpath and contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,189 @@
1
+ # @gatedpath/preflight
2
+
3
+ ```
4
+ For agents
5
+ - Run `redbelly-preflight --project . --chain 153 --account <keystore-name> --admin <safe>` before
6
+ any deploy and read every line. Exit 1 means do not deploy; fix the failing line, run it again.
7
+ - It never takes a key. --account names a Foundry keystore that cast reads; --address is for a
8
+ read-only run. If you are tempted to pass a hex string, stop.
9
+ - Mainnet (151) refuses an EOA admin and a Safe with threshold below 2. Testnet warns instead.
10
+ - `--json` for CI and for the MCP server. Pins: solc 0.8.30, EVM prague (verified 2026-09-12).
11
+ ```
12
+
13
+ Seven checks, each printed as `pass`, `fail`, `warn` or `skip` with one sentence saying why,
14
+ then a non-zero exit if anything failed. The package also carries `redbelly-doctor` (the machine
15
+ before the first command) and the `redbelly` bin whose subcommands are `preflight`, `doctor`, `gas`
16
+ (gas per function in RBNT and US cents) and `ship` (the mainnet gate that writes its own report);
17
+ each has a section below. This is the gate PLAN.md sections 5.4 and 6 (Deploy)
18
+ describe, corrected by section 15: there is no network-wide verifier address to match, so the
19
+ deployer's `permission.isAllowed` reading replaces it. Node 18 or newer, TypeScript, one
20
+ runtime dependency (`@gatedpath/chains`, by path). Network traffic is JSON-RPC to the
21
+ one RPC you name and nothing else.
22
+
23
+ ## The checks
24
+
25
+ | id | What passes | What fails |
26
+ |---|---|---|
27
+ | `chain-id` | The chain in `foundry.toml` (`chain_id`), the Hardhat network's `chainId`, or `--chain` equals the RPC's `eth_chainId` | A mismatch, an RPC that is neither 151 nor 153, or an RPC that does not answer. Skips when nothing states an expected chain. |
28
+ | `deployer-verified` | The deployer passes `permission.isAllowed` on the target chain, read through the bootstrap registry | `isAllowed` is false: the wallet has not been through access.redbelly.network. Warns, whatever `isAllowed` says, when the deployer is one of the ten anvil/Hardhat default accounts, whose keys are public and two of which pass `isAllowed` on both networks (RESEARCH.md question 30) |
29
+ | `admin-safe` | `--admin` holds a SafeProxy whose slot 0 is the canonical Safe 1.4.1 singleton or SafeL2 singleton, that singleton's code hashes to the 1.4.1 release, and `getThreshold()` is 2 or more | On 151 an EOA, a non-Safe contract, foreign singleton code or threshold 1 fails; on 153 each of those is a warning |
30
+ | `compiler-pins` | `solc_version` (or `solc`) is 0.8.30 and `evm_version` is prague; Hardhat `solidity.version` and `settings.evmVersion` likewise | Anything else, or no config file |
31
+ | `git-secrets` | No added line anywhere in `git log -p --all` is secret-shaped, and no `.env` is tracked | A bare 64-hex value, a 0x 64-hex value on a line about a key, `PRIVATE_KEY=` with a real value, twelve consecutive BIP-39 words, or a keystore JSON. Skips outside git. |
32
+ | `balance` | Deployer balance covers `--gas` times the latest base fee, priced through the on-chain feed by `gasCostUsd`, plus 25 percent | Short, with the RBNT and USD figures and where to get testnet coins |
33
+ | `slither-report` | `reports/slither.json` or `reports/slither.md` (or `--slither-report`) exists and describes the sources as they are: when a `slither.sources.sha256` sidecar sits beside it, the SHA-256 of every `.sol` under the source directory must equal it; without a sidecar, the report must be newer than every source | Missing, a sidecar that no longer matches, or (with no sidecar) older than a source |
34
+
35
+ "Newer" inside a git repository means the commit time of the file's last change, or the
36
+ working-tree mtime for a modified or untracked file; a checkout gives every file the same
37
+ mtime, so mtimes alone would say nothing. Outside git, mtimes. The time rule has a hole,
38
+ found on 2026-09-12 while running the prompt cards: a clean project's report has no findings,
39
+ so re-running Slither writes byte-identical JSON, git records no change, and the report can
40
+ never become "newer" than a source that was edited after it. Hence the sidecar: the scaffold's
41
+ `npm run lint:slither` writes `reports/slither.sources.sha256` (SHA-256 over every `.sol`
42
+ under `src/`, sorted by POSIX path, each as `path`, newline, bytes, NUL), and when the sidecar
43
+ exists the check compares hashes and ignores times. `hashSources(dir)` is exported for tools
44
+ that write the sidecar.
45
+
46
+ The secrets check streams `git log -p --all --unified=0` and looks only at added lines, so a
47
+ key that was committed and later deleted still fails: it is in the history and history is
48
+ what gets cloned. Findings name the file, the commit and the kind of match, never the text.
49
+ Lockfiles, `lib/`, `out/`, `node_modules/` and minified files are skipped; lines that mention
50
+ sha256, keccak, integrity, a tx hash or a JSON `"id"` are treated as hashes.
51
+
52
+ ## redbelly-doctor
53
+
54
+ The machine before the first command (PLAN.md 18.3). `redbelly-doctor` (also `redbelly doctor`,
55
+ `npm run doctor` in a scaffold, and the MCP server's `doctor` tool) prints one line per check with
56
+ `pass`, `warn` or `fail` and the fix on the same line, and exits 0 only when every required check
57
+ passes; `--json` for agents. The fix text is the failure table's (`@gatedpath/agent-rules`), so
58
+ `explain_failure` says the same thing about a pasted doctor line.
59
+
60
+ | id | required | What it checks |
61
+ |---|---|---|
62
+ | `node` | yes | Node 22 or later |
63
+ | `git` | yes | git on PATH |
64
+ | `forge`, `anvil`, `cast` | yes | each on PATH and answering `--version` |
65
+ | `foundry-version` | yes | the three report one version |
66
+ | `foundry-exit-code` | yes | each hands its exit code back. The `@foundry-rs` npm packages (1.7.1) install a Node shim that exits 0 whatever the binary returned, so a failed `forge build` reports success; doctor probes with a flag no binary knows (the real ones exit 2, the shim 0), names the shim, and prints the `ln -sf` line to the real binary it found in the platform package beside it |
67
+ | `slither`, `aderyn` | no | on PATH; a warning when missing or not at the version CI pins (0.11.6, 0.6.8) |
68
+ | `env-tracked` | yes | no `.env` or `.env.*` (other than `.env.example`) in `git ls-files` |
69
+ | `vendor` | yes | in a scaffold, each `vendor/redbelly-*/package.json` version equals the record the scaffolder wrote under `redbelly.vendor` in the root `package.json`; a scaffold without the record warns |
70
+
71
+ ```
72
+ redbelly-doctor [--project <dir>] [--json]
73
+ ```
74
+
75
+ ## redbelly gas
76
+
77
+ A gas report in the unit the chain charges in (PLAN.md 18.3). `redbelly gas` (also `npm run gas` in a
78
+ scaffold and the MCP server's `gas_report` tool) reads `.gas-snapshot` when the project has one (one
79
+ row per test; fuzz lines by their mean) or runs `forge test --gas-report --json` (the deployment and
80
+ every function of the contracts in `gas_reports`), prices each row at the latest base fee and the
81
+ on-chain feed price with the same integer arithmetic as `gasCostUsd`, and prints gas, RBNT and US
82
+ cents per row. `--diff <file>` compares against a previous snapshot or report, either format, and
83
+ adds the gas and cents difference and what was added or gone. Two RPC reads, nothing else.
84
+
85
+ ```
86
+ redbelly gas [--project <dir>] [--chain 151|153] [--rpc <url>] [--snapshot <file>] [--report] [--diff <file>] [--json]
87
+ ```
88
+
89
+ Recorded runs against the real feeds, read-only, on 14 September 2026: `reports/gas-153-2026-09-14.md`
90
+ and `reports/gas-151-2026-09-14.md`. On both, 21,000 gas came to 1.000 US cents and the scaffold's
91
+ `GatedERC20` deploys for 1,898,391 gas, 383.7 RBNT, 90.4 cents at a feed price of US$0.002356 per RBNT.
92
+
93
+ ## redbelly ship
94
+
95
+ The mainnet gate that produces the document you wanted anyway (PLAN.md 18.3). `redbelly ship --chain
96
+ <151|153> --account <keystore-name> [--admin <safe>] [--gas <n>]` (also `npm run ship` in a scaffold
97
+ and the MCP server's `ship_report` tool) runs the gas report first (its deployment row is the gas the
98
+ balance check prices), then pre-flight's seven checks with the keystore's address, checks the verifier
99
+ (`--verifier` or `VERIFIER` in `.env`; required on 151, where a missing one fails and on 153 warns that
100
+ a `ReceptorMock` will be deployed), runs `forge test --json` and counts the suites that inherit
101
+ `GatedTest`, reads the admin (Safe version, singleton, threshold, owners, `isAllowed`) and writes
102
+ `deployments/ship-<chain>-<date>.md`: a header line `status` reads back, every check with its result,
103
+ the deployer and admin readings, the contracts from `deployments/<chain>-*.json` with their
104
+ constructor arguments and the exact `forge verify-contract` command, the gas table, the tests, the
105
+ Slither line, the compiler pins, and the list of what the report does not prove. It refuses to write
106
+ when any check fails and exits 1 with the failing lines. Run it again after the deploy to fill in the
107
+ contract section; the file is dated in UTC.
108
+
109
+ The scaffold's `RedbellyDeployScript.sol` refuses to broadcast on 151 without `deployments/ship-151-<today>.md`
110
+ (a dry run says so and continues; 153 warns), and the Hardhat deploy script does the same. Tested on
111
+ a local chain 151 with the mocked registry, the real Safe 1.4.1 bytecode and a keystore `cast wallet
112
+ new` created for the test; nothing signs against a real network.
113
+
114
+ ## Usage
115
+
116
+ ```
117
+ redbelly-preflight [--project <dir>] [--profile <name>] [--chain 151|153] [--rpc <url>]
118
+ [--account <keystore-name> [--password-file <path>] | --address <0x…>]
119
+ [--admin <0x…>] [--gas <units>] [--slither-report <path>]
120
+ [--skip <id,id>] [--json]
121
+ ```
122
+
123
+ `--account` runs `cast wallet address --account <name>` in the project directory; cast
124
+ decrypts the keystore, this process sees the address. Foundry looks for the keystore under
125
+ `$HOME/.foundry/keystores`. Without a terminal, give cast the password with
126
+ `--password-file` or `ETH_PASSWORD`. `--address` does the same checks read-only.
127
+
128
+ Flags you leave out are filled from the project's `.env` (parsed, never executed) or the
129
+ environment, using the names the scaffolder's templates already use: `DEPLOYER` for
130
+ `--address`, `ADMIN_SAFE` for `--admin`, `CHAIN_ID` for `--chain` when the config names no
131
+ chain. So inside a `create-redbelly-dapp` project the whole call is `redbelly-preflight`.
132
+ `--gas` defaults to 3,000,000; pass what `forge script` estimated. With `--json` the whole
133
+ report (below) goes to stdout.
134
+
135
+ This is the same set of checks the scaffolder's `RedbellyDeployScript.preflight()` makes in
136
+ Solidity at broadcast time and its `scripts/preflight.mjs` makes offline, generalised so any
137
+ Foundry or Hardhat project can run them, with the history scan, the compiler pins, the
138
+ balance margin and the Slither freshness added.
139
+
140
+ Exit codes: 0 no check failed (warnings allowed), 1 at least one fail, 2 bad arguments. An
141
+ argument that looks like a private key is a bad argument.
142
+
143
+ ```ts
144
+ import { runPreflight, renderText } from '@gatedpath/preflight';
145
+ const report = await runPreflight({ project: '.', chain: 153, address: '0x…', admin: '0x…', gas: 1_200_000 });
146
+ process.stdout.write(renderText(report));
147
+ if (!report.ok) process.exit(1);
148
+ ```
149
+
150
+ The report: `{ tool, version, generatedAt, project, config: { kind, file, profile }, chain:
151
+ { expected, reported, rpc, network }, deployer, admin, checks: [{ id, status, reason, data }],
152
+ ok }`. `data` carries the numbers (threshold, balances, feed price, findings); it never
153
+ carries a secret.
154
+
155
+ ## Tests
156
+
157
+ `npm test` builds, then runs `test/unit.test.mjs` (TOML and Hardhat readers, the secret
158
+ scanner, the Safe fixture hashes) and `test/preflight.test.mjs`, which spawns `anvil`
159
+ twice, on chain 151 and on 153, and:
160
+
161
+ - etches hand-assembled bytecode for the bootstrap registry, the permission contract and the
162
+ price feed at the addresses `@gatedpath/chains` records (`test/mocks.mjs`), so the
163
+ real helpers run their real code paths;
164
+ - etches the genuine Safe 1.4.1 runtime code, read from testnet with `eth_getCode` on
165
+ 2026-09-12 and byte-identical on mainnet (`test/fixtures/safe-1.4.1/`), at the canonical
166
+ addresses, and deploys proxies through the real `SafeProxyFactory.createProxyWithNonce`
167
+ from an unlocked anvil account, so `getThreshold()` and the singleton reads are answered by
168
+ Safe's own logic. Etching, not compiling: no Solidity toolchain in the test and the bytes
169
+ are the ones on chain;
170
+ - builds temporary git repositories with pinned commit dates for the secrets and Slither
171
+ checks;
172
+ - creates a throwaway keystore with `cast wallet new` for the `--account` path (the key is
173
+ generated inside cast, encrypted at once and never printed);
174
+ - checks the hardcoded list of anvil default accounts against the spawned anvil's `eth_accounts`.
175
+
176
+ 29 tests. `npm run test:live` runs the read-only checks against the real testnet RPC using
177
+ the known allowed address from `@gatedpath/chains` as both deployer and admin.
178
+
179
+ ## Last verified
180
+
181
+ 2026-09-12. `npm test`: 29 passed (8 unit, 21 against anvil on 151 and 153). `npm run
182
+ test:live`: 1 passed. The live run's output is committed under `reports/` as
183
+ `testnet-153-2026-09-12.txt` and `.json`: chain, deployer, compiler, secrets, balance and
184
+ Slither pass; the admin warns because the known allowed address is an EOA, which is exactly
185
+ what the check is for. 1,000,000 gas cost 195.96 RBNT, US$0.4762 at the feed's US$0.002431,
186
+ which is the fixed-USD gas model in one line.
187
+
188
+ Anvil's base fee is not Redbelly's, so the balance figures in the anvil tests only prove
189
+ the arithmetic and the margin; the live run proves the price.
@@ -0,0 +1 @@
1
+ export declare const BIP39_ENGLISH: ReadonlySet<string>;
@@ -0,0 +1,132 @@
1
+ // BIP-39 English wordlist (2048 words), fetched from github.com/bitcoin/bips/blob/master/bip-0039/english.txt
2
+ // on 2026-09-12. Used only to spot a seed phrase pasted into a file; nothing here generates keys.
3
+ export const BIP39_ENGLISH = new Set([
4
+ "abandon", "ability", "able", "about", "above", "absent", "absorb", "abstract", "absurd", "abuse", "access", "accident", "account", "accuse", "achieve", "acid",
5
+ "acoustic", "acquire", "across", "act", "action", "actor", "actress", "actual", "adapt", "add", "addict", "address", "adjust", "admit", "adult", "advance",
6
+ "advice", "aerobic", "affair", "afford", "afraid", "again", "age", "agent", "agree", "ahead", "aim", "air", "airport", "aisle", "alarm", "album",
7
+ "alcohol", "alert", "alien", "all", "alley", "allow", "almost", "alone", "alpha", "already", "also", "alter", "always", "amateur", "amazing", "among",
8
+ "amount", "amused", "analyst", "anchor", "ancient", "anger", "angle", "angry", "animal", "ankle", "announce", "annual", "another", "answer", "antenna", "antique",
9
+ "anxiety", "any", "apart", "apology", "appear", "apple", "approve", "april", "arch", "arctic", "area", "arena", "argue", "arm", "armed", "armor",
10
+ "army", "around", "arrange", "arrest", "arrive", "arrow", "art", "artefact", "artist", "artwork", "ask", "aspect", "assault", "asset", "assist", "assume",
11
+ "asthma", "athlete", "atom", "attack", "attend", "attitude", "attract", "auction", "audit", "august", "aunt", "author", "auto", "autumn", "average", "avocado",
12
+ "avoid", "awake", "aware", "away", "awesome", "awful", "awkward", "axis", "baby", "bachelor", "bacon", "badge", "bag", "balance", "balcony", "ball",
13
+ "bamboo", "banana", "banner", "bar", "barely", "bargain", "barrel", "base", "basic", "basket", "battle", "beach", "bean", "beauty", "because", "become",
14
+ "beef", "before", "begin", "behave", "behind", "believe", "below", "belt", "bench", "benefit", "best", "betray", "better", "between", "beyond", "bicycle",
15
+ "bid", "bike", "bind", "biology", "bird", "birth", "bitter", "black", "blade", "blame", "blanket", "blast", "bleak", "bless", "blind", "blood",
16
+ "blossom", "blouse", "blue", "blur", "blush", "board", "boat", "body", "boil", "bomb", "bone", "bonus", "book", "boost", "border", "boring",
17
+ "borrow", "boss", "bottom", "bounce", "box", "boy", "bracket", "brain", "brand", "brass", "brave", "bread", "breeze", "brick", "bridge", "brief",
18
+ "bright", "bring", "brisk", "broccoli", "broken", "bronze", "broom", "brother", "brown", "brush", "bubble", "buddy", "budget", "buffalo", "build", "bulb",
19
+ "bulk", "bullet", "bundle", "bunker", "burden", "burger", "burst", "bus", "business", "busy", "butter", "buyer", "buzz", "cabbage", "cabin", "cable",
20
+ "cactus", "cage", "cake", "call", "calm", "camera", "camp", "can", "canal", "cancel", "candy", "cannon", "canoe", "canvas", "canyon", "capable",
21
+ "capital", "captain", "car", "carbon", "card", "cargo", "carpet", "carry", "cart", "case", "cash", "casino", "castle", "casual", "cat", "catalog",
22
+ "catch", "category", "cattle", "caught", "cause", "caution", "cave", "ceiling", "celery", "cement", "census", "century", "cereal", "certain", "chair", "chalk",
23
+ "champion", "change", "chaos", "chapter", "charge", "chase", "chat", "cheap", "check", "cheese", "chef", "cherry", "chest", "chicken", "chief", "child",
24
+ "chimney", "choice", "choose", "chronic", "chuckle", "chunk", "churn", "cigar", "cinnamon", "circle", "citizen", "city", "civil", "claim", "clap", "clarify",
25
+ "claw", "clay", "clean", "clerk", "clever", "click", "client", "cliff", "climb", "clinic", "clip", "clock", "clog", "close", "cloth", "cloud",
26
+ "clown", "club", "clump", "cluster", "clutch", "coach", "coast", "coconut", "code", "coffee", "coil", "coin", "collect", "color", "column", "combine",
27
+ "come", "comfort", "comic", "common", "company", "concert", "conduct", "confirm", "congress", "connect", "consider", "control", "convince", "cook", "cool", "copper",
28
+ "copy", "coral", "core", "corn", "correct", "cost", "cotton", "couch", "country", "couple", "course", "cousin", "cover", "coyote", "crack", "cradle",
29
+ "craft", "cram", "crane", "crash", "crater", "crawl", "crazy", "cream", "credit", "creek", "crew", "cricket", "crime", "crisp", "critic", "crop",
30
+ "cross", "crouch", "crowd", "crucial", "cruel", "cruise", "crumble", "crunch", "crush", "cry", "crystal", "cube", "culture", "cup", "cupboard", "curious",
31
+ "current", "curtain", "curve", "cushion", "custom", "cute", "cycle", "dad", "damage", "damp", "dance", "danger", "daring", "dash", "daughter", "dawn",
32
+ "day", "deal", "debate", "debris", "decade", "december", "decide", "decline", "decorate", "decrease", "deer", "defense", "define", "defy", "degree", "delay",
33
+ "deliver", "demand", "demise", "denial", "dentist", "deny", "depart", "depend", "deposit", "depth", "deputy", "derive", "describe", "desert", "design", "desk",
34
+ "despair", "destroy", "detail", "detect", "develop", "device", "devote", "diagram", "dial", "diamond", "diary", "dice", "diesel", "diet", "differ", "digital",
35
+ "dignity", "dilemma", "dinner", "dinosaur", "direct", "dirt", "disagree", "discover", "disease", "dish", "dismiss", "disorder", "display", "distance", "divert", "divide",
36
+ "divorce", "dizzy", "doctor", "document", "dog", "doll", "dolphin", "domain", "donate", "donkey", "donor", "door", "dose", "double", "dove", "draft",
37
+ "dragon", "drama", "drastic", "draw", "dream", "dress", "drift", "drill", "drink", "drip", "drive", "drop", "drum", "dry", "duck", "dumb",
38
+ "dune", "during", "dust", "dutch", "duty", "dwarf", "dynamic", "eager", "eagle", "early", "earn", "earth", "easily", "east", "easy", "echo",
39
+ "ecology", "economy", "edge", "edit", "educate", "effort", "egg", "eight", "either", "elbow", "elder", "electric", "elegant", "element", "elephant", "elevator",
40
+ "elite", "else", "embark", "embody", "embrace", "emerge", "emotion", "employ", "empower", "empty", "enable", "enact", "end", "endless", "endorse", "enemy",
41
+ "energy", "enforce", "engage", "engine", "enhance", "enjoy", "enlist", "enough", "enrich", "enroll", "ensure", "enter", "entire", "entry", "envelope", "episode",
42
+ "equal", "equip", "era", "erase", "erode", "erosion", "error", "erupt", "escape", "essay", "essence", "estate", "eternal", "ethics", "evidence", "evil",
43
+ "evoke", "evolve", "exact", "example", "excess", "exchange", "excite", "exclude", "excuse", "execute", "exercise", "exhaust", "exhibit", "exile", "exist", "exit",
44
+ "exotic", "expand", "expect", "expire", "explain", "expose", "express", "extend", "extra", "eye", "eyebrow", "fabric", "face", "faculty", "fade", "faint",
45
+ "faith", "fall", "false", "fame", "family", "famous", "fan", "fancy", "fantasy", "farm", "fashion", "fat", "fatal", "father", "fatigue", "fault",
46
+ "favorite", "feature", "february", "federal", "fee", "feed", "feel", "female", "fence", "festival", "fetch", "fever", "few", "fiber", "fiction", "field",
47
+ "figure", "file", "film", "filter", "final", "find", "fine", "finger", "finish", "fire", "firm", "first", "fiscal", "fish", "fit", "fitness",
48
+ "fix", "flag", "flame", "flash", "flat", "flavor", "flee", "flight", "flip", "float", "flock", "floor", "flower", "fluid", "flush", "fly",
49
+ "foam", "focus", "fog", "foil", "fold", "follow", "food", "foot", "force", "forest", "forget", "fork", "fortune", "forum", "forward", "fossil",
50
+ "foster", "found", "fox", "fragile", "frame", "frequent", "fresh", "friend", "fringe", "frog", "front", "frost", "frown", "frozen", "fruit", "fuel",
51
+ "fun", "funny", "furnace", "fury", "future", "gadget", "gain", "galaxy", "gallery", "game", "gap", "garage", "garbage", "garden", "garlic", "garment",
52
+ "gas", "gasp", "gate", "gather", "gauge", "gaze", "general", "genius", "genre", "gentle", "genuine", "gesture", "ghost", "giant", "gift", "giggle",
53
+ "ginger", "giraffe", "girl", "give", "glad", "glance", "glare", "glass", "glide", "glimpse", "globe", "gloom", "glory", "glove", "glow", "glue",
54
+ "goat", "goddess", "gold", "good", "goose", "gorilla", "gospel", "gossip", "govern", "gown", "grab", "grace", "grain", "grant", "grape", "grass",
55
+ "gravity", "great", "green", "grid", "grief", "grit", "grocery", "group", "grow", "grunt", "guard", "guess", "guide", "guilt", "guitar", "gun",
56
+ "gym", "habit", "hair", "half", "hammer", "hamster", "hand", "happy", "harbor", "hard", "harsh", "harvest", "hat", "have", "hawk", "hazard",
57
+ "head", "health", "heart", "heavy", "hedgehog", "height", "hello", "helmet", "help", "hen", "hero", "hidden", "high", "hill", "hint", "hip",
58
+ "hire", "history", "hobby", "hockey", "hold", "hole", "holiday", "hollow", "home", "honey", "hood", "hope", "horn", "horror", "horse", "hospital",
59
+ "host", "hotel", "hour", "hover", "hub", "huge", "human", "humble", "humor", "hundred", "hungry", "hunt", "hurdle", "hurry", "hurt", "husband",
60
+ "hybrid", "ice", "icon", "idea", "identify", "idle", "ignore", "ill", "illegal", "illness", "image", "imitate", "immense", "immune", "impact", "impose",
61
+ "improve", "impulse", "inch", "include", "income", "increase", "index", "indicate", "indoor", "industry", "infant", "inflict", "inform", "inhale", "inherit", "initial",
62
+ "inject", "injury", "inmate", "inner", "innocent", "input", "inquiry", "insane", "insect", "inside", "inspire", "install", "intact", "interest", "into", "invest",
63
+ "invite", "involve", "iron", "island", "isolate", "issue", "item", "ivory", "jacket", "jaguar", "jar", "jazz", "jealous", "jeans", "jelly", "jewel",
64
+ "job", "join", "joke", "journey", "joy", "judge", "juice", "jump", "jungle", "junior", "junk", "just", "kangaroo", "keen", "keep", "ketchup",
65
+ "key", "kick", "kid", "kidney", "kind", "kingdom", "kiss", "kit", "kitchen", "kite", "kitten", "kiwi", "knee", "knife", "knock", "know",
66
+ "lab", "label", "labor", "ladder", "lady", "lake", "lamp", "language", "laptop", "large", "later", "latin", "laugh", "laundry", "lava", "law",
67
+ "lawn", "lawsuit", "layer", "lazy", "leader", "leaf", "learn", "leave", "lecture", "left", "leg", "legal", "legend", "leisure", "lemon", "lend",
68
+ "length", "lens", "leopard", "lesson", "letter", "level", "liar", "liberty", "library", "license", "life", "lift", "light", "like", "limb", "limit",
69
+ "link", "lion", "liquid", "list", "little", "live", "lizard", "load", "loan", "lobster", "local", "lock", "logic", "lonely", "long", "loop",
70
+ "lottery", "loud", "lounge", "love", "loyal", "lucky", "luggage", "lumber", "lunar", "lunch", "luxury", "lyrics", "machine", "mad", "magic", "magnet",
71
+ "maid", "mail", "main", "major", "make", "mammal", "man", "manage", "mandate", "mango", "mansion", "manual", "maple", "marble", "march", "margin",
72
+ "marine", "market", "marriage", "mask", "mass", "master", "match", "material", "math", "matrix", "matter", "maximum", "maze", "meadow", "mean", "measure",
73
+ "meat", "mechanic", "medal", "media", "melody", "melt", "member", "memory", "mention", "menu", "mercy", "merge", "merit", "merry", "mesh", "message",
74
+ "metal", "method", "middle", "midnight", "milk", "million", "mimic", "mind", "minimum", "minor", "minute", "miracle", "mirror", "misery", "miss", "mistake",
75
+ "mix", "mixed", "mixture", "mobile", "model", "modify", "mom", "moment", "monitor", "monkey", "monster", "month", "moon", "moral", "more", "morning",
76
+ "mosquito", "mother", "motion", "motor", "mountain", "mouse", "move", "movie", "much", "muffin", "mule", "multiply", "muscle", "museum", "mushroom", "music",
77
+ "must", "mutual", "myself", "mystery", "myth", "naive", "name", "napkin", "narrow", "nasty", "nation", "nature", "near", "neck", "need", "negative",
78
+ "neglect", "neither", "nephew", "nerve", "nest", "net", "network", "neutral", "never", "news", "next", "nice", "night", "noble", "noise", "nominee",
79
+ "noodle", "normal", "north", "nose", "notable", "note", "nothing", "notice", "novel", "now", "nuclear", "number", "nurse", "nut", "oak", "obey",
80
+ "object", "oblige", "obscure", "observe", "obtain", "obvious", "occur", "ocean", "october", "odor", "off", "offer", "office", "often", "oil", "okay",
81
+ "old", "olive", "olympic", "omit", "once", "one", "onion", "online", "only", "open", "opera", "opinion", "oppose", "option", "orange", "orbit",
82
+ "orchard", "order", "ordinary", "organ", "orient", "original", "orphan", "ostrich", "other", "outdoor", "outer", "output", "outside", "oval", "oven", "over",
83
+ "own", "owner", "oxygen", "oyster", "ozone", "pact", "paddle", "page", "pair", "palace", "palm", "panda", "panel", "panic", "panther", "paper",
84
+ "parade", "parent", "park", "parrot", "party", "pass", "patch", "path", "patient", "patrol", "pattern", "pause", "pave", "payment", "peace", "peanut",
85
+ "pear", "peasant", "pelican", "pen", "penalty", "pencil", "people", "pepper", "perfect", "permit", "person", "pet", "phone", "photo", "phrase", "physical",
86
+ "piano", "picnic", "picture", "piece", "pig", "pigeon", "pill", "pilot", "pink", "pioneer", "pipe", "pistol", "pitch", "pizza", "place", "planet",
87
+ "plastic", "plate", "play", "please", "pledge", "pluck", "plug", "plunge", "poem", "poet", "point", "polar", "pole", "police", "pond", "pony",
88
+ "pool", "popular", "portion", "position", "possible", "post", "potato", "pottery", "poverty", "powder", "power", "practice", "praise", "predict", "prefer", "prepare",
89
+ "present", "pretty", "prevent", "price", "pride", "primary", "print", "priority", "prison", "private", "prize", "problem", "process", "produce", "profit", "program",
90
+ "project", "promote", "proof", "property", "prosper", "protect", "proud", "provide", "public", "pudding", "pull", "pulp", "pulse", "pumpkin", "punch", "pupil",
91
+ "puppy", "purchase", "purity", "purpose", "purse", "push", "put", "puzzle", "pyramid", "quality", "quantum", "quarter", "question", "quick", "quit", "quiz",
92
+ "quote", "rabbit", "raccoon", "race", "rack", "radar", "radio", "rail", "rain", "raise", "rally", "ramp", "ranch", "random", "range", "rapid",
93
+ "rare", "rate", "rather", "raven", "raw", "razor", "ready", "real", "reason", "rebel", "rebuild", "recall", "receive", "recipe", "record", "recycle",
94
+ "reduce", "reflect", "reform", "refuse", "region", "regret", "regular", "reject", "relax", "release", "relief", "rely", "remain", "remember", "remind", "remove",
95
+ "render", "renew", "rent", "reopen", "repair", "repeat", "replace", "report", "require", "rescue", "resemble", "resist", "resource", "response", "result", "retire",
96
+ "retreat", "return", "reunion", "reveal", "review", "reward", "rhythm", "rib", "ribbon", "rice", "rich", "ride", "ridge", "rifle", "right", "rigid",
97
+ "ring", "riot", "ripple", "risk", "ritual", "rival", "river", "road", "roast", "robot", "robust", "rocket", "romance", "roof", "rookie", "room",
98
+ "rose", "rotate", "rough", "round", "route", "royal", "rubber", "rude", "rug", "rule", "run", "runway", "rural", "sad", "saddle", "sadness",
99
+ "safe", "sail", "salad", "salmon", "salon", "salt", "salute", "same", "sample", "sand", "satisfy", "satoshi", "sauce", "sausage", "save", "say",
100
+ "scale", "scan", "scare", "scatter", "scene", "scheme", "school", "science", "scissors", "scorpion", "scout", "scrap", "screen", "script", "scrub", "sea",
101
+ "search", "season", "seat", "second", "secret", "section", "security", "seed", "seek", "segment", "select", "sell", "seminar", "senior", "sense", "sentence",
102
+ "series", "service", "session", "settle", "setup", "seven", "shadow", "shaft", "shallow", "share", "shed", "shell", "sheriff", "shield", "shift", "shine",
103
+ "ship", "shiver", "shock", "shoe", "shoot", "shop", "short", "shoulder", "shove", "shrimp", "shrug", "shuffle", "shy", "sibling", "sick", "side",
104
+ "siege", "sight", "sign", "silent", "silk", "silly", "silver", "similar", "simple", "since", "sing", "siren", "sister", "situate", "six", "size",
105
+ "skate", "sketch", "ski", "skill", "skin", "skirt", "skull", "slab", "slam", "sleep", "slender", "slice", "slide", "slight", "slim", "slogan",
106
+ "slot", "slow", "slush", "small", "smart", "smile", "smoke", "smooth", "snack", "snake", "snap", "sniff", "snow", "soap", "soccer", "social",
107
+ "sock", "soda", "soft", "solar", "soldier", "solid", "solution", "solve", "someone", "song", "soon", "sorry", "sort", "soul", "sound", "soup",
108
+ "source", "south", "space", "spare", "spatial", "spawn", "speak", "special", "speed", "spell", "spend", "sphere", "spice", "spider", "spike", "spin",
109
+ "spirit", "split", "spoil", "sponsor", "spoon", "sport", "spot", "spray", "spread", "spring", "spy", "square", "squeeze", "squirrel", "stable", "stadium",
110
+ "staff", "stage", "stairs", "stamp", "stand", "start", "state", "stay", "steak", "steel", "stem", "step", "stereo", "stick", "still", "sting",
111
+ "stock", "stomach", "stone", "stool", "story", "stove", "strategy", "street", "strike", "strong", "struggle", "student", "stuff", "stumble", "style", "subject",
112
+ "submit", "subway", "success", "such", "sudden", "suffer", "sugar", "suggest", "suit", "summer", "sun", "sunny", "sunset", "super", "supply", "supreme",
113
+ "sure", "surface", "surge", "surprise", "surround", "survey", "suspect", "sustain", "swallow", "swamp", "swap", "swarm", "swear", "sweet", "swift", "swim",
114
+ "swing", "switch", "sword", "symbol", "symptom", "syrup", "system", "table", "tackle", "tag", "tail", "talent", "talk", "tank", "tape", "target",
115
+ "task", "taste", "tattoo", "taxi", "teach", "team", "tell", "ten", "tenant", "tennis", "tent", "term", "test", "text", "thank", "that",
116
+ "theme", "then", "theory", "there", "they", "thing", "this", "thought", "three", "thrive", "throw", "thumb", "thunder", "ticket", "tide", "tiger",
117
+ "tilt", "timber", "time", "tiny", "tip", "tired", "tissue", "title", "toast", "tobacco", "today", "toddler", "toe", "together", "toilet", "token",
118
+ "tomato", "tomorrow", "tone", "tongue", "tonight", "tool", "tooth", "top", "topic", "topple", "torch", "tornado", "tortoise", "toss", "total", "tourist",
119
+ "toward", "tower", "town", "toy", "track", "trade", "traffic", "tragic", "train", "transfer", "trap", "trash", "travel", "tray", "treat", "tree",
120
+ "trend", "trial", "tribe", "trick", "trigger", "trim", "trip", "trophy", "trouble", "truck", "true", "truly", "trumpet", "trust", "truth", "try",
121
+ "tube", "tuition", "tumble", "tuna", "tunnel", "turkey", "turn", "turtle", "twelve", "twenty", "twice", "twin", "twist", "two", "type", "typical",
122
+ "ugly", "umbrella", "unable", "unaware", "uncle", "uncover", "under", "undo", "unfair", "unfold", "unhappy", "uniform", "unique", "unit", "universe", "unknown",
123
+ "unlock", "until", "unusual", "unveil", "update", "upgrade", "uphold", "upon", "upper", "upset", "urban", "urge", "usage", "use", "used", "useful",
124
+ "useless", "usual", "utility", "vacant", "vacuum", "vague", "valid", "valley", "valve", "van", "vanish", "vapor", "various", "vast", "vault", "vehicle",
125
+ "velvet", "vendor", "venture", "venue", "verb", "verify", "version", "very", "vessel", "veteran", "viable", "vibrant", "vicious", "victory", "video", "view",
126
+ "village", "vintage", "violin", "virtual", "virus", "visa", "visit", "visual", "vital", "vivid", "vocal", "voice", "void", "volcano", "volume", "vote",
127
+ "voyage", "wage", "wagon", "wait", "walk", "wall", "walnut", "want", "warfare", "warm", "warrior", "wash", "wasp", "waste", "water", "wave",
128
+ "way", "wealth", "weapon", "wear", "weasel", "weather", "web", "wedding", "weekend", "weird", "welcome", "west", "wet", "whale", "what", "wheat",
129
+ "wheel", "when", "where", "whip", "whisper", "wide", "width", "wife", "wild", "will", "win", "window", "wine", "wing", "wink", "winner",
130
+ "winter", "wire", "wisdom", "wise", "wish", "witness", "wolf", "woman", "wonder", "wood", "wool", "word", "work", "world", "worry", "worth",
131
+ "wrap", "wreck", "wrestle", "wrist", "write", "wrong", "yard", "year", "yellow", "you", "young", "youth", "zebra", "zero", "zone", "zoo",
132
+ ]);
@@ -0,0 +1,24 @@
1
+ import type { CheckResult } from '../types.js';
2
+ /**
3
+ * keccak256 of the runtime code at the canonical Safe 1.4.1 addresses, read with eth_getCode
4
+ * from both governors RPCs on 2026-09-12 (identical on 151 and 153). A singleton whose code
5
+ * hashes to something else is not the Safe 1.4.1 release, whatever address it sits at.
6
+ */
7
+ export declare const SAFE_141_CODE_HASHES: {
8
+ readonly safeSingleton: "0x1fe2df852ba3299d6534ef416eefa406e56ced995bca886ab7a553e6d0c5e1c4";
9
+ readonly safeL2Singleton: "0xb1f926978a0f44a2c0ec8fe822418ae969bd8c3f18d61e5103100339894f81ff";
10
+ readonly safeProxyFactory: "0x50c3cdc4074750a7a974204a716c999edd37482f907608d960b2b025ee0b3317";
11
+ readonly safeFallbackHandler: "0x7c6007a5d711cea8dfd5d91f5940ec29c7f200fe511eb1fc1397b367af3c42f9";
12
+ };
13
+ /** keccak256 of runtime code given as a 0x hex string, hashing the bytes rather than the text. */
14
+ export declare function codeHash(codeHex: string): string;
15
+ export interface AdminReading {
16
+ address: string;
17
+ isContract: boolean;
18
+ singleton: string | null;
19
+ singletonKind: 'safeSingleton' | 'safeL2Singleton' | null;
20
+ singletonCodeMatches: boolean | null;
21
+ threshold: number | null;
22
+ }
23
+ export declare function readAdmin(address: string, rpc: string, network: 'mainnet' | 'testnet'): Promise<AdminReading>;
24
+ export declare function checkAdminSafe(admin: string | null, rpc: string, chain: number | null): Promise<CheckResult>;
@@ -0,0 +1,81 @@
1
+ // Is the admin a Safe? On mainnet the admin address must hold a SafeProxy whose singleton is
2
+ // the canonical Safe 1.4.1 (L1 or L2 singleton), the singleton must carry the 1.4.1 bytecode,
3
+ // and getThreshold() must be at least 2. On testnet the same reads run but an EOA or a low
4
+ // threshold is a warning, because testnet is for trying things.
5
+ import { addresses, ethCall, keccak256Hex, toCaller } from '@gatedpath/chains';
6
+ /**
7
+ * keccak256 of the runtime code at the canonical Safe 1.4.1 addresses, read with eth_getCode
8
+ * from both governors RPCs on 2026-09-12 (identical on 151 and 153). A singleton whose code
9
+ * hashes to something else is not the Safe 1.4.1 release, whatever address it sits at.
10
+ */
11
+ export const SAFE_141_CODE_HASHES = {
12
+ safeSingleton: '0x1fe2df852ba3299d6534ef416eefa406e56ced995bca886ab7a553e6d0c5e1c4',
13
+ safeL2Singleton: '0xb1f926978a0f44a2c0ec8fe822418ae969bd8c3f18d61e5103100339894f81ff',
14
+ safeProxyFactory: '0x50c3cdc4074750a7a974204a716c999edd37482f907608d960b2b025ee0b3317',
15
+ safeFallbackHandler: '0x7c6007a5d711cea8dfd5d91f5940ec29c7f200fe511eb1fc1397b367af3c42f9',
16
+ };
17
+ const GET_THRESHOLD = '0xe75235b8'; // getThreshold()
18
+ /** keccak256 of runtime code given as a 0x hex string, hashing the bytes rather than the text. */
19
+ export function codeHash(codeHex) {
20
+ return keccak256Hex(Buffer.from(codeHex.replace(/^0x/, ''), 'hex'));
21
+ }
22
+ export async function readAdmin(address, rpc, network) {
23
+ const caller = toCaller(rpc);
24
+ const code = (await caller('eth_getCode', [address, 'latest']));
25
+ const reading = { address, isContract: code !== '0x' && code !== '0x0' && code.length > 2, singleton: null, singletonKind: null, singletonCodeMatches: null, threshold: null };
26
+ if (!reading.isContract)
27
+ return reading;
28
+ const slot0 = (await caller('eth_getStorageAt', [address, '0x0', 'latest']));
29
+ const singleton = '0x' + slot0.slice(-40);
30
+ reading.singleton = singleton;
31
+ const known = addresses[network];
32
+ for (const kind of ['safeSingleton', 'safeL2Singleton']) {
33
+ if (known[kind].address.toLowerCase() === singleton.toLowerCase())
34
+ reading.singletonKind = kind;
35
+ }
36
+ if (reading.singletonKind) {
37
+ const singletonCode = (await caller('eth_getCode', [singleton, 'latest']));
38
+ reading.singletonCodeMatches = codeHash(singletonCode) === SAFE_141_CODE_HASHES[reading.singletonKind];
39
+ }
40
+ try {
41
+ const ret = await ethCall(rpc, address, GET_THRESHOLD);
42
+ if (ret && ret !== '0x')
43
+ reading.threshold = Number(BigInt(ret));
44
+ }
45
+ catch {
46
+ reading.threshold = null;
47
+ }
48
+ return reading;
49
+ }
50
+ export async function checkAdminSafe(admin, rpc, chain) {
51
+ if (!admin)
52
+ return { id: 'admin-safe', status: 'skip', reason: 'No admin given; pass --admin <0x…> with the address that will own the deployed contracts.' };
53
+ if (!/^0x[0-9a-fA-F]{40}$/.test(admin))
54
+ return { id: 'admin-safe', status: 'fail', reason: `"${admin}" is not a 20-byte hex address.` };
55
+ if (chain !== 151 && chain !== 153)
56
+ return { id: 'admin-safe', status: 'skip', reason: 'The RPC is not a Redbelly chain.' };
57
+ const network = chain === 151 ? 'mainnet' : 'testnet';
58
+ const bad = chain === 151 ? 'fail' : 'warn';
59
+ let r;
60
+ try {
61
+ r = await readAdmin(admin, rpc, network);
62
+ }
63
+ catch (e) {
64
+ return { id: 'admin-safe', status: 'fail', reason: `Could not read the admin address: ${e.message}.` };
65
+ }
66
+ const data = { ...r };
67
+ if (!r.isContract) {
68
+ return chain === 151
69
+ ? { id: 'admin-safe', status: 'fail', reason: `Admin ${admin} is an externally owned account; on mainnet the admin must be a Safe 1.4.1 with a threshold of at least 2.`, data }
70
+ : { id: 'admin-safe', status: 'warn', reason: `Admin ${admin} is an externally owned account; fine on testnet, but mainnet pre-flight will refuse it, so put a Safe in place now.`, data };
71
+ }
72
+ if (!r.singletonKind)
73
+ return { id: 'admin-safe', status: bad, reason: `Admin ${admin} is a contract but its storage slot 0 (${r.singleton}) is not the canonical Safe 1.4.1 singleton or SafeL2 singleton on this chain.`, data };
74
+ if (r.singletonCodeMatches === false)
75
+ return { id: 'admin-safe', status: bad, reason: `Admin ${admin} points at ${r.singleton}, whose code does not hash to the Safe 1.4.1 release.`, data };
76
+ if (r.threshold === null)
77
+ return { id: 'admin-safe', status: bad, reason: `Admin ${admin} looks like a Safe but getThreshold() did not answer; the proxy may not be set up.`, data };
78
+ if (r.threshold < 2)
79
+ return { id: 'admin-safe', status: bad, reason: `Admin ${admin} is a Safe 1.4.1 with threshold ${r.threshold}; one signer is one stolen key away from your contracts, so use 2 or more.`, data };
80
+ return { id: 'admin-safe', status: 'pass', reason: `Admin ${admin} is a Safe 1.4.1 (${r.singletonKind === 'safeL2Singleton' ? 'SafeL2' : 'Safe'} singleton, code hash verified) with threshold ${r.threshold}.`, data };
81
+ }
@@ -0,0 +1,3 @@
1
+ import type { CheckResult } from '../types.js';
2
+ export declare const MARGIN_PERCENT = 25;
3
+ export declare function checkBalance(deployer: string | null, gas: bigint, rpc: string, chain: number | null): Promise<CheckResult>;
@@ -0,0 +1,39 @@
1
+ // Does the deployer hold enough RBNT? Cost is gas times the latest base fee, priced in USD
2
+ // through the on-chain feed by gasCostUsd from @gatedpath/chains, with 25 percent on
3
+ // top because the base fee moves with the RBNT price between now and the send.
4
+ import { formatUnits, gasCostUsd, toCaller } from '@gatedpath/chains';
5
+ export const MARGIN_PERCENT = 25;
6
+ export async function checkBalance(deployer, gas, rpc, chain) {
7
+ if (!deployer)
8
+ return { id: 'balance', status: 'skip', reason: 'No deployer given, so there is no balance to read.' };
9
+ if (chain !== 151 && chain !== 153)
10
+ return { id: 'balance', status: 'skip', reason: 'The RPC is not a Redbelly chain, so the price feed is not there to read.' };
11
+ try {
12
+ const caller = toCaller(rpc);
13
+ const balance = BigInt((await caller('eth_getBalance', [deployer, 'latest'])));
14
+ const cost = await gasCostUsd({ gasUsed: gas, rpc });
15
+ const required = (cost.wei * BigInt(100 + MARGIN_PERCENT)) / 100n;
16
+ const data = {
17
+ deployer,
18
+ gas: gas.toString(),
19
+ gasPriceWei: cost.gasPriceWei.toString(),
20
+ costWei: cost.wei.toString(),
21
+ costRbnt: cost.rbnt,
22
+ costUsd: cost.usd,
23
+ usdPerRbnt: cost.usdPerRbnt,
24
+ requiredWei: required.toString(),
25
+ requiredRbnt: formatUnits(required, 18),
26
+ balanceWei: balance.toString(),
27
+ balanceRbnt: formatUnits(balance, 18),
28
+ marginPercent: MARGIN_PERCENT,
29
+ };
30
+ const short = (s) => (s.includes('.') ? s.replace(/(\.\d{4})\d+$/, '$1') : s);
31
+ if (balance >= required) {
32
+ return { id: 'balance', status: 'pass', reason: `${deployer} holds ${short(data.balanceRbnt)} RBNT; ${gas.toLocaleString('en-US')} gas costs about ${short(cost.rbnt)} RBNT (US$${cost.usd.toFixed(4)}) and ${short(data.requiredRbnt)} RBNT covers it with ${MARGIN_PERCENT}% margin.`, data };
33
+ }
34
+ return { id: 'balance', status: 'fail', reason: `${deployer} holds ${short(data.balanceRbnt)} RBNT but ${gas.toLocaleString('en-US')} gas needs ${short(data.requiredRbnt)} RBNT with ${MARGIN_PERCENT}% margin (US$${cost.usd.toFixed(4)} at the feed price); ${chain === 153 ? 'get testnet RBNT at https://redbelly.faucetme.pro/' : 'fund the deployer first'}.`, data };
35
+ }
36
+ catch (e) {
37
+ return { id: 'balance', status: 'fail', reason: `Could not price the deployment: ${e.message}.`, data: { deployer, gas: gas.toString() } };
38
+ }
39
+ }
@@ -0,0 +1,2 @@
1
+ import type { CheckResult } from '../types.js';
2
+ export declare function checkChainId(expected: number | null, reported: number | null, expectedSource: string, rpc: string): CheckResult;
@@ -0,0 +1,11 @@
1
+ export function checkChainId(expected, reported, expectedSource, rpc) {
2
+ if (reported === null)
3
+ return { id: 'chain-id', status: 'fail', reason: `The RPC at ${rpc} did not answer eth_chainId.` };
4
+ if (reported !== 151 && reported !== 153)
5
+ return { id: 'chain-id', status: 'fail', reason: `The RPC reports chain ${reported}, which is neither Redbelly mainnet (151) nor testnet (153).`, data: { expected, reported } };
6
+ if (expected === null)
7
+ return { id: 'chain-id', status: 'skip', reason: `No chain ID in the project config; pass --chain to compare it with the RPC's ${reported}.`, data: { expected, reported } };
8
+ if (expected !== reported)
9
+ return { id: 'chain-id', status: 'fail', reason: `${expectedSource} says chain ${expected} but the RPC at ${rpc} reports ${reported}.`, data: { expected, reported } };
10
+ return { id: 'chain-id', status: 'pass', reason: `${expectedSource} says chain ${expected} and the RPC reports ${reported}.`, data: { expected, reported } };
11
+ }
@@ -0,0 +1,5 @@
1
+ import type { ProjectConfig } from '../config.js';
2
+ import type { CheckResult } from '../types.js';
3
+ export declare const SOLC_PIN = "0.8.30";
4
+ export declare const EVM_PIN = "prague";
5
+ export declare function checkCompilerPins(config: ProjectConfig): CheckResult;
@@ -0,0 +1,20 @@
1
+ export const SOLC_PIN = '0.8.30';
2
+ export const EVM_PIN = 'prague';
3
+ export function checkCompilerPins(config) {
4
+ if (config.kind === 'none')
5
+ return { id: 'compiler-pins', status: 'fail', reason: 'No foundry.toml or hardhat.config.* found, so the compiler and EVM pins cannot be confirmed.' };
6
+ const where = `${config.file}${config.profile ? ` (${config.kind === 'foundry' ? 'profile' : 'network'} ${config.profile})` : ''}`;
7
+ const data = { solc: config.solc, evmVersion: config.evmVersion, file: config.file, profile: config.profile };
8
+ const problems = [];
9
+ if (config.solc === null)
10
+ problems.push(`no ${config.kind === 'foundry' ? 'solc_version' : 'solidity version'} is pinned`);
11
+ else if (config.solc !== SOLC_PIN)
12
+ problems.push(`solc is ${config.solc}, not ${SOLC_PIN}`);
13
+ if (config.evmVersion === null)
14
+ problems.push(`no ${config.kind === 'foundry' ? 'evm_version' : 'evmVersion'} is set`);
15
+ else if (config.evmVersion.toLowerCase() !== EVM_PIN)
16
+ problems.push(`evm version is ${config.evmVersion}, not ${EVM_PIN}`);
17
+ if (problems.length > 0)
18
+ return { id: 'compiler-pins', status: 'fail', reason: `In ${where}, ${problems.join(' and ')}; Redbelly is Prague with solc ${SOLC_PIN} on both networks (verified 2026-09-12).`, data };
19
+ return { id: 'compiler-pins', status: 'pass', reason: `${where} pins solc ${SOLC_PIN} and EVM ${EVM_PIN}.`, data };
20
+ }
@@ -0,0 +1,14 @@
1
+ import type { CheckResult } from '../types.js';
2
+ export interface DeployerSource {
3
+ account?: string | undefined;
4
+ address?: string | undefined;
5
+ passwordFile?: string | undefined;
6
+ cwd: string;
7
+ env: NodeJS.ProcessEnv;
8
+ }
9
+ export declare function resolveDeployer(src: DeployerSource): Promise<{
10
+ address: string | null;
11
+ how: string;
12
+ error?: string;
13
+ }>;
14
+ export declare function checkDeployerVerified(address: string | null, how: string, error: string | undefined, rpc: string, chain: number | null): Promise<CheckResult>;