@gatedpath/preflight 0.2.2 → 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 +20 -0
- 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 +6 -1
- package/dist/gas.js +6 -4
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/redbelly.js +5 -3
- package/dist/ship.d.ts +2 -0
- package/dist/ship.js +26 -3
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -90,6 +90,26 @@ 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
|
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;
|
|
@@ -114,7 +116,10 @@ export declare function priceRows(rows: readonly GasRow[], quote: Pick<GasQuote,
|
|
|
114
116
|
added: string[];
|
|
115
117
|
removed: string[];
|
|
116
118
|
};
|
|
117
|
-
|
|
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>;
|
|
118
123
|
/** A scaffold root's contracts/ when that is where foundry.toml is; otherwise the directory itself. */
|
|
119
124
|
export declare function foundryDir(project: string): string;
|
|
120
125
|
export declare function readGasSource(dir: string, opts: Pick<GasOptions, 'snapshot' | 'report' | 'env'>): Promise<{
|
package/dist/gas.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
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;
|
|
@@ -105,8 +105,9 @@ export function priceRows(rows, quote, before) {
|
|
|
105
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)) : [] };
|
|
106
106
|
}
|
|
107
107
|
// ---- the two reads ----
|
|
108
|
-
|
|
109
|
-
|
|
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);
|
|
110
111
|
const reported = Number(BigInt((await caller('eth_chainId'))));
|
|
111
112
|
if (reported !== 151 && reported !== 153)
|
|
112
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)`);
|
|
@@ -115,12 +116,13 @@ export async function quoteGas(rpc, expected) {
|
|
|
115
116
|
const block = (await caller('eth_getBlockByNumber', ['latest', false]));
|
|
116
117
|
if (!block?.baseFeePerGas)
|
|
117
118
|
throw new Error('the latest block has no baseFeePerGas');
|
|
118
|
-
const price = await getLatestPrice({ rpc });
|
|
119
|
+
const price = await getLatestPrice({ rpc: caller });
|
|
119
120
|
return {
|
|
120
121
|
chain: reported,
|
|
121
122
|
network: reported === 151 ? 'mainnet' : 'testnet',
|
|
122
123
|
rpc,
|
|
123
124
|
block: Number(BigInt(block.number ?? '0x0')),
|
|
125
|
+
blockTimestamp: new Date(Number(BigInt(block.timestamp ?? '0x0')) * 1000).toISOString(),
|
|
124
126
|
baseFeeWei: BigInt(block.baseFeePerGas),
|
|
125
127
|
priceRaw: price.raw,
|
|
126
128
|
usdPerRbnt: price.usdPerRbnt,
|
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.d.ts
CHANGED
|
@@ -33,6 +33,8 @@ export interface ShipContract {
|
|
|
33
33
|
readonly name: string;
|
|
34
34
|
readonly address: string;
|
|
35
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;
|
|
36
38
|
readonly deployer: string | null;
|
|
37
39
|
readonly admin: string | null;
|
|
38
40
|
readonly verifier: string | null;
|
package/dist/ship.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
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';
|
|
@@ -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']),
|
|
@@ -158,7 +164,7 @@ export async function runShip(opts) {
|
|
|
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))
|
|
@@ -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');
|
|
@@ -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
|
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",
|