143 lines
5.8 KiB
JavaScript
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);
|
|
});
|