@blockula/mcp 0.2.0 → 0.5.1
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 +69 -13
- package/dist/index.js +940 -98
- package/dist/network.d.ts +101 -0
- package/dist/network.js +231 -0
- package/dist/release.d.ts +79 -0
- package/dist/release.js +193 -0
- package/dist/signed.d.ts +31 -0
- package/dist/signed.js +72 -0
- package/dist/spend.d.ts +42 -0
- package/dist/spend.js +165 -0
- package/dist/untrusted.d.ts +11 -0
- package/dist/untrusted.js +54 -0
- package/npm-shrinkwrap.json +1303 -0
- package/package.json +16 -5
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
export type NetworkKey = "mainnet" | "testnet" | "local" | "custom";
|
|
2
|
+
/** What the node must report in `/api/status`. */
|
|
3
|
+
export interface Pin {
|
|
4
|
+
chainId?: string;
|
|
5
|
+
genesisHash?: string;
|
|
6
|
+
prefix?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface Network {
|
|
9
|
+
key: NetworkKey;
|
|
10
|
+
label: string;
|
|
11
|
+
rpc: string;
|
|
12
|
+
pin: Pin;
|
|
13
|
+
/** Real money: the copy and the defaults get stricter. */
|
|
14
|
+
mainnet: boolean;
|
|
15
|
+
/** Remember the first chain this node reports (TOFU) across restarts. Off for local dev chains, which `--reset` replaces. */
|
|
16
|
+
remember: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Independent nodes of the same chain (BLOCKULA_WITNESSES, comma-separated): names that end up
|
|
19
|
+
* in a signature are proven against them (audit gap-7-1). Empty: a name is the node's word.
|
|
20
|
+
*/
|
|
21
|
+
witnesses: string[];
|
|
22
|
+
}
|
|
23
|
+
export declare const LOCAL_RPC = "http://localhost:7070";
|
|
24
|
+
export declare const TESTNET_RPC = "https://blockula-testnet.fly.dev";
|
|
25
|
+
/** Public mainnet RPC endpoints, run by independent operators. Empty until launch: set BLOCKULA_RPC. */
|
|
26
|
+
export declare const MAINNET_RPCS: string[];
|
|
27
|
+
/**
|
|
28
|
+
* Chains whose genesis is known: any node claiming one of these chain ids must report this
|
|
29
|
+
* genesis hash, whatever network it was configured as. `blockula-1`'s hash is added here once the
|
|
30
|
+
* genesis ceremony has produced it (docs/GENESIS_CEREMONY.md).
|
|
31
|
+
*/
|
|
32
|
+
export declare const KNOWN_GENESIS: Record<string, string>;
|
|
33
|
+
/**
|
|
34
|
+
* The genesis clock of chains whose genesis is known (`created_at_ms` and `block_time_ms` of the
|
|
35
|
+
* genesis.json that hashes to `KNOWN_GENESIS[id]`), added with the hash. With it the client
|
|
36
|
+
* refuses a node reporting a height the chain can't have reached by this machine's clock, before
|
|
37
|
+
* anything is signed (audit review-r3-clients-2; `Client.checkSigningHeight`).
|
|
38
|
+
*/
|
|
39
|
+
export declare const KNOWN_CLOCKS: Record<string, GenesisClock>;
|
|
40
|
+
/** A chain's genesis clock: its genesis.json's `created_at_ms` and `block_time_ms`. */
|
|
41
|
+
export interface GenesisClock {
|
|
42
|
+
createdAtMs: number;
|
|
43
|
+
blockTimeMs: number;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The genesis clock in a node's `/api/genesis` answer for chain `chainId`, or `undefined` when it
|
|
47
|
+
* isn't one (another chain's genesis, missing or malformed fields). The node's word: the MCP takes
|
|
48
|
+
* it on first use, as it does the genesis hash, and remembers it with that hash.
|
|
49
|
+
*/
|
|
50
|
+
export declare function clockFrom(genesis: unknown, chainId: string): GenesisClock | undefined;
|
|
51
|
+
/** The networks the MCP knows by name. */
|
|
52
|
+
export declare const KNOWN_NETWORKS: Record<Exclude<NetworkKey, "custom">, Omit<Network, "rpc" | "witnesses"> & {
|
|
53
|
+
rpc?: string;
|
|
54
|
+
}>;
|
|
55
|
+
/** A node URL: http(s) without credentials; plain http only on this machine or a private network. */
|
|
56
|
+
export declare function checkRpcUrl(u: string, what?: string): string;
|
|
57
|
+
/** `--network <name>` or `--network=<name>` from the command line. */
|
|
58
|
+
export declare function networkArg(argv: string[]): string | undefined;
|
|
59
|
+
/**
|
|
60
|
+
* The network from the configuration: `--network` (or BLOCKULA_NETWORK) names one, BLOCKULA_RPC
|
|
61
|
+
* gives the node, BLOCKULA_CHAIN_ID / BLOCKULA_GENESIS_HASH add pins. With none of them, the
|
|
62
|
+
* public testnet. Contradictions are errors, never guesses.
|
|
63
|
+
*/
|
|
64
|
+
export declare function resolveNetwork(env: Record<string, string | undefined>, argv?: string[]): Network;
|
|
65
|
+
/** Why a node's status doesn't match a pin, or null if it does. Known chain ids must come with their known genesis. */
|
|
66
|
+
export declare function mismatch(pin: Pin, status: {
|
|
67
|
+
chain_id?: unknown;
|
|
68
|
+
genesis_hash?: unknown;
|
|
69
|
+
address_prefix?: unknown;
|
|
70
|
+
}): string | null;
|
|
71
|
+
/** The pin after a first status: every field the node reported that wasn't pinned yet. */
|
|
72
|
+
export declare function completePin(pin: Pin, status: {
|
|
73
|
+
chain_id?: unknown;
|
|
74
|
+
genesis_hash?: unknown;
|
|
75
|
+
address_prefix?: unknown;
|
|
76
|
+
}): Pin;
|
|
77
|
+
/** Chains remembered on first use, by network and node URL (BLOCKULA_HOME/known-chains.json). */
|
|
78
|
+
export declare class KnownChains {
|
|
79
|
+
readonly file: string;
|
|
80
|
+
constructor(file: string);
|
|
81
|
+
private all;
|
|
82
|
+
static id(net: Network): string;
|
|
83
|
+
get(net: Network): Pin | undefined;
|
|
84
|
+
save(net: Network, pin: Pin): void;
|
|
85
|
+
/** Genesis clocks are a property of the chain (its id and genesis hash), whichever node reported them. */
|
|
86
|
+
static clockId(chainId: string, genesisHash: string): string;
|
|
87
|
+
/** The genesis clock remembered for this chain (`clockFrom` of the first `/api/genesis` read for it). */
|
|
88
|
+
clock(net: Network, chainId: string, genesisHash: string): GenesisClock | undefined;
|
|
89
|
+
saveClock(net: Network, chainId: string, genesisHash: string, clock: GenesisClock): void;
|
|
90
|
+
private write;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The pin to enforce for a network: its configured pin, filled from what was remembered for it.
|
|
94
|
+
* A remembered field never overrides a configured one; a contradiction means the configuration
|
|
95
|
+
* changed, and the configuration wins. What was remembered for another chain id (the network was
|
|
96
|
+
* relaunched under a new one, which this release pins) isn't used at all: the new chain is
|
|
97
|
+
* pinned on first use instead of refused for the old one's genesis.
|
|
98
|
+
*/
|
|
99
|
+
export declare function effectivePin(net: Network, remembered?: Pin): Pin;
|
|
100
|
+
/** The error an agent sees when a node is on the wrong chain: what happened and what the user can do. */
|
|
101
|
+
export declare function mismatchError(net: Network, why: string, knownChainsFile: string): Error;
|
package/dist/network.js
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// Which chain the MCP talks to, and how it makes sure it stays on it (WAL-4).
|
|
2
|
+
//
|
|
3
|
+
// The network is chosen once, at startup, by the user: `--network <name>` / BLOCKULA_NETWORK, or
|
|
4
|
+
// BLOCKULA_RPC. Nothing switches it later (no probing localhost, no tool that changes it). Each
|
|
5
|
+
// network pins a chain id and, where known, a genesis hash; BLOCKULA_CHAIN_ID and
|
|
6
|
+
// BLOCKULA_GENESIS_HASH pin them for any node. Whatever isn't pinned is pinned from the first
|
|
7
|
+
// status the node reports (trust on first use) and, except for throwaway local dev chains,
|
|
8
|
+
// remembered in BLOCKULA_HOME/known-chains.json, so a node that later reports another chain is
|
|
9
|
+
// refused, across restarts too.
|
|
10
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { dirname } from "node:path";
|
|
12
|
+
export const LOCAL_RPC = "http://localhost:7070";
|
|
13
|
+
export const TESTNET_RPC = "https://blockula-testnet.fly.dev";
|
|
14
|
+
/** Public mainnet RPC endpoints, run by independent operators. Empty until launch: set BLOCKULA_RPC. */
|
|
15
|
+
export const MAINNET_RPCS = [];
|
|
16
|
+
/**
|
|
17
|
+
* Chains whose genesis is known: any node claiming one of these chain ids must report this
|
|
18
|
+
* genesis hash, whatever network it was configured as. `blockula-1`'s hash is added here once the
|
|
19
|
+
* genesis ceremony has produced it (docs/GENESIS_CEREMONY.md).
|
|
20
|
+
*/
|
|
21
|
+
export const KNOWN_GENESIS = { "blockula-testnet-2": "b004e2f91b63429fc009088b16ec02b6d5388a03642699ae3dc6e325939f19b2" };
|
|
22
|
+
/**
|
|
23
|
+
* The genesis clock of chains whose genesis is known (`created_at_ms` and `block_time_ms` of the
|
|
24
|
+
* genesis.json that hashes to `KNOWN_GENESIS[id]`), added with the hash. With it the client
|
|
25
|
+
* refuses a node reporting a height the chain can't have reached by this machine's clock, before
|
|
26
|
+
* anything is signed (audit review-r3-clients-2; `Client.checkSigningHeight`).
|
|
27
|
+
*/
|
|
28
|
+
export const KNOWN_CLOCKS = { "blockula-testnet-2": { createdAtMs: 1792328803000, blockTimeMs: 1000 } };
|
|
29
|
+
/**
|
|
30
|
+
* The genesis clock in a node's `/api/genesis` answer for chain `chainId`, or `undefined` when it
|
|
31
|
+
* isn't one (another chain's genesis, missing or malformed fields). The node's word: the MCP takes
|
|
32
|
+
* it on first use, as it does the genesis hash, and remembers it with that hash.
|
|
33
|
+
*/
|
|
34
|
+
export function clockFrom(genesis, chainId) {
|
|
35
|
+
const g = genesis && typeof genesis === "object" ? genesis : {};
|
|
36
|
+
const { created_at_ms: createdAtMs, block_time_ms: blockTimeMs } = g;
|
|
37
|
+
if (g.chain_id !== chainId || !Number.isSafeInteger(createdAtMs) || createdAtMs < 0 || !Number.isSafeInteger(blockTimeMs) || blockTimeMs < 1)
|
|
38
|
+
return undefined;
|
|
39
|
+
return { createdAtMs: createdAtMs, blockTimeMs: blockTimeMs };
|
|
40
|
+
}
|
|
41
|
+
/** The networks the MCP knows by name. */
|
|
42
|
+
export const KNOWN_NETWORKS = {
|
|
43
|
+
mainnet: { key: "mainnet", label: "Blockula mainnet", rpc: MAINNET_RPCS[0], pin: { chainId: "blockula-1", prefix: "boo" }, mainnet: true, remember: true },
|
|
44
|
+
// The protocol v1 public testnet (deploy/launch.sh): its own chain id (never reused) and the
|
|
45
|
+
// testnet prefix (DEC-7). Its genesis hash exists only once it launches: until a release adds it
|
|
46
|
+
// to KNOWN_GENESIS, the first status the node reports pins it (trust on first use, remembered).
|
|
47
|
+
testnet: { key: "testnet", label: "public testnet", rpc: TESTNET_RPC, pin: { chainId: "blockula-testnet-2", prefix: "tboo" }, mainnet: false, remember: true },
|
|
48
|
+
local: { key: "local", label: "local dev node", rpc: LOCAL_RPC, pin: {}, mainnet: false, remember: false },
|
|
49
|
+
};
|
|
50
|
+
const isLocalUrl = (u) => {
|
|
51
|
+
try {
|
|
52
|
+
return ["localhost", "127.0.0.1", "[::1]"].includes(new URL(u).hostname);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
/** This machine, or a private network (where plain http can't be intercepted from the internet). */
|
|
59
|
+
const isPrivateUrl = (u) => {
|
|
60
|
+
if (isLocalUrl(u))
|
|
61
|
+
return true;
|
|
62
|
+
const h = new URL(u).hostname;
|
|
63
|
+
return /^(10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)/.test(h) || /\.(internal|local|lan)$/.test(h);
|
|
64
|
+
};
|
|
65
|
+
/** A node URL: http(s) without credentials; plain http only on this machine or a private network. */
|
|
66
|
+
export function checkRpcUrl(u, what = "BLOCKULA_RPC") {
|
|
67
|
+
let url;
|
|
68
|
+
try {
|
|
69
|
+
url = new URL(u);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
throw new Error(`${what} isn't a URL: ${u}`);
|
|
73
|
+
}
|
|
74
|
+
if (!/^https?:$/.test(url.protocol) || url.username || url.password)
|
|
75
|
+
throw new Error(`${what} must be an http(s) URL without credentials: ${u}`);
|
|
76
|
+
if (url.protocol === "http:" && !isPrivateUrl(u))
|
|
77
|
+
throw new Error(`${what} must use https unless the node is on this machine or a private network: ${u}`);
|
|
78
|
+
return u.replace(/\/+$/, "");
|
|
79
|
+
}
|
|
80
|
+
/** `--network <name>` or `--network=<name>` from the command line. */
|
|
81
|
+
export function networkArg(argv) {
|
|
82
|
+
for (let i = 0; i < argv.length; i++) {
|
|
83
|
+
if (argv[i] === "--network")
|
|
84
|
+
return argv[i + 1];
|
|
85
|
+
if (argv[i].startsWith("--network="))
|
|
86
|
+
return argv[i].slice("--network=".length);
|
|
87
|
+
}
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The network from the configuration: `--network` (or BLOCKULA_NETWORK) names one, BLOCKULA_RPC
|
|
92
|
+
* gives the node, BLOCKULA_CHAIN_ID / BLOCKULA_GENESIS_HASH add pins. With none of them, the
|
|
93
|
+
* public testnet. Contradictions are errors, never guesses.
|
|
94
|
+
*/
|
|
95
|
+
export function resolveNetwork(env, argv = []) {
|
|
96
|
+
const named = (networkArg(argv) ?? env.BLOCKULA_NETWORK)?.trim().toLowerCase() || undefined;
|
|
97
|
+
if (named !== undefined && !["mainnet", "testnet", "local", "custom"].includes(named))
|
|
98
|
+
throw new Error(`unknown network "${named}" (mainnet, testnet, local or custom)`);
|
|
99
|
+
const rpc = env.BLOCKULA_RPC?.trim() ? checkRpcUrl(env.BLOCKULA_RPC.trim()) : undefined;
|
|
100
|
+
let key;
|
|
101
|
+
if (named)
|
|
102
|
+
key = named;
|
|
103
|
+
else if (rpc)
|
|
104
|
+
key = Object.values(KNOWN_NETWORKS).find((n) => n.rpc && n.rpc === rpc)?.key ?? (isLocalUrl(rpc) ? "local" : "custom");
|
|
105
|
+
else
|
|
106
|
+
key = "testnet";
|
|
107
|
+
const base = key === "custom" ? { key, label: "custom node", pin: {}, mainnet: false, remember: true } : KNOWN_NETWORKS[key];
|
|
108
|
+
const url = rpc ?? base.rpc;
|
|
109
|
+
if (!url) {
|
|
110
|
+
throw new Error(key === "mainnet" ? "no public mainnet RPC is built in yet: set BLOCKULA_RPC to a mainnet node you trust" : "the custom network needs BLOCKULA_RPC");
|
|
111
|
+
}
|
|
112
|
+
if (key === "local" && !isLocalUrl(url))
|
|
113
|
+
throw new Error(`the local network is a node on this machine, not ${url}`);
|
|
114
|
+
const pin = { ...base.pin };
|
|
115
|
+
const chainId = env.BLOCKULA_CHAIN_ID?.trim() || undefined;
|
|
116
|
+
const genesisHash = env.BLOCKULA_GENESIS_HASH?.trim().toLowerCase() || undefined;
|
|
117
|
+
if (chainId) {
|
|
118
|
+
if (pin.chainId && pin.chainId !== chainId)
|
|
119
|
+
throw new Error(`BLOCKULA_CHAIN_ID=${chainId} contradicts the ${base.label} (${pin.chainId})`);
|
|
120
|
+
pin.chainId = chainId;
|
|
121
|
+
}
|
|
122
|
+
if (genesisHash) {
|
|
123
|
+
if (!/^[0-9a-f]{64}$/.test(genesisHash))
|
|
124
|
+
throw new Error("BLOCKULA_GENESIS_HASH must be 64 hex characters");
|
|
125
|
+
pin.genesisHash = genesisHash;
|
|
126
|
+
}
|
|
127
|
+
const known = pin.chainId ? KNOWN_GENESIS[pin.chainId] : undefined;
|
|
128
|
+
if (known && pin.genesisHash && known !== pin.genesisHash)
|
|
129
|
+
throw new Error(`BLOCKULA_GENESIS_HASH contradicts ${pin.chainId}'s known genesis ${known}`);
|
|
130
|
+
pin.genesisHash ??= known;
|
|
131
|
+
// A real-money chain is never the local or a custom network by accident.
|
|
132
|
+
const mainnet = base.mainnet || pin.chainId === KNOWN_NETWORKS.mainnet.pin.chainId;
|
|
133
|
+
const witnesses = (env.BLOCKULA_WITNESSES ?? "").split(",").map((w) => w.trim()).filter(Boolean).map((w) => checkRpcUrl(w, "BLOCKULA_WITNESSES"));
|
|
134
|
+
if (witnesses.includes(url))
|
|
135
|
+
throw new Error("BLOCKULA_WITNESSES must be other nodes than BLOCKULA_RPC's");
|
|
136
|
+
if (new Set(witnesses).size !== witnesses.length)
|
|
137
|
+
throw new Error("BLOCKULA_WITNESSES lists a node twice");
|
|
138
|
+
return { key, label: base.label, rpc: url, pin, mainnet, remember: base.remember, witnesses };
|
|
139
|
+
}
|
|
140
|
+
/** Why a node's status doesn't match a pin, or null if it does. Known chain ids must come with their known genesis. */
|
|
141
|
+
export function mismatch(pin, status) {
|
|
142
|
+
const chainId = typeof status.chain_id === "string" ? status.chain_id : undefined;
|
|
143
|
+
const genesis = typeof status.genesis_hash === "string" ? status.genesis_hash.toLowerCase() : undefined;
|
|
144
|
+
const prefix = typeof status.address_prefix === "string" ? status.address_prefix : undefined;
|
|
145
|
+
if (!chainId)
|
|
146
|
+
return "the node doesn't report a chain id";
|
|
147
|
+
if (pin.chainId && chainId !== pin.chainId)
|
|
148
|
+
return `the node is on chain "${chainId}", not "${pin.chainId}"`;
|
|
149
|
+
const wantGenesis = pin.genesisHash ?? KNOWN_GENESIS[chainId];
|
|
150
|
+
if (wantGenesis && genesis !== wantGenesis)
|
|
151
|
+
return `the node's "${chainId}" starts from genesis ${genesis ?? "(none reported)"}, not ${wantGenesis}`;
|
|
152
|
+
if (pin.prefix && prefix && prefix !== pin.prefix)
|
|
153
|
+
return `the node uses "${prefix}1…" addresses, not "${pin.prefix}1…"`;
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
/** The pin after a first status: every field the node reported that wasn't pinned yet. */
|
|
157
|
+
export function completePin(pin, status) {
|
|
158
|
+
return {
|
|
159
|
+
chainId: pin.chainId ?? (typeof status.chain_id === "string" ? status.chain_id : undefined),
|
|
160
|
+
genesisHash: pin.genesisHash ?? (typeof status.genesis_hash === "string" ? status.genesis_hash.toLowerCase() : undefined),
|
|
161
|
+
prefix: pin.prefix ?? (typeof status.address_prefix === "string" ? status.address_prefix : undefined),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/** Chains remembered on first use, by network and node URL (BLOCKULA_HOME/known-chains.json). */
|
|
165
|
+
export class KnownChains {
|
|
166
|
+
file;
|
|
167
|
+
constructor(file) {
|
|
168
|
+
this.file = file;
|
|
169
|
+
}
|
|
170
|
+
all() {
|
|
171
|
+
try {
|
|
172
|
+
return existsSync(this.file) ? JSON.parse(readFileSync(this.file, "utf8")) : {};
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
throw new Error(`${this.file} is corrupt; fix or delete it (it pins which chain each node must be on)`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
static id(net) {
|
|
179
|
+
return `${net.key} ${net.rpc}`;
|
|
180
|
+
}
|
|
181
|
+
get(net) {
|
|
182
|
+
return net.remember ? this.all()[KnownChains.id(net)] : undefined;
|
|
183
|
+
}
|
|
184
|
+
save(net, pin) {
|
|
185
|
+
if (!net.remember)
|
|
186
|
+
return;
|
|
187
|
+
this.write(KnownChains.id(net), pin);
|
|
188
|
+
}
|
|
189
|
+
/** Genesis clocks are a property of the chain (its id and genesis hash), whichever node reported them. */
|
|
190
|
+
static clockId(chainId, genesisHash) {
|
|
191
|
+
return `clock ${chainId} ${genesisHash}`;
|
|
192
|
+
}
|
|
193
|
+
/** The genesis clock remembered for this chain (`clockFrom` of the first `/api/genesis` read for it). */
|
|
194
|
+
clock(net, chainId, genesisHash) {
|
|
195
|
+
if (!net.remember)
|
|
196
|
+
return undefined;
|
|
197
|
+
const c = this.all()[KnownChains.clockId(chainId, genesisHash)];
|
|
198
|
+
return clockFrom(c && typeof c === "object" ? { ...c, chain_id: chainId } : undefined, chainId);
|
|
199
|
+
}
|
|
200
|
+
saveClock(net, chainId, genesisHash, clock) {
|
|
201
|
+
if (!net.remember)
|
|
202
|
+
return;
|
|
203
|
+
this.write(KnownChains.clockId(chainId, genesisHash), { created_at_ms: clock.createdAtMs, block_time_ms: clock.blockTimeMs });
|
|
204
|
+
}
|
|
205
|
+
write(key, value) {
|
|
206
|
+
const all = this.all();
|
|
207
|
+
all[key] = value;
|
|
208
|
+
mkdirSync(dirname(this.file), { recursive: true });
|
|
209
|
+
writeFileSync(this.file, JSON.stringify(all, null, 2), { mode: 0o600 });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* The pin to enforce for a network: its configured pin, filled from what was remembered for it.
|
|
214
|
+
* A remembered field never overrides a configured one; a contradiction means the configuration
|
|
215
|
+
* changed, and the configuration wins. What was remembered for another chain id (the network was
|
|
216
|
+
* relaunched under a new one, which this release pins) isn't used at all: the new chain is
|
|
217
|
+
* pinned on first use instead of refused for the old one's genesis.
|
|
218
|
+
*/
|
|
219
|
+
export function effectivePin(net, remembered) {
|
|
220
|
+
const r = remembered && (!net.pin.chainId || !remembered.chainId || remembered.chainId === net.pin.chainId) ? remembered : undefined;
|
|
221
|
+
return { chainId: net.pin.chainId ?? r?.chainId, genesisHash: net.pin.genesisHash ?? r?.genesisHash, prefix: net.pin.prefix ?? r?.prefix };
|
|
222
|
+
}
|
|
223
|
+
/** The error an agent sees when a node is on the wrong chain: what happened and what the user can do. */
|
|
224
|
+
export function mismatchError(net, why, knownChainsFile) {
|
|
225
|
+
const fix = net.remember
|
|
226
|
+
? ` If this network was relaunched on purpose, the user can remove its entry ("${KnownChains.id(net)}") from ${knownChainsFile}, or set BLOCKULA_GENESIS_HASH.`
|
|
227
|
+
: net.key === "local"
|
|
228
|
+
? " A reset dev chain starts from a new genesis: call start_dev_node to use the one running now."
|
|
229
|
+
: "";
|
|
230
|
+
return new Error(`Refusing ${net.rpc} (${net.label}): ${why}. Nothing was signed or sent.${fix}`);
|
|
231
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export declare const REPO = "jackdishman/blockula";
|
|
2
|
+
/**
|
|
3
|
+
* BLAKE3 of the release public key (docs/OPERATIONS.md "The release key"). Empty until the key
|
|
4
|
+
* ceremony fills it in, together with scripts/install.sh's and node/src/release_cli.rs's
|
|
5
|
+
* BUILTIN_KEY_ID (a test keeps the three equal); BLOCKULA_RELEASE_KEY_ID sets it meanwhile.
|
|
6
|
+
*/
|
|
7
|
+
export declare const RELEASE_KEY_ID = "";
|
|
8
|
+
/** The release key id pin `blockula release install` writes (release_cli.rs PIN_FILE), in $BLOCKULA_HOME. */
|
|
9
|
+
export declare const PIN_FILE = "release-key-id";
|
|
10
|
+
/** Where install_node puts the node binary, and where `blockula release install` and install.sh put theirs (never overwritten by install_node). */
|
|
11
|
+
export declare function binPaths(home: string): {
|
|
12
|
+
mcp: string;
|
|
13
|
+
cli: string;
|
|
14
|
+
};
|
|
15
|
+
/** A node binary the MCP could run, and what its `--version` printed (undefined: it didn't run). */
|
|
16
|
+
export interface NodeBinary {
|
|
17
|
+
path: string;
|
|
18
|
+
version?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The node binary start_dev_node and summon_chain run when BLOCKULA_BIN isn't set (audit
|
|
22
|
+
* review-r4-supply-3). `found` lists the installed ones, `blockula release install`'s first (it was
|
|
23
|
+
* checked against the pinned key), then install_node's. The first whose `--version` is this MCP's
|
|
24
|
+
* wins; else the first that runs; else the first; with none, `blockula` from PATH. `matches` says
|
|
25
|
+
* whether it's this MCP's version, so a stale binary is never run without saying so.
|
|
26
|
+
*/
|
|
27
|
+
export declare function pickNodeBinary(found: NodeBinary[], mcpVersion: string): NodeBinary & {
|
|
28
|
+
matches: boolean;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* The release key id pinned in `file` (`blockula release pin` / the first `release install`), or
|
|
32
|
+
* null if there's no pin. Same rules as release_cli.rs read_pin: one 64-hex key id (any case,
|
|
33
|
+
* surrounding whitespace ignored), never ignored when it's malformed, and refused when the group
|
|
34
|
+
* or others can write it (they could have swapped the key it vouches for).
|
|
35
|
+
*/
|
|
36
|
+
export declare function readReleasePin(file: string): string | null;
|
|
37
|
+
/** A release tag. Only these are installed: `v*` tags are admin-only (deploy/github/ruleset-tags.json). */
|
|
38
|
+
export declare const RELEASE_TAG: RegExp;
|
|
39
|
+
/** The first line of a release's checksums.txt, before the tag (scripts/release-checksums.sh). */
|
|
40
|
+
export declare const TAG_LINE_PREFIX = "# blockula-release ";
|
|
41
|
+
/**
|
|
42
|
+
* The release key id to trust: the one pinned on this machine (`blockula release install`'s pin,
|
|
43
|
+
* `pinned`, read from `pinFile` by readReleasePin), BLOCKULA_RELEASE_KEY_ID (from an independent
|
|
44
|
+
* channel) or the one built in. Every one that's set must be the same key: a mismatch means one of
|
|
45
|
+
* them isn't genuine (or the pin moved by a rotation this MCP predates: update the MCP).
|
|
46
|
+
*/
|
|
47
|
+
export declare function releaseKeyIdFrom(given: string | undefined, builtIn: string, pinned?: string | null, pinFile?: string): string;
|
|
48
|
+
/** The tag GitHub's /releases/latest redirected to (the final URL, …/releases/tag/<tag>). */
|
|
49
|
+
export declare function latestTagFrom(finalUrl: string): string;
|
|
50
|
+
/** Throws unless `blockula --version` printed release `tag`'s version. */
|
|
51
|
+
export declare function checkBinaryVersion(versionOutput: string, tag: string): void;
|
|
52
|
+
/** Throws unless checksums.txt names release `tag` on its first line. */
|
|
53
|
+
export declare function checkTagLine(sums: Buffer, tag: string): void;
|
|
54
|
+
export interface ReleaseDeps {
|
|
55
|
+
/** GET a release file; `null` if it doesn't exist (404). */
|
|
56
|
+
download(url: string): Promise<Buffer | null>;
|
|
57
|
+
/**
|
|
58
|
+
* Runs `gh attestation verify` on the archive's bytes, requiring provenance from `refs/tags/<tag>`:
|
|
59
|
+
* `"ok"`, `"failed"` (with the reason), or `"unavailable"` (no gh, or not logged in).
|
|
60
|
+
*/
|
|
61
|
+
attest(archive: Buffer, name: string, tag: string): Promise<{
|
|
62
|
+
result: "ok" | "failed" | "unavailable";
|
|
63
|
+
detail?: string;
|
|
64
|
+
}>;
|
|
65
|
+
}
|
|
66
|
+
export interface VerifiedRelease {
|
|
67
|
+
archive: Buffer;
|
|
68
|
+
sha256: string;
|
|
69
|
+
/** What proved the release genuine (empty: only the checksum). */
|
|
70
|
+
verified: string[];
|
|
71
|
+
/** Shown to the user as-is. */
|
|
72
|
+
warning?: string;
|
|
73
|
+
}
|
|
74
|
+
/** Downloads `asset` from `base` (release `tag`'s download URL) and checks it as described above. Throws instead of returning anything unverified that must not be installed. The caller still checks the binary's `--version` (checkBinaryVersion). */
|
|
75
|
+
export declare function fetchVerifiedRelease(base: string, asset: string, opts: {
|
|
76
|
+
releaseKeyId: string;
|
|
77
|
+
requireVerify: boolean;
|
|
78
|
+
tag: string;
|
|
79
|
+
}, deps: ReleaseDeps): Promise<VerifiedRelease>;
|
package/dist/release.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// install_node's checks on a release download, the same as scripts/install.sh (docs/OPERATIONS.md
|
|
2
|
+
// "Releases"):
|
|
3
|
+
// 0. The version (a vX.Y.Z tag; `latest` is resolved to its tag first) is bound to everything
|
|
4
|
+
// below: checksums.txt must name it on its first line ("# blockula-release vX.Y.Z", covered
|
|
5
|
+
// by the signature), provenance must come from its tag (--source-ref), and install_node checks
|
|
6
|
+
// the extracted binary's `--version` before it installs it. An older, genuinely signed
|
|
7
|
+
// release served as the one asked for is refused (audit supply-chain-B-2).
|
|
8
|
+
// 1. SHA-256 against the release's checksums.txt. Always. It only catches a corrupted download:
|
|
9
|
+
// whoever can change the release can change checksums.txt too.
|
|
10
|
+
// 2. SLSA build provenance with a logged-in GitHub CLI (`gh attestation verify`): the archive was
|
|
11
|
+
// built by this repo's release workflow. A failed check is fatal; no gh skips it.
|
|
12
|
+
// 3. The offline ML-DSA-65 signature on checksums.txt (checksums.txt.sig), verified with the
|
|
13
|
+
// SDK against the release key id. Once a key id is known (pinned in
|
|
14
|
+
// $BLOCKULA_HOME/release-key-id by `blockula release install`, built in, or
|
|
15
|
+
// BLOCKULA_RELEASE_KEY_ID; all that are set must agree), every release must carry a valid
|
|
16
|
+
// signature: a missing or invalid .sig is fatal. Leaving the .sig out is exactly what a
|
|
17
|
+
// compromised GitHub account or release workflow would do, and it can't forge the offline
|
|
18
|
+
// signature (audit F69). With no key id (before the key ceremony, nothing pinned) there's
|
|
19
|
+
// nothing to check it against, so it's skipped.
|
|
20
|
+
// install_node installs to a path of its own ($BLOCKULA_HOME/mcp-bin/blockula), never over the
|
|
21
|
+
// binary `blockula release install` puts in $BLOCKULA_HOME/bin (audit
|
|
22
|
+
// review-r3-integrate-compose-1).
|
|
23
|
+
// The checks add up (AND): provenance and the signature each refuse a release they fail. If
|
|
24
|
+
// neither ran, the result says so loudly. BLOCKULA_REQUIRE_VERIFY=1 refuses a release it can only
|
|
25
|
+
// checksum, and once a key id is known it requires provenance too (both checks).
|
|
26
|
+
import { createHash } from "node:crypto";
|
|
27
|
+
import { readFileSync, statSync } from "node:fs";
|
|
28
|
+
import { join } from "node:path";
|
|
29
|
+
import { verifyReleaseSignature } from "@blockula/sdk";
|
|
30
|
+
export const REPO = "jackdishman/blockula";
|
|
31
|
+
/**
|
|
32
|
+
* BLAKE3 of the release public key (docs/OPERATIONS.md "The release key"). Empty until the key
|
|
33
|
+
* ceremony fills it in, together with scripts/install.sh's and node/src/release_cli.rs's
|
|
34
|
+
* BUILTIN_KEY_ID (a test keeps the three equal); BLOCKULA_RELEASE_KEY_ID sets it meanwhile.
|
|
35
|
+
*/
|
|
36
|
+
export const RELEASE_KEY_ID = "";
|
|
37
|
+
/** The release key id pin `blockula release install` writes (release_cli.rs PIN_FILE), in $BLOCKULA_HOME. */
|
|
38
|
+
export const PIN_FILE = "release-key-id";
|
|
39
|
+
/** Where install_node puts the node binary, and where `blockula release install` and install.sh put theirs (never overwritten by install_node). */
|
|
40
|
+
export function binPaths(home) {
|
|
41
|
+
return { mcp: join(home, "mcp-bin", "blockula"), cli: join(home, "bin", "blockula") };
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The node binary start_dev_node and summon_chain run when BLOCKULA_BIN isn't set (audit
|
|
45
|
+
* review-r4-supply-3). `found` lists the installed ones, `blockula release install`'s first (it was
|
|
46
|
+
* checked against the pinned key), then install_node's. The first whose `--version` is this MCP's
|
|
47
|
+
* wins; else the first that runs; else the first; with none, `blockula` from PATH. `matches` says
|
|
48
|
+
* whether it's this MCP's version, so a stale binary is never run without saying so.
|
|
49
|
+
*/
|
|
50
|
+
export function pickNodeBinary(found, mcpVersion) {
|
|
51
|
+
const same = found.find((b) => b.version === `blockula ${mcpVersion}`);
|
|
52
|
+
if (same)
|
|
53
|
+
return { ...same, matches: true };
|
|
54
|
+
return { ...(found.find((b) => b.version !== undefined) ?? found[0] ?? { path: "blockula" }), matches: false };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The release key id pinned in `file` (`blockula release pin` / the first `release install`), or
|
|
58
|
+
* null if there's no pin. Same rules as release_cli.rs read_pin: one 64-hex key id (any case,
|
|
59
|
+
* surrounding whitespace ignored), never ignored when it's malformed, and refused when the group
|
|
60
|
+
* or others can write it (they could have swapped the key it vouches for).
|
|
61
|
+
*/
|
|
62
|
+
export function readReleasePin(file) {
|
|
63
|
+
let text;
|
|
64
|
+
try {
|
|
65
|
+
text = readFileSync(file, "utf8");
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
if (e.code === "ENOENT")
|
|
69
|
+
return null;
|
|
70
|
+
throw new Error(`reading ${file}: ${e.message}`);
|
|
71
|
+
}
|
|
72
|
+
if (process.platform !== "win32") {
|
|
73
|
+
const mode = statSync(file).mode;
|
|
74
|
+
if (mode & 0o022)
|
|
75
|
+
throw new Error(`${file} is writable by others (mode ${(mode & 0o777).toString(8)}), so it can't vouch for a release key: chmod 644 it once you've checked it holds your key id; not installing`);
|
|
76
|
+
}
|
|
77
|
+
const k = text.trim().toLowerCase();
|
|
78
|
+
if (!/^[0-9a-f]{64}$/.test(k))
|
|
79
|
+
throw new Error(`${file} isn't a release key id pin: fix it or remove it; not installing`);
|
|
80
|
+
return k;
|
|
81
|
+
}
|
|
82
|
+
/** A release tag. Only these are installed: `v*` tags are admin-only (deploy/github/ruleset-tags.json). */
|
|
83
|
+
export const RELEASE_TAG = /^v\d+\.\d+\.\d+$/;
|
|
84
|
+
/** The first line of a release's checksums.txt, before the tag (scripts/release-checksums.sh). */
|
|
85
|
+
export const TAG_LINE_PREFIX = "# blockula-release ";
|
|
86
|
+
/**
|
|
87
|
+
* The release key id to trust: the one pinned on this machine (`blockula release install`'s pin,
|
|
88
|
+
* `pinned`, read from `pinFile` by readReleasePin), BLOCKULA_RELEASE_KEY_ID (from an independent
|
|
89
|
+
* channel) or the one built in. Every one that's set must be the same key: a mismatch means one of
|
|
90
|
+
* them isn't genuine (or the pin moved by a rotation this MCP predates: update the MCP).
|
|
91
|
+
*/
|
|
92
|
+
export function releaseKeyIdFrom(given, builtIn, pinned = null, pinFile = PIN_FILE) {
|
|
93
|
+
const norm = (k, what) => {
|
|
94
|
+
const v = (k ?? "").trim().toLowerCase();
|
|
95
|
+
if (v && !/^[0-9a-f]{64}$/.test(v))
|
|
96
|
+
throw new Error(`${what} must be 64 hex characters`);
|
|
97
|
+
return v;
|
|
98
|
+
};
|
|
99
|
+
const g = norm(given, "BLOCKULA_RELEASE_KEY_ID");
|
|
100
|
+
const b = norm(builtIn, "the built-in release key id");
|
|
101
|
+
const p = norm(pinned, `the release key id pinned in ${pinFile}`);
|
|
102
|
+
if (p && g && g !== p)
|
|
103
|
+
throw new Error(`the release key id pinned in ${pinFile} is ${p}, not BLOCKULA_RELEASE_KEY_ID (${g}); not installing. A new release key is accepted only through a rotation statement the pinned key signed (blockula release pin --key-id <new> --rotation …, docs/OPERATIONS.md "The release key").`);
|
|
104
|
+
if (p && b && b !== p)
|
|
105
|
+
throw new Error(`this MCP was built with release key ${b}, but ${pinFile} pins ${p}; not installing. One of them isn't genuine, or the key was rotated after this MCP was released: check the pin (blockula release pin) against SECURITY.md and your operators' channel, and update the MCP.`);
|
|
106
|
+
if (g && b && g !== b)
|
|
107
|
+
throw new Error(`this MCP trusts release key ${b}, which isn't BLOCKULA_RELEASE_KEY_ID (${g}); not installing. Check both against SECURITY.md and the website.`);
|
|
108
|
+
return p || g || b;
|
|
109
|
+
}
|
|
110
|
+
/** The tag GitHub's /releases/latest redirected to (the final URL, …/releases/tag/<tag>). */
|
|
111
|
+
export function latestTagFrom(finalUrl) {
|
|
112
|
+
const m = /\/releases\/tag\/([^/?#]+)$/.exec(finalUrl);
|
|
113
|
+
const tag = m ? decodeURIComponent(m[1]) : "";
|
|
114
|
+
if (!RELEASE_TAG.test(tag))
|
|
115
|
+
throw new Error(`the latest release (${finalUrl}) isn't a release tag (vX.Y.Z); pass a version`);
|
|
116
|
+
return tag;
|
|
117
|
+
}
|
|
118
|
+
/** Throws unless `blockula --version` printed release `tag`'s version. */
|
|
119
|
+
export function checkBinaryVersion(versionOutput, tag) {
|
|
120
|
+
const got = versionOutput.trim();
|
|
121
|
+
if (got !== `blockula ${tag.slice(1)}`)
|
|
122
|
+
throw new Error(`the downloaded binary is ${got || "unrunnable here"}, not ${tag.slice(1)}; not installing`);
|
|
123
|
+
}
|
|
124
|
+
/** Throws unless checksums.txt names release `tag` on its first line. */
|
|
125
|
+
export function checkTagLine(sums, tag) {
|
|
126
|
+
const first = sums.toString("utf8").split("\n", 1)[0];
|
|
127
|
+
if (first === TAG_LINE_PREFIX + tag)
|
|
128
|
+
return;
|
|
129
|
+
if (first.startsWith(TAG_LINE_PREFIX))
|
|
130
|
+
throw new Error(`this checksums.txt is for ${first.slice(TAG_LINE_PREFIX.length)}, not ${tag}: an older or other release served as the one asked for; not installing`);
|
|
131
|
+
throw new Error(`this checksums.txt names no release (releases before v0.5.0 don't), so it can't be bound to ${tag}; not installing`);
|
|
132
|
+
}
|
|
133
|
+
const sha256 = (b) => createHash("sha256").update(b).digest("hex");
|
|
134
|
+
/** Downloads `asset` from `base` (release `tag`'s download URL) and checks it as described above. Throws instead of returning anything unverified that must not be installed. The caller still checks the binary's `--version` (checkBinaryVersion). */
|
|
135
|
+
export async function fetchVerifiedRelease(base, asset, opts, deps) {
|
|
136
|
+
if (!RELEASE_TAG.test(opts.tag))
|
|
137
|
+
throw new Error(`a release is a vX.Y.Z tag, got ${opts.tag}`);
|
|
138
|
+
const [archive, sums] = await Promise.all([deps.download(`${base}/${asset}`), deps.download(`${base}/checksums.txt`)]);
|
|
139
|
+
if (!archive)
|
|
140
|
+
throw new Error(`${asset} isn't in this release`);
|
|
141
|
+
if (!sums)
|
|
142
|
+
throw new Error("this release has no checksums.txt; not installing");
|
|
143
|
+
// 0. The release checksums.txt names (signed with it, below).
|
|
144
|
+
checkTagLine(sums, opts.tag);
|
|
145
|
+
// 1. Checksum.
|
|
146
|
+
const expected = sums.toString("utf8").split("\n").map((l) => l.trim().split(/\s+/)).find(([, f]) => f === asset)?.[0];
|
|
147
|
+
const actual = sha256(archive);
|
|
148
|
+
if (!expected || expected !== actual)
|
|
149
|
+
throw new Error(`checksum mismatch for ${asset}; not installing`);
|
|
150
|
+
const verified = [];
|
|
151
|
+
// 2. Build provenance.
|
|
152
|
+
const att = await deps.attest(archive, asset, opts.tag);
|
|
153
|
+
if (att.result === "failed")
|
|
154
|
+
throw new Error(`build provenance check FAILED for ${asset} (not built by the release workflow from tag ${opts.tag}); not installing${att.detail ? ` (${att.detail.trim().split("\n")[0]})` : ""}. Releases up to v0.4.x predate provenance and have no attestation.`);
|
|
155
|
+
if (att.result === "ok")
|
|
156
|
+
verified.push(`build provenance (GitHub attestation, ${REPO} release workflow, tag ${opts.tag})`);
|
|
157
|
+
// 3. Release signature, against the pinned key id (never one taken from the download). Required
|
|
158
|
+
// once a key id is known.
|
|
159
|
+
if (opts.releaseKeyId) {
|
|
160
|
+
const sigFile = await deps.download(`${base}/checksums.txt.sig`);
|
|
161
|
+
if (!sigFile) {
|
|
162
|
+
throw new Error(`this release has no checksums.txt.sig, but releases are signed offline with the release key ${opts.releaseKeyId.slice(0, 16)}…; not installing. An unsigned release is what a compromised GitHub account or release workflow would publish.`);
|
|
163
|
+
}
|
|
164
|
+
let sig;
|
|
165
|
+
try {
|
|
166
|
+
sig = JSON.parse(sigFile.toString("utf8"));
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
throw new Error("checksums.txt.sig isn't valid JSON; not installing");
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
verifyReleaseSignature(sig, new Uint8Array(sums), opts.releaseKeyId);
|
|
173
|
+
}
|
|
174
|
+
catch (e) {
|
|
175
|
+
throw new Error(`checksums.txt signature check FAILED: ${e.message}; not installing`);
|
|
176
|
+
}
|
|
177
|
+
verified.push(`release signature (ML-DSA-65 key ${opts.releaseKeyId.slice(0, 16)}…)`);
|
|
178
|
+
if (opts.requireVerify && att.result !== "ok") {
|
|
179
|
+
throw new Error("BLOCKULA_REQUIRE_VERIFY=1 requires build provenance as well as the release signature, and provenance couldn't be checked (log in to the GitHub CLI with `gh auth login`); not installing");
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (verified.length)
|
|
183
|
+
return { archive, sha256: actual, verified };
|
|
184
|
+
if (opts.requireVerify)
|
|
185
|
+
throw new Error("BLOCKULA_REQUIRE_VERIFY=1 and only the checksum could be verified (log in to the GitHub CLI with `gh auth login`, or set the release key id); not installing");
|
|
186
|
+
return {
|
|
187
|
+
archive,
|
|
188
|
+
sha256: actual,
|
|
189
|
+
verified,
|
|
190
|
+
warning: "ONLY THE CHECKSUM WAS VERIFIED. That proves the download wasn't corrupted, not that this release is genuine: anyone who can change the release can change checksums.txt too. " +
|
|
191
|
+
"To verify it, log in to the GitHub CLI (gh auth login) and install again, or check it by hand (docs/OPERATIONS.md \"Releases\"). Set BLOCKULA_REQUIRE_VERIFY=1 to refuse unverified installs. Don't run a validator on an unverified binary.",
|
|
192
|
+
};
|
|
193
|
+
}
|
package/dist/signed.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** What tx_status needs about a tx this MCP signed. */
|
|
2
|
+
export interface SignedTx {
|
|
3
|
+
/** The last height it can execute at, as signed. */
|
|
4
|
+
valid_until: number;
|
|
5
|
+
/** The account it acts from, and its nonce: at most one tx per (account, nonce) executes. */
|
|
6
|
+
account: string;
|
|
7
|
+
nonce: number;
|
|
8
|
+
/** The chain it was signed for (the tx's chain_id and genesis_hash). Records written before these were kept have neither. */
|
|
9
|
+
chain_id?: string;
|
|
10
|
+
genesis_hash?: string;
|
|
11
|
+
}
|
|
12
|
+
/** How many signed txs are kept (oldest dropped first). */
|
|
13
|
+
export declare const MAX_SIGNED = 500;
|
|
14
|
+
/** Whether a record was signed for this chain: a record that doesn't name its chain never is. */
|
|
15
|
+
export declare function onChain(tx: SignedTx, chain: {
|
|
16
|
+
chainId?: string;
|
|
17
|
+
genesisHash?: string;
|
|
18
|
+
}): boolean;
|
|
19
|
+
export declare class SignedTxs {
|
|
20
|
+
readonly file: string;
|
|
21
|
+
readonly max: number;
|
|
22
|
+
constructor(file: string, max?: number);
|
|
23
|
+
private read;
|
|
24
|
+
private static copy;
|
|
25
|
+
/** The record of a tx this MCP signed, if it's still kept. */
|
|
26
|
+
get(id: string): SignedTx | undefined;
|
|
27
|
+
/** The other txs this MCP signed for the same chain with the same (account, nonce), oldest first: at most one of them all executes. */
|
|
28
|
+
sameSlot(mine: SignedTx, except: string): string[];
|
|
29
|
+
/** Records a tx about to be submitted. Written whole (temp file, then rename). */
|
|
30
|
+
add(id: string, tx: SignedTx): void;
|
|
31
|
+
}
|