@gatedpath/preflight 0.2.1 → 0.2.3
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.
- package/README.md +32 -2
- package/dist/checks/compiler.js +9 -0
- package/dist/checks/deployer.js +2 -2
- package/dist/config.d.ts +2 -0
- package/dist/config.js +3 -1
- package/dist/dotenv.d.ts +1 -0
- package/dist/dotenv.js +3 -1
- package/dist/fees-cli.d.ts +8 -0
- package/dist/fees-cli.js +96 -0
- package/dist/fees.d.ts +85 -0
- package/dist/fees.js +128 -0
- package/dist/gas.d.ts +23 -1
- package/dist/gas.js +24 -4
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/redbelly.js +5 -3
- package/dist/ship-cli.d.ts +1 -1
- package/dist/ship-cli.js +15 -4
- package/dist/ship.d.ts +6 -2
- package/dist/ship.js +33 -7
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -90,10 +90,31 @@ Recorded runs against the real feeds, read-only, on 14 September 2026: `reports/
|
|
|
90
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
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
92
|
|
|
93
|
+
## redbelly fees
|
|
94
|
+
|
|
95
|
+
What the calls a builder meets cost today, the command-line twin of the site's fee checker (`/fees/`,
|
|
96
|
+
"What it costs"). `redbelly fees` reads both chains and prints, for each, the head block and its time,
|
|
97
|
+
the base fee in gwei, RBNT in US dollars from the on-chain feed with the feed's time, then four
|
|
98
|
+
measured calls (a native transfer, deploying the recipient guard, deploying the gated token, enabling
|
|
99
|
+
a wallet) in gas, RBNT and US cents at today's base fee through the same integer arithmetic as
|
|
100
|
+
`redbelly gas`, and the enable line against the network's fixed rate of US$0.01 per 21,000 gas. On
|
|
101
|
+
mainnet it also reads the base fee at nine block heights from the first week to the head and prices
|
|
102
|
+
the enable call at each, which is the site's two-year chart as a table. The gas figures are the ones
|
|
103
|
+
measured on 17 October 2026 and carried in the report with the run that measured each. `--chain`
|
|
104
|
+
limits it to one chain, `--rpc` names another endpoint (a fork works; with no `--chain` the RPC says
|
|
105
|
+
which chain it is), `--no-history` skips the mainnet history, `--json` for agents. Exit 0 when every
|
|
106
|
+
chain was read, 1 when one could not be (the line says which and why), 2 on bad arguments. Read-only,
|
|
107
|
+
no key, nothing written.
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
redbelly fees [--chain 151|153] [--rpc <url>] [--timeout <ms>] [--no-history] [--json]
|
|
111
|
+
```
|
|
112
|
+
|
|
93
113
|
## redbelly ship
|
|
94
114
|
|
|
95
115
|
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>]` (
|
|
116
|
+
<151|153> --account <keystore-name> [--admin <safe>] [--gas <n>]` (or `--address <0x…>` in place of
|
|
117
|
+
`--account`, or `DEPLOYER` in `.env`; also `npm run ship` in a scaffold
|
|
97
118
|
and the MCP server's `ship_report` tool) runs the gas report first (its deployment row is the gas the
|
|
98
119
|
balance check prices), then pre-flight's seven checks with the keystore's address, checks the verifier
|
|
99
120
|
(`--verifier` or `VERIFIER` in `.env`; required on 151, where a missing one fails and on 153 warns that
|
|
@@ -104,7 +125,16 @@ the deployer and admin readings, the contracts from `deployments/<chain>-*.json`
|
|
|
104
125
|
constructor arguments and the exact `forge verify-contract` command, the gas table, the tests, the
|
|
105
126
|
Slither line, the compiler pins, and the list of what the report does not prove. It refuses to write
|
|
106
127
|
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.
|
|
128
|
+
contract section; the file is dated in UTC. On 153 the report also says how many faucet claims the
|
|
129
|
+
deployment needs at today's base fee (the deployment rows plus the 25% margin, 500 test RBNT a claim,
|
|
130
|
+
one a day); `redbelly gas` prints the same line.
|
|
131
|
+
|
|
132
|
+
Two things learnt on the first real deploy (17 October 2026). `cast wallet address --account` asks for
|
|
133
|
+
the keystore password on a terminal, so run `redbelly ship` in one, not from an editor's run button;
|
|
134
|
+
without a terminal pass `--password-file`, or `--address` and no cast call is made. And the deploy
|
|
135
|
+
command itself needs `--sender <your address>` beside `--account`: without it forge rehearses as its
|
|
136
|
+
default sender, which fails `isAllowed`, and the scaffold's deploy script now says so and refuses to
|
|
137
|
+
broadcast.
|
|
108
138
|
|
|
109
139
|
The scaffold's `RedbellyDeployScript.sol` refuses to broadcast on 151 without `deployments/ship-151-<today>.md`
|
|
110
140
|
(a dry run says so and continues; 153 warns), and the Hardhat deploy script does the same. Tested on
|
package/dist/checks/compiler.js
CHANGED
|
@@ -6,6 +6,15 @@ export function checkCompilerPins(config) {
|
|
|
6
6
|
const where = `${config.file}${config.profile ? ` (${config.kind === 'foundry' ? 'profile' : 'network'} ${config.profile})` : ''}`;
|
|
7
7
|
const data = { solc: config.solc, evmVersion: config.evmVersion, file: config.file, profile: config.profile };
|
|
8
8
|
const problems = [];
|
|
9
|
+
// A project that vendors a suite with its own compiler (ERC-3643's reference contracts pin 0.8.17, which
|
|
10
|
+
// knows neither prague nor cancun) can only build with solc detected per file. The pin then lives in each
|
|
11
|
+
// file's pragma, which this check does not read, so it warns instead of failing (card 15, 17 October 2026).
|
|
12
|
+
if (config.kind === 'foundry' && config.solc === null && config.autoDetectSolc) {
|
|
13
|
+
const evmOk = config.evmVersion !== null && config.evmVersion.toLowerCase() === EVM_PIN;
|
|
14
|
+
return { id: 'compiler-pins', status: evmOk ? 'warn' : 'fail', reason: evmOk
|
|
15
|
+
? `${where} detects solc per file (auto_detect_solc = true) and sets EVM ${EVM_PIN}. The pin is not in the config: your own files must say pragma solidity ${SOLC_PIN}; a vendored dependency may compile with its own solc for the newest EVM it knows.`
|
|
16
|
+
: `${where} detects solc per file but ${config.evmVersion === null ? 'sets no evm_version' : `sets evm version ${config.evmVersion}, not ${EVM_PIN}`}; Redbelly is Prague.`, data };
|
|
17
|
+
}
|
|
9
18
|
if (config.solc === null)
|
|
10
19
|
problems.push(`no ${config.kind === 'foundry' ? 'solc_version' : 'solidity version'} is pinned`);
|
|
11
20
|
else if (config.solc !== SOLC_PIN)
|
package/dist/checks/deployer.js
CHANGED
|
@@ -16,12 +16,12 @@ export async function resolveDeployer(src) {
|
|
|
16
16
|
const args = ['wallet', 'address', '--account', src.account];
|
|
17
17
|
if (src.passwordFile)
|
|
18
18
|
args.push('--password-file', src.passwordFile);
|
|
19
|
-
const r = await run('cast', args, { cwd: src.cwd, env: src.env, timeoutMs:
|
|
19
|
+
const r = await run('cast', args, { cwd: src.cwd, env: src.env, timeoutMs: 180_000, stdin: '' });
|
|
20
20
|
if (r.spawnError)
|
|
21
21
|
return { address: null, how: '--account', error: `cast is not installed or not on PATH (${r.spawnError}); install Foundry or pass --address for a read-only run.` };
|
|
22
22
|
const m = /0x[0-9a-fA-F]{40}/.exec(r.stdout);
|
|
23
23
|
if (r.code !== 0 || !m)
|
|
24
|
-
return { address: null, how: '--account', error: `cast wallet address --account ${src.account} failed: ${(r.stderr || r.stdout).trim().split('\n').pop() ?? 'no output'}.` };
|
|
24
|
+
return { address: null, how: '--account', error: `cast wallet address --account ${src.account} failed: ${(r.stderr || r.stdout).trim().split('\n').pop() ?? 'no output'}. cast asks for the keystore password on a terminal; without one pass --password-file or --address.` };
|
|
25
25
|
return { address: m[0], how: `cast wallet address --account ${src.account}` };
|
|
26
26
|
}
|
|
27
27
|
return { address: null, how: 'none' };
|
package/dist/config.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export interface ProjectConfig {
|
|
|
3
3
|
file: string | null;
|
|
4
4
|
profile: string | null;
|
|
5
5
|
solc: string | null;
|
|
6
|
+
/** foundry `auto_detect_solc = true`: solc chosen per file from its pragma; the pin cannot be read from the config alone. */
|
|
7
|
+
autoDetectSolc: boolean;
|
|
6
8
|
evmVersion: string | null;
|
|
7
9
|
optimizerRuns: number | null;
|
|
8
10
|
/** foundry: `src`; hardhat: `contracts`. */
|
package/dist/config.js
CHANGED
|
@@ -16,7 +16,7 @@ export function readProjectConfig(project, profile, env = process.env) {
|
|
|
16
16
|
if (existsSync(file))
|
|
17
17
|
return readHardhatConfig(file, profile ?? null);
|
|
18
18
|
}
|
|
19
|
-
return { kind: 'none', file: null, profile: null, solc: null, evmVersion: null, optimizerRuns: null, sourceDir: 'src', chainId: null, rpcEndpoints: {}, rpcUrl: null };
|
|
19
|
+
return { kind: 'none', file: null, profile: null, solc: null, autoDetectSolc: false, evmVersion: null, optimizerRuns: null, sourceDir: 'src', chainId: null, rpcEndpoints: {}, rpcUrl: null };
|
|
20
20
|
}
|
|
21
21
|
/** Enough TOML for foundry.toml: sections, dotted headers, scalars, one-line arrays, comments. */
|
|
22
22
|
export function parseToml(text) {
|
|
@@ -125,6 +125,7 @@ export function readFoundryConfig(file, profile) {
|
|
|
125
125
|
file,
|
|
126
126
|
profile,
|
|
127
127
|
solc: str('solc') ?? str('solc_version'),
|
|
128
|
+
autoDetectSolc: get('auto_detect_solc') === true,
|
|
128
129
|
evmVersion: str('evm_version'),
|
|
129
130
|
optimizerRuns: num('optimizer_runs'),
|
|
130
131
|
sourceDir: str('src') ?? 'src',
|
|
@@ -166,6 +167,7 @@ export function readHardhatConfig(file, network) {
|
|
|
166
167
|
file,
|
|
167
168
|
profile: chosen,
|
|
168
169
|
solc,
|
|
170
|
+
autoDetectSolc: false,
|
|
169
171
|
evmVersion: evm ? evm[1] : null,
|
|
170
172
|
optimizerRuns: runs ? Number(runs[1]) : null,
|
|
171
173
|
sourceDir: contractsDir ? contractsDir[1].replace(/^\.\//, '') : 'contracts',
|
package/dist/dotenv.d.ts
CHANGED
|
@@ -3,5 +3,6 @@ export declare const ENV_NAMES: {
|
|
|
3
3
|
readonly admin: "ADMIN_SAFE";
|
|
4
4
|
readonly chain: "CHAIN_ID";
|
|
5
5
|
readonly legacyAdmin: "PREFLIGHT_ADMIN";
|
|
6
|
+
readonly verifier: "VERIFIER";
|
|
6
7
|
};
|
|
7
8
|
export declare function readDotEnv(project: string, env: NodeJS.ProcessEnv): Record<string, string>;
|
package/dist/dotenv.js
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
// git-secrets check covers what should never be in that file.
|
|
5
5
|
import { existsSync, readFileSync } from 'node:fs';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
|
-
|
|
7
|
+
// VERIFIER joined on 25 October 2026: the deploy script reads it with vm.envOr, which sees only the
|
|
8
|
+
// process environment, so ship has to see it there too or the two disagree (card 9's run, 23 October).
|
|
9
|
+
export const ENV_NAMES = { deployer: 'DEPLOYER', admin: 'ADMIN_SAFE', chain: 'CHAIN_ID', legacyAdmin: 'PREFLIGHT_ADMIN', verifier: 'VERIFIER' };
|
|
8
10
|
export function readDotEnv(project, env) {
|
|
9
11
|
const out = {};
|
|
10
12
|
const file = join(project, '.env');
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { FeesOptions } from './fees.js';
|
|
2
|
+
export declare const FEES_HELP = "redbelly fees [options]\n\nReads both chains (151 mainnet, 153 testnet) and prints, for each, the head block and its time,\nthe base fee, RBNT in US dollars from the on-chain feed, the measured calls a builder meets (a\ntransfer, the recipient guard, the gated token, enabling a wallet) in gas, RBNT and US cents at\ntoday's base fee, and on mainnet the enable call at nine block heights since the first week.\nThe command-line twin of the site's fee checker. Read-only, no key.\n\n --chain <151|153> one chain instead of both\n --rpc <url> JSON-RPC endpoint in place of the chain's governors RPC (a fork works)\n --timeout <ms> per request (default 10000)\n --no-history skip the mainnet history reads\n --json print the report as JSON\n --version, --help\n";
|
|
3
|
+
export declare function parseFeesArgs(argv: readonly string[]): FeesOptions & {
|
|
4
|
+
json: boolean;
|
|
5
|
+
help: boolean;
|
|
6
|
+
version: boolean;
|
|
7
|
+
};
|
|
8
|
+
export declare function feesMain(argv?: readonly string[]): Promise<number>;
|
package/dist/fees-cli.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// `redbelly fees`: what the measured calls cost on each chain today. Exit 0 when every chain was
|
|
2
|
+
// read, 1 when one could not be (the line says which and why), 2 on bad arguments. --json for agents.
|
|
3
|
+
import { FEES_VERSION, feesReportJson, renderFeesText, runFees } from './fees.js';
|
|
4
|
+
export const FEES_HELP = `redbelly fees [options]
|
|
5
|
+
|
|
6
|
+
Reads both chains (151 mainnet, 153 testnet) and prints, for each, the head block and its time,
|
|
7
|
+
the base fee, RBNT in US dollars from the on-chain feed, the measured calls a builder meets (a
|
|
8
|
+
transfer, the recipient guard, the gated token, enabling a wallet) in gas, RBNT and US cents at
|
|
9
|
+
today's base fee, and on mainnet the enable call at nine block heights since the first week.
|
|
10
|
+
The command-line twin of the site's fee checker. Read-only, no key.
|
|
11
|
+
|
|
12
|
+
--chain <151|153> one chain instead of both
|
|
13
|
+
--rpc <url> JSON-RPC endpoint in place of the chain's governors RPC (a fork works)
|
|
14
|
+
--timeout <ms> per request (default 10000)
|
|
15
|
+
--no-history skip the mainnet history reads
|
|
16
|
+
--json print the report as JSON
|
|
17
|
+
--version, --help
|
|
18
|
+
`;
|
|
19
|
+
export function parseFeesArgs(argv) {
|
|
20
|
+
const o = { json: false, help: false, version: false };
|
|
21
|
+
for (let i = 0; i < argv.length; i++) {
|
|
22
|
+
const a = argv[i];
|
|
23
|
+
const [flag, inline] = a.includes('=') ? [a.slice(0, a.indexOf('=')), a.slice(a.indexOf('=') + 1)] : [a, undefined];
|
|
24
|
+
const next = () => {
|
|
25
|
+
if (inline !== undefined)
|
|
26
|
+
return inline;
|
|
27
|
+
const v = argv[++i];
|
|
28
|
+
if (v === undefined)
|
|
29
|
+
throw new Error(`${flag} needs a value`);
|
|
30
|
+
return v;
|
|
31
|
+
};
|
|
32
|
+
switch (flag) {
|
|
33
|
+
case '--chain': {
|
|
34
|
+
const c = Number(next());
|
|
35
|
+
if (c !== 151 && c !== 153)
|
|
36
|
+
throw new Error('--chain must be 151 or 153');
|
|
37
|
+
o.chain = c;
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
case '--rpc':
|
|
41
|
+
o.rpc = next();
|
|
42
|
+
break;
|
|
43
|
+
case '--timeout': {
|
|
44
|
+
const t = Number(next());
|
|
45
|
+
if (!Number.isInteger(t) || t <= 0)
|
|
46
|
+
throw new Error('--timeout must be a whole number of milliseconds');
|
|
47
|
+
o.timeoutMs = t;
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
case '--no-history':
|
|
51
|
+
o.history = false;
|
|
52
|
+
break;
|
|
53
|
+
case '--json':
|
|
54
|
+
o.json = true;
|
|
55
|
+
break;
|
|
56
|
+
case '--help':
|
|
57
|
+
case '-h':
|
|
58
|
+
o.help = true;
|
|
59
|
+
break;
|
|
60
|
+
case '--version':
|
|
61
|
+
o.version = true;
|
|
62
|
+
break;
|
|
63
|
+
default:
|
|
64
|
+
if (/^(0x)?[0-9a-fA-F]{64}$/.test(a))
|
|
65
|
+
throw new Error('that argument looks like a private key; redbelly fees reads the chain and never takes one');
|
|
66
|
+
throw new Error(`unknown argument "${a}"\n\n${FEES_HELP}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return o;
|
|
70
|
+
}
|
|
71
|
+
export async function feesMain(argv = process.argv.slice(2)) {
|
|
72
|
+
let args;
|
|
73
|
+
try {
|
|
74
|
+
args = parseFeesArgs(argv);
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
process.stderr.write(`${e.message}\n`);
|
|
78
|
+
return 2;
|
|
79
|
+
}
|
|
80
|
+
if (args.help) {
|
|
81
|
+
process.stdout.write(FEES_HELP);
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
if (args.version) {
|
|
85
|
+
process.stdout.write(`${FEES_VERSION}\n`);
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
const { json, help: _h, version: _v, ...opts } = args;
|
|
89
|
+
const report = await runFees(opts);
|
|
90
|
+
process.stdout.write(json ? feesReportJson(report) : renderFeesText(report));
|
|
91
|
+
// The failures are already in the report; repeat them on stderr so a pipe that keeps only stdout still shows the reason in a terminal.
|
|
92
|
+
for (const c of report.chains)
|
|
93
|
+
if (!c.ok)
|
|
94
|
+
process.stderr.write(`redbelly fees: chain ${c.chain ?? 'at ' + c.rpc} could not be read: ${c.error}\n`);
|
|
95
|
+
return report.ok ? 0 : 1;
|
|
96
|
+
}
|
package/dist/fees.d.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { GasQuote } from './gas.js';
|
|
2
|
+
export declare const FEES_VERSION: string;
|
|
3
|
+
export interface ReferenceCall {
|
|
4
|
+
readonly key: string;
|
|
5
|
+
readonly label: string;
|
|
6
|
+
readonly gas: bigint;
|
|
7
|
+
/** Where the gas figure comes from, with the date. */
|
|
8
|
+
readonly measured: string;
|
|
9
|
+
}
|
|
10
|
+
/** Calls a builder meets, with gas as measured, oldest measurement last. Same table as the site. */
|
|
11
|
+
export declare const REFERENCE_CALLS: readonly ReferenceCall[];
|
|
12
|
+
/** The network prices gas in US dollars: Vine's figure, measured in RESEARCH.md question 3 (US$0.01 for a 21,000-gas transfer). */
|
|
13
|
+
export declare const USD_PER_GAS: number;
|
|
14
|
+
/** Mainnet block heights sampled for the history: genesis week, then roughly every four months to the head. */
|
|
15
|
+
export declare const HISTORY_BLOCKS_151: readonly number[];
|
|
16
|
+
/** The call the history is drawn for: enabling a wallet. */
|
|
17
|
+
export declare const HISTORY_GAS = 2154375n;
|
|
18
|
+
export interface PricedCall extends ReferenceCall {
|
|
19
|
+
readonly wei: bigint;
|
|
20
|
+
readonly rbnt: string;
|
|
21
|
+
readonly usd: number;
|
|
22
|
+
/** US cents to three decimal places, as a number. */
|
|
23
|
+
readonly cents: number;
|
|
24
|
+
}
|
|
25
|
+
export interface HistoryPoint {
|
|
26
|
+
readonly block: number;
|
|
27
|
+
readonly time: string;
|
|
28
|
+
readonly baseFeeWei: bigint;
|
|
29
|
+
/** The enable call at that block's base fee. */
|
|
30
|
+
readonly rbnt: string;
|
|
31
|
+
/** The same call at the network's fixed dollar rate; the line that should not move. */
|
|
32
|
+
readonly usd: number;
|
|
33
|
+
}
|
|
34
|
+
export interface FeesChainOk {
|
|
35
|
+
readonly ok: true;
|
|
36
|
+
readonly chain: 151 | 153;
|
|
37
|
+
readonly network: 'mainnet' | 'testnet';
|
|
38
|
+
readonly rpc: string;
|
|
39
|
+
readonly latencyMs: number;
|
|
40
|
+
readonly quote: GasQuote;
|
|
41
|
+
readonly calls: readonly PricedCall[];
|
|
42
|
+
/** The history, mainnet only; null on testnet, and null with a reason when the reads failed. */
|
|
43
|
+
readonly history: readonly HistoryPoint[] | null;
|
|
44
|
+
readonly historyError: string | null;
|
|
45
|
+
}
|
|
46
|
+
export interface FeesChainFailed {
|
|
47
|
+
readonly ok: false;
|
|
48
|
+
readonly chain: 151 | 153 | null;
|
|
49
|
+
readonly network: 'mainnet' | 'testnet' | null;
|
|
50
|
+
readonly rpc: string;
|
|
51
|
+
readonly latencyMs: number;
|
|
52
|
+
readonly error: string;
|
|
53
|
+
}
|
|
54
|
+
export type FeesChain = FeesChainOk | FeesChainFailed;
|
|
55
|
+
export interface FeesReport {
|
|
56
|
+
readonly tool: 'redbelly-fees';
|
|
57
|
+
readonly version: string;
|
|
58
|
+
readonly generatedAt: string;
|
|
59
|
+
readonly chains: readonly FeesChain[];
|
|
60
|
+
/** True when every chain was read. */
|
|
61
|
+
readonly ok: boolean;
|
|
62
|
+
}
|
|
63
|
+
export interface FeesOptions {
|
|
64
|
+
/** One chain; both when absent. With --rpc and no chain, the RPC says which it is. */
|
|
65
|
+
chain?: 151 | 153;
|
|
66
|
+
rpc?: string;
|
|
67
|
+
/** Per request. Default 10 seconds. */
|
|
68
|
+
timeoutMs?: number;
|
|
69
|
+
/** Tests pass a fake; otherwise the global fetch. */
|
|
70
|
+
fetch?: typeof fetch;
|
|
71
|
+
/** Skip the mainnet history reads. */
|
|
72
|
+
history?: boolean;
|
|
73
|
+
}
|
|
74
|
+
export declare function priceCalls(quote: Pick<GasQuote, 'baseFeeWei' | 'priceRaw'>, calls?: readonly ReferenceCall[]): PricedCall[];
|
|
75
|
+
/** The base fee at past block heights, each priced for the enable call; blocks the node does not have are left out. */
|
|
76
|
+
export declare function readFeeHistory(rpc: string, blocks: readonly number[], opts?: {
|
|
77
|
+
fetch?: typeof fetch;
|
|
78
|
+
gas?: bigint;
|
|
79
|
+
}): Promise<HistoryPoint[]>;
|
|
80
|
+
/** One chain: the quote, the four calls priced, and on mainnet the history. Never throws; a failed read is a result. */
|
|
81
|
+
export declare function readFeesChain(rpc: string, expected: 151 | 153 | undefined, opts?: FeesOptions): Promise<FeesChain>;
|
|
82
|
+
export declare function runFees(opts?: FeesOptions): Promise<FeesReport>;
|
|
83
|
+
export declare function renderFeesText(report: FeesReport): string;
|
|
84
|
+
/** JSON with BigInt as decimal strings. */
|
|
85
|
+
export declare function feesReportJson(report: FeesReport): string;
|
package/dist/fees.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// redbelly fees: the command-line twin of the site's fee checker (/fees/, "What it costs"). Per chain
|
|
2
|
+
// it reads the head block, the base fee and the feed price (the same three reads as `redbelly gas`,
|
|
3
|
+
// through quoteGas), prices the measured calls a builder meets at today's base fee, and on mainnet
|
|
4
|
+
// reads the base fee at nine block heights so the enable call can be seen across the network's life.
|
|
5
|
+
// Read-only, no key, nothing written. The gas figures below are copied from the site's data file;
|
|
6
|
+
// each one names the run that measured it and is never typed from a document.
|
|
7
|
+
import { createRpc, chainById } from '@gatedpath/chains';
|
|
8
|
+
import { GAS_VERSION, priceGas, quoteGas } from './gas.js';
|
|
9
|
+
export const FEES_VERSION = GAS_VERSION;
|
|
10
|
+
/** Calls a builder meets, with gas as measured, oldest measurement last. Same table as the site. */
|
|
11
|
+
export const REFERENCE_CALLS = [
|
|
12
|
+
{ key: 'transfer', label: 'A native transfer', gas: 21000n, measured: 'the EVM constant; Vine prices it at US$0.01' },
|
|
13
|
+
{ key: 'guard', label: 'Deploying the recipient guard (project 13)', gas: 503685n, measured: 'measured on 153, 17 October 2026, block 3,052,324' },
|
|
14
|
+
{ key: 'token', label: 'Deploying the gated token (the scaffold)', gas: 1971486n, measured: 'the ship report on 153, 17 October 2026' },
|
|
15
|
+
{ key: 'enable', label: 'Enabling a wallet on the network', gas: 2154375n, measured: 'measured on 151, 17 October 2026, block 3,228,354' },
|
|
16
|
+
];
|
|
17
|
+
/** The network prices gas in US dollars: Vine's figure, measured in RESEARCH.md question 3 (US$0.01 for a 21,000-gas transfer). */
|
|
18
|
+
export const USD_PER_GAS = 0.01 / 21_000;
|
|
19
|
+
/** Mainnet block heights sampled for the history: genesis week, then roughly every four months to the head. */
|
|
20
|
+
export const HISTORY_BLOCKS_151 = [204, 400_000, 800_000, 1_200_000, 1_600_000, 2_000_000, 2_400_000, 2_800_000, 3_100_000];
|
|
21
|
+
/** The call the history is drawn for: enabling a wallet. */
|
|
22
|
+
export const HISTORY_GAS = 2154375n;
|
|
23
|
+
/** Each request gets its own deadline; a node that hangs is reported as a timeout rather than waited on. */
|
|
24
|
+
function withTimeout(doFetch, timeoutMs) {
|
|
25
|
+
return (input, init) => doFetch(input, { ...init, signal: AbortSignal.timeout(timeoutMs) });
|
|
26
|
+
}
|
|
27
|
+
export function priceCalls(quote, calls = REFERENCE_CALLS) {
|
|
28
|
+
return calls.map((c) => ({ ...c, ...priceGas(c.gas, quote) }));
|
|
29
|
+
}
|
|
30
|
+
/** The base fee at past block heights, each priced for the enable call; blocks the node does not have are left out. */
|
|
31
|
+
export async function readFeeHistory(rpc, blocks, opts = {}) {
|
|
32
|
+
const caller = createRpc(rpc, opts.fetch ? { fetch: opts.fetch } : {});
|
|
33
|
+
const gas = opts.gas ?? HISTORY_GAS;
|
|
34
|
+
const results = await Promise.all(blocks.map((b) => caller('eth_getBlockByNumber', ['0x' + b.toString(16), false])));
|
|
35
|
+
const points = [];
|
|
36
|
+
for (const b of results) {
|
|
37
|
+
if (!b?.number || !b.baseFeePerGas)
|
|
38
|
+
continue;
|
|
39
|
+
const baseFeeWei = BigInt(b.baseFeePerGas);
|
|
40
|
+
// The dollar figure is the fixed rate, not the feed at that block: it is the line the chart shows flat.
|
|
41
|
+
points.push({ block: Number(BigInt(b.number)), time: new Date(Number(BigInt(b.timestamp ?? '0x0')) * 1000).toISOString(), baseFeeWei, rbnt: priceGas(gas, { baseFeeWei, priceRaw: 0n }).rbnt, usd: Number(gas) * USD_PER_GAS });
|
|
42
|
+
}
|
|
43
|
+
return points.sort((a, b) => a.block - b.block);
|
|
44
|
+
}
|
|
45
|
+
/** One chain: the quote, the four calls priced, and on mainnet the history. Never throws; a failed read is a result. */
|
|
46
|
+
export async function readFeesChain(rpc, expected, opts = {}) {
|
|
47
|
+
const doFetch = withTimeout(opts.fetch ?? fetch, opts.timeoutMs ?? 10_000);
|
|
48
|
+
const t0 = Date.now();
|
|
49
|
+
let quote;
|
|
50
|
+
try {
|
|
51
|
+
quote = await quoteGas(rpc, expected, { fetch: doFetch });
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
const chain = expected ?? null;
|
|
55
|
+
return { ok: false, chain, network: chain === null ? null : chain === 151 ? 'mainnet' : 'testnet', rpc, latencyMs: Date.now() - t0, error: e.message };
|
|
56
|
+
}
|
|
57
|
+
const calls = priceCalls(quote);
|
|
58
|
+
let history = null;
|
|
59
|
+
let historyError = null;
|
|
60
|
+
if (quote.chain === 151 && opts.history !== false) {
|
|
61
|
+
try {
|
|
62
|
+
// The head is read again by height so the last point and the quote name the same block.
|
|
63
|
+
history = await readFeeHistory(rpc, [...HISTORY_BLOCKS_151.filter((b) => b < quote.block), quote.block], { fetch: doFetch });
|
|
64
|
+
}
|
|
65
|
+
catch (e) {
|
|
66
|
+
historyError = e.message;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return { ok: true, chain: quote.chain, network: quote.network, rpc, latencyMs: Date.now() - t0, quote, calls, history, historyError };
|
|
70
|
+
}
|
|
71
|
+
export async function runFees(opts = {}) {
|
|
72
|
+
let chains;
|
|
73
|
+
if (opts.rpc) {
|
|
74
|
+
chains = [await readFeesChain(opts.rpc, opts.chain, opts)];
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
const ids = opts.chain ? [opts.chain] : [151, 153];
|
|
78
|
+
chains = await Promise.all(ids.map((id) => readFeesChain(chainById(id).rpcUrls.default.http[0], id, opts)));
|
|
79
|
+
}
|
|
80
|
+
return { tool: 'redbelly-fees', version: FEES_VERSION, generatedAt: new Date().toISOString(), chains, ok: chains.every((c) => c.ok) };
|
|
81
|
+
}
|
|
82
|
+
// ---- text ----
|
|
83
|
+
const num = (n) => n.toLocaleString('en-US');
|
|
84
|
+
const fixed = (n, d) => n.toLocaleString('en-US', { minimumFractionDigits: d, maximumFractionDigits: d });
|
|
85
|
+
const gwei = (wei) => `${fixed(Number(wei) / 1e9, 2)} gwei`;
|
|
86
|
+
const rbnt = (s) => {
|
|
87
|
+
const n = Number(s);
|
|
88
|
+
return n === 0 ? '0' : n < 0.000001 ? '<0.000001' : fixed(n, n < 1 ? 6 : 3);
|
|
89
|
+
};
|
|
90
|
+
const when = (iso) => iso.replace('T', ' ').replace(/\.\d+Z$/, ' UTC');
|
|
91
|
+
export function renderFeesText(report) {
|
|
92
|
+
const lines = [];
|
|
93
|
+
lines.push(`redbelly fees ${report.version} ${when(report.generatedAt)}`);
|
|
94
|
+
for (const c of report.chains) {
|
|
95
|
+
lines.push('');
|
|
96
|
+
if (!c.ok) {
|
|
97
|
+
lines.push(`chain ${c.chain ?? '?'}${c.network ? ` (${c.network})` : ''} via ${c.rpc}: could not be read: ${c.error}`);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const q = c.quote;
|
|
101
|
+
lines.push(`chain ${c.chain} (${c.network}) via ${c.rpc} read in ${num(c.latencyMs)} ms`);
|
|
102
|
+
lines.push(`head block ${num(q.block)} at ${when(q.blockTimestamp)} base fee ${gwei(q.baseFeeWei)}`);
|
|
103
|
+
lines.push(`RBNT US$${fixed(q.usdPerRbnt, 6)} from the feed at ${when(q.priceTimestamp)}`);
|
|
104
|
+
lines.push('');
|
|
105
|
+
const w = Math.max(...c.calls.map((r) => r.label.length));
|
|
106
|
+
lines.push(['Call'.padEnd(w), 'Gas'.padStart(11), 'RBNT'.padStart(12), 'US cents'.padStart(10)].join(' '));
|
|
107
|
+
for (const r of c.calls)
|
|
108
|
+
lines.push([r.label.padEnd(w), num(r.gas).padStart(11), rbnt(r.rbnt).padStart(12), fixed(r.cents, 3).padStart(10)].join(' '));
|
|
109
|
+
const enable = c.calls.find((r) => r.key === 'enable');
|
|
110
|
+
if (enable)
|
|
111
|
+
lines.push('', `enable: ${num(enable.gas)} gas = ${rbnt(enable.rbnt)} RBNT = US$${fixed(enable.usd, 4)} now; at the network's fixed rate of US$0.01 per 21,000 gas it is US$${fixed(Number(enable.gas) * USD_PER_GAS, 2)}, whatever RBNT trades at.`);
|
|
112
|
+
if (c.history && c.history.length) {
|
|
113
|
+
lines.push('', `the enable call across mainnet's life (${num(HISTORY_GAS)} gas at the base fee of each block; dollars at the fixed rate):`);
|
|
114
|
+
lines.push(['Block'.padStart(11), 'Date'.padEnd(10), 'Base fee'.padStart(20), 'RBNT'.padStart(12), 'US$'.padStart(8)].join(' '));
|
|
115
|
+
for (const p of c.history)
|
|
116
|
+
lines.push([num(p.block).padStart(11), p.time.slice(0, 10).padEnd(10), gwei(p.baseFeeWei).padStart(20), rbnt(p.rbnt).padStart(12), fixed(p.usd, 2).padStart(8)].join(' '));
|
|
117
|
+
}
|
|
118
|
+
else if (c.historyError) {
|
|
119
|
+
lines.push('', `history: not read (${c.historyError})`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
lines.push('', 'Gas is priced in US dollars and converted to RBNT at execution from the feed, so the cents column is the stable one; the RBNT column moves with the price. Base fee only: the chain charges no priority fee.');
|
|
123
|
+
return lines.join('\n') + '\n';
|
|
124
|
+
}
|
|
125
|
+
/** JSON with BigInt as decimal strings. */
|
|
126
|
+
export function feesReportJson(report) {
|
|
127
|
+
return JSON.stringify(report, (_k, v) => (typeof v === 'bigint' ? v.toString() : v), 2) + '\n';
|
|
128
|
+
}
|
package/dist/gas.d.ts
CHANGED
|
@@ -26,6 +26,8 @@ export interface GasQuote {
|
|
|
26
26
|
readonly network: 'mainnet' | 'testnet';
|
|
27
27
|
readonly rpc: string;
|
|
28
28
|
readonly block: number;
|
|
29
|
+
/** The head block's timestamp as ISO, so a quote can be dated without a second block read. */
|
|
30
|
+
readonly blockTimestamp: string;
|
|
29
31
|
readonly baseFeeWei: bigint;
|
|
30
32
|
/** The feed's raw integer (six decimals) and the derived USD per RBNT. */
|
|
31
33
|
readonly priceRaw: bigint;
|
|
@@ -62,7 +64,24 @@ export interface GasReport {
|
|
|
62
64
|
readonly added: readonly string[];
|
|
63
65
|
readonly removed: readonly string[];
|
|
64
66
|
} | null;
|
|
67
|
+
/** Testnet only: what the deployment rows cost in faucet claims, with pre-flight's 25% margin, at this quote. */
|
|
68
|
+
readonly faucet: FaucetDays | null;
|
|
65
69
|
}
|
|
70
|
+
export interface FaucetDays {
|
|
71
|
+
/** What one FAUCETME claim pays, in test RBNT, and how often (read 15 September 2026). */
|
|
72
|
+
readonly perClaimRbnt: number;
|
|
73
|
+
readonly claimsPerDay: number;
|
|
74
|
+
/** Deployment gas summed over the rows, plus the 25% pre-flight's balance check adds. */
|
|
75
|
+
readonly gasWithMargin: bigint;
|
|
76
|
+
readonly rbnt: number;
|
|
77
|
+
readonly claims: number;
|
|
78
|
+
readonly days: number;
|
|
79
|
+
readonly pricedAt: string;
|
|
80
|
+
}
|
|
81
|
+
export declare const FAUCET_PER_CLAIM_RBNT = 500;
|
|
82
|
+
export declare const FAUCET_CLAIMS_PER_DAY = 1;
|
|
83
|
+
/** Faucet claims a project needs before its deploy: the deployment rows, 25% on top, at the quote's base fee and price. 153 only. */
|
|
84
|
+
export declare function faucetDays(rows: readonly PricedRow[], quote: GasQuote): FaucetDays | null;
|
|
66
85
|
export interface GasOptions {
|
|
67
86
|
/** A Foundry project, or a scaffold root (its contracts/ is used). Default: cwd. */
|
|
68
87
|
project?: string;
|
|
@@ -97,7 +116,10 @@ export declare function priceRows(rows: readonly GasRow[], quote: Pick<GasQuote,
|
|
|
97
116
|
added: string[];
|
|
98
117
|
removed: string[];
|
|
99
118
|
};
|
|
100
|
-
|
|
119
|
+
/** `fetch` is for tests and for callers that wrap a timeout round the global one; the reads are the same either way. */
|
|
120
|
+
export declare function quoteGas(rpc: string, expected?: 151 | 153, opts?: {
|
|
121
|
+
fetch?: typeof fetch;
|
|
122
|
+
}): Promise<GasQuote>;
|
|
101
123
|
/** A scaffold root's contracts/ when that is where foundry.toml is; otherwise the directory itself. */
|
|
102
124
|
export declare function foundryDir(project: string): string;
|
|
103
125
|
export declare function readGasSource(dir: string, opts: Pick<GasOptions, 'snapshot' | 'report' | 'env'>): Promise<{
|
package/dist/gas.js
CHANGED
|
@@ -7,10 +7,25 @@
|
|
|
7
7
|
import { existsSync, readFileSync } from 'node:fs';
|
|
8
8
|
import { createRequire } from 'node:module';
|
|
9
9
|
import { basename, join, relative, resolve } from 'node:path';
|
|
10
|
-
import { PRICE_FEED_DECIMALS, chainById, formatUnits, getLatestPrice, toCaller } from '@gatedpath/chains';
|
|
10
|
+
import { PRICE_FEED_DECIMALS, chainById, createRpc, formatUnits, getLatestPrice, toCaller } from '@gatedpath/chains';
|
|
11
11
|
import { run } from './proc.js';
|
|
12
12
|
const require = createRequire(import.meta.url);
|
|
13
13
|
export const GAS_VERSION = require('../package.json').version;
|
|
14
|
+
export const FAUCET_PER_CLAIM_RBNT = 500;
|
|
15
|
+
export const FAUCET_CLAIMS_PER_DAY = 1;
|
|
16
|
+
/** Faucet claims a project needs before its deploy: the deployment rows, 25% on top, at the quote's base fee and price. 153 only. */
|
|
17
|
+
export function faucetDays(rows, quote) {
|
|
18
|
+
if (quote.chain !== 153)
|
|
19
|
+
return null;
|
|
20
|
+
const deploys = rows.filter((r) => r.item === 'deployment');
|
|
21
|
+
if (deploys.length === 0)
|
|
22
|
+
return null;
|
|
23
|
+
const gas = deploys.reduce((a, r) => a + r.gas, 0n);
|
|
24
|
+
const gasWithMargin = gas + gas / 4n;
|
|
25
|
+
const rbnt = Number(gasWithMargin * quote.baseFeeWei) / 1e18;
|
|
26
|
+
const claims = Math.max(1, Math.ceil(rbnt / FAUCET_PER_CLAIM_RBNT));
|
|
27
|
+
return { perClaimRbnt: FAUCET_PER_CLAIM_RBNT, claimsPerDay: FAUCET_CLAIMS_PER_DAY, gasWithMargin, rbnt, claims, days: Math.ceil(claims / FAUCET_CLAIMS_PER_DAY), pricedAt: quote.priceTimestamp };
|
|
28
|
+
}
|
|
14
29
|
// ---- parsing, pure ----
|
|
15
30
|
const SNAPSHOT_LINE = /^([A-Za-z0-9_$]+):(\S.*?)\s+\((.*)\)\s*$/;
|
|
16
31
|
/** `.gas-snapshot`: `Contract:test() (gas: N)`, fuzz lines by their mean (μ), invariant lines (no gas) skipped. */
|
|
@@ -90,8 +105,9 @@ export function priceRows(rows, quote, before) {
|
|
|
90
105
|
return { rows: priced, added: before ? rows.filter((r) => !prior.has(r.name)).map((r) => r.name) : [], removed: before ? [...prior.keys()].filter((n) => !names.has(n)) : [] };
|
|
91
106
|
}
|
|
92
107
|
// ---- the two reads ----
|
|
93
|
-
|
|
94
|
-
|
|
108
|
+
/** `fetch` is for tests and for callers that wrap a timeout round the global one; the reads are the same either way. */
|
|
109
|
+
export async function quoteGas(rpc, expected, opts = {}) {
|
|
110
|
+
const caller = opts.fetch ? createRpc(rpc, { fetch: opts.fetch }) : toCaller(rpc);
|
|
95
111
|
const reported = Number(BigInt((await caller('eth_chainId'))));
|
|
96
112
|
if (reported !== 151 && reported !== 153)
|
|
97
113
|
throw new Error(`the RPC at ${rpc} reports chain ${reported}; redbelly gas prices through the feed on 151 or 153 (a fork of either works)`);
|
|
@@ -100,12 +116,13 @@ export async function quoteGas(rpc, expected) {
|
|
|
100
116
|
const block = (await caller('eth_getBlockByNumber', ['latest', false]));
|
|
101
117
|
if (!block?.baseFeePerGas)
|
|
102
118
|
throw new Error('the latest block has no baseFeePerGas');
|
|
103
|
-
const price = await getLatestPrice({ rpc });
|
|
119
|
+
const price = await getLatestPrice({ rpc: caller });
|
|
104
120
|
return {
|
|
105
121
|
chain: reported,
|
|
106
122
|
network: reported === 151 ? 'mainnet' : 'testnet',
|
|
107
123
|
rpc,
|
|
108
124
|
block: Number(BigInt(block.number ?? '0x0')),
|
|
125
|
+
blockTimestamp: new Date(Number(BigInt(block.timestamp ?? '0x0')) * 1000).toISOString(),
|
|
109
126
|
baseFeeWei: BigInt(block.baseFeePerGas),
|
|
110
127
|
priceRaw: price.raw,
|
|
111
128
|
usdPerRbnt: price.usdPerRbnt,
|
|
@@ -175,6 +192,7 @@ export async function runGas(opts = {}) {
|
|
|
175
192
|
quote,
|
|
176
193
|
rows: priced.rows,
|
|
177
194
|
diff: diffFile ? { file: diffFile, rows: before.length, added: priced.added, removed: priced.removed } : null,
|
|
195
|
+
faucet: faucetDays(priced.rows, quote),
|
|
178
196
|
};
|
|
179
197
|
}
|
|
180
198
|
// ---- text ----
|
|
@@ -212,6 +230,8 @@ export function renderGasText(report) {
|
|
|
212
230
|
lines.push('');
|
|
213
231
|
if (deploys.length)
|
|
214
232
|
lines.push(`deployment: ${deploys.map((d) => `${d.contract} ${num(d.gas)} gas = ${rbnt6(d.rbnt)} RBNT = US$${fixed(d.usd, 4)}`).join('; ')}. Pre-flight's balance check adds 25% on top; pass its --gas the figure here.`);
|
|
233
|
+
if (report.faucet)
|
|
234
|
+
lines.push(`faucet: ${num(report.faucet.gasWithMargin)} gas with the 25% margin = ${fixed(report.faucet.rbnt, 1)} test RBNT at this base fee = ${report.faucet.claims} claim${report.faucet.claims === 1 ? '' : 's'} of ${report.faucet.perClaimRbnt} (${report.faucet.days} day${report.faucet.days === 1 ? '' : 's'}, one claim a day) before you can deploy. Priced at ${report.faucet.pricedAt}; the base fee moves, so re-run on the day.`);
|
|
215
235
|
lines.push(`Gas is priced in US dollars and converted to RBNT at execution from the feed, so the cents column is the stable one; the RBNT column moves with the price. Base fee only: the chain charges no priority fee.`);
|
|
216
236
|
return lines.join('\n') + '\n';
|
|
217
237
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -21,6 +21,9 @@ export { doctorMain, parseDoctorArgs, DOCTOR_HELP } from './doctor-cli.js';
|
|
|
21
21
|
export { runGas, quoteGas, priceGas, priceRows, parseGasSnapshot, parseGasReportJson, parseGasFile, readGasSource, foundryDir, renderGasText, gasReportJson } from './gas.js';
|
|
22
22
|
export type { GasRow, GasSource, GasQuote, PricedRow, GasReport, GasOptions } from './gas.js';
|
|
23
23
|
export { gasMain, parseGasArgs, GAS_HELP } from './gas-cli.js';
|
|
24
|
+
export { runFees, readFeesChain, readFeeHistory, priceCalls, renderFeesText, feesReportJson, REFERENCE_CALLS, USD_PER_GAS, HISTORY_BLOCKS_151, HISTORY_GAS, FEES_VERSION } from './fees.js';
|
|
25
|
+
export type { ReferenceCall, PricedCall, HistoryPoint, FeesChain, FeesChainOk, FeesChainFailed, FeesReport, FeesOptions } from './fees.js';
|
|
26
|
+
export { feesMain, parseFeesArgs, FEES_HELP } from './fees-cli.js';
|
|
24
27
|
export { findGatedSuites, summariseTests } from './suites.js';
|
|
25
28
|
export type { ForgeTestJson, SuiteSummary } from './suites.js';
|
|
26
29
|
export { runShip, renderShipMarkdown, renderShipText, shipReportJson, shipReportPath, parseShipHeader, readShipContracts, routescanBase, utcDate, SHIP_HEADER } from './ship.js';
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,8 @@ export { runDoctor, renderDoctorText, whichOnPath, toolVersion, exitCodePropagat
|
|
|
15
15
|
export { doctorMain, parseDoctorArgs, DOCTOR_HELP } from './doctor-cli.js';
|
|
16
16
|
export { runGas, quoteGas, priceGas, priceRows, parseGasSnapshot, parseGasReportJson, parseGasFile, readGasSource, foundryDir, renderGasText, gasReportJson } from './gas.js';
|
|
17
17
|
export { gasMain, parseGasArgs, GAS_HELP } from './gas-cli.js';
|
|
18
|
+
export { runFees, readFeesChain, readFeeHistory, priceCalls, renderFeesText, feesReportJson, REFERENCE_CALLS, USD_PER_GAS, HISTORY_BLOCKS_151, HISTORY_GAS, FEES_VERSION } from './fees.js';
|
|
19
|
+
export { feesMain, parseFeesArgs, FEES_HELP } from './fees-cli.js';
|
|
18
20
|
export { findGatedSuites, summariseTests } from './suites.js';
|
|
19
21
|
export { runShip, renderShipMarkdown, renderShipText, shipReportJson, shipReportPath, parseShipHeader, readShipContracts, routescanBase, utcDate, SHIP_HEADER } from './ship.js';
|
|
20
22
|
export { shipMain, parseShipArgs, SHIP_HELP } from './ship-cli.js';
|
package/dist/redbelly.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// `redbelly <command>`: one bin for the checks a builder runs by hand. `preflight` is the seven
|
|
3
|
-
// checks, `doctor` the machine, `gas` the report in RBNT and US cents, `
|
|
4
|
-
// writes its own report. Each is also its own bin (redbelly-preflight, redbelly-doctor) or an MCP tool.
|
|
3
|
+
// checks, `doctor` the machine, `gas` the report in RBNT and US cents, `fees` what the measured calls
|
|
4
|
+
// cost on each chain today, `ship` the mainnet gate that writes its own report. Each is also its own bin (redbelly-preflight, redbelly-doctor) or an MCP tool.
|
|
5
5
|
import { realpathSync } from 'node:fs';
|
|
6
6
|
import { pathToFileURL } from 'node:url';
|
|
7
7
|
import { main as preflightMain } from './cli.js';
|
|
@@ -11,7 +11,8 @@ const HELP = `redbelly <command> [options]
|
|
|
11
11
|
preflight the seven checks before a deploy (redbelly-preflight)
|
|
12
12
|
doctor the machine before the first command (redbelly-doctor)
|
|
13
13
|
gas gas per function in RBNT and US cents at the chain's base fee and feed price
|
|
14
|
-
|
|
14
|
+
fees what the measured calls cost on 151 and 153 today: head block, base fee, feed price, RBNT and US cents
|
|
15
|
+
ship pre-flight, Slither, the five-state tests and the gas figures, written to deployments/ship-<chain>-<date>.md
|
|
15
16
|
|
|
16
17
|
redbelly <command> --help for each. Nothing here takes a key; signing tools take a keystore name.
|
|
17
18
|
`;
|
|
@@ -19,6 +20,7 @@ const commands = {
|
|
|
19
20
|
preflight: preflightMain,
|
|
20
21
|
doctor: doctorMain,
|
|
21
22
|
gas: async (argv) => (await import('./gas-cli.js')).gasMain(argv),
|
|
23
|
+
fees: async (argv) => (await import('./fees-cli.js')).feesMain(argv),
|
|
22
24
|
ship: async (argv) => (await import('./ship-cli.js')).shipMain(argv),
|
|
23
25
|
};
|
|
24
26
|
export async function redbellyMain(argv = process.argv.slice(2)) {
|
package/dist/ship-cli.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ShipOptions } from './ship.js';
|
|
2
|
-
export declare const SHIP_HELP = "redbelly ship --chain <151|153> --account <keystore-name> [options]\n\nRuns pre-flight (seven checks) with the keystore's address, checks the Slither sidecar, runs\nforge test and the five-state suites, prices the gas report at the chain's base fee and feed\nprice, reads the deployer and the admin (isAllowed, Safe 1.4.1, threshold, owners), and writes\ndeployments/ship-<chain>-<date>.md with every check and its result, the addresses, the\nconstructor arguments and the verify command. Refuses to write when any check fails. The\ndeploy script refuses to broadcast on 151 without a report dated today; 153 warns.\n\n --chain <151|153> required\n --account <name>
|
|
2
|
+
export declare const SHIP_HELP = "redbelly ship --chain <151|153> --account <keystore-name> [options]\n (or --address <0x\u2026> in place of --account; DEPLOYER in .env also works)\n\nRuns pre-flight (seven checks) with the keystore's address, checks the Slither sidecar, runs\nforge test and the five-state suites, prices the gas report at the chain's base fee and feed\nprice, reads the deployer and the admin (isAllowed, Safe 1.4.1, threshold, owners), and writes\ndeployments/ship-<chain>-<date>.md with every check and its result, the addresses, the\nconstructor arguments and the verify command. Refuses to write when any check fails. The\ndeploy script refuses to broadcast on 151 without a report dated today; 153 warns.\n\n --chain <151|153> required\n --account <name> Foundry keystore name, resolved with cast wallet address. cast asks for the\n keystore password on the terminal, so run this in one (not from an editor's\n run button); without a terminal pass --password-file, or use --address\n --address <0x\u2026> the deployer's address, no cast call; the report says it came this way\n --password-file <path> passed through to cast (or set ETH_PASSWORD)\n --admin <0x\u2026> the Safe that receives every role (or ADMIN_SAFE in .env); required on 151\n --gas <units> deployment gas for the balance check (default: the gas report's deployment row)\n --verifier <0x\u2026> the dApp's verifier (or VERIFIER in .env); required on 151\n --project <dir> Foundry project or scaffold root (default: current directory)\n --rpc <url> JSON-RPC endpoint (default: the governors RPC for --chain; a fork works)\n --out <path> write the report here instead\n --json print the report as JSON (the file is still written when checks pass)\n --version, --help\n";
|
|
3
3
|
export declare function parseShipArgs(argv: readonly string[]): (Partial<ShipOptions> & {
|
|
4
4
|
json: boolean;
|
|
5
5
|
help: boolean;
|
package/dist/ship-cli.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// arguments. --json for agents. Never takes a key: --account names a keystore that cast reads.
|
|
4
4
|
import { SHIP_VERSION, renderShipText, runShip, shipReportJson, ShipUsageError } from './ship.js';
|
|
5
5
|
export const SHIP_HELP = `redbelly ship --chain <151|153> --account <keystore-name> [options]
|
|
6
|
+
(or --address <0x…> in place of --account; DEPLOYER in .env also works)
|
|
6
7
|
|
|
7
8
|
Runs pre-flight (seven checks) with the keystore's address, checks the Slither sidecar, runs
|
|
8
9
|
forge test and the five-state suites, prices the gas report at the chain's base fee and feed
|
|
@@ -12,7 +13,10 @@ constructor arguments and the verify command. Refuses to write when any check fa
|
|
|
12
13
|
deploy script refuses to broadcast on 151 without a report dated today; 153 warns.
|
|
13
14
|
|
|
14
15
|
--chain <151|153> required
|
|
15
|
-
--account <name>
|
|
16
|
+
--account <name> Foundry keystore name, resolved with cast wallet address. cast asks for the
|
|
17
|
+
keystore password on the terminal, so run this in one (not from an editor's
|
|
18
|
+
run button); without a terminal pass --password-file, or use --address
|
|
19
|
+
--address <0x…> the deployer's address, no cast call; the report says it came this way
|
|
16
20
|
--password-file <path> passed through to cast (or set ETH_PASSWORD)
|
|
17
21
|
--admin <0x…> the Safe that receives every role (or ADMIN_SAFE in .env); required on 151
|
|
18
22
|
--gas <units> deployment gas for the balance check (default: the gas report's deployment row)
|
|
@@ -50,6 +54,9 @@ export function parseShipArgs(argv) {
|
|
|
50
54
|
case '--account':
|
|
51
55
|
o.account = next();
|
|
52
56
|
break;
|
|
57
|
+
case '--address':
|
|
58
|
+
o.address = next();
|
|
59
|
+
break;
|
|
53
60
|
case '--password-file':
|
|
54
61
|
o.passwordFile = next();
|
|
55
62
|
break;
|
|
@@ -91,9 +98,13 @@ export function parseShipArgs(argv) {
|
|
|
91
98
|
if (!o.help && !o.version) {
|
|
92
99
|
if (!o.chain)
|
|
93
100
|
throw new Error(`--chain 151 or 153 is required\n\n${SHIP_HELP}`);
|
|
94
|
-
if (!o.account)
|
|
95
|
-
throw new Error(`--account <keystore-name> is required: the report carries the deployer's address and its isAllowed reading\n\n${SHIP_HELP}`);
|
|
96
|
-
if (
|
|
101
|
+
if (!o.account && !o.address && !process.env['DEPLOYER'])
|
|
102
|
+
throw new Error(`--account <keystore-name> is required (or --address <0x…>, or DEPLOYER in the environment): the report carries the deployer's address and its isAllowed reading\n\n${SHIP_HELP}`);
|
|
103
|
+
if (o.account && o.address)
|
|
104
|
+
throw new Error('give --account or --address, not both');
|
|
105
|
+
if (o.address && !/^0x[0-9a-fA-F]{40}$/.test(o.address))
|
|
106
|
+
throw new Error('--address must be a 20-byte hex address');
|
|
107
|
+
if (o.account && !/^[A-Za-z0-9._-]{1,64}$/.test(o.account))
|
|
97
108
|
throw new Error('--account: keystore names use letters, digits, dot, dash and underscore only');
|
|
98
109
|
}
|
|
99
110
|
return o;
|
package/dist/ship.d.ts
CHANGED
|
@@ -7,8 +7,10 @@ export interface ShipOptions {
|
|
|
7
7
|
/** Foundry project or scaffold root (its contracts/ is used). Default: cwd. */
|
|
8
8
|
project?: string;
|
|
9
9
|
chain: 151 | 153;
|
|
10
|
-
/** Foundry keystore name; cast resolves the address, nothing else is read from it. */
|
|
11
|
-
account
|
|
10
|
+
/** Foundry keystore name; cast resolves the address, nothing else is read from it. One of account, address or DEPLOYER in .env. */
|
|
11
|
+
account?: string;
|
|
12
|
+
/** The deployer's address given directly: no cast call, no password prompt. */
|
|
13
|
+
address?: string;
|
|
12
14
|
passwordFile?: string;
|
|
13
15
|
/** The Safe that receives every role. Required on 151; ADMIN_SAFE in .env fills it in. */
|
|
14
16
|
admin?: string;
|
|
@@ -31,6 +33,8 @@ export interface ShipContract {
|
|
|
31
33
|
readonly name: string;
|
|
32
34
|
readonly address: string;
|
|
33
35
|
readonly record: string;
|
|
36
|
+
/** When the record file was last written, so a report made before a deploy is read as such. */
|
|
37
|
+
readonly writtenAt: string;
|
|
34
38
|
readonly deployer: string | null;
|
|
35
39
|
readonly admin: string | null;
|
|
36
40
|
readonly verifier: string | null;
|
package/dist/ship.js
CHANGED
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
// refuses to write when any check fails; the deploy script refuses to broadcast on 151 without a
|
|
7
7
|
// report dated today. Nothing here signs: the account name resolves to an address through cast,
|
|
8
8
|
// the RPC is read, forge runs tests on the local machine.
|
|
9
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
10
10
|
import { createRequire } from 'node:module';
|
|
11
11
|
import { basename, join, relative, resolve } from 'node:path';
|
|
12
12
|
import { chainById, decodeWords, ethCall, isAllowed, toCaller } from '@gatedpath/chains';
|
|
13
13
|
import { readProjectConfig } from './config.js';
|
|
14
14
|
import { readDotEnv } from './dotenv.js';
|
|
15
15
|
import { readAdmin } from './checks/admin.js';
|
|
16
|
-
import { parseGasReportJson, priceRows, quoteGas } from './gas.js';
|
|
16
|
+
import { faucetDays, parseGasReportJson, priceRows, quoteGas } from './gas.js';
|
|
17
17
|
import { run } from './proc.js';
|
|
18
18
|
import { DEFAULT_GAS, runPreflight } from './run.js';
|
|
19
19
|
import { findGatedSuites, summariseTests } from './suites.js';
|
|
@@ -66,10 +66,15 @@ export function readShipContracts(dir, chain, pins) {
|
|
|
66
66
|
for (const name of readdirSync(dep).sort()) {
|
|
67
67
|
if (!(name === `${chain}.json` || (name.startsWith(`${chain}-`) && name.endsWith('.json'))))
|
|
68
68
|
continue;
|
|
69
|
+
// The deploy script writes a dry run's record as <chain>-<Name>.dry-run.json (0.2.3) and says
|
|
70
|
+
// "broadcast": false inside it; neither describes a contract anyone can verify.
|
|
71
|
+
if (name.endsWith('.dry-run.json'))
|
|
72
|
+
continue;
|
|
69
73
|
const record = readJson(join(dep, name));
|
|
70
74
|
const address = str(record?.['contract']);
|
|
71
|
-
if (!record || !address)
|
|
75
|
+
if (!record || !address || record['broadcast'] === false)
|
|
72
76
|
continue;
|
|
77
|
+
const writtenAt = statSync(join(dep, name)).mtime.toISOString();
|
|
73
78
|
const contractName = str(record['contractName']) ?? basename(name, '.json').replace(new RegExp(`^${chain}-`), '');
|
|
74
79
|
const sig = str(record['constructorSignature']);
|
|
75
80
|
const args = str(record['constructorArgs']);
|
|
@@ -79,6 +84,7 @@ export function readShipContracts(dir, chain, pins) {
|
|
|
79
84
|
name: contractName,
|
|
80
85
|
address,
|
|
81
86
|
record: join(dep, name),
|
|
87
|
+
writtenAt,
|
|
82
88
|
deployer: str(record['deployer']),
|
|
83
89
|
admin: str(record['admin']),
|
|
84
90
|
verifier: str(record['verifier']),
|
|
@@ -151,14 +157,14 @@ export async function runShip(opts) {
|
|
|
151
157
|
// 2. pre-flight, seven checks, with the keystore's address and the measured deployment gas
|
|
152
158
|
const gasForBalance = opts.gas !== undefined ? BigInt(opts.gas) : (deploymentGas ?? DEFAULT_GAS);
|
|
153
159
|
const admin = opts.admin ?? rootEnv['ADMIN_SAFE'];
|
|
154
|
-
const preflight = await runPreflight({ project: dir, chain, rpc, account: opts.account, ...(opts.passwordFile ? { passwordFile: opts.passwordFile } : {}), ...(admin ? { admin } : {}), gas: gasForBalance, env });
|
|
160
|
+
const preflight = await runPreflight({ project: dir, chain, rpc, ...(opts.account ? { account: opts.account } : {}), ...(opts.address ? { address: opts.address } : {}), ...(opts.passwordFile ? { passwordFile: opts.passwordFile } : {}), ...(admin ? { admin } : {}), gas: gasForBalance, env });
|
|
155
161
|
for (const c of preflight.checks)
|
|
156
162
|
checks.push({ id: c.id, status: c.status, reason: c.reason, ...(c.data ? { data: c.data } : {}) });
|
|
157
163
|
if (chain === 151 && !admin)
|
|
158
164
|
checks.push({ id: 'admin-required', status: 'fail', reason: 'Chain 151 needs --admin <safe> (or ADMIN_SAFE in .env): the Safe 1.4.1 that receives every role.' });
|
|
159
165
|
// 3. the verifier the deploy script will bind
|
|
160
166
|
const verifierAddr = opts.verifier ?? rootEnv['VERIFIER'] ?? null;
|
|
161
|
-
const verifierSource = opts.verifier ? '--verifier' : rootEnv['VERIFIER'] ? 'VERIFIER in .env' : 'none';
|
|
167
|
+
const verifierSource = opts.verifier ? '--verifier' : env['VERIFIER'] ? 'VERIFIER in the environment' : rootEnv['VERIFIER'] ? 'VERIFIER in .env' : 'none';
|
|
162
168
|
let verifierIsContract = null;
|
|
163
169
|
if (verifierAddr) {
|
|
164
170
|
if (!/^0x[0-9a-fA-F]{40}$/.test(verifierAddr))
|
|
@@ -214,7 +220,7 @@ export async function runShip(opts) {
|
|
|
214
220
|
const deployerCheck = preflight.checks.find((c) => c.id === 'deployer-verified');
|
|
215
221
|
const deployer = {
|
|
216
222
|
address: preflight.deployer,
|
|
217
|
-
how: deployerCheck?.data?.['how'] ?? (opts.account ? `cast wallet address --account ${opts.account}` : 'none'),
|
|
223
|
+
how: deployerCheck?.data?.['how'] ?? (opts.account ? `cast wallet address --account ${opts.account}` : opts.address ? '--address' : 'none'),
|
|
218
224
|
isAllowed: typeof deployerCheck?.data?.['allowed'] === 'boolean' ? deployerCheck.data['allowed'] : null,
|
|
219
225
|
wellKnownDevAccount: deployerCheck?.data?.['wellKnownDevAccount'] === true,
|
|
220
226
|
};
|
|
@@ -286,7 +292,22 @@ export class ShipUsageError extends Error {
|
|
|
286
292
|
// ---- the document ----
|
|
287
293
|
const fmt = (n) => n.toLocaleString('en-US');
|
|
288
294
|
const short = (s) => (s && s.includes('.') ? s.replace(/(\.\d{4})\d+$/, '$1') : s ?? '');
|
|
295
|
+
/** The document goes into deployments/, which is committed and, for a public project, published. So it
|
|
296
|
+
* names the project by its last two path segments, never by a path on the machine that wrote it, and
|
|
297
|
+
* says `<keystore-name>` where the terminal said the real one (found 25 October 2026 by the project
|
|
298
|
+
* export's scan on the escrow's report). The JSON and the terminal keep both. */
|
|
299
|
+
function forTheRecord(text, r) {
|
|
300
|
+
const shown = r.project.split(/[\\/]/).filter(Boolean).slice(-2).join('/');
|
|
301
|
+
let out = text.split(r.project).join(shown);
|
|
302
|
+
const account = /--account (\S+)/.exec(r.deployer.how)?.[1];
|
|
303
|
+
if (account)
|
|
304
|
+
out = out.split(`--account ${account}`).join('--account <keystore-name>');
|
|
305
|
+
return out;
|
|
306
|
+
}
|
|
289
307
|
export function renderShipMarkdown(r) {
|
|
308
|
+
return forTheRecord(renderShipBody(r), r);
|
|
309
|
+
}
|
|
310
|
+
function renderShipBody(r) {
|
|
290
311
|
const L = [];
|
|
291
312
|
const fails = r.checks.filter((c) => c.status === 'fail');
|
|
292
313
|
const warns = r.checks.filter((c) => c.status === 'warn');
|
|
@@ -316,7 +337,7 @@ export function renderShipMarkdown(r) {
|
|
|
316
337
|
L.push('## Contracts');
|
|
317
338
|
L.push('');
|
|
318
339
|
if (r.contracts.length === 0) {
|
|
319
|
-
L.push(`No deployment record for chain ${r.chain} under \`deployments/\` yet. Deploy with \`forge script script/Deploy.s.sol --rpc-url ${r.network === 'mainnet' ? 'redbelly_mainnet' : 'redbelly_testnet'} --account <keystore-name> --broadcast\`, then run \`redbelly ship\` again today: this section fills in with the address, the constructor arguments and the exact verify command.`);
|
|
340
|
+
L.push(`No deployment record for chain ${r.chain} under \`deployments/\` yet. Deploy with \`forge script script/Deploy.s.sol --rpc-url ${r.network === 'mainnet' ? 'redbelly_mainnet' : 'redbelly_testnet'} --account <keystore-name> --sender ${r.deployer.address ?? '<your-address>'} --broadcast\`, then run \`redbelly ship\` again today: this section fills in with the address, the constructor arguments and the exact verify command.`);
|
|
320
341
|
}
|
|
321
342
|
else {
|
|
322
343
|
for (const c of r.contracts) {
|
|
@@ -324,6 +345,8 @@ export function renderShipMarkdown(r) {
|
|
|
324
345
|
L.push('');
|
|
325
346
|
L.push(`Record \`${relative(r.project, c.record)}\`${c.deployer ? `, deployed by ${c.deployer}` : ''}${c.admin ? `, admin ${c.admin}` : ''}${c.verifier ? `, verifier ${c.verifier}` : ''}${c.requestId ? `, request id ${c.requestId}` : ''}. Explorer: ${c.explorer}`);
|
|
326
347
|
L.push('');
|
|
348
|
+
L.push(`This section describes the record as it stood at ${c.writtenAt}. A deploy made after that rewrites the record; run \`redbelly ship\` again afterwards and the command below follows the new address.`);
|
|
349
|
+
L.push('');
|
|
327
350
|
if (c.constructorSignature)
|
|
328
351
|
L.push(`Constructor \`${c.constructorSignature}\`, arguments as encoded by the deploy script:`);
|
|
329
352
|
else
|
|
@@ -343,6 +366,9 @@ export function renderShipMarkdown(r) {
|
|
|
343
366
|
const q = r.gas.quote;
|
|
344
367
|
L.push(`Priced at block ${fmt(q.block)}: base fee ${(Number(q.baseFeeWei) / 1e9).toLocaleString('en-US', { maximumFractionDigits: 2 })} gwei, RBNT US$${q.usdPerRbnt} on the feed at ${q.priceFeed} (${q.priceTimestamp}). Gas is priced in US dollars on this chain and converted to RBNT at execution, so the cents column is the stable one.`);
|
|
345
368
|
L.push('');
|
|
369
|
+
const faucet = faucetDays(r.gas.rows, r.gas.quote);
|
|
370
|
+
if (faucet)
|
|
371
|
+
L.push(`Faucet: the deployment rows with pre-flight's 25% margin come to ${fmt(faucet.gasWithMargin)} gas = ${faucet.rbnt.toFixed(1)} test RBNT at this base fee, ${faucet.claims} claim${faucet.claims === 1 ? '' : 's'} of ${faucet.perClaimRbnt} from the faucet (${faucet.days} day${faucet.days === 1 ? '' : 's'}, one claim a day). The base fee moves; re-run on the day.`, '');
|
|
346
372
|
if (r.gas.deployment)
|
|
347
373
|
L.push(`Deployment of ${r.gas.deployment.contract}: ${fmt(r.gas.deployment.gas)} gas = ${short(r.gas.deployment.rbnt)} RBNT = ${r.gas.deployment.cents.toFixed(2)} US cents. Pre-flight's balance check used ${fmt(BigInt(r.preflight.checks.find((c) => c.id === 'balance')?.data?.['gas'] ?? '0'))} gas with 25% on top.`);
|
|
348
374
|
L.push('');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gatedpath/preflight",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Pre-flight checks before deploying to Redbelly Network (151 mainnet, 153 testnet): chain ID, deployer verification, Safe admin, compiler pins, secrets in git history, RBNT balance, Slither report.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -35,13 +35,13 @@
|
|
|
35
35
|
],
|
|
36
36
|
"scripts": {
|
|
37
37
|
"build": "node scripts/ensure-deps-built.mjs && tsc -p tsconfig.json && node -e \"for (const f of ['cli.js','doctor-cli.js','redbelly.js']) require('fs').chmodSync('dist/'+f,0o755)\"",
|
|
38
|
-
"test": "npm run build && node --test --test-reporter=spec --test-concurrency=1 test/unit.test.mjs test/preflight.test.mjs test/doctor.test.mjs test/gas.test.mjs test/ship.test.mjs",
|
|
38
|
+
"test": "npm run build && node --test --test-reporter=spec --test-concurrency=1 test/unit.test.mjs test/preflight.test.mjs test/doctor.test.mjs test/gas.test.mjs test/fees.test.mjs test/ship.test.mjs",
|
|
39
39
|
"test:live": "npm run build && node --test --test-reporter=spec test/live.test.mjs",
|
|
40
40
|
"prepack": "npm run build"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@gatedpath/agent-rules": "0.2.
|
|
44
|
-
"@gatedpath/chains": "0.2.
|
|
43
|
+
"@gatedpath/agent-rules": "0.2.3",
|
|
44
|
+
"@gatedpath/chains": "0.2.3"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@types/node": "^22.20.2",
|