#!/usr/bin/env node /** * Deployment script for Alkahest Natural Language Agreement Oracle * * This script deploys all necessary contracts to a blockchain network * and saves the deployment addresses for use by the oracle CLI. */ import { parseArgs } from "util"; import { createWalletClient, http, publicActions, parseEther } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { mainnet, sepolia, baseSepolia, foundry } from "viem/chains"; import { writeFileSync, existsSync, mkdirSync } from "fs"; import { resolve } from "path"; import { fixtures, contracts } from "alkahest-ts"; import { getPrivateKey } from "../utils.js"; // Helper function to display usage function displayHelp() { console.log(` Alkahest Contract Deployment CLI Usage: bun deploy.ts [options] Options: --network Network to deploy to: mainnet, sepolia, base-sepolia, localhost (required) --rpc-url Custom RPC URL (overrides network default) --private-key Deployer's private key (required) --output Output file for deployment addresses (default: ./cli/deployments/.json) --help, -h Display this help message Environment Variables (alternative to CLI options): PRIVATE_KEY Deployer's private key RPC_URL Custom RPC URL Networks: mainnet Ethereum Mainnet sepolia Ethereum Sepolia Testnet base-sepolia Base Sepolia Testnet localhost Local development (Anvil/Hardhat) Examples: # Deploy to local Anvil bun deploy.ts --network localhost --private-key 0x... # Deploy to Sepolia bun deploy.ts --network sepolia --private-key 0x... --rpc-url https://sepolia.infura.io/v3/YOUR-KEY # Using environment variables export PRIVATE_KEY=0x... bun deploy.ts --network localhost `); } // Parse command line arguments function parseCliArgs() { const { values } = parseArgs({ args: process.argv.slice(2), options: { "network": { type: "string" }, "rpc-url": { type: "string" }, "private-key": { type: "string" }, "output": { type: "string" }, "help": { type: "boolean", short: "h" }, }, strict: true, }); return values; } // Get chain configuration function getChain(network: string) { switch (network.toLowerCase()) { case "mainnet": return mainnet; case "sepolia": return sepolia; case "base-sepolia": return baseSepolia; case "localhost": case "local": return foundry; default: throw new Error(`Unknown network: ${network}. Use mainnet, sepolia, base-sepolia, or localhost`); } } async function main() { try { const args = parseCliArgs(); // Display help if requested if (args.help) { displayHelp(); process.exit(0); } // Get configuration const network = args.network; const privateKey = args["private-key"] || getPrivateKey(); let rpcUrl = args["rpc-url"] || process.env.RPC_URL; // Validate required parameters if (!network) { console.error("āŒ Error: Network is required. Use --network "); console.error("Run with --help for usage information."); process.exit(1); } if (!privateKey) { console.error("āŒ Error: Private key is required."); console.error("\nšŸ’” You can either:"); console.error(" 1. Set it globally: nla wallet:set --private-key "); console.error(" 2. Use for this command only: --private-key "); console.error(" 3. Set PRIVATE_KEY environment variable"); console.error("\nRun with --help for usage information."); process.exit(1); } // Get chain config const chain = getChain(network); // Set default RPC URL if not provided if (!rpcUrl) { if (network === "localhost") { rpcUrl = "http://localhost:8545"; } else { console.error("āŒ Error: RPC URL is required for non-localhost networks. Use --rpc-url"); process.exit(1); } } console.log("šŸš€ Starting Alkahest Contract Deployment\n"); console.log("Configuration:"); console.log(` 🌐 Network: ${network}`); console.log(` šŸ“” RPC URL: ${rpcUrl}`); console.log(` šŸ”‘ Deployer: ${privateKey.slice(0, 6)}...${privateKey.slice(-4)}\n`); // Create deployer account and client const account = privateKeyToAccount(privateKey as `0x${string}`); const client = createWalletClient({ account, chain, transport: http(rpcUrl), }).extend(publicActions); console.log(`āœ… Deployer address: ${account.address}\n`); // Check balance const balance = await client.getBalance({ address: account.address }); console.log(`šŸ’° Deployer balance: ${parseFloat((balance / 10n ** 18n).toString()).toFixed(4)} ETH\n`); if (balance === 0n) { console.error("āŒ Error: Deployer has no ETH. Please fund the account first."); process.exit(1); } // Import contract artifacts from alkahest console.log("šŸ“¦ Loading contract artifacts...\n"); const EAS = fixtures.EAS; const SchemaRegistry = fixtures.SchemaRegistry; const MockERC20Permit = fixtures.MockERC20Permit; const TrustedOracleArbiter = contracts.TrustedOracleArbiter; const StringObligation = contracts.StringObligation; const ERC20EscrowObligation = contracts.ERC20EscrowObligation; const ERC20PaymentObligation = contracts.ERC20PaymentObligation; const ERC20BarterUtils = contracts.ERC20BarterUtils; console.log("āœ… Contract artifacts loaded\n"); // Deployment addresses const addresses: Record = {}; // Helper to deploy contracts async function deployContract( name: string, abi: any, bytecode: string, args: any[] = [] ): Promise { console.log(`šŸ“ Deploying ${name}...`); const hash = await client.deployContract({ abi, bytecode: bytecode as `0x${string}`, args, }); console.log(` Transaction: ${hash}`); const receipt = await client.waitForTransactionReceipt({ hash }); if (!receipt.contractAddress) { throw new Error(`Failed to deploy ${name}`); } console.log(` āœ… Deployed at: ${receipt.contractAddress}\n`); return receipt.contractAddress; } // Deploy core contracts console.log("šŸ—ļø Deploying core contracts...\n"); addresses.easSchemaRegistry = await deployContract( "EAS Schema Registry", SchemaRegistry.abi, SchemaRegistry.bytecode.object ); addresses.eas = await deployContract( "EAS", EAS.abi, EAS.bytecode.object, [addresses.easSchemaRegistry] ); // Deploy arbiters console.log("āš–ļø Deploying arbiters...\n"); addresses.trustedOracleArbiter = await deployContract( "Trusted Oracle Arbiter", TrustedOracleArbiter.abi.abi, TrustedOracleArbiter.abi.bytecode.object, [addresses.eas] ); // Deploy obligations console.log("šŸ“‹ Deploying obligations...\n"); addresses.stringObligation = await deployContract( "String Obligation", StringObligation.abi.abi, StringObligation.abi.bytecode.object, [addresses.eas, addresses.easSchemaRegistry] ); addresses.erc20EscrowObligation = await deployContract( "ERC20 Escrow Obligation", ERC20EscrowObligation.abi.abi, ERC20EscrowObligation.abi.bytecode.object, [addresses.eas, addresses.easSchemaRegistry] ); addresses.erc20PaymentObligation = await deployContract( "ERC20 Payment Obligation", ERC20PaymentObligation.abi.abi, ERC20PaymentObligation.abi.bytecode.object, [addresses.eas, addresses.easSchemaRegistry] ); console.log("šŸ”„ Deploying barter utils...\n"); addresses.erc20BarterUtils = await deployContract( "ERC20 Barter Utils", ERC20BarterUtils.abi.abi, ERC20BarterUtils.abi.bytecode.object, [ addresses.eas, addresses.erc20EscrowObligation, addresses.erc20PaymentObligation, "0x0000000000000000000000000000000000000000", // erc721Escrow (not used) "0x0000000000000000000000000000000000000000", // erc721Payment (not used) "0x0000000000000000000000000000000000000000", // erc1155Escrow (not used) "0x0000000000000000000000000000000000000000", // erc1155Payment (not used) "0x0000000000000000000000000000000000000000", // tokenBundleEscrow (not used) "0x0000000000000000000000000000000000000000", // tokenBundlePayment (not used) "0x0000000000000000000000000000000000000000", // nativeEscrow (not used) "0x0000000000000000000000000000000000000000", // nativePayment (not used) ] ); // Deploy mock ERC20 tokens for testing console.log("šŸŖ™ Deploying mock ERC20 tokens...\n"); addresses.mockERC20A = await deployContract( "Mock ERC20 Token A", MockERC20Permit.abi, MockERC20Permit.bytecode.object, ["Test Token A", "TSTA"] ); addresses.mockERC20B = await deployContract( "Mock ERC20 Token B", MockERC20Permit.abi, MockERC20Permit.bytecode.object, ["Test Token B", "TSTB"] ); addresses.mockERC20C = await deployContract( "Mock ERC20 Token C", MockERC20Permit.abi, MockERC20Permit.bytecode.object, ["Test Token C", "TSTC"] ); // Distribute tokens to known Anvil accounts for testing if (network === "localhost") { console.log("šŸ’ø Distributing tokens to test accounts...\n"); // Get Anvil test accounts dynamically const testAccounts = await client.request({ method: "eth_accounts", }) as `0x${string}`[]; // Use first 3 accounts const recipients = testAccounts.slice(0, 3); for (const testAccount of recipients) { // Skip if it's the deployer account if (testAccount.toLowerCase() === account.address.toLowerCase()) { continue; } // Transfer 10000 tokens to each account for (const [tokenName, tokenAddress] of [ ["Token A", addresses.mockERC20A], ["Token B", addresses.mockERC20B], ["Token C", addresses.mockERC20C], ]) { const hash = await client.writeContract({ address: tokenAddress as `0x${string}`, abi: MockERC20Permit.abi, functionName: "transfer", args: [testAccount, parseEther("10000")], }); await client.waitForTransactionReceipt({ hash }); } console.log(` āœ… Distributed tokens to ${testAccount}`); } console.log("\nāœ… Tokens distributed to test accounts\n"); } // Save deployment addresses // Get the script directory and go up to project root, then into cli/deployments const scriptDir = import.meta.dir; const projectRoot = resolve(scriptDir, "../.."); // Map localhost to devnet for file naming const deploymentFileName = network === "localhost" ? "devnet" : network; const outputPath = args.output || resolve(projectRoot, `cli/deployments/${deploymentFileName}.json`); const outputDir = resolve(outputPath, ".."); if (!existsSync(outputDir)) { mkdirSync(outputDir, { recursive: true }); } const deployment = { network, chainId: chain.id, rpcUrl, deployedAt: new Date().toISOString(), deployer: account.address, addresses, }; writeFileSync(outputPath, JSON.stringify(deployment, null, 2)); console.log("✨ Deployment complete!\n"); console.log("šŸ“„ Deployment details saved to:", outputPath); console.log("\nšŸ“‹ Deployed Addresses:"); Object.entries(addresses).forEach(([name, address]) => { console.log(` ${name}: ${address}`); }); if (network === "localhost") { console.log("\nšŸŖ™ Mock ERC20 Tokens:"); console.log(` Token A (TSTA): ${addresses.mockERC20A}`); console.log(` Token B (TSTB): ${addresses.mockERC20B}`); console.log(` Token C (TSTC): ${addresses.mockERC20C}`); console.log("\nšŸ’° Each test account has 10000 of each token"); } console.log("\nšŸŽÆ Next steps:"); console.log("1. Start the oracle:"); console.log(` nla start-oracle`); } catch (error) { console.error("āŒ Deployment failed:", error); process.exit(1); } } // Run the deployment main();