@kadena/hardhat-chainweb
Version:
Hardhat plugin for Kadena's Chainweb network
216 lines (190 loc) • 6.6 kB
text/typescript
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;
}