@oaath/cli 0.3.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/README.md +89 -0
- package/dist/cli.mjs +1030 -0
- package/dist/deploy-BtvOPlub.mjs +161 -0
- package/dist/journal-DkNIXY4X.mjs +92 -0
- package/package.json +43 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { n as components, r as kernelDeployment, t as doctor } from "./cli.mjs";
|
|
2
|
+
import { createWalletClient, custom, keccak256 } from "viem";
|
|
3
|
+
//#region src/deploy.ts
|
|
4
|
+
var DeploymentError = class extends Error {
|
|
5
|
+
code;
|
|
6
|
+
constructor(code) {
|
|
7
|
+
super(code);
|
|
8
|
+
this.code = code;
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
/** The core ECDSA set plus the passkey-session modules; never other optional rows. */
|
|
12
|
+
function deploys(row) {
|
|
13
|
+
return row.required || row.passkeySession;
|
|
14
|
+
}
|
|
15
|
+
function validateReadiness(report) {
|
|
16
|
+
if (report.error || report.components.some((row) => deploys(row) && (row.status === "mismatch" || row.status === "unreadable"))) throw new DeploymentError("deployment_evidence_invalid");
|
|
17
|
+
for (const id of ["entryPoint", "create2Deployer"]) if (report.components.find((row) => row.id === id)?.status !== "verified") throw new DeploymentError("deployment_prerequisite_missing");
|
|
18
|
+
if (report.components.find((row) => row.id === "kernelFactory")?.status === "verified" && report.factoryBinding !== "verified") throw new DeploymentError("deployment_evidence_invalid");
|
|
19
|
+
}
|
|
20
|
+
function recordObject(value) {
|
|
21
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new DeploymentError("deployment_evidence_invalid");
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
function quantity(value) {
|
|
25
|
+
if (typeof value !== "string" || !/^0x(?:0|[1-9a-f][0-9a-f]*)$/u.test(value) || value.length > 66) throw new DeploymentError("deployment_evidence_invalid");
|
|
26
|
+
return BigInt(value);
|
|
27
|
+
}
|
|
28
|
+
function hash(value) {
|
|
29
|
+
if (typeof value !== "string" || !/^0x[0-9a-f]{64}$/u.test(value)) throw new DeploymentError("deployment_evidence_invalid");
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
/** Only canonical finalized evidence releases an attempted transaction's lane. */
|
|
33
|
+
async function observe(record, manifest, rpc, journal, attempts) {
|
|
34
|
+
const component = manifest.find((row) => row.id === record.component);
|
|
35
|
+
if (!component?.deploymentInput || record.address !== component.address || record.inputHash !== keccak256(component.deploymentInput)) throw new DeploymentError("deployment_journal_conflict");
|
|
36
|
+
if (record.state === "confirmed") return true;
|
|
37
|
+
if (record.state === "reverted") throw new DeploymentError("deployment_reverted");
|
|
38
|
+
const deployer = kernelDeployment({ chainId: record.chainId }).create2Deployer;
|
|
39
|
+
for (let count = 0; count < attempts; count += 1) {
|
|
40
|
+
if (count > 0) await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
41
|
+
try {
|
|
42
|
+
const value = await rpc.request("eth_getTransactionReceipt", [record.transactionHash]);
|
|
43
|
+
if (value === null) continue;
|
|
44
|
+
const receipt = recordObject(value);
|
|
45
|
+
if (hash(receipt.transactionHash) !== record.transactionHash || receipt.from !== record.wallet || receipt.to !== deployer || receipt.status !== "0x1" && receipt.status !== "0x0") throw new DeploymentError("deployment_evidence_invalid");
|
|
46
|
+
const blockNumber = quantity(receipt.blockNumber);
|
|
47
|
+
const blockHash = hash(receipt.blockHash);
|
|
48
|
+
const finalized = recordObject(await rpc.request("eth_getBlockByNumber", ["finalized", false]));
|
|
49
|
+
const finalizedHash = hash(finalized.hash);
|
|
50
|
+
if (quantity(finalized.number) < blockNumber) continue;
|
|
51
|
+
const finalizedCanonical = recordObject(await rpc.request("eth_getBlockByNumber", [finalized.number, false]));
|
|
52
|
+
if (hash(finalizedCanonical.hash) !== finalizedHash || quantity(finalizedCanonical.number) !== quantity(finalized.number)) throw new DeploymentError("deployment_evidence_invalid");
|
|
53
|
+
const canonical = recordObject(await rpc.request("eth_getBlockByNumber", [receipt.blockNumber, false]));
|
|
54
|
+
if (hash(canonical.hash) !== blockHash || quantity(canonical.number) !== blockNumber) throw new DeploymentError("deployment_evidence_invalid");
|
|
55
|
+
const transaction = recordObject(await rpc.request("eth_getTransactionByHash", [record.transactionHash]));
|
|
56
|
+
if (hash(transaction.hash) !== record.transactionHash || transaction.from !== record.wallet || transaction.to !== deployer || transaction.input !== component.deploymentInput || quantity(transaction.nonce) !== BigInt(record.nonce) || quantity(transaction.value) !== 0n || quantity(transaction.chainId) !== BigInt(record.chainId) || hash(transaction.blockHash) !== blockHash || quantity(transaction.blockNumber) !== blockNumber) throw new DeploymentError("deployment_evidence_invalid");
|
|
57
|
+
if (receipt.status === "0x0") {
|
|
58
|
+
journal.finish(record, "reverted");
|
|
59
|
+
throw new DeploymentError("deployment_reverted");
|
|
60
|
+
}
|
|
61
|
+
const code = await rpc.request("eth_getCode", [component.address, receipt.blockNumber]);
|
|
62
|
+
if (typeof code !== "string" || !/^0x(?:[0-9a-f]{2})+$/u.test(code) || code.length > 131074 || component.runtimeCodeHash !== null && keccak256(code) !== component.runtimeCodeHash) throw new DeploymentError("deployment_evidence_invalid");
|
|
63
|
+
journal.finish(record, "confirmed");
|
|
64
|
+
return true;
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (error instanceof DeploymentError) throw error;
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
async function deployRuntime(input) {
|
|
73
|
+
const { chainId, rpc, journal } = input;
|
|
74
|
+
const manifest = components(chainId);
|
|
75
|
+
const deployer = kernelDeployment({ chainId }).create2Deployer;
|
|
76
|
+
let report = await doctor(chainId, rpc);
|
|
77
|
+
validateReadiness(report);
|
|
78
|
+
const result = (status, transactionHash = null) => ({
|
|
79
|
+
version: "oaath.runtime-deployment/v1",
|
|
80
|
+
chainId,
|
|
81
|
+
status,
|
|
82
|
+
missing: report.components.filter((row) => deploys(row) && row.status === "missing").map((row) => row.id),
|
|
83
|
+
transactionHash,
|
|
84
|
+
readiness: report
|
|
85
|
+
});
|
|
86
|
+
if (input.dryRun) return result("planned");
|
|
87
|
+
const pending = journal.pending(chainId);
|
|
88
|
+
if (pending && !await observe(pending, manifest, rpc, journal, input.observationAttempts ?? 30)) return result("pending", pending.transactionHash);
|
|
89
|
+
const wallet = () => createWalletClient({
|
|
90
|
+
account: input.account(),
|
|
91
|
+
transport: custom({ request: ({ method, params }) => rpc.request(method, params) }, { retryCount: 0 })
|
|
92
|
+
});
|
|
93
|
+
for (const component of manifest.filter((row) => deploys(row) && row.deploymentInput)) {
|
|
94
|
+
const code = await rpc.request("eth_getCode", [component.address, "latest"]);
|
|
95
|
+
if (code !== "0x") {
|
|
96
|
+
if (typeof code !== "string" || !/^0x(?:[0-9a-f]{2})+$/u.test(code) || component.runtimeCodeHash !== null && keccak256(code) !== component.runtimeCodeHash) throw new DeploymentError("deployment_evidence_invalid");
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const retained = journal.get(chainId, component.id);
|
|
100
|
+
if (retained) {
|
|
101
|
+
if (retained.state === "confirmed") throw new DeploymentError("deployment_evidence_invalid");
|
|
102
|
+
if (!await observe(retained, manifest, rpc, journal, input.observationAttempts ?? 30)) return result("pending", retained.transactionHash);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const client = wallet();
|
|
106
|
+
const request = await client.prepareTransactionRequest({
|
|
107
|
+
account: client.account,
|
|
108
|
+
chain: null,
|
|
109
|
+
to: deployer,
|
|
110
|
+
data: component.deploymentInput,
|
|
111
|
+
value: 0n
|
|
112
|
+
});
|
|
113
|
+
if (request.chainId !== chainId || !Number.isSafeInteger(request.nonce) || request.nonce < 0 || request.gas <= 0n || request.gas > 10000000n) throw new DeploymentError("deployment_transaction_invalid");
|
|
114
|
+
const transaction = {
|
|
115
|
+
chainId,
|
|
116
|
+
nonce: request.nonce,
|
|
117
|
+
gas: request.gas,
|
|
118
|
+
to: deployer,
|
|
119
|
+
data: component.deploymentInput,
|
|
120
|
+
value: 0n
|
|
121
|
+
};
|
|
122
|
+
let signed;
|
|
123
|
+
if (request.type === "eip1559" && typeof request.maxFeePerGas === "bigint" && typeof request.maxPriorityFeePerGas === "bigint") signed = await client.account.signTransaction({
|
|
124
|
+
...transaction,
|
|
125
|
+
type: "eip1559",
|
|
126
|
+
maxFeePerGas: request.maxFeePerGas,
|
|
127
|
+
maxPriorityFeePerGas: request.maxPriorityFeePerGas
|
|
128
|
+
});
|
|
129
|
+
else if (request.type === "legacy" && typeof request.gasPrice === "bigint") signed = await client.account.signTransaction({
|
|
130
|
+
...transaction,
|
|
131
|
+
type: "legacy",
|
|
132
|
+
gasPrice: request.gasPrice
|
|
133
|
+
});
|
|
134
|
+
else throw new DeploymentError("deployment_transaction_invalid");
|
|
135
|
+
const attempt = {
|
|
136
|
+
chainId,
|
|
137
|
+
component: component.id,
|
|
138
|
+
address: component.address,
|
|
139
|
+
wallet: client.account.address.toLowerCase(),
|
|
140
|
+
inputHash: keccak256(component.deploymentInput),
|
|
141
|
+
transactionHash: keccak256(signed),
|
|
142
|
+
nonce: request.nonce,
|
|
143
|
+
state: "attempted"
|
|
144
|
+
};
|
|
145
|
+
const reservation = journal.reserve(attempt);
|
|
146
|
+
if (reservation.inserted) try {
|
|
147
|
+
await rpc.request("eth_sendRawTransaction", [signed]);
|
|
148
|
+
} catch {}
|
|
149
|
+
if (!await observe(reservation.record, manifest, rpc, journal, input.observationAttempts ?? 30)) return result("pending", reservation.record.transactionHash);
|
|
150
|
+
if (reservation.record.component !== component.id) {
|
|
151
|
+
report = await doctor(chainId, rpc);
|
|
152
|
+
return result(report.ready && report.passkeySessionsReady ? "ready" : "planned");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
report = await doctor(chainId, rpc);
|
|
156
|
+
validateReadiness(report);
|
|
157
|
+
if (!report.ready || !report.passkeySessionsReady) throw new DeploymentError("deployment_evidence_invalid");
|
|
158
|
+
return result("ready");
|
|
159
|
+
}
|
|
160
|
+
//#endregion
|
|
161
|
+
export { DeploymentError, deployRuntime };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { DatabaseSync } from "node:sqlite";
|
|
4
|
+
//#region src/journal.ts
|
|
5
|
+
function capture(row) {
|
|
6
|
+
if (row === void 0) return null;
|
|
7
|
+
if (!row || typeof row !== "object") throw new Error("deployment_journal_invalid");
|
|
8
|
+
const value = row;
|
|
9
|
+
if (!Number.isSafeInteger(value.chainId) || Number(value.chainId) < 1 || !Number.isSafeInteger(value.nonce) || Number(value.nonce) < 0 || typeof value.component !== "string" || !/^[a-zA-Z][a-zA-Z0-9]{0,39}$/u.test(value.component) || ![
|
|
10
|
+
"attempted",
|
|
11
|
+
"confirmed",
|
|
12
|
+
"reverted"
|
|
13
|
+
].includes(String(value.state))) throw new Error("deployment_journal_invalid");
|
|
14
|
+
for (const field of ["address", "wallet"]) if (typeof value[field] !== "string" || !/^0x[0-9a-f]{40}$/u.test(value[field])) throw new Error("deployment_journal_invalid");
|
|
15
|
+
for (const field of ["inputHash", "transactionHash"]) if (typeof value[field] !== "string" || !/^0x[0-9a-f]{64}$/u.test(value[field])) throw new Error("deployment_journal_invalid");
|
|
16
|
+
return Object.freeze({ ...value });
|
|
17
|
+
}
|
|
18
|
+
/** Public attempt metadata only: never a private key or a signed transaction. */
|
|
19
|
+
var DeploymentJournal = class {
|
|
20
|
+
#db;
|
|
21
|
+
#closed = false;
|
|
22
|
+
constructor(path) {
|
|
23
|
+
mkdirSync(dirname(path), {
|
|
24
|
+
recursive: true,
|
|
25
|
+
mode: 448
|
|
26
|
+
});
|
|
27
|
+
this.#db = new DatabaseSync(path);
|
|
28
|
+
try {
|
|
29
|
+
this.#db.exec("PRAGMA busy_timeout=5000; PRAGMA synchronous=FULL; BEGIN IMMEDIATE");
|
|
30
|
+
const version = this.#db.prepare("PRAGMA user_version").get()?.user_version;
|
|
31
|
+
if (version === 0) {
|
|
32
|
+
if (this.#db.prepare("SELECT name FROM sqlite_master WHERE type='table'").get()) throw new Error("deployment_journal_schema_invalid");
|
|
33
|
+
this.#db.exec(`CREATE TABLE deployments (
|
|
34
|
+
chainId INTEGER NOT NULL, component TEXT NOT NULL, address TEXT NOT NULL,
|
|
35
|
+
wallet TEXT NOT NULL, inputHash TEXT NOT NULL, transactionHash TEXT NOT NULL,
|
|
36
|
+
nonce INTEGER NOT NULL, state TEXT NOT NULL CHECK(state IN ('attempted','confirmed','reverted')),
|
|
37
|
+
PRIMARY KEY(chainId, component)
|
|
38
|
+
);
|
|
39
|
+
CREATE UNIQUE INDEX active_chain ON deployments(chainId) WHERE state='attempted';
|
|
40
|
+
PRAGMA application_id=1329676628;
|
|
41
|
+
PRAGMA user_version=1;`);
|
|
42
|
+
} else if (version !== 1 || this.#db.prepare("PRAGMA application_id").get()?.application_id !== 1329676628) throw new Error("deployment_journal_schema_invalid");
|
|
43
|
+
this.#db.exec("COMMIT");
|
|
44
|
+
} catch (error) {
|
|
45
|
+
this.#db.close();
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
pending(chainId) {
|
|
50
|
+
return capture(this.#db.prepare("SELECT * FROM deployments WHERE chainId=? AND state='attempted'").get(chainId));
|
|
51
|
+
}
|
|
52
|
+
get(chainId, component) {
|
|
53
|
+
return capture(this.#db.prepare("SELECT * FROM deployments WHERE chainId=? AND component=?").get(chainId, component));
|
|
54
|
+
}
|
|
55
|
+
reserve(attempt) {
|
|
56
|
+
if (capture(attempt)?.state !== "attempted") throw new Error("deployment_journal_invalid");
|
|
57
|
+
this.#db.exec("BEGIN IMMEDIATE");
|
|
58
|
+
try {
|
|
59
|
+
const existing = this.pending(attempt.chainId) ?? this.get(attempt.chainId, attempt.component);
|
|
60
|
+
if (existing) {
|
|
61
|
+
this.#db.exec("COMMIT");
|
|
62
|
+
return {
|
|
63
|
+
inserted: false,
|
|
64
|
+
record: existing
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
this.#db.prepare("INSERT INTO deployments VALUES (?,?,?,?,?,?,?,?)").run(attempt.chainId, attempt.component, attempt.address, attempt.wallet, attempt.inputHash, attempt.transactionHash, attempt.nonce, attempt.state);
|
|
68
|
+
this.#db.exec("COMMIT");
|
|
69
|
+
return {
|
|
70
|
+
inserted: true,
|
|
71
|
+
record: attempt
|
|
72
|
+
};
|
|
73
|
+
} catch (error) {
|
|
74
|
+
this.#db.exec("ROLLBACK");
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
finish(record, state) {
|
|
79
|
+
if (this.#db.prepare("UPDATE deployments SET state=? WHERE chainId=? AND component=? AND transactionHash=? AND state='attempted'").run(state, record.chainId, record.component, record.transactionHash).changes !== 1) {
|
|
80
|
+
const existing = this.get(record.chainId, record.component);
|
|
81
|
+
if (existing?.state !== state || existing.transactionHash !== record.transactionHash) throw new Error("deployment_journal_conflict");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
close() {
|
|
85
|
+
if (!this.#closed) {
|
|
86
|
+
this.#db.close();
|
|
87
|
+
this.#closed = true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
//#endregion
|
|
92
|
+
export { DeploymentJournal };
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@oaath/cli",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "OAAth runtime deployment and readiness commands.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"author": "taek <leekt216@gmail.com>",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/leekt/oaath.git",
|
|
11
|
+
"directory": "packages/cli"
|
|
12
|
+
},
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=22.13"
|
|
15
|
+
},
|
|
16
|
+
"bin": {
|
|
17
|
+
"oaath": "./dist/cli.mjs"
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"README.md"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsdown",
|
|
28
|
+
"prepack": "bun run build",
|
|
29
|
+
"typecheck": "tsc --noEmit",
|
|
30
|
+
"test": "vitest run",
|
|
31
|
+
"test:anvil": "OAATH_REQUIRE_ANVIL=1 vitest run test/doctor.anvil.test.ts test/deploy.anvil.test.ts",
|
|
32
|
+
"release:publish": "bun publish --access public --tolerate-republish",
|
|
33
|
+
"release:check": "bun pm pack --dry-run"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@oaath/sdk": "0.3.0",
|
|
37
|
+
"viem": "2.55.8"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "22.13.0",
|
|
41
|
+
"@account-abstraction/contracts": "0.7.0"
|
|
42
|
+
}
|
|
43
|
+
}
|