UNPKG

@nori-zk/mina-token-bridge

Version:

Nori ethereum state settelment and nETH token bridge zkApp

256 lines 11.7 kB
// Load environment variables from .env file import 'dotenv/config'; // Other imports import { Mina, PrivateKey, PublicKey, AccountUpdate, Bool, Field, Poseidon, UInt8, } from 'o1js'; import { Logger, LogPrinter } from 'esm-iso-logger'; import { writeFileSync } from 'fs'; import { resolve } from 'path'; import { rootDir } from '../utils.js'; import { NoriTokenBridge } from '../NoriTokenBridge.js'; import { NoriStorageInterface } from '../NoriStorageInterface.js'; import { FungibleToken } from '../TokenBase.js'; import { Bytes20, Bytes32, Bytes32FieldPair, bridgeHeadNoriSP1HeliosProgramPi0, compileAndVerifyContracts, proofConversionSP1ToPlonkPO2, } from '@nori-zk/o1js-zk-utils'; import { FrC } from '@nori-zk/proof-conversion/min'; import { noriTokenBridgeVkHash } from '../integrity/NoriTokenBridge.VkHash.js'; import { noriStorageInterfaceVkHash } from '../integrity/NoriStorageInterface.VkHash.js'; import { fungibleTokenVkHash } from '../integrity/FungibleToken.VkHash.js'; const logger = new Logger('Deploy'); new LogPrinter('NoriTokenBridge'); // Collect all inputs upfront const possibleNetworkUrl = process.env.MINA_RPC_NETWORK_URL; const possibleNetwork = process.env.MINA_NETWORK; const possibleDeployerKeyBase58 = process.env.MINA_SENDER_PRIVATE_KEY; const fee = Number(process.env.MINA_TX_FEE || 0.1) * 1e9; const possibleStoreHashHex = process.argv[2]; const possibleEthTokenBridgeAddressHex = process.argv[3]; const possibleGenesisRootHex = process.argv[4]; const possibleAdminPublicKeyBase58 = process.argv[5]; // Validate everything in one pass const issues = []; if (!possibleNetworkUrl) issues.push('Missing required env: MINA_RPC_NETWORK_URL'); if (!possibleNetwork) issues.push('Missing required env: MINA_NETWORK'); if (!possibleDeployerKeyBase58) issues.push('Missing required env: MINA_SENDER_PRIVATE_KEY (must be the contract deployer private key)'); if (process.env.NORI_MINA_TOKEN_BRIDGE_PRIVATE_KEY) issues.push('NORI_MINA_TOKEN_BRIDGE_PRIVATE_KEY must not be set for initial deployment — this script generates a random key. Remove it.'); if (process.env.NORI_MINA_TOKEN_BASE_PRIVATE_KEY) issues.push('NORI_MINA_TOKEN_BASE_PRIVATE_KEY must not be set for initial deployment — this script generates a random key. Remove it.'); if (!possibleStoreHashHex) issues.push('Missing required first argument: storeHashHex'); if (!possibleEthTokenBridgeAddressHex) issues.push('Missing required second argument: ethTokenBridgeAddressHex'); if (!possibleGenesisRootHex) issues.push('Missing required third argument: genesisRootHex'); let possibleDeployerKey; if (possibleDeployerKeyBase58) { try { possibleDeployerKey = PrivateKey.fromBase58(possibleDeployerKeyBase58); } catch (e) { issues.push(`MINA_SENDER_PRIVATE_KEY (contract deployer) is not a valid private key: ${e.message}`); } } let possibleStoreHash; if (possibleStoreHashHex) { if (possibleStoreHashHex.length !== 64) { issues.push(`storeHashHex '${possibleStoreHashHex}' must be exactly 64 hex characters (32 bytes), got ${possibleStoreHashHex.length}`); } else { try { possibleStoreHash = Bytes32.fromHex(possibleStoreHashHex); } catch (e) { issues.push(`storeHashHex '${possibleStoreHashHex}' is not a valid 32-byte hex string: ${e.message}`); } } } let possibleAdminPublicKey; if (possibleAdminPublicKeyBase58) { try { possibleAdminPublicKey = PublicKey.fromBase58(possibleAdminPublicKeyBase58); } catch (e) { issues.push(`adminPublicKeyBase58 argument '${possibleAdminPublicKeyBase58}' is not a valid public key: ${e.message}`); } } let possibleEthTokenBridgeAddress; if (possibleEthTokenBridgeAddressHex) { if (possibleEthTokenBridgeAddressHex.length !== 40) { issues.push(`ethTokenBridgeAddressHex '${possibleEthTokenBridgeAddressHex}' must be exactly 40 hex characters (20 bytes), got ${possibleEthTokenBridgeAddressHex.length}`); } else { try { possibleEthTokenBridgeAddress = Bytes20.fromHex(possibleEthTokenBridgeAddressHex).toField(); } catch (e) { issues.push(`ethTokenBridgeAddressHex '${possibleEthTokenBridgeAddressHex}' is not a valid 20-byte hex string: ${e.message}`); } } } let possibleGenesisRoot; if (possibleGenesisRootHex) { if (possibleGenesisRootHex.length !== 64) { issues.push(`genesisRootHex '${possibleGenesisRootHex}' must be exactly 64 hex characters (32 bytes), got ${possibleGenesisRootHex.length}`); } else { try { possibleGenesisRoot = Poseidon.hash(Bytes32.fromHex(possibleGenesisRootHex).toFields()); } catch (e) { issues.push(`genesisRootHex '${possibleGenesisRootHex}' is not a valid 32-byte hex string: ${e.message}`); } } } if (issues.length) { const formatted = [ 'Deploy encountered issues:', ...issues.flatMap((issue, idx) => { const lines = issue.split('\n'); return lines.map((line, lineIdx) => lineIdx === 0 ? `\t${idx + 1}: ${line}` : `\t ${line}`); }), ].join('\n'); logger.fatal(formatted); process.exit(1); } // Type guards — all required values are guaranteed defined after the issues exit above function isPrivateKey(val) { return val !== undefined; } function isBytes32(val) { return val !== undefined; } function isPublicKey(val) { return val !== undefined; } function isField(val) { return val !== undefined; } function isString(val) { return val !== undefined; } if (!isPrivateKey(possibleDeployerKey) || !isBytes32(possibleStoreHash) || !isField(possibleEthTokenBridgeAddress) || !isField(possibleGenesisRoot) || !isString(possibleNetworkUrl) || !isString(possibleNetwork)) { logger.fatal('Internal error: required values undefined after validation.'); process.exit(1); } const deployerKey = possibleDeployerKey; const storeHash = possibleStoreHash; const ethTokenBridgeAddress = possibleEthTokenBridgeAddress; const genesisRoot = possibleGenesisRoot; const networkUrl = possibleNetworkUrl; const networkId = possibleNetwork === 'mainnet' ? 'mainnet' : 'testnet'; // Generate fresh keys for both contracts const tokenBridgePrivateKey = PrivateKey.random(); const tokenBridgePrivateKeyBase58 = tokenBridgePrivateKey.toBase58(); const tokenBasePrivateKey = PrivateKey.random(); const tokenBasePrivateKeyBase58 = tokenBasePrivateKey.toBase58(); const tokenBaseAllowUpdates = true; let adminPublicKey; if (isPublicKey(possibleAdminPublicKey)) { logger.log(`adminPublicKeyBase58 provided: '${possibleAdminPublicKeyBase58}'`); adminPublicKey = possibleAdminPublicKey; } else { logger.warn('No adminPublicKeyBase58 provided as second argument. Defaulting to the public key derived from MINA_SENDER_PRIVATE_KEY.'); adminPublicKey = deployerKey.toPublicKey(); } logger.log(`storeHashHex provided: '${possibleStoreHashHex}'`); logger.log(`ethTokenBridgeAddressHex provided: '${possibleEthTokenBridgeAddressHex}'`); function writeSuccessDetailsToEnvFile(tokenBridgeAddressBase58, tokenBaseAddressBase58, tokenBaseTokenId, tokenBridgeTokenId) { const env = { NORI_MINA_TOKEN_BRIDGE_PRIVATE_KEY: tokenBridgePrivateKeyBase58, NORI_MINA_TOKEN_BRIDGE_ADDRESS: tokenBridgeAddressBase58, NORI_MINA_TOKEN_BASE_PRIVATE_KEY: tokenBasePrivateKeyBase58, NORI_MINA_TOKEN_BASE_ADDRESS: tokenBaseAddressBase58, NORI_MINA_TOKEN_BRIDGE_ADMIN: adminPublicKey.toBase58(), NORI_MINA_TOKEN_BASE_TOKEN_ID: tokenBaseTokenId, NORI_MINA_TOKEN_BRIDGE_TOKEN_ID: tokenBridgeTokenId, NORI_MINA_TOKEN_BASE_ALLOW_VK_UPDATE: tokenBaseAllowUpdates.toString(), // ALWAYS TRUE NORI_MINA_TOKEN_BRIDGE_ALLOW_VK_UPDATE: 'false', }; const envFileStr = Object.entries(env) .map(([key, value]) => `${key}=${value}`) .join('\n') + `\n`; const envFileOutputPath = resolve(rootDir, '..', '.env.nori-mina-token-bridge'); logger.info(`Writing env file with the details: '${envFileOutputPath}'`); writeFileSync(envFileOutputPath, envFileStr, 'utf8'); logger.log(`Wrote '${envFileOutputPath}' successfully.`); } async function deploy() { const deployerAccount = deployerKey.toPublicKey(); const tokenBridgeAddress = tokenBridgePrivateKey.toPublicKey(); const tokenBaseAddress = tokenBasePrivateKey.toPublicKey(); logger.log(`Deployer address: '${deployerAccount.toBase58()}'.`); logger.log(`NoriTokenBridge address: '${tokenBridgeAddress.toBase58()}'.`); logger.log(`FungibleToken address: '${tokenBaseAddress.toBase58()}'.`); const Network = Mina.Network({ networkId, mina: networkUrl }); Mina.setActiveInstance(Network); // Compile and verify all three contracts const { NoriStorageInterfaceVerificationKey, NoriTokenBridgeVerificationKey } = await compileAndVerifyContracts(logger, [ { name: 'NoriStorageInterface', program: NoriStorageInterface, integrityHash: noriStorageInterfaceVkHash, }, { name: 'FungibleToken', program: FungibleToken, integrityHash: fungibleTokenVkHash, }, { name: 'NoriTokenBridge', program: NoriTokenBridge, integrityHash: noriTokenBridgeVkHash, }, ]); const tokenBridge = new NoriTokenBridge(tokenBridgeAddress); const tokenBase = new FungibleToken(tokenBaseAddress); const initialStoreHash = Bytes32FieldPair.fromBytes32(storeHash); logger.log('Creating deployment transaction...'); const txn = await Mina.transaction({ fee, sender: deployerAccount }, async () => { AccountUpdate.fundNewAccount(deployerAccount, 3); logger.log(`Deploying NoriTokenBridge with verification key hash: '${NoriTokenBridgeVerificationKey.hash}'`); await tokenBridge.deploy({ verificationKey: NoriTokenBridgeVerificationKey, adminPublicKey, tokenBaseAddress, storageVKHash: NoriStorageInterfaceVerificationKey.hash, newStoreHash: initialStoreHash, ethTokenBridgeAddress, genesisRoot, noriHeliosProgramPi0: FrC.from(bridgeHeadNoriSP1HeliosProgramPi0), proofConversionPO2: Field.from(proofConversionSP1ToPlonkPO2), }); logger.log('Deploying FungibleToken.'); await tokenBase.deploy({ symbol: 'nETH', src: 'https://github.com/Nori-zk/nori-bridge-sdk', allowUpdates: tokenBaseAllowUpdates, }); await tokenBase.initialize(tokenBridgeAddress, UInt8.from(6), Bool(false)); }); logger.log('Proving transaction'); await txn.prove(); const signedTx = txn.sign([deployerKey, tokenBridgePrivateKey, tokenBasePrivateKey]); logger.log('Sending transaction...'); const pendingTx = await signedTx.send(); logger.log('Waiting for transaction to be included in a block...'); await pendingTx.wait(); const tokenBaseTokenId = tokenBase.deriveTokenId().toString(); const tokenBridgeTokenId = tokenBridge.deriveTokenId().toString(); logger.log(`Token Base Token ID: ${tokenBaseTokenId}`); logger.log(`NoriTokenBridge Token ID: ${tokenBridgeTokenId}`); logger.log('Deployment successful!'); writeSuccessDetailsToEnvFile(tokenBridgeAddress.toBase58(), tokenBaseAddress.toBase58(), tokenBaseTokenId, tokenBridgeTokenId); } deploy().catch((err) => { logger.fatal(`Deploy function encountered an error.\n${String(err)}`); process.exit(1); }); //# sourceMappingURL=deploy.js.map