contracts/scripts/lastlight-eip712-flow.cjs
Edut LLC 32141a89f4
Some checks are pending
check / contracts (push) Waiting to run
contracts: add last light controller and gas-only identity mint
2026-02-20 15:43:16 -08:00

143 lines
5.8 KiB
JavaScript

#!/usr/bin/env node
const { ethers } = require("ethers");
const ABI = [
"function releaseRecord(bytes32 releaseId) view returns (bytes32 payloadManifestHash, bytes32 encryptedPayloadRootHash, bytes32 decryptionKeyHash, bytes32 guardianSetHash, bytes32 metadataRefHash, uint64 timelockSeconds, uint64 stagedAt, uint64 armedAt, uint64 executeNotBefore, uint64 armEpoch, uint8 threshold, uint8 status)",
"function armReleaseWithSignatures(bytes32 releaseId, bytes32 reasonHash, uint64 deadline, bytes[] signatures)",
"function cancelReleaseWithSignatures(bytes32 releaseId, bytes32 reasonHash, uint64 deadline, bytes[] signatures)",
"function executeReleaseWithSignatures(bytes32 releaseId, bytes32 decryptionKey, bytes32 manifestRefHash, uint64 deadline, bytes[] signatures)"
];
function required(name) {
const value = process.env[name];
if (!value || !String(value).trim()) {
throw new Error(`missing required env ${name}`);
}
return String(value).trim();
}
function normalizeBytes32(value, fallbackLabel) {
if (!value || !String(value).trim()) {
if (!fallbackLabel) {
throw new Error("missing bytes32 input");
}
return ethers.utils.id(fallbackLabel);
}
const trimmed = String(value).trim();
if (ethers.utils.isHexString(trimmed, 32)) {
return trimmed;
}
return ethers.utils.id(trimmed);
}
async function main() {
const action = String(process.env.LASTLIGHT_ACTION || "arm").trim().toLowerCase();
const rpc = required("BASE_SEPOLIA_RPC_URL");
const contractAddress = required("LASTLIGHT_CONTRACT_ADDRESS");
const releaseId = normalizeBytes32(process.env.LASTLIGHT_RELEASE_ID, "last-light-release");
const deadlineSeconds = Number(process.env.LASTLIGHT_DEADLINE_SECONDS || "3600");
if (!Number.isFinite(deadlineSeconds) || deadlineSeconds <= 0) {
throw new Error(`invalid LASTLIGHT_DEADLINE_SECONDS=${process.env.LASTLIGHT_DEADLINE_SECONDS}`);
}
const provider = new ethers.providers.JsonRpcProvider(rpc);
const relayer = new ethers.Wallet(required("DEPLOYER_PRIVATE_KEY"), provider);
const contract = new ethers.Contract(contractAddress, ABI, relayer);
const network = await provider.getNetwork();
const latestBlock = await provider.getBlock("latest");
const deadline = Number(latestBlock.timestamp) + deadlineSeconds;
const record = await contract.releaseRecord(releaseId);
const armEpoch = Number(record.armEpoch);
const status = Number(record.status);
if (status !== 1 && action === "arm") {
throw new Error(`release must be staged before arm, current status=${status}`);
}
if (status !== 1 && status !== 2 && action === "cancel") {
throw new Error(`release must be staged/armed before cancel, current status=${status}`);
}
if (status !== 2 && action === "execute") {
throw new Error(`release must be armed before execute, current status=${status}`);
}
const guardianKeysRaw = required("LASTLIGHT_GUARDIAN_PRIVATE_KEYS");
const guardianKeys = guardianKeysRaw
.split(",")
.map((v) => v.trim())
.filter(Boolean);
if (guardianKeys.length === 0) {
throw new Error("no guardian keys provided");
}
const guardians = guardianKeys.map((k) => new ethers.Wallet(k, provider));
const domain = {
name: "EDUT Last Light",
version: "1",
chainId: network.chainId,
verifyingContract: contractAddress
};
let tx;
if (action === "arm") {
const reasonHash = normalizeBytes32(process.env.LASTLIGHT_REASON_HASH, "owner-incapacity");
const types = {
ArmIntent: [
{ name: "releaseId", type: "bytes32" },
{ name: "reasonHash", type: "bytes32" },
{ name: "deadline", type: "uint64" },
{ name: "armEpoch", type: "uint64" }
]
};
const value = { releaseId, reasonHash, deadline, armEpoch };
const signatures = await Promise.all(guardians.map((g) => g._signTypedData(domain, types, value)));
tx = await contract.armReleaseWithSignatures(releaseId, reasonHash, deadline, signatures);
} else if (action === "cancel") {
const reasonHash = normalizeBytes32(process.env.LASTLIGHT_REASON_HASH, "cancelled-by-guardians");
const types = {
CancelIntent: [
{ name: "releaseId", type: "bytes32" },
{ name: "reasonHash", type: "bytes32" },
{ name: "deadline", type: "uint64" },
{ name: "armEpoch", type: "uint64" }
]
};
const value = { releaseId, reasonHash, deadline, armEpoch };
const signatures = await Promise.all(guardians.map((g) => g._signTypedData(domain, types, value)));
tx = await contract.cancelReleaseWithSignatures(releaseId, reasonHash, deadline, signatures);
} else if (action === "execute") {
const decryptionKey = normalizeBytes32(process.env.LASTLIGHT_DECRYPTION_KEY, "");
const manifestRefHash = normalizeBytes32(process.env.LASTLIGHT_MANIFEST_REF_HASH, "manifest-ref");
const types = {
ExecuteIntent: [
{ name: "releaseId", type: "bytes32" },
{ name: "decryptionKey", type: "bytes32" },
{ name: "manifestRefHash", type: "bytes32" },
{ name: "deadline", type: "uint64" },
{ name: "armEpoch", type: "uint64" }
]
};
const value = { releaseId, decryptionKey, manifestRefHash, deadline, armEpoch };
const signatures = await Promise.all(guardians.map((g) => g._signTypedData(domain, types, value)));
tx = await contract.executeReleaseWithSignatures(releaseId, decryptionKey, manifestRefHash, deadline, signatures);
} else {
throw new Error(`unsupported LASTLIGHT_ACTION=${action}`);
}
const receipt = await tx.wait();
console.log(JSON.stringify({
ok: true,
action,
chain_id: network.chainId,
contract: contractAddress,
release_id: releaseId,
arm_epoch: armEpoch,
tx_hash: tx.hash,
block_number: receipt.blockNumber
}, null, 2));
}
main().catch((err) => {
console.error(JSON.stringify({ ok: false, error: err.message }, null, 2));
process.exit(1);
});