Some checks failed
CI / Frontend Lint (pull_request) Failing after 7s
CI / Frontend Type Check (pull_request) Failing after 8s
CI / Frontend Build (pull_request) Failing after 5s
CI / Frontend E2E Tests (pull_request) Failing after 8s
CI / Orchestrator Build (pull_request) Failing after 5s
CI / Orchestrator Unit Tests (pull_request) Failing after 6s
CI / Orchestrator E2E (Testcontainers) (pull_request) Has been skipped
CI / Contracts Compile (pull_request) Failing after 7s
CI / Contracts Test (pull_request) Failing after 5s
Code Quality / SonarQube Analysis (pull_request) Failing after 19s
Code Quality / Code Quality Checks (pull_request) Failing after 7s
Security Scan / Dependency Vulnerability Scan (pull_request) Failing after 3s
Security Scan / OWASP ZAP Scan (pull_request) Failing after 3s
- contracts/scripts/deploy-notary-registry.ts: self-compiling ethers v6
deploy for NotaryRegistry.sol (solc-js in-process — avoids hardhat's
HH1006 on contracts/node_modules), with NOTARY_DRY_RUN mode and a
machine-readable JSON envelope as last stdout line.
- contracts/hardhat.config.ts: chain138 network (RPC defaults to the
public endpoint that resolves EXT-CHAIN138-CI-RPC).
- orchestrator/Dockerfile: multi-stage node:20-alpine build, non-root
user, dumb-init, /health HEALTHCHECK on :8080.
- Dockerfile (root, portal): multi-stage vite build → nginx:1.27-alpine,
VITE_ORCHESTRATOR_URL baked at build time.
- nginx.conf: SPA fallback + long-cache /assets, sourcemaps denied.
- docker-compose.yml: full sandbox stack (postgres 15 + redis 7 +
orchestrator + portal), all secrets parameterised via env_file.
- .env.sandbox.example: template with EXT-* blocker env vars documented
and CHAIN_138_RPC_URL defaulting to the resolved public endpoint.
- .dockerignore: excludes node_modules, artifacts, cache, terraform, k8s.
- orchestrator/src/config/env.ts: emptyToUndefined() preprocess so zod
optional regex fields validate empty-string identically to unset
(fixes docker-compose NOTARY_REGISTRY_ADDRESS= sandbox booting).
Headless smoke test on this box:
- docker compose --env-file .env.sandbox up -d → all 4 containers
reported Healthy.
- curl /ready → {"ready":true}
- curl portal / → HTTP 200 with correct <title>.
- orchestrator boot log prints all 7 EXT-* IDs (6 active, 1 resolved).
- /health returns 503 on this particular builder because memory is
'critical' — DB + Redis both 'up'; this is environment-specific and
not caused by PR Z.
Unit: 13 suites / 167 tests still pass after env.ts preprocess change.
Co-Authored-By: Nakamoto, S <defi@defi-oracle.io>
244 lines
7.8 KiB
TypeScript
244 lines
7.8 KiB
TypeScript
/**
|
|
* Dedicated NotaryRegistry deploy script.
|
|
*
|
|
* Self-compiles NotaryRegistry.sol + its two interfaces + the OpenZeppelin
|
|
* Ownable dependency via solc-js in-process, so it does NOT depend on
|
|
* `hardhat compile` (hardhat's source-glob picks up node_modules under
|
|
* contracts/ and trips HH1006 on this repo — see E2E helper
|
|
* orchestrator/tests/e2e/helpers/compileNotaryRegistry.ts for the same
|
|
* trick).
|
|
*
|
|
* Environment inputs (all read from `process.env`, no CLI args):
|
|
*
|
|
* NOTARY_RPC_URL RPC endpoint (required unless NOTARY_DRY_RUN=1)
|
|
* NOTARY_DEPLOYER_PRIVATE_KEY Hex-encoded funded deployer key (required unless NOTARY_DRY_RUN=1)
|
|
* NOTARY_INITIAL_OWNER Address that receives ownership (defaults to deployer)
|
|
* NOTARY_DRY_RUN "1" to compile + print calldata shape + skip sending
|
|
*
|
|
* Usage:
|
|
*
|
|
* # From contracts/:
|
|
* NOTARY_RPC_URL=https://rpc.public-0138.defi-oracle.io \
|
|
* NOTARY_DEPLOYER_PRIVATE_KEY=0x... \
|
|
* npx ts-node scripts/deploy-notary-registry.ts
|
|
*
|
|
* # Dry run (no RPC contact, no key required — CI smoke test):
|
|
* NOTARY_DRY_RUN=1 npx ts-node scripts/deploy-notary-registry.ts
|
|
*
|
|
* The script prints a machine-readable JSON envelope as its LAST line so
|
|
* callers (Makefile, CI, scripts piping into .env.sandbox) can grep the
|
|
* address out:
|
|
*
|
|
* {"contract":"NotaryRegistry","address":"0x...","txHash":"0x...","chainId":138}
|
|
*/
|
|
|
|
import { readFileSync } from "node:fs";
|
|
import { dirname, join, resolve } from "node:path";
|
|
import { ContractFactory, JsonRpcProvider, Wallet, isAddress } from "ethers";
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
|
|
const solc = require("solc");
|
|
|
|
const CONTRACTS_ROOT = resolve(__dirname, "..");
|
|
const OZ_ROOT = join(CONTRACTS_ROOT, "node_modules", "@openzeppelin");
|
|
|
|
type AbiFragment = Record<string, unknown>;
|
|
|
|
interface CompiledArtifact {
|
|
abi: AbiFragment[];
|
|
bytecode: string;
|
|
}
|
|
|
|
interface SolcSource {
|
|
content: string;
|
|
}
|
|
|
|
interface SolcInput {
|
|
language: "Solidity";
|
|
sources: Record<string, SolcSource>;
|
|
settings: {
|
|
optimizer: { enabled: true; runs: number };
|
|
outputSelection: Record<string, Record<string, string[]>>;
|
|
};
|
|
}
|
|
|
|
interface SolcOutput {
|
|
errors?: Array<{ severity: "error" | "warning"; formattedMessage: string }>;
|
|
contracts: Record<
|
|
string,
|
|
Record<string, { abi: AbiFragment[]; evm: { bytecode: { object: string } } }>
|
|
>;
|
|
}
|
|
|
|
function findImports(requestedPath: string): { contents: string } | { error: string } {
|
|
if (requestedPath.startsWith("@openzeppelin/")) {
|
|
const rel = requestedPath.replace("@openzeppelin/", "");
|
|
try {
|
|
return { contents: readFileSync(join(OZ_ROOT, rel), "utf8") };
|
|
} catch (e) {
|
|
return { error: `Could not read ${requestedPath}: ${(e as Error).message}` };
|
|
}
|
|
}
|
|
try {
|
|
return { contents: readFileSync(join(CONTRACTS_ROOT, requestedPath), "utf8") };
|
|
} catch (e) {
|
|
return { error: (e as Error).message };
|
|
}
|
|
}
|
|
|
|
function collectSources(entryPath: string): Record<string, SolcSource> {
|
|
const sources: Record<string, SolcSource> = {};
|
|
const stack: string[] = [entryPath];
|
|
const seen = new Set<string>();
|
|
|
|
while (stack.length > 0) {
|
|
const cur = stack.pop()!;
|
|
if (seen.has(cur)) continue;
|
|
seen.add(cur);
|
|
|
|
let content: string;
|
|
if (cur === entryPath) {
|
|
content = readFileSync(join(CONTRACTS_ROOT, "NotaryRegistry.sol"), "utf8");
|
|
} else {
|
|
const resolved = findImports(cur);
|
|
if ("error" in resolved) {
|
|
throw new Error(`Unresolved import: ${cur} (${resolved.error})`);
|
|
}
|
|
content = resolved.contents;
|
|
}
|
|
sources[cur] = { content };
|
|
|
|
const importRe = /^\s*import\s+(?:\{[^}]+\}\s+from\s+)?"([^"]+)";/gm;
|
|
let m: RegExpExecArray | null;
|
|
while ((m = importRe.exec(content)) !== null) {
|
|
const rawImport = m[1];
|
|
let normalised: string;
|
|
if (rawImport.startsWith("@openzeppelin/")) {
|
|
normalised = rawImport;
|
|
} else if (rawImport.startsWith("./") || rawImport.startsWith("../")) {
|
|
const curDir = cur.includes("/") ? dirname(cur) : ".";
|
|
const joined = join(curDir, rawImport);
|
|
normalised = joined.startsWith(".") ? joined.slice(2) : joined;
|
|
} else {
|
|
normalised = rawImport;
|
|
}
|
|
if (!seen.has(normalised)) stack.push(normalised);
|
|
}
|
|
}
|
|
|
|
return sources;
|
|
}
|
|
|
|
function compileNotaryRegistry(): CompiledArtifact {
|
|
const entry = "NotaryRegistry.sol";
|
|
const sources = collectSources(entry);
|
|
const input: SolcInput = {
|
|
language: "Solidity",
|
|
sources,
|
|
settings: {
|
|
optimizer: { enabled: true, runs: 200 },
|
|
outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } },
|
|
},
|
|
};
|
|
const output: SolcOutput = JSON.parse(
|
|
solc.compile(JSON.stringify(input), { import: findImports }),
|
|
);
|
|
const fatal = (output.errors ?? []).filter((e) => e.severity === "error");
|
|
if (fatal.length > 0) {
|
|
throw new Error(
|
|
`[deploy-notary-registry] solc compile failed:\n${fatal
|
|
.map((e) => e.formattedMessage)
|
|
.join("\n")}`,
|
|
);
|
|
}
|
|
const artifact = output.contracts[entry]?.NotaryRegistry;
|
|
if (!artifact) {
|
|
throw new Error(
|
|
"[deploy-notary-registry] solc did not emit NotaryRegistry artifact",
|
|
);
|
|
}
|
|
return {
|
|
abi: artifact.abi,
|
|
bytecode: "0x" + artifact.evm.bytecode.object,
|
|
};
|
|
}
|
|
|
|
function require1(name: string): string {
|
|
const v = process.env[name];
|
|
if (!v) {
|
|
throw new Error(`[deploy-notary-registry] ${name} is required`);
|
|
}
|
|
return v;
|
|
}
|
|
|
|
async function main(): Promise<void> {
|
|
const dryRun = process.env.NOTARY_DRY_RUN === "1";
|
|
const artifact = compileNotaryRegistry();
|
|
|
|
if (dryRun) {
|
|
const initialOwner =
|
|
process.env.NOTARY_INITIAL_OWNER ||
|
|
"0x0000000000000000000000000000000000000001";
|
|
if (!isAddress(initialOwner)) {
|
|
throw new Error(
|
|
`[deploy-notary-registry] NOTARY_INITIAL_OWNER is not a valid address: ${initialOwner}`,
|
|
);
|
|
}
|
|
const factory = new ContractFactory(artifact.abi, artifact.bytecode);
|
|
const deployTx = await factory.getDeployTransaction(initialOwner);
|
|
const envelope = {
|
|
contract: "NotaryRegistry",
|
|
dryRun: true,
|
|
initialOwner,
|
|
bytecodeLength: artifact.bytecode.length,
|
|
calldataLength: (deployTx.data as string).length,
|
|
abiEntryCount: artifact.abi.length,
|
|
};
|
|
console.log(JSON.stringify(envelope));
|
|
return;
|
|
}
|
|
|
|
const rpcUrl = require1("NOTARY_RPC_URL");
|
|
const pk = require1("NOTARY_DEPLOYER_PRIVATE_KEY");
|
|
const provider = new JsonRpcProvider(rpcUrl, undefined, {
|
|
staticNetwork: true,
|
|
cacheTimeout: -1,
|
|
});
|
|
const wallet = new Wallet(pk, provider);
|
|
const deployerAddr = await wallet.getAddress();
|
|
const initialOwner = process.env.NOTARY_INITIAL_OWNER || deployerAddr;
|
|
if (!isAddress(initialOwner)) {
|
|
throw new Error(
|
|
`[deploy-notary-registry] NOTARY_INITIAL_OWNER is not a valid address: ${initialOwner}`,
|
|
);
|
|
}
|
|
const net = await provider.getNetwork();
|
|
const bal = await provider.getBalance(deployerAddr);
|
|
console.error(
|
|
`[deploy-notary-registry] deployer=${deployerAddr} chainId=${net.chainId} balance=${bal} initialOwner=${initialOwner}`,
|
|
);
|
|
if (bal === BigInt(0)) {
|
|
throw new Error(
|
|
`[deploy-notary-registry] deployer ${deployerAddr} has zero balance on chainId=${net.chainId}. Fund the account before deploying.`,
|
|
);
|
|
}
|
|
|
|
const factory = new ContractFactory(artifact.abi, artifact.bytecode, wallet);
|
|
const contract = await factory.deploy(initialOwner);
|
|
const receipt = await contract.deploymentTransaction()?.wait();
|
|
const address = await contract.getAddress();
|
|
const envelope = {
|
|
contract: "NotaryRegistry",
|
|
address,
|
|
txHash: receipt?.hash,
|
|
chainId: Number(net.chainId),
|
|
initialOwner,
|
|
};
|
|
console.log(JSON.stringify(envelope));
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error(err);
|
|
process.exit(1);
|
|
});
|