@forestrie/forestrie-cli 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +425 -0
- package/TRUST-MODEL.md +8 -0
- package/dist/cli.js +33811 -0
- package/dist/decode-receipt.d.ts +19 -0
- package/dist/decode-receipt.d.ts.map +1 -0
- package/dist/decode-receipt.js +19 -0
- package/dist/decode-receipt.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/decode-receipt-cbor.d.ts +26 -0
- package/dist/lib/decode-receipt-cbor.d.ts.map +1 -0
- package/dist/lib/decode-receipt-cbor.js +182 -0
- package/dist/lib/decode-receipt-cbor.js.map +1 -0
- package/dist/lib/decode-receipt-decode.d.ts +103 -0
- package/dist/lib/decode-receipt-decode.d.ts.map +1 -0
- package/dist/lib/decode-receipt-decode.js +237 -0
- package/dist/lib/decode-receipt-decode.js.map +1 -0
- package/dist/lib/decode-receipt-labels.d.ts +54 -0
- package/dist/lib/decode-receipt-labels.d.ts.map +1 -0
- package/dist/lib/decode-receipt-labels.js +117 -0
- package/dist/lib/decode-receipt-labels.js.map +1 -0
- package/dist/lib/decode-receipt-render.d.ts +11 -0
- package/dist/lib/decode-receipt-render.d.ts.map +1 -0
- package/dist/lib/decode-receipt-render.js +242 -0
- package/dist/lib/decode-receipt-render.js.map +1 -0
- package/package.json +83 -0
- package/src/cli.ts +5 -0
- package/src/command.ts +51 -0
- package/src/commands/admin-onboard-token.ts +51 -0
- package/src/commands/admin.ts +20 -0
- package/src/commands/complete-grant.ts +41 -0
- package/src/commands/create-consistency-proof.ts +44 -0
- package/src/commands/create-log.ts +95 -0
- package/src/commands/decode-receipt.ts +20 -0
- package/src/commands/delegate.ts +55 -0
- package/src/commands/deploy.ts +72 -0
- package/src/commands/fetch-accumulator.ts +49 -0
- package/src/commands/onboard-genesis.ts +70 -0
- package/src/commands/onboard-request.ts +59 -0
- package/src/commands/register-grant.ts +74 -0
- package/src/commands/register.ts +54 -0
- package/src/commands/resolve-receipt.ts +101 -0
- package/src/commands/sign-statement.ts +53 -0
- package/src/commands/verify-grant.ts +89 -0
- package/src/commands/verify.ts +92 -0
- package/src/commoncli.ts +34 -0
- package/src/decode-receipt.ts +56 -0
- package/src/index.ts +10 -0
- package/src/lib/bytes.ts +7 -0
- package/src/lib/checkpoint-provider.ts +297 -0
- package/src/lib/complete-grant-derive.ts +237 -0
- package/src/lib/consistency-proof.ts +271 -0
- package/src/lib/create-log-build.ts +168 -0
- package/src/lib/create-log-prepare.ts +75 -0
- package/src/lib/create-receipt-chain-verify.ts +267 -0
- package/src/lib/create-receipt-derive.ts +208 -0
- package/src/lib/create-receipt-inputs.ts +79 -0
- package/src/lib/decode-checkpoint-calldata.ts +188 -0
- package/src/lib/decode-receipt-cbor.ts +202 -0
- package/src/lib/decode-receipt-decode.ts +333 -0
- package/src/lib/decode-receipt-labels.ts +132 -0
- package/src/lib/decode-receipt-render.ts +284 -0
- package/src/lib/delegate-flow.ts +289 -0
- package/src/lib/delegate-voucher.ts +122 -0
- package/src/lib/deploy-artifact.ts +79 -0
- package/src/lib/deploy-bootstrap.ts +100 -0
- package/src/lib/deploy-flow.ts +184 -0
- package/src/lib/deploy-genesis-log-id.ts +29 -0
- package/src/lib/deploy-rpc.ts +40 -0
- package/src/lib/deployment-record.ts +65 -0
- package/src/lib/fsio.ts +91 -0
- package/src/lib/grant-flags-local.ts +19 -0
- package/src/lib/onboard-attestation.ts +120 -0
- package/src/lib/onboard-genesis-body.ts +94 -0
- package/src/lib/openssl-error-queue.ts +56 -0
- package/src/lib/register-flow.ts +292 -0
- package/src/lib/register-grant-build.ts +173 -0
- package/src/lib/register-grant-complete.ts +47 -0
- package/src/lib/register-grant-flow.ts +258 -0
- package/src/lib/resolve-receipt-freshen.ts +221 -0
- package/src/lib/sign-statement-build.ts +117 -0
- package/src/lib/sign-statement-key.ts +145 -0
- package/src/lib/verify-anchored.ts +326 -0
- package/src/lib/verify-checkpoint-chain.ts +160 -0
- package/src/lib/verify-eventscan.ts +214 -0
- package/src/lib/verify-inputs.ts +143 -0
- package/src/lib/verify-known-accumulator.ts +161 -0
- package/src/lib/verify-report.ts +265 -0
- package/src/lib/x402-payment.ts +115 -0
- package/src/main/admin-onboard-token.ts +216 -0
- package/src/main/complete-grant.ts +265 -0
- package/src/main/create-consistency-proof.ts +83 -0
- package/src/main/create-log.ts +347 -0
- package/src/main/create-receipt.ts +763 -0
- package/src/main/decode-receipt.ts +72 -0
- package/src/main/delegate.ts +141 -0
- package/src/main/deploy.ts +273 -0
- package/src/main/fetch-accumulator.ts +181 -0
- package/src/main/not-implemented.ts +43 -0
- package/src/main/onboard-genesis.ts +192 -0
- package/src/main/onboard-request.ts +333 -0
- package/src/main/register-grant.ts +270 -0
- package/src/main/register.ts +243 -0
- package/src/main/sign-statement.ts +168 -0
- package/src/main/verify.ts +767 -0
- package/src/options/admin-onboard-token.ts +48 -0
- package/src/options/common.ts +69 -0
- package/src/options/complete-grant.ts +40 -0
- package/src/options/create-consistency-proof.ts +63 -0
- package/src/options/create-log.ts +99 -0
- package/src/options/create-receipt.ts +243 -0
- package/src/options/decode-receipt.ts +25 -0
- package/src/options/delegate.ts +93 -0
- package/src/options/deploy.ts +105 -0
- package/src/options/fetch-accumulator.ts +62 -0
- package/src/options/onboard-genesis.ts +74 -0
- package/src/options/onboard-request.ts +57 -0
- package/src/options/register-grant.ts +72 -0
- package/src/options/register.ts +71 -0
- package/src/options/sign-statement.ts +64 -0
- package/src/options/verify.ts +250 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
import type { Out } from "@forestrie/cli-kit/reporting";
|
|
4
|
+
import type { DecodeReceiptOptions } from "../options/decode-receipt.js";
|
|
5
|
+
import {
|
|
6
|
+
DecodeReceiptError,
|
|
7
|
+
decodeReceipt,
|
|
8
|
+
type DecodedReceipt,
|
|
9
|
+
} from "../lib/decode-receipt-decode.js";
|
|
10
|
+
import { renderReceipt } from "../lib/decode-receipt-render.js";
|
|
11
|
+
|
|
12
|
+
/** `--json` decode failure shape (stage names the parse step that failed). */
|
|
13
|
+
export type DecodeReceiptFailure = {
|
|
14
|
+
error: "decode_failed";
|
|
15
|
+
command: "decode-receipt";
|
|
16
|
+
stage: string;
|
|
17
|
+
message: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function readReceiptBytes(options: DecodeReceiptOptions): Uint8Array {
|
|
21
|
+
const source = options.receipt ?? "-";
|
|
22
|
+
try {
|
|
23
|
+
// fd 0: read stdin when the positional is omitted or `-`.
|
|
24
|
+
const buffer = source === "-" ? readFileSync(0) : readFileSync(source);
|
|
25
|
+
return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
28
|
+
throw new DecodeReceiptError(
|
|
29
|
+
"input",
|
|
30
|
+
source === "-"
|
|
31
|
+
? `cannot read receipt from stdin: ${detail}`
|
|
32
|
+
: `cannot read receipt file ${source}: ${detail}`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* FOR-346: decode a COSE receipt so the audience sees it is just COSE
|
|
39
|
+
* (Sign1 + MMR inclusion). Display only — no signature verification
|
|
40
|
+
* (that is `forestrie verify`, FOR-347).
|
|
41
|
+
*/
|
|
42
|
+
export async function runDecodeReceipt(
|
|
43
|
+
out: Out,
|
|
44
|
+
options: DecodeReceiptOptions,
|
|
45
|
+
): Promise<void> {
|
|
46
|
+
let decoded: DecodedReceipt;
|
|
47
|
+
try {
|
|
48
|
+
decoded = decodeReceipt(readReceiptBytes(options));
|
|
49
|
+
} catch (error) {
|
|
50
|
+
const stage = error instanceof DecodeReceiptError ? error.stage : "decode";
|
|
51
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
52
|
+
if (options.json) {
|
|
53
|
+
const failure: DecodeReceiptFailure = {
|
|
54
|
+
error: "decode_failed",
|
|
55
|
+
command: "decode-receipt",
|
|
56
|
+
stage,
|
|
57
|
+
message,
|
|
58
|
+
};
|
|
59
|
+
out.out(JSON.stringify(failure, null, 2));
|
|
60
|
+
} else {
|
|
61
|
+
out.warn("forestrie decode-receipt: %s: %s", stage, message);
|
|
62
|
+
}
|
|
63
|
+
process.exitCode = 1;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (options.json) {
|
|
68
|
+
out.out(JSON.stringify(decoded, null, 2));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
out.out(renderReceipt(decoded));
|
|
72
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { Out } from "@forestrie/cli-kit/reporting";
|
|
2
|
+
import { readTextFile, writeOutputFile } from "../lib/fsio.js";
|
|
3
|
+
import type { DelegateOptions } from "../options/delegate.js";
|
|
4
|
+
import {
|
|
5
|
+
DelegateFlowError,
|
|
6
|
+
runDelegateFlow,
|
|
7
|
+
type DelegateFlowDeps,
|
|
8
|
+
type DelegateFlowResult,
|
|
9
|
+
} from "../lib/delegate-flow.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* FOR-390 / ADR-0052: authorize a custodian-vouched sealer to publish
|
|
13
|
+
* checkpoints for a log the caller owns (K(L)). Reads the ES256 root PEM, runs
|
|
14
|
+
* the public delegation flow (`delegate-flow.ts`) — which verifies the
|
|
15
|
+
* registrar voucher against the caller-known registrar key before binding —
|
|
16
|
+
* and reports the bound lease. Public coordinator endpoints only.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** `--json` success shape. */
|
|
20
|
+
export type DelegateReport = {
|
|
21
|
+
command: "delegate";
|
|
22
|
+
status: "submitted";
|
|
23
|
+
logId: string;
|
|
24
|
+
sealerId: string;
|
|
25
|
+
epoch: number | string;
|
|
26
|
+
mmrStart: number;
|
|
27
|
+
mmrEnd: number;
|
|
28
|
+
expiresAt: number;
|
|
29
|
+
/** Present when `--out-b64` was given. */
|
|
30
|
+
outB64?: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** `--json` failure shape. */
|
|
34
|
+
export type DelegateErrorReport = {
|
|
35
|
+
command: "delegate";
|
|
36
|
+
error: "key_read_failed" | "delegation_failed";
|
|
37
|
+
message: string;
|
|
38
|
+
httpStatus?: number;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** Test seam: effects injected by the delegate tests (real by default). */
|
|
42
|
+
export type DelegateRunDeps = DelegateFlowDeps;
|
|
43
|
+
|
|
44
|
+
async function readPemFile(path: string, flag: string): Promise<string> {
|
|
45
|
+
return readTextFile(path, `${flag} key file not found: ${path}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function reportError(
|
|
49
|
+
out: Out,
|
|
50
|
+
options: DelegateOptions,
|
|
51
|
+
report: DelegateErrorReport,
|
|
52
|
+
): void {
|
|
53
|
+
if (options.json) {
|
|
54
|
+
out.out(JSON.stringify(report, null, 2));
|
|
55
|
+
} else {
|
|
56
|
+
out.warn("forestrie delegate: %s", report.message);
|
|
57
|
+
}
|
|
58
|
+
process.exitCode = 1;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function reportResult(
|
|
62
|
+
out: Out,
|
|
63
|
+
options: DelegateOptions,
|
|
64
|
+
result: DelegateFlowResult,
|
|
65
|
+
): Promise<void> {
|
|
66
|
+
if (options.outB64 !== undefined) {
|
|
67
|
+
await writeOutputFile(
|
|
68
|
+
options.outB64,
|
|
69
|
+
Buffer.from(result.certificate).toString("base64"),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
if (options.json) {
|
|
73
|
+
const report: DelegateReport = {
|
|
74
|
+
command: "delegate",
|
|
75
|
+
status: "submitted",
|
|
76
|
+
logId: options.logId,
|
|
77
|
+
sealerId: result.sealerId,
|
|
78
|
+
epoch: result.epoch,
|
|
79
|
+
mmrStart: result.mmrStart,
|
|
80
|
+
mmrEnd: result.mmrEnd,
|
|
81
|
+
expiresAt: result.expiresAt,
|
|
82
|
+
};
|
|
83
|
+
if (options.outB64 !== undefined) report.outB64 = options.outB64;
|
|
84
|
+
out.out(JSON.stringify(report, null, 2));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
out.print("standing: sealerId %s epoch %s", result.sealerId, String(result.epoch));
|
|
88
|
+
out.print("voucher: ok — verifies against the known sealer-voucher key");
|
|
89
|
+
out.print("horizon: mmr %d..%d", result.mmrStart, result.mmrEnd);
|
|
90
|
+
out.print("submit: ok");
|
|
91
|
+
if (options.outB64 !== undefined) {
|
|
92
|
+
out.print("wrote certificate (base64) to %s", options.outB64);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function runDelegate(
|
|
97
|
+
out: Out,
|
|
98
|
+
options: DelegateOptions,
|
|
99
|
+
deps: DelegateRunDeps = {},
|
|
100
|
+
): Promise<void> {
|
|
101
|
+
// 1. Root key material (K(L)).
|
|
102
|
+
let rootPem: string;
|
|
103
|
+
try {
|
|
104
|
+
rootPem = await readPemFile(options.signWith, "--sign-with");
|
|
105
|
+
} catch (err) {
|
|
106
|
+
reportError(out, options, {
|
|
107
|
+
command: "delegate",
|
|
108
|
+
error: "key_read_failed",
|
|
109
|
+
message: err instanceof Error ? err.message : String(err),
|
|
110
|
+
});
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 2. Run the public delegation flow (voucher verify is mandatory inside).
|
|
115
|
+
try {
|
|
116
|
+
const result = await runDelegateFlow(
|
|
117
|
+
{
|
|
118
|
+
coordinatorUrl: options.coordinatorUrl,
|
|
119
|
+
logId: options.logId,
|
|
120
|
+
rootPem,
|
|
121
|
+
knownSealerKey: options.knownSealerKey,
|
|
122
|
+
horizonMmrEnd: options.horizonMmrEnd,
|
|
123
|
+
ttlSeconds: options.ttlSeconds,
|
|
124
|
+
},
|
|
125
|
+
deps,
|
|
126
|
+
);
|
|
127
|
+
await reportResult(out, options, result);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
if (err instanceof DelegateFlowError) {
|
|
130
|
+
const report: DelegateErrorReport = {
|
|
131
|
+
command: "delegate",
|
|
132
|
+
error: err.code ?? "delegation_failed",
|
|
133
|
+
message: err.message,
|
|
134
|
+
};
|
|
135
|
+
if (err.httpStatus !== undefined) report.httpStatus = err.httpStatus;
|
|
136
|
+
reportError(out, options, report);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
throw err;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import type { Out } from "@forestrie/cli-kit/reporting";
|
|
2
|
+
import { writeOutputFile } from "../lib/fsio.js";
|
|
3
|
+
import { getAddress, type Hex } from "viem";
|
|
4
|
+
import type { BootstrapAlg, DeployOptions } from "../options/deploy.js";
|
|
5
|
+
import {
|
|
6
|
+
loadImutableArtifact,
|
|
7
|
+
type ImutableDeployArtifact,
|
|
8
|
+
} from "../lib/deploy-artifact.js";
|
|
9
|
+
import {
|
|
10
|
+
resolveDeployBootstrapKey,
|
|
11
|
+
type ResolvedDeployBootstrap,
|
|
12
|
+
} from "../lib/deploy-bootstrap.js";
|
|
13
|
+
import {
|
|
14
|
+
DeployFlowError,
|
|
15
|
+
runDeployFlow,
|
|
16
|
+
type DeployFlowResult,
|
|
17
|
+
type DeployRpc,
|
|
18
|
+
} from "../lib/deploy-flow.js";
|
|
19
|
+
import { createDeployRpc } from "../lib/deploy-rpc.js";
|
|
20
|
+
import { errorMessage } from "../lib/sign-statement-key.js";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* FOR-341..347 sibling — FOR-340: deploy a univocity instance.
|
|
24
|
+
*
|
|
25
|
+
* ES256 paved path (forestrie-demo-01.md §R1): resolve/generate the
|
|
26
|
+
* ES256 bootstrap key, fetch + verify the univocity release manifest,
|
|
27
|
+
* build the ImutableUnivocity creation tx via `@forestrie/deploy-core`,
|
|
28
|
+
* submit it directly from the funded deployer EOA, and write the
|
|
29
|
+
* deployment record. KS256 (Safe / ERC-1271) is a pre-provisioned aside
|
|
30
|
+
* — no live Safe flow is implemented here.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Deployment record (`--out` / stdout). The demo contract is the first
|
|
35
|
+
* four fields — `jq -r .imutableUnivocity` / `.genesisLogId` must keep
|
|
36
|
+
* working; the rest is provenance.
|
|
37
|
+
*/
|
|
38
|
+
export type DeploymentRecord = {
|
|
39
|
+
kind: "imutable-deployment";
|
|
40
|
+
version: 1;
|
|
41
|
+
imutableUnivocity: string;
|
|
42
|
+
genesisLogId: string;
|
|
43
|
+
bootstrapAlg: BootstrapAlg;
|
|
44
|
+
chainId: number;
|
|
45
|
+
from: string;
|
|
46
|
+
txHash: string;
|
|
47
|
+
releaseId: string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** `--json` success shape. */
|
|
51
|
+
export type DeployReport = DeploymentRecord & {
|
|
52
|
+
command: "deploy";
|
|
53
|
+
status: "deployed";
|
|
54
|
+
/** Present when the bootstrap key was generated. */
|
|
55
|
+
bootstrapPemOut?: string;
|
|
56
|
+
/** Present when `--out` was given. */
|
|
57
|
+
out?: string;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** `--json` failure shape. */
|
|
61
|
+
export type DeployErrorReport = {
|
|
62
|
+
error:
|
|
63
|
+
| "bootstrap_alg_unsupported"
|
|
64
|
+
| "bootstrap_key_failed"
|
|
65
|
+
| "manifest_failed"
|
|
66
|
+
| "owner_mismatch"
|
|
67
|
+
| "network_failed"
|
|
68
|
+
| "deploy_failed";
|
|
69
|
+
command: "deploy";
|
|
70
|
+
message: string;
|
|
71
|
+
detail?: string;
|
|
72
|
+
txHash?: string;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** Test seam: effects injected by the deploy tests (real by default). */
|
|
76
|
+
export type DeployRunDeps = {
|
|
77
|
+
resolveBootstrap?: typeof resolveDeployBootstrapKey;
|
|
78
|
+
loadArtifact?: typeof loadImutableArtifact;
|
|
79
|
+
createRpc?: (rpcUrl: string, deployerKey: Hex) => DeployRpc;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const FLOW_ERROR_CODES = {
|
|
83
|
+
network: "network_failed",
|
|
84
|
+
deploy: "deploy_failed",
|
|
85
|
+
} as const;
|
|
86
|
+
|
|
87
|
+
function reportError(
|
|
88
|
+
out: Out,
|
|
89
|
+
options: DeployOptions,
|
|
90
|
+
report: DeployErrorReport,
|
|
91
|
+
): void {
|
|
92
|
+
if (options.json) {
|
|
93
|
+
out.out(JSON.stringify(report, null, 2));
|
|
94
|
+
} else {
|
|
95
|
+
out.warn("forestrie deploy: %s", report.message);
|
|
96
|
+
if (report.detail !== undefined && !report.message.includes(report.detail)) {
|
|
97
|
+
out.warn(" detail: %s", report.detail);
|
|
98
|
+
}
|
|
99
|
+
if (report.txHash !== undefined) {
|
|
100
|
+
out.warn(" txHash: %s", report.txHash);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
process.exitCode = 1;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function deploymentRecord(
|
|
107
|
+
options: DeployOptions,
|
|
108
|
+
artifact: ImutableDeployArtifact,
|
|
109
|
+
result: DeployFlowResult,
|
|
110
|
+
): DeploymentRecord {
|
|
111
|
+
return {
|
|
112
|
+
kind: "imutable-deployment",
|
|
113
|
+
version: 1,
|
|
114
|
+
imutableUnivocity: result.imutableUnivocity,
|
|
115
|
+
genesisLogId: result.genesisLogId,
|
|
116
|
+
bootstrapAlg: options.bootstrapAlg,
|
|
117
|
+
chainId: result.chainId,
|
|
118
|
+
from: result.from,
|
|
119
|
+
txHash: result.txHash,
|
|
120
|
+
releaseId: artifact.releaseId,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function reportDeployed(
|
|
125
|
+
out: Out,
|
|
126
|
+
options: DeployOptions,
|
|
127
|
+
bootstrap: ResolvedDeployBootstrap,
|
|
128
|
+
record: DeploymentRecord,
|
|
129
|
+
): Promise<void> {
|
|
130
|
+
const json = JSON.stringify(record, null, 2);
|
|
131
|
+
if (options.out !== undefined) {
|
|
132
|
+
await writeOutputFile(options.out, `${json}\n`);
|
|
133
|
+
}
|
|
134
|
+
if (options.json) {
|
|
135
|
+
const report: DeployReport = {
|
|
136
|
+
command: "deploy",
|
|
137
|
+
status: "deployed",
|
|
138
|
+
...record,
|
|
139
|
+
};
|
|
140
|
+
if (bootstrap.pemOut !== undefined) report.bootstrapPemOut = bootstrap.pemOut;
|
|
141
|
+
if (options.out !== undefined) report.out = options.out;
|
|
142
|
+
out.out(JSON.stringify(report, null, 2));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (bootstrap.pemOut !== undefined) {
|
|
146
|
+
out.print("wrote ES256 bootstrap PEM to %s", bootstrap.pemOut);
|
|
147
|
+
}
|
|
148
|
+
if (options.out !== undefined) {
|
|
149
|
+
// Record went to --out; keep stdout clean, summarize on stderr.
|
|
150
|
+
out.print("ImutableUnivocity deployed at: %s", record.imutableUnivocity);
|
|
151
|
+
out.print("genesisLogId: %s", record.genesisLogId);
|
|
152
|
+
out.print("chainId: %d txHash: %s", record.chainId, record.txHash);
|
|
153
|
+
out.print("wrote deployment record to %s", options.out);
|
|
154
|
+
} else {
|
|
155
|
+
// No --out: the record itself is the pipeable product (demo jq's it).
|
|
156
|
+
out.out("%s", json);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function runDeploy(
|
|
161
|
+
out: Out,
|
|
162
|
+
options: DeployOptions,
|
|
163
|
+
deps: DeployRunDeps = {},
|
|
164
|
+
): Promise<void> {
|
|
165
|
+
if (options.bootstrapAlg !== "es256") {
|
|
166
|
+
reportError(out, options, {
|
|
167
|
+
error: "bootstrap_alg_unsupported",
|
|
168
|
+
command: "deploy",
|
|
169
|
+
message:
|
|
170
|
+
"ks256 (Safe / ERC-1271) is the pre-provisioned aside — forestrie deploy implements the ES256 paved path only; use the univocity-tools deployer Safe flow",
|
|
171
|
+
});
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const resolveBootstrap = deps.resolveBootstrap ?? resolveDeployBootstrapKey;
|
|
176
|
+
const loadArtifact = deps.loadArtifact ?? loadImutableArtifact;
|
|
177
|
+
const createRpc = deps.createRpc ?? createDeployRpc;
|
|
178
|
+
|
|
179
|
+
let bootstrap: ResolvedDeployBootstrap;
|
|
180
|
+
try {
|
|
181
|
+
bootstrap = await resolveBootstrap({
|
|
182
|
+
generate: options.bootstrapEs256Generate,
|
|
183
|
+
pemOut: options.bootstrapEs256PemOut,
|
|
184
|
+
pemPath: options.bootstrapEs256Pem,
|
|
185
|
+
});
|
|
186
|
+
} catch (err) {
|
|
187
|
+
reportError(out, options, {
|
|
188
|
+
error: "bootstrap_key_failed",
|
|
189
|
+
command: "deploy",
|
|
190
|
+
message: errorMessage(err),
|
|
191
|
+
});
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let artifact: ImutableDeployArtifact;
|
|
196
|
+
try {
|
|
197
|
+
artifact = await loadArtifact({
|
|
198
|
+
manifestPath: options.releaseManifest,
|
|
199
|
+
releaseTag: options.releaseTag,
|
|
200
|
+
});
|
|
201
|
+
} catch (err) {
|
|
202
|
+
reportError(out, options, {
|
|
203
|
+
error: "manifest_failed",
|
|
204
|
+
command: "deploy",
|
|
205
|
+
message: `cannot load a verified deploy manifest: ${errorMessage(err)}`,
|
|
206
|
+
});
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const rpc = createRpc(options.rpcUrl, options.deployerKey);
|
|
211
|
+
|
|
212
|
+
if (options.ownerAddress !== undefined) {
|
|
213
|
+
let owner: string;
|
|
214
|
+
try {
|
|
215
|
+
owner = getAddress(options.ownerAddress);
|
|
216
|
+
} catch {
|
|
217
|
+
reportError(out, options, {
|
|
218
|
+
error: "owner_mismatch",
|
|
219
|
+
command: "deploy",
|
|
220
|
+
message: `--owner-address is not a valid address: ${options.ownerAddress}`,
|
|
221
|
+
});
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (owner !== rpc.from) {
|
|
225
|
+
reportError(out, options, {
|
|
226
|
+
error: "owner_mismatch",
|
|
227
|
+
command: "deploy",
|
|
228
|
+
message: `--owner-address ${owner} does not match the --deployer-key account ${rpc.from}`,
|
|
229
|
+
});
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
out.log(
|
|
235
|
+
"deploying ImutableUnivocity (release %s) from %s via %s",
|
|
236
|
+
artifact.releaseId,
|
|
237
|
+
rpc.from,
|
|
238
|
+
options.rpcUrl,
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
let result: DeployFlowResult;
|
|
242
|
+
try {
|
|
243
|
+
result = await runDeployFlow(rpc, {
|
|
244
|
+
creationBytecode: artifact.creationBytecode,
|
|
245
|
+
bootstrap: bootstrap.bootstrap,
|
|
246
|
+
});
|
|
247
|
+
} catch (err) {
|
|
248
|
+
if (err instanceof DeployFlowError) {
|
|
249
|
+
const report: DeployErrorReport = {
|
|
250
|
+
error: FLOW_ERROR_CODES[err.stage],
|
|
251
|
+
command: "deploy",
|
|
252
|
+
message: err.message,
|
|
253
|
+
};
|
|
254
|
+
if (err.detail !== undefined) report.detail = err.detail;
|
|
255
|
+
if (err.txHash !== undefined) report.txHash = err.txHash;
|
|
256
|
+
reportError(out, options, report);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
reportError(out, options, {
|
|
260
|
+
error: "deploy_failed",
|
|
261
|
+
command: "deploy",
|
|
262
|
+
message: errorMessage(err),
|
|
263
|
+
});
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
await reportDeployed(
|
|
268
|
+
out,
|
|
269
|
+
options,
|
|
270
|
+
bootstrap,
|
|
271
|
+
deploymentRecord(options, artifact, result),
|
|
272
|
+
);
|
|
273
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { writeFileSync } from "node:fs";
|
|
2
|
+
import type { Out } from "@forestrie/cli-kit/reporting";
|
|
3
|
+
import { ethRpc, hexAddressToBytes, normalizeHexAddress } from "@forestrie/chain-rpc";
|
|
4
|
+
import type { FetchAccumulatorOptions } from "../options/fetch-accumulator.js";
|
|
5
|
+
import { decodeLogStateResult, toContractLogId } from "../lib/verify-anchored.js";
|
|
6
|
+
import {
|
|
7
|
+
encodeKnownAccumulator,
|
|
8
|
+
type KnownAccumulator,
|
|
9
|
+
} from "../lib/verify-known-accumulator.js";
|
|
10
|
+
import {
|
|
11
|
+
fetchPublishedCheckpoints,
|
|
12
|
+
selectPublishedAtBlock,
|
|
13
|
+
} from "../lib/verify-eventscan.js";
|
|
14
|
+
|
|
15
|
+
/** Selector for `logState(bytes32)` (mirrors verify-anchored). */
|
|
16
|
+
const LOG_STATE_SELECTOR = "0xeecac1b7";
|
|
17
|
+
|
|
18
|
+
/** `--json` success shape on stdout — stable for demo scripting. */
|
|
19
|
+
export type FetchAccumulatorReport = {
|
|
20
|
+
command: "fetch-accumulator";
|
|
21
|
+
chainId: string;
|
|
22
|
+
univocity: string;
|
|
23
|
+
logId: string;
|
|
24
|
+
anchoredSize: string;
|
|
25
|
+
peakCount: number;
|
|
26
|
+
blockNumber: string;
|
|
27
|
+
blockHash: string;
|
|
28
|
+
out: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type FetchAccumulatorErrorReport = {
|
|
32
|
+
error: "fetch_accumulator_failed";
|
|
33
|
+
command: "fetch-accumulator";
|
|
34
|
+
message: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function hexToBigint(v: unknown, what: string): bigint {
|
|
38
|
+
if (typeof v !== "string" || !/^0x[0-9a-fA-F]+$/.test(v)) {
|
|
39
|
+
throw new Error(`${what}: expected a hex quantity, got ${String(v)}`);
|
|
40
|
+
}
|
|
41
|
+
return BigInt(v);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function hexToBytes32(v: unknown, what: string): Uint8Array {
|
|
45
|
+
if (typeof v !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(v)) {
|
|
46
|
+
throw new Error(`${what}: expected a 32-byte hex value, got ${String(v)}`);
|
|
47
|
+
}
|
|
48
|
+
const hex = v.slice(2);
|
|
49
|
+
const out = new Uint8Array(32);
|
|
50
|
+
for (let i = 0; i < 32; i++) {
|
|
51
|
+
out[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Read `logState(logId)` pinned to a specific block: fetch the latest block
|
|
58
|
+
* header first, then `eth_call` AT that block number, so the snapshot's
|
|
59
|
+
* `(blockNumber, blockHash)` binding is exact — the falsifiability handle
|
|
60
|
+
* that lets anyone re-run the read and confirm or disprove the snapshot.
|
|
61
|
+
*/
|
|
62
|
+
export async function runFetchAccumulator(
|
|
63
|
+
out: Out,
|
|
64
|
+
options: FetchAccumulatorOptions,
|
|
65
|
+
): Promise<void> {
|
|
66
|
+
try {
|
|
67
|
+
const address = normalizeHexAddress(options.univocity);
|
|
68
|
+
if (address === null) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`--univocity is not a valid address: '${options.univocity}'`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
const contractLogId = toContractLogId(options.logId);
|
|
74
|
+
|
|
75
|
+
const chainId = hexToBigint(
|
|
76
|
+
await ethRpc(options.rpcUrl, "eth_chainId", []),
|
|
77
|
+
"eth_chainId",
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
let state: { size: bigint; accumulator: Uint8Array[] };
|
|
81
|
+
let blockNumber: bigint;
|
|
82
|
+
let blockHash: Uint8Array;
|
|
83
|
+
if (options.atBlock !== undefined) {
|
|
84
|
+
// FOR-368: historical snapshot from the CheckpointPublished record —
|
|
85
|
+
// events survive where contract state does not, so no archive node
|
|
86
|
+
// is needed. The snapshot binds to the block the anchor landed in.
|
|
87
|
+
const published = await fetchPublishedCheckpoints({
|
|
88
|
+
univocity: options.univocity,
|
|
89
|
+
logId: options.logId,
|
|
90
|
+
rpcUrl: options.rpcUrl,
|
|
91
|
+
fromBlock: options.fromBlock,
|
|
92
|
+
});
|
|
93
|
+
const chosen = selectPublishedAtBlock(published, options.atBlock);
|
|
94
|
+
if (chosen === null) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`no CheckpointPublished at or before block ${options.atBlock} for log ${options.logId}` +
|
|
97
|
+
(published.length > 0
|
|
98
|
+
? ` (earliest anchor is block ${published[0]!.blockNumber})`
|
|
99
|
+
: " (no anchors found in the scanned range)"),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
state = { size: chosen.size, accumulator: chosen.accumulator };
|
|
103
|
+
blockNumber = chosen.blockNumber;
|
|
104
|
+
blockHash = hexToBytes32(chosen.blockHash, "event block hash");
|
|
105
|
+
} else {
|
|
106
|
+
const block = (await ethRpc(options.rpcUrl, "eth_getBlockByNumber", [
|
|
107
|
+
"latest",
|
|
108
|
+
false,
|
|
109
|
+
])) as { number?: unknown; hash?: unknown } | null;
|
|
110
|
+
if (block === null || typeof block !== "object") {
|
|
111
|
+
throw new Error("eth_getBlockByNumber returned no block");
|
|
112
|
+
}
|
|
113
|
+
blockNumber = hexToBigint(block.number, "block number");
|
|
114
|
+
blockHash = hexToBytes32(block.hash, "block hash");
|
|
115
|
+
|
|
116
|
+
const data = LOG_STATE_SELECTOR + contractLogId.slice(2);
|
|
117
|
+
const result = await ethRpc(options.rpcUrl, "eth_call", [
|
|
118
|
+
{ to: `0x${address}`, data },
|
|
119
|
+
`0x${blockNumber.toString(16)}`,
|
|
120
|
+
]);
|
|
121
|
+
if (typeof result !== "string" || result === "0x") {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`logState eth_call returned no data for log ${options.logId} at ${options.univocity}`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
state = decodeLogStateResult(result);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const snapshot: KnownAccumulator = {
|
|
130
|
+
version: 1,
|
|
131
|
+
chainId,
|
|
132
|
+
univocity: hexAddressToBytes(`0x${address}`),
|
|
133
|
+
logId: hexToBytes32(contractLogId, "log id"),
|
|
134
|
+
size: state.size,
|
|
135
|
+
accumulator: state.accumulator,
|
|
136
|
+
blockNumber,
|
|
137
|
+
blockHash,
|
|
138
|
+
};
|
|
139
|
+
writeFileSync(options.out, encodeKnownAccumulator(snapshot));
|
|
140
|
+
|
|
141
|
+
if (options.json) {
|
|
142
|
+
const report: FetchAccumulatorReport = {
|
|
143
|
+
command: "fetch-accumulator",
|
|
144
|
+
chainId: chainId.toString(),
|
|
145
|
+
univocity: `0x${address}`,
|
|
146
|
+
logId: contractLogId,
|
|
147
|
+
anchoredSize: state.size.toString(),
|
|
148
|
+
peakCount: state.accumulator.length,
|
|
149
|
+
blockNumber: blockNumber.toString(),
|
|
150
|
+
blockHash: `0x${Array.from(blockHash, (b) =>
|
|
151
|
+
b.toString(16).padStart(2, "0"),
|
|
152
|
+
).join("")}`,
|
|
153
|
+
out: options.out,
|
|
154
|
+
};
|
|
155
|
+
out.out(JSON.stringify(report, null, 2));
|
|
156
|
+
} else {
|
|
157
|
+
out.print(
|
|
158
|
+
"fetch-accumulator: read logState at block %s (chain %s)",
|
|
159
|
+
blockNumber.toString(),
|
|
160
|
+
chainId.toString(),
|
|
161
|
+
);
|
|
162
|
+
out.out(
|
|
163
|
+
`wrote known accumulator: ${options.out} (anchored size ${state.size}, ${state.accumulator.length} peaks, block ${blockNumber})`,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
process.exitCode = 0;
|
|
167
|
+
} catch (err) {
|
|
168
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
169
|
+
if (options.json) {
|
|
170
|
+
const report: FetchAccumulatorErrorReport = {
|
|
171
|
+
error: "fetch_accumulator_failed",
|
|
172
|
+
command: "fetch-accumulator",
|
|
173
|
+
message,
|
|
174
|
+
};
|
|
175
|
+
out.out(JSON.stringify(report, null, 2));
|
|
176
|
+
} else {
|
|
177
|
+
out.warn("forestrie fetch-accumulator: %s", message);
|
|
178
|
+
}
|
|
179
|
+
process.exitCode = 1;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Out } from "@forestrie/cli-kit/reporting";
|
|
2
|
+
import type { ForestrieCommonOptions } from "../options/common.js";
|
|
3
|
+
|
|
4
|
+
/** Structured not-implemented report — the `--json` shape is a contract. */
|
|
5
|
+
export type NotImplementedReport = {
|
|
6
|
+
error: "not_implemented";
|
|
7
|
+
command: string;
|
|
8
|
+
issue: string;
|
|
9
|
+
message: string;
|
|
10
|
+
options: Record<string, unknown>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Every subcommand stub terminates here: parsing succeeded (the arg
|
|
15
|
+
* surface is real), the behaviour ships under the referenced FOR-34x
|
|
16
|
+
* issue. Exits non-zero via `process.exitCode` so citty does not wrap
|
|
17
|
+
* the message in its own error formatting.
|
|
18
|
+
*
|
|
19
|
+
* Human mode: one line on stderr. `--json`: the report on stdout.
|
|
20
|
+
*/
|
|
21
|
+
export function reportNotImplemented(
|
|
22
|
+
out: Out,
|
|
23
|
+
common: ForestrieCommonOptions,
|
|
24
|
+
command: string,
|
|
25
|
+
issue: string,
|
|
26
|
+
options: Record<string, unknown>,
|
|
27
|
+
): void {
|
|
28
|
+
const message = `forestrie ${command}: not implemented yet — ${issue}`;
|
|
29
|
+
if (common.json) {
|
|
30
|
+
const report: NotImplementedReport = {
|
|
31
|
+
error: "not_implemented",
|
|
32
|
+
command,
|
|
33
|
+
issue,
|
|
34
|
+
message,
|
|
35
|
+
options,
|
|
36
|
+
};
|
|
37
|
+
out.out(JSON.stringify(report, null, 2));
|
|
38
|
+
} else {
|
|
39
|
+
out.warn(message);
|
|
40
|
+
out.log("parsed options: %s", JSON.stringify(options));
|
|
41
|
+
}
|
|
42
|
+
process.exitCode = 1;
|
|
43
|
+
}
|