UNPKG

@kadena/hardhat-chainweb

Version:
216 lines (190 loc) 6.6 kB
import { ethers } from 'ethers'; import { distance } from './chainweb-graph.js'; import { sleep } from './sleep.js'; import { wordToAddress } from './ethers-helpers.js'; import { logError, Logger, logInfo } from './logger.js'; import { ChainwebInProcessConfig } from '../type.js'; import { KadenaNetworkConfig, NetworksConfig } from 'hardhat/types'; import { Chain } from './chain.js'; import { HardhatEthersProvider } from '@nomicfoundation/hardhat-ethers/internal/hardhat-ethers-provider.js'; import { getNetworkStem } from '../pure-utils.js'; interface INetworkOptions { chainweb: ChainwebInProcessConfig; networks: NetworksConfig; chainwebName: string; overrideForking?: { url: string; blockNumber?: number }; } export class ChainwebNetwork { private logger: Logger; public chains: Record<number, Chain>; public graph: Record<number, number[]>; constructor(private config: INetworkOptions) { this.logger = { info: (msg) => logInfo('reset', '-', msg), error: (msg) => logError('reset', '-', msg), }; this.chains = makeChainweb(this.logger, this.config); this.graph = config.chainweb.graph; } getProvider(cid: number) { const chain = this.chains[cid]; if (chain === undefined) { throw new Error(`Chain not found in Chainweb ${cid}`); } const provider = chain.provider; if (provider === null) { throw new Error(`Chain network is not running ${cid}`); } return provider; } async start() { try { this.logger.info('Starting chain networks'); await Promise.all( Object.values(this.chains).map((chain) => { return chain.start(); }), ); this.logger.info('Chainweb chains initialized'); } catch (e) { this.logger.error(`Failure while starting networks: ${e}, ${e.stack}`); await this.stop(); } } async stop() { this.logger.info('Stopping chain networks'); await Promise.all(Object.values(this.chains).map((chain) => chain.stop())); this.logger.info('Stopped chain networks'); } // Mock getProof: // // Call our chainweb SPV api with the necesasry proof parameters. // // This mocks the call of the follwing API: // // http://localhost:1848/chainweb/0.0/evm-development/chain/${trgChain}/spv/chain/${origin.chain}/height/${origin.height}/transaction/${origin.txIdx}/event/${origin.eventIdx} // async getSpvProof( trgChain: number, origin: Omit<Origin, 'originContractAddress'>, ) { // get origin chain const provider = new HardhatEthersProvider( this.getProvider(Number(origin.chain)), `${getNetworkStem(this.config.chainwebName)}${origin.chain}`, ); // Query Event information from origin chain const blockLogs = await provider.getLogs({ fromBlock: origin.height, toBlock: origin.height, }); const txLogs = blockLogs.filter( (l) => BigInt(l.transactionIndex) === origin.txIdx, ); const log = txLogs[Number(origin.eventIdx)]; if (log === undefined || log.removed) { new Error('No log entry found at origin'); } const topics = log.topics; if (topics.length != 4) { throw new Error( `Expected exactly four topics at origin, but got ${topics.length}`, ); } // for target chain to advance enough blocks such that the origin information // is available. // // TODO should fail at least once so that the caller has to wait? // const src = this.chains[Number(origin.chain)]; const trg = this.chains[trgChain]; if (src === undefined || trg === undefined) { throw new Error(`Chain not found in Chainweb`); } const dist = BigInt(distance(src.cid, trg.cid, this.graph)); let trgHeight = BigInt(await trg.getBlockNumber()); while (trgHeight < origin.height + dist) { console.log( `waiting for SPV proof to become available on chain ${trgChain}; current height ${trgHeight}; required height ${origin.height + dist}`, ); await trg.mineRequest(); sleep(100); trgHeight = BigInt(await trg.getBlockNumber()); } const coder = ethers.AbiCoder.defaultAbiCoder(); // FIXME: double check the event signature // (uint32,address,uint64,uint64,uint64) const xorigin = Object.values({ chainId: origin.chain, address: log.address, height: origin.height, txIdx: origin.txIdx, eventIdx: origin.eventIdx, }); // (uint32,address,uint64,(uint32,address,uint64,uint64,uint64)) const xmsg = Object.values({ trgChainId: ethers.toNumber(topics[1]), trgAddress: wordToAddress(topics[2]), opType: ethers.toNumber(topics[3]), data: coder.decode(['bytes'], log.data)[0], origin: xorigin, }); const params = 'tuple(uint32,address,uint64,bytes,tuple(uint32,address,uint64,uint64,uint64))'; const payload = coder.encode([params], [xmsg]); const hash = ethers.keccak256(payload); return ethers.concat([hash, payload]); } } /* *************************************************************************** */ /* Chainweb Network */ function makeChainweb( logger: Logger, config: { chainweb: ChainwebInProcessConfig; networks: NetworksConfig; chainwebName: string; overrideForking?: { url: string; blockNumber?: number }; }, ) { const graph = config.chainweb.graph; const networks = config.networks; // Create Individual Chains logger.info('creating chains'); const chains: Record<number, Chain> = {}; for (const networkName in networks) { if (networkName.includes(getNetworkStem(config.chainwebName))) { const networkConfig = networks[networkName] as KadenaNetworkConfig; if (config.overrideForking?.url) { networkConfig.forking = { enabled: true, ...config.overrideForking }; } chains[networkConfig.chainwebChainId!] = new Chain( networkConfig, config.chainweb.logging, ); } } // Put Chains into the Chainweb Graph logger.info('integrating chains into Chainweb'); for (const c in chains) { if (graph[c] === undefined) { console.log(c, graph); throw new Error(`Missing configuration for chain ${c}`); } chains[c].adjacents = graph[c].map((x) => { const a = chains[x]; if (a === undefined) { throw new Error(`Missing configuration for chain ${x}`); } return chains[x]; }); } return chains; } export interface Origin { chain: bigint; originContractAddress: string; height: bigint; txIdx: bigint; eventIdx: bigint; }