import memd from 'memd'; import alot from 'alot'; import { class_Uri } from 'atma-utils'; import { File, env, Directory } from 'atma-io'; import type { ContractBase } from 'dequanto/contracts/ContractBase'; import type { TAbiItem } from 'dequanto/types/TAbi'; import type { TEth } from 'dequanto/models/TEth'; import { EoAccount } from 'dequanto/models/TAccount'; import { HardhatWeb3Client } from 'dequanto/hardhat/HardhatWeb3Client'; import { Web3Client } from 'dequanto/clients/Web3Client'; import { $logger } from 'dequanto/utils/$logger'; import { $number } from 'dequanto/utils/$number'; import { $require } from 'dequanto/utils/$require'; import { IGeneratorSources } from 'dequanto/gen/Generator'; import { $path } from 'dequanto/utils/$path'; import { ContractClassFactory, IContractWrapped } from 'dequanto/contracts/ContractClassFactory'; import { BlockchainExplorerFactory } from 'dequanto/explorer/BlockchainExplorerFactory'; import { IWeb3EndpointOptions } from 'dequanto/clients/interfaces/IWeb3EndpointOptions'; import { TPlatform } from 'dequanto/models/TPlatform'; import { Web3ClientFactory } from 'dequanto/clients/Web3ClientFactory'; import { ContractDeployment } from 'dequanto/contracts/deploy/ContractDeployment'; import { ContractDeployer } from 'dequanto/contracts/deploy/ContractDeployer'; import { $sig } from 'dequanto/utils/$sig'; import { $address } from 'dequanto/utils/$address'; import { $promise } from 'dequanto/utils/$promise'; import { $date } from 'dequanto/utils/$date'; import { $hex } from 'dequanto/utils/$hex'; import { TAddress } from 'dequanto/models/TAddress'; import { $contract } from 'dequanto/utils/$contract'; import { $dependency } from 'dequanto/utils/$dependency'; import { TTransport } from 'dequanto/rpc/transports/ITransport'; import { Constructor } from 'dequanto/utils/types'; import { BlockchainExplorer } from 'dequanto/explorer/BlockchainExplorer'; import { Rpc } from 'dequanto/rpc/Rpc'; type THardhatLib = typeof import('hardhat'); const rgx_CONTRACT_NAME = /contract\s+(?[\w_]+)/ig; export class HardhatProvider { @memd.deco.memoize() client(network: 'hardhat' | 'localhost' = 'hardhat', opts: IWeb3EndpointOptions = null) { opts ??= {}; if (opts.web3 == null && opts.endpoints == null) { if (network == 'localhost') { opts.endpoints = [ { url: 'http://127.0.0.1:8545' }, // Use `manual` for subscriptions only, otherwise BatchRequests will fail, as they are not implemented yet // https://github.com/NomicFoundation/hardhat/issues/1324 { url: 'ws://127.0.0.1:8545' }, ]; } else { opts.web3 = this.getHardhatProvider(); } } const client = new HardhatWeb3Client(opts); return client; } /** * Creates an independent in-memory fork and returns its HardhatWeb3Client. * Each call creates a new provider with the requested chainId and fork state, * allowing multiple forks to coexist without resetting the shared instance. * Use this when a separate chainId is needed, since hardhat_reset preserves * the existing provider's chainId. Supports Hardhat 2 and 3. */ async createFork (params: { chainId: number platform?: TPlatform url?: string block?: number | 'latest' }) { let { block, platform } = params; let url = params.url ?? await this.getRpcUrl(platform); let chainId = $require.Number(params.chainId, 'Invalid fork chainId'); const hh = await this.getHardhat(); const forking = { enabled: true, url, ...(block != null && block !== 'latest' ? { blockNumber: block } : {}), }; // Hardhat 3 exposes a network manager; earlier releases use connect(). const network = hh.network as typeof hh.network & { create?: (options: any) => Promise<{ provider: any }>; connect?: (options: any) => Promise<{ provider: any }>; }; const create = network.create ?? network.connect; let provider; if (typeof create === 'function') { const networkName = Object.keys(hh.config.networks) .find(name => (hh.config.networks[name] as any).type === 'edr-simulated'); $require.notNull(networkName, 'Hardhat 3 requires an edr-simulated network configuration'); const connection = await create.call(network, { network: networkName, override: { chainId, forking }, }); provider = connection.provider; } else { // Hardhat 2 has no public factory. Load its internal factory only here. const { createProvider } = await $dependency.load( 'hardhat/internal/core/providers/construction.js' ); const config = { ...hh.config, networks: { ...hh.config.networks, hardhat: { ...hh.config.networks.hardhat, chainId, forking }, }, }; provider = await createProvider(config, 'hardhat', hh.artifacts); } const client = new HardhatWeb3Client({ web3: provider, chainId: chainId }); client.configureFork(params.platform); return client; } async forked (params: { platform: TPlatform, url?: string, block?: number | 'latest' }) { const client = await this.client('hardhat'); let { platform, block } = params; let url = params.url ?? await this.getRpcUrl(platform); await client.debug.reset({ forking: { jsonRpcUrl: url, blockNumber: block, } }); client.configureFork(params.platform); return client; } @memd.deco.memoize() async explorer (network: 'hardhat' | 'localhost' = 'hardhat') { let client = await this.client(network); return new BlockchainExplorer({ platform: 'hardhat', getWeb3: () => client, ABI_CACHE: '', }); } @memd.deco.memoize() deployer(index: number = 0): EoAccount { // const hh = await this.getHardhat(); // const accounts: any = hh.config.networks.hardhat.accounts; const mnemonic = `test test test test test test test test test test test junk`; const account = $sig.$account.fromMnemonic(mnemonic, index); return { key: account.key, address: account.address, }; } async getFactoryForClass (Ctor: Constructor, options?: { deployer?: EoAccount arguments?: any[] client?: Web3Client }): Promise<{ factory: ContractDeployment abi: TAbiItem[] bytecode: TEth.Hex deployedBytecode: TEth.Hex }> { let client = options?.client ?? this.client(); let signer = options?.deployer ?? this.deployer(); let params = options?.arguments ?? []; let factory = await this.getFactory(Ctor, client, signer, params); return { factory, bytecode: factory.ctx.bytecode, deployedBytecode: factory.ctx.deployedBytecode, abi: factory.ctx.abi } } async deployClass(Ctor: Constructor, options?: { deployer?: EoAccount arguments?: any[] client?: Web3Client tx?: TEth.Hex }): Promise<{ contract: T receipt: TEth.TxReceipt abi: TAbiItem[] bytecode: TEth.Hex deployedBytecode: TEth.Hex }> { let client = options?.client ?? this.client(); let { factory, abi } = await this.getFactoryForClass(Ctor, options); let receipt: TEth.TxReceipt; try { receipt = options?.tx ? await client.getTransactionReceipt(options.tx) : await factory.deploy(); } catch (error) { let wrapped = new Error(`Deploy ${Ctor.name} failed: ` + error.message + `\n ${error.stack}`); (wrapped as any).data = error.data; throw wrapped; } let address = $address.toChecksum(receipt.contractAddress); $logger.log(`${receipt.status ? '✅' : '⛔' } Contract bold<${Ctor.name}> deployed to bold<${address}> in tx:${receipt.transactionHash}`); await $promise.waitForTrue(async () => { let code = await client.getCode(address); if ($hex.isEmpty(code)) { $logger.log(`⏳ Waiting for the contract data to be indexed...`); return false; } return true; }, { timeoutMessage: `${receipt.transactionHash} did not deploy in 30s`, timeoutMs: $date.parseTimespan('30s') }); let contract = new Ctor(address, client); return { contract, receipt, abi: contract.abi, bytecode: factory.ctx.bytecode, deployedBytecode: factory.ctx.deployedBytecode }; } async deploySol (solContractPath: string, options?: { client?: Web3Client arguments?: any[], deployer?: EoAccount, paths?: { root?: string artifacts?: string }, contractName?: string tmpDir?: string }): Promise<{ contract: TReturn ContractCtor: Constructor, receipt: TEth.TxReceipt abi: TAbiItem[] bytecode: TEth.Hex source: IGeneratorSources }> { const client = options?.client ?? this.client(); const args = options?.arguments ?? []; const signer = options?.deployer ?? this.deployer(); const { abi, bytecode, source } = await this.compileSol(solContractPath, options); const Factory = await this.getFactory([abi, bytecode], client, signer, args); const receipt = await Factory.deploy(); const explorer = await this.explorer(); const meta = { name: options?.contractName, abi: abi, address: receipt.contractAddress }; explorer.inMemoryDb.push(meta); $contract.store.register(meta); const { contract, ContractCtor } = await ContractClassFactory.fromAbi(receipt.contractAddress, abi, client, explorer, { contractName: meta.name }); return { contract, ContractCtor, receipt, abi, bytecode, source }; } async deployBytecode (hex: TEth.Hex, options?: { client?: Web3Client arguments?: any[], deployer?: EoAccount, paths?: { root?: string artifacts?: string }, contractName?: string abi?: TAbiItem[] }): Promise<{ contract: TReturn ContractCtor: Constructor receipt: TEth.TxReceipt abi: TAbiItem[] bytecode: TEth.Hex }> { const client = options?.client ?? this.client(); const args = options?.arguments ?? []; const signer = options?.deployer ?? this.deployer(); const { abi } = options ?? {}; const bytecode = hex; const Factory = await this.getFactory([ abi , bytecode ], client, signer, args); const receipt = await Factory.deploy(); const { contract, ContractCtor } = await ContractClassFactory.fromAbi(receipt.contractAddress, abi, client, null); const explorer = await this.explorer(); const meta = { name: options.contractName, abi: abi, address: receipt.contractAddress }; explorer.inMemoryDb.push(meta); $contract.store.register(meta); return { contract, ContractCtor, abi, bytecode, receipt: receipt, }; } async compileSolDirectory (dir: string, options?: { paths?: { root?: string artifacts?: string }, contractName?: string tsgen?: boolean install?: string }): Promise<{ abi: TAbiItem[] bytecode: TEth.Hex output: string // renamed output artifact: string source: IGeneratorSources ContractCtor: Constructor linkReferences?: Record> }[]> { dir = $path.normalize(dir); if (await Directory.existsAsync(dir) === false) { throw new Error(`Directory "${dir}" does not exist`); } const paths = { sources: dir, root: options?.paths?.root, artifacts: options?.paths?.artifacts, }; const hhOptions = { ...paths, tsgen: options?.tsgen ?? false, install: options?.install ?? void 0 }; const hh = await this.getHardhat(); await hh.run('compile', hhOptions); const files = await Directory.readFilesAsync(paths.sources, '**/*.sol'); const results = await alot(files).mapAsync(async file => { let solContractPath = file.uri.toLocalFile(); return await this.getContractFromSolPath(solContractPath, { paths }); }).toArrayAsync(); return results.filter(x => x != null); } async compileSol (solContractPath: string, options?: { paths?: { root?: string artifacts?: string }, contractName?: string tsgen?: boolean install?: string }): Promise<{ abi: TAbiItem[] bytecode: TEth.Hex output: string // renamed output artifact: string source: IGeneratorSources ContractCtor: Constructor linkReferences?: Record> }> { solContractPath = $path.normalize(solContractPath); const dir = $path.getDirectory(solContractPath); const paths = { sources: dir, root: options?.paths?.root, artifacts: options?.paths?.artifacts, }; let hhOptions = { ...paths, tsgen: options?.tsgen ?? false, install: options?.install ?? void 0 }; const hh = await this.getHardhat(); await hh.run('compile', hhOptions); return await this.getContractFromSolPath (solContractPath, { contractName: options?.contractName, paths }); } async deployCode ( solidityCode: string, options: Parameters[1] = {} ) { let { tmpFile, tmpDir, options: optionsNormalized } = await this.createTmpFile(solidityCode, options); try { return await this.deploySol (tmpFile, optionsNormalized); } finally { try { await Directory.removeAsync(tmpDir); } catch (_) { } } } async linkReferences ( bytecode: TEth.Hex, linkReferences: Record>, addresses: Record ): Promise { for (let path in linkReferences) { for (let name in linkReferences[path]) { let address = addresses[name]; $require.AddressNotEmpty(address, `Address for "${path}:${name}" not found`); let str = `${path}:${name}`; let hash = $contract.keccak256(str, 'hex').substring(2, 34 + 2); let placeholder = `__$${hash}$__`; bytecode = bytecode.replaceAll(placeholder, address.substring(2)) as TEth.Hex; } } return bytecode; } async compileCode (solidityCode: string, options: Parameters[1] = {}) { let { tmpFile, tmpDir, options: optionsNormalized } = await this.createTmpFile(solidityCode, options); try { return await this.compileSol(tmpFile, optionsNormalized); } finally { try { await Directory.removeAsync(tmpDir); } catch (_) { } } } private async createTmpFile(solidityCode: string, options: Parameters[1] = {}) { // Ensure the code includes the necessary pragmas and license header if (solidityCode.includes('pragma solidity') === false) { solidityCode = `pragma solidity ^0.8.0;\n${solidityCode}`; } if (solidityCode.includes('SPDX-License-Identifier') === false) { solidityCode = `// SPDX-License-Identifier: MIT\n${solidityCode}`; } let contractName = options.contractName; if (contractName == null) { let matches = Array.from(solidityCode.matchAll(/contract\s+(?[\w]+)/g)); contractName = matches[matches.length - 1].groups.name; options.contractName = contractName; } $require.notNull(contractName, `Contract name not resolved from the code`); let rnd = $number.randomInt(0, 10**10); let path = `hardhat/contracts/${contractName}_${rnd}.sol`; let tmp = options?.tmpDir ? class_Uri.combine(options.tmpDir, `tmp${rnd}`, path) : env.getTmpPath(path); let root = tmp.replace(/contracts\/[^/]+$/, ''); options.paths = { root }; await File.writeAsync(tmp, solidityCode); return { tmpFile: tmp, tmpDir: root, options } } private async getFactory ( mix: string | Constructor | [ abi: TAbiItem[], bytecode: TEth.Hex ], client: Web3Client, signer: EoAccount, params: any[], ): Promise { let deployer = new ContractDeployer(client, signer); if (typeof mix === 'function') { return await deployer.prepareDeployment({ contract: mix, params }) } if (typeof mix === 'string') { if (mix.endsWith('json')) { return await deployer.prepareDeployment({ path: mix, params }) } return await deployer.prepareDeployment({ name: mix, params }) } let [ abi, bytecode ] = mix; return await deployer.prepareDeployment({ bytecode, abi, params }) } @memd.deco.memoize() public async getHardhat (): Promise { let loaded; try { loaded = await $dependency.load('hardhat'); } catch (error) { if (error.code !== 'ERR_REQUIRE_ESM' && error.code !== 'ERR_REQUIRE_ASYNC_MODULE') { throw error; } // Preserve native import when this file is compiled to CommonJS/AMD. loaded = await new Function('return import("hardhat")')(); } return loaded.default ?? loaded; } private async getHardhatProvider (): Promise { const hh = await this.getHardhat(); return hh.network.provider as any as TTransport.Transport; } private async getArtifactJsonPath (solContractPath: string, options: { contractName?: string paths: { root: string artifacts: string } }) { let outputDir = class_Uri.combine(options.paths.artifacts, solContractPath, '/'); if (await Directory.existsAsync(outputDir) === false) { let content = await this.getSolFileContent(solContractPath, options); if (rgx_CONTRACT_NAME.test(content) === false) { return null; } throw new Error(`No JSON files were found in ${outputDir} for ${solContractPath}`); } if (options.contractName != null) { let artifactJsonPath = class_Uri.combine(outputDir, `${options.contractName}.json`); if (await File.existsAsync(artifactJsonPath)) { return artifactJsonPath; } } const { filename } = $path.getFilename(solContractPath); if (filename != null) { let artifactJsonPath = class_Uri.combine(outputDir, `${filename}.json`); if (await File.existsAsync(artifactJsonPath)) { return artifactJsonPath; } } if (filename == null) { throw new Error(`Filename was not extracted from ${solContractPath}`); } let files = await Directory.readFilesAsync(outputDir); let jsons = files.filter(x => /(? file.uri.file ).join(', ')}`); throw new Error(`Compiled JSON data not found for "${filename}" in "${outputDir}/"`); } let artifactJsonPath = jsonFile.uri.toString(); return artifactJsonPath; function getJsonFile (files: InstanceType[], filename: string) { return files.find(file => { return file.uri.file === `${filename}.json`; }); } } private async getPaths (paths?: { root?: string, artifacts?: string }) { let root = paths?.root; let artifacts = paths?.artifacts; if (root == null) { root = 'file://' + $path.normalize(process.cwd()); } if (artifacts == null) { artifacts = class_Uri.combine(root, 'artifacts/'); } return { root, artifacts }; } private async getSolFileContent (solContractPath, options: { paths: { root }}) { let sourceFile = solContractPath; if (await File.existsAsync(sourceFile) === false) { sourceFile = class_Uri.combine(options.paths.root, sourceFile); } if (await File.existsAsync(sourceFile) === false) { throw new Error(`Source file "${solContractPath}" not found in ${options.paths.root}`); } let source = await File.readAsync(sourceFile); return source; } public async getContractFromSolPath (solContractPath: string, options?: { contractName?: string paths: { sources?: string root?: string; artifacts?: string; }; }) { let { root, artifacts } = await this.getPaths (options?.paths); let solContractPathRootRelative = $path.getRelativePath(solContractPath, root); let artifactJsonPath = await this.getArtifactJsonPath(solContractPathRootRelative, { contractName: options?.contractName, paths: { root, artifacts, }, }); if (artifactJsonPath == null) { return null; } let { abi, bytecode, contractName, linkReferences } = await File.readAsync (artifactJsonPath); let { contract, ContractCtor } = ContractClassFactory.fromAbi(null, abi, null, null, { contractName, $meta: { name: contractName, source: solContractPathRootRelative, artifact: artifactJsonPath } }) let files = options?.paths?.sources ? await Directory.readFilesAsync(options.paths.sources, '*.sol') : []; let fileMap = await alot(files) .mapAsync(async file => { return { key: file.uri.toString(), content: await file.readAsync() }; }) .toDictionaryAsync(x => x.key, x => ({ content: x.content })); return { abi, bytecode, output: artifactJsonPath, artifact: artifactJsonPath, source: { contractName: contractName ?? options?.contractName, files: fileMap }, linkReferences, ContractCtor }; } private async getRpcUrl (platform: TPlatform) { $require.notNull(platform, `Platform is required to resolve the RPC url for`); let platformClient = await Web3ClientFactory.get(platform); let url = await platformClient.getNodeURL({ ws: false }); // Hardhat seems to support only HTTPS nodes as fork sources $require.True(/^(http)/.test(url), `Requires the HTTP path of a node to fork: ${url}`); return url; } } interface IJsonArtifact { abi: TEth.Abi.Item[] bytecode: TEth.Hex contractName: string linkReferences: Record> }