dkg-evm-module
Version:
Smart contracts for OriginTrail V8
563 lines (485 loc) • 17.1 kB
text/typescript
/* eslint-disable @typescript-eslint/no-explicit-any */
import 'dotenv/config';
import { execSync } from 'child_process';
import * as fs from 'fs';
import { ApiPromise, HttpProvider } from '@polkadot/api';
import { Keyring } from '@polkadot/keyring';
import { u8aToHex } from '@polkadot/util';
import * as polkadotCryptoUtils from '@polkadot/util-crypto';
import { KeypairType } from '@polkadot/util-crypto/types';
import { AddressLike, Contract } from 'ethers';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
import { HubLib } from '../typechain/contracts/storage/Hub';
type AbiEntry = {
inputs?: Array<{ internalType: string; name: string; type: string }>;
name?: string;
outputs?: Array<{ internalType: string; name: string; type: string }>;
stateMutability?: string;
type: string;
};
type ContractDeployments = {
contracts: {
[contractName: string]: {
evmAddress: string;
substrateAddress?: string;
version: string;
gitBranch: string;
gitCommitHash: string;
deploymentBlock: number;
deploymentTimestamp: number;
deployed: boolean;
migration?: boolean;
};
};
};
type DeploymentParameters = {
newContractName: string;
newContractNameInHub?: string;
passHubInConstructor?: boolean;
setContractInHub?: boolean;
setAssetStorageInHub?: boolean;
additionalArgs?: Array<unknown>;
deterministicDeployment?: boolean;
};
type ContractParameter = {
getterArgs?: any[];
desiredValue?: any[];
setter?: string;
setterArgs: any[];
};
type ContractParametersConfig = {
[parameter: string]: ContractParameter | string | ContractParameter[];
};
type BaseEnvironmentParametersConfig = {
[contractName: string]: ContractParametersConfig;
};
type OverridesConfig = {
overrides?: {
[network: string]: {
[contractName: string]: ContractParametersConfig;
};
};
};
type EnvironmentParametersConfig = BaseEnvironmentParametersConfig &
OverridesConfig;
type ParametersConfig = {
[environment: string]: EnvironmentParametersConfig;
};
type EvmWallet = {
address: string;
mnemonic: string;
privateKey: string;
};
type SubstrateWallet = {
address: string;
mnemonic: string;
privateKey: string;
};
export class Helpers {
hre: HardhatRuntimeEnvironment;
provider: HttpProvider;
repositoryPath: string;
contractDeployments: ContractDeployments;
parametersConfig: ParametersConfig;
newContracts: HubLib.ContractStruct[];
newAssetStorageContracts: HubLib.ContractStruct[];
contractsForReinitialization: AddressLike[];
setParametersEncodedData: HubLib.ForwardCallInputArgsStruct[];
newHashFunctions: Array<string>;
newScoreFunctions: Array<string>;
constructor(hre: HardhatRuntimeEnvironment) {
this.hre = hre;
const endpoint = process.env[`RPC_${this.hre.network.name.toUpperCase()}`];
this.provider = new HttpProvider(endpoint);
this.repositoryPath = this._getGitRepositoryPath();
const deploymentsConfig = `./deployments/${this.hre.network.name}_contracts.json`;
if (fs.existsSync(deploymentsConfig)) {
this.contractDeployments = JSON.parse(
fs.readFileSync(deploymentsConfig).toString(),
);
} else {
this.contractDeployments = { contracts: {} };
}
const parametersConfig = './deployments/parameters.json';
if (fs.existsSync(parametersConfig)) {
this.parametersConfig = JSON.parse(
fs.readFileSync(parametersConfig).toString(),
);
} else {
this.parametersConfig = {
development: {},
devnet: {},
testnet: {},
mainnet: {},
};
}
this.newContracts = [];
this.newAssetStorageContracts = [];
this.contractsForReinitialization = [];
this.setParametersEncodedData = [];
this.newHashFunctions = [];
this.newScoreFunctions = [];
}
public async deploy({
newContractName,
newContractNameInHub = undefined,
passHubInConstructor = true,
setContractInHub = true,
setAssetStorageInHub = false,
additionalArgs = [],
deterministicDeployment = false,
}: DeploymentParameters): Promise<Contract> {
const { deployer } = await this.hre.getNamedAccounts();
const nameInHub = newContractNameInHub
? newContractNameInHub
: newContractName;
if (this.isDeployed(nameInHub)) {
const contractInstance = await this.hre.ethers.getContractAt(
this.getAbi(newContractName),
this.contractDeployments.contracts[nameInHub].evmAddress,
);
if (this.hasFunction(newContractName, 'initialize')) {
// TODO: Reinitialize only if any dependency contract was redeployed
this.contractsForReinitialization.push(
await contractInstance.getAddress(),
);
}
return contractInstance;
}
let hubAddress;
if ('Hub' in this.contractDeployments.contracts) {
hubAddress = this.contractDeployments.contracts['Hub'].evmAddress;
} else {
hubAddress = (await this.hre.deployments.get('Hub')).address;
}
let newContract;
try {
newContract = await this.hre.deployments.deploy(nameInHub, {
contract: newContractName,
from: deployer,
args: passHubInConstructor
? [hubAddress, ...additionalArgs]
: additionalArgs,
deterministicDeployment,
log: true,
});
} catch (error) {
if (this.hre.network.config.environment !== 'development') {
this.saveDeploymentsJson('deployments');
}
let message;
if (error instanceof Error) message = error.message;
else message = String(error);
throw Error(message);
}
const Hub = await this.hre.ethers.getContractAt('Hub', hubAddress);
let tx;
if (setContractInHub) {
if (this.hre.network.config.environment === 'development') {
tx = await Hub.setContractAddress(nameInHub, newContract.address);
await tx.wait();
} else {
this.newContracts.push({ name: nameInHub, addr: newContract.address });
}
} else if (setAssetStorageInHub) {
if (this.hre.network.config.environment === 'development') {
tx = await Hub.setAssetStorageAddress(nameInHub, newContract.address);
await tx.wait();
} else {
this.newAssetStorageContracts.push({
name: nameInHub,
addr: newContract.address,
});
}
}
if (this.hasFunction(newContractName, 'initialize')) {
if (
(setContractInHub || setAssetStorageInHub) &&
this.hre.network.config.environment === 'development'
) {
const newContractInterface = new this.hre.ethers.Interface(
this.getAbi(newContractName),
);
const initializeTx = await Hub.forwardCall(
newContract.address,
newContractInterface.encodeFunctionData('initialize'),
);
await initializeTx.wait();
}
this.contractsForReinitialization.push(newContract.address);
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await this.updateDeploymentsJson(
nameInHub,
newContract.address,
newContract.receipt!.blockNumber,
newContractName,
);
if (this.hre.network.config.environment !== 'development') {
this.saveDeploymentsJson('deployments');
}
return this.hre.ethers.getContractAt(
this.getAbi(newContractName),
newContract.address,
);
}
public async updateContractParameters(
contractName: string,
contract: Contract,
) {
let parameters =
this.parametersConfig[this.hre.network.config.environment]?.[
contractName
];
const overrideParameters =
this.parametersConfig[this.hre.network.config.environment]?.overrides?.[
this.hre.network.name
]?.[contractName];
parameters = { ...parameters, ...overrideParameters };
if (!parameters) {
return;
}
const forwardCall: HubLib.ForwardCallInputArgsStruct = {
contractName,
encodedData: [],
};
for (const [getterName, paramValue] of Object.entries(parameters)) {
const values = Array.isArray(paramValue) ? paramValue : [paramValue];
for (const value of values) {
let getterArgs = [];
let desiredValue;
let setterName;
let setterArgs;
if (value instanceof Object) {
getterArgs = value.getterArgs ? value.getterArgs : [];
desiredValue =
value.setterArgs.length === 1
? value.setterArgs[0]
: value.desiredValue;
setterName = value.setter
? value.setter
: `set${getterName.charAt(0).toUpperCase() + getterName.slice(1)}`;
setterArgs = value.setterArgs;
} else {
desiredValue = value;
setterName = `set${getterName.charAt(0).toUpperCase() + getterName.slice(1)}`;
setterArgs = [value];
}
if (contract.interface.hasFunction(getterName)) {
const currentValue = await contract[getterName](...getterArgs);
if (currentValue.toString() !== desiredValue.toString()) {
console.log(
`Parameter '${getterName}' for ${contractName} in the contract isn't the same as define in config. Blockchain: ${currentValue}. Config: ${desiredValue}.`,
);
if (contract.interface.hasFunction(setterName)) {
const encodedFunctionData = contract.interface.encodeFunctionData(
setterName,
setterArgs,
);
if (this.hre.network.config.environment === 'development') {
console.log(
`[${contractName}] Setting parameter '${getterName}' value to be ${desiredValue}.`,
);
const targetContractAddress =
this.contractDeployments.contracts[contractName].evmAddress;
const hubAddress =
this.contractDeployments.contracts['Hub'].evmAddress;
const Hub = await this.hre.ethers.getContractAt(
'Hub',
hubAddress,
);
const tx = await Hub.forwardCall(
targetContractAddress,
encodedFunctionData,
);
await tx.wait();
} else {
console.log(
`[${contractName}] Adding parameter '${getterName}' value to be set to ${desiredValue} using Hub.`,
);
forwardCall.encodedData.push(encodedFunctionData);
}
} else {
throw Error(
`Setter '${setterName}' doesn't exist in the contract '${contractName}'.`,
);
}
}
} else {
throw Error(
`Parameter '${getterName}' doesn't exist in the contract '${contractName}'.`,
);
}
}
}
if (forwardCall.encodedData.length !== 0) {
this.setParametersEncodedData.push(forwardCall);
}
}
public inConfig(contractName: string): boolean {
return contractName in this.contractDeployments.contracts;
}
public isDeployed(contractName: string): boolean {
if (this.inConfig(contractName)) {
return this.contractDeployments.contracts[contractName].deployed;
} else {
return false;
}
}
public hasFunction(contractName: string, functionName: string): boolean {
const contractAbi = this.getAbi(contractName);
return contractAbi.some(
(entry) => entry.type === 'function' && entry.name === functionName,
);
}
public getAbi(contractName: string): AbiEntry[] {
return JSON.parse(fs.readFileSync(`./abi/${contractName}.json`, 'utf-8'));
}
public resetDeploymentsJson() {
this.contractDeployments = { contracts: {} };
}
public async updateDeploymentsJson(
newContractName: string,
newContractAddress: string,
deploymentBlock: number,
originalContractName: string | null = null,
) {
const contractABI = this.getAbi(originalContractName ?? newContractName);
const isVersionedContract = contractABI.some(
(abiEntry) => abiEntry.type === 'function' && abiEntry.name === 'version',
);
let contractVersion;
if (isVersionedContract) {
const VersionedContract = await this.hre.ethers.getContractAt(
originalContractName ?? newContractName,
newContractAddress,
);
contractVersion = await VersionedContract.version();
} else {
contractVersion = null;
}
this.contractDeployments.contracts[newContractName] = {
evmAddress: newContractAddress,
substrateAddress: this.hre.network.name.startsWith('neuro')
? this.convertEvmWallet(newContractAddress)
: undefined,
version: contractVersion,
gitBranch: this.getCurrentGitBranch(),
gitCommitHash: this.getCurrentGitCommitHash(),
deploymentBlock: deploymentBlock,
deploymentTimestamp: Date.now(),
deployed: true,
};
}
public saveDeploymentsJson(folder: string) {
console.log(
`New or redeployed contracts: ${JSON.stringify(this.newContracts)}`,
);
console.log(
`New or redeployed Asset Storage contracts: ${JSON.stringify(this.newAssetStorageContracts)}`,
);
console.log(
`New or redeployed hash functions set in the proxy: ${JSON.stringify(this.newHashFunctions)}`,
);
console.log(
`New or redeployed score functions set in the proxy: ${JSON.stringify(this.newScoreFunctions)}`,
);
console.log(
`Initialized contracts: ${JSON.stringify(this.contractsForReinitialization)}`,
);
console.log(
`Encoded data for parameters settings: ${JSON.stringify(this.setParametersEncodedData)}`,
);
fs.writeFileSync(
`${folder}/${this.hre.network.name}_contracts.json`,
JSON.stringify(this.contractDeployments, null, 4),
);
}
public async sendNeuro(address: string | undefined, tokenAmount = 2) {
if (address === undefined) {
throw Error('Address cannot be undefined!');
}
const api = await ApiPromise.create({
provider: this.provider,
noInitWarn: true,
});
const transfer = api.tx.balances.transferKeepAlive(
address,
Number(this.hre.ethers.parseUnits(`${tokenAmount}`, 12)),
);
const keyring = new Keyring({ type: 'sr25519' });
const accountUri =
process.env[
`ACCOUNT_WITH_NEURO_URI_${this.hre.network.name.toUpperCase()}`
];
if (!accountUri) {
throw Error('URI for account with Neuro is required!');
}
const account = keyring.createFromUri(accountUri);
const txHash = await transfer.signAndSend(account, { nonce: -1 });
console.log(
`2 Neuro sent to contract at address ${address}. Transaction hash: ${txHash.toHuman()}`,
);
await this._delay(40000);
}
public generateEvmWallet(): EvmWallet {
const wallet = this.hre.ethers.Wallet.createRandom();
return {
address: wallet.address,
mnemonic: wallet.mnemonic!.phrase,
privateKey: wallet.privateKey,
};
}
public async generateSubstrateWallet(
type: KeypairType = 'sr25519',
ss58Prefix = 101,
): Promise<SubstrateWallet> {
await polkadotCryptoUtils.cryptoWaitReady();
const keyring = new Keyring({ type: type, ss58Format: ss58Prefix });
const mnemonic = polkadotCryptoUtils.mnemonicGenerate();
const mnemonicMini = polkadotCryptoUtils.mnemonicToMiniSecret(mnemonic);
const substratePrivateKey = u8aToHex(mnemonicMini);
const substrateAddress = keyring.createFromUri(substratePrivateKey).address;
return {
address: substrateAddress,
mnemonic: mnemonic,
privateKey: substratePrivateKey,
};
}
public convertEvmWallet(evmAddress: string): string {
const address = evmAddress.startsWith('0x')
? evmAddress.slice(2)
: evmAddress;
const substrateAddress = execSync(
`utils/converters/${process.platform}-evm-contract-into-substrate-address ${address}`,
)
.toString()
.replace(/[\r\n]/gm, '');
return substrateAddress;
}
public getCurrentGitCommitHash(): string {
return this._executeGitCommandSync('git rev-parse HEAD');
}
public getCurrentGitBranch(): string {
return this._executeGitCommandSync('git rev-parse --abbrev-ref HEAD');
}
private _delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
private _executeGitCommandSync(command: string): string {
try {
const stdout = execSync(command, { cwd: this.repositoryPath });
return stdout.toString().trim();
} catch (error) {
throw new Error(`exec error: ${error}`);
}
}
private _getGitRepositoryPath(): string {
try {
const stdout = execSync('git rev-parse --show-toplevel');
return stdout.toString().trim();
} catch (error) {
throw new Error(`Could not determine the repository path: ${error}`);
}
}
}