mirror of
https://github.com/datahaven-xyz/datahaven
synced 2026-05-24 09:50:01 +00:00
test: port common tests from moonbeam (#223)
## Overview Ports essential test suites and helper utilities from Moonbeam's battle-tested framework to validate DataHaven's EVM compatibility and precompile functionality. ## What's Included ### 🧪 Test Suites - **Contract Creation Tests** (`test-contract-creation.ts`) - EVM contract deployment verification - CREATE/CREATE2 opcode validation - Smart contract nonce management - Bytecode storage and retrieval - Block fee verification - **Precompile Batch Tests** (`test-precompile-batch.ts`) - Batch operation gas consumption - Recursive batch calls - Call permit integration - EIP-712 signature validation ### 🛠️ Helper Utilities - **Block helpers**: Fee verification, transaction analysis, block exploration - **EVM helpers**: Result validation, signature parsing, receipt handling - **Fee helpers**: Treasury/burn split calculations - **Runtime helpers**: Parameter fetching, versioned constants - **Contract helpers**: Artifact loading, bytecode management ### 📝 Test Contracts - `SimpleContractFactory`: CREATE/CREATE2 test harness - `CallBatchFromConstructor`: Precompile integration tests
This commit is contained in:
parent
782321e5d0
commit
82c7156fd2
14 changed files with 1218 additions and 142 deletions
30
test/datahaven/contracts/src/CallBatchFromConstructor.sol
Normal file
30
test/datahaven/contracts/src/CallBatchFromConstructor.sol
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.10;
|
||||
|
||||
import "../../../../precompiles/batch/Batch.sol";
|
||||
|
||||
contract BatchCaller {
|
||||
function inner(address to, bytes[] memory callData) internal {
|
||||
address[] memory toAddress = new address[](1);
|
||||
toAddress[0] = to;
|
||||
uint256[] memory value = new uint256[](1);
|
||||
value[0] = 0;
|
||||
uint64[] memory gasLimit = new uint64[](1);
|
||||
gasLimit[0] = 0;
|
||||
BATCH_CONTRACT.batchAll(toAddress, value, callData, gasLimit);
|
||||
}
|
||||
}
|
||||
|
||||
contract CallBatchPrecompileFromConstructor is BatchCaller {
|
||||
constructor(address to, bytes[] memory callData) {
|
||||
inner(to, callData);
|
||||
}
|
||||
}
|
||||
|
||||
contract CallBatchPrecompileFromConstructorInSubCall {
|
||||
CallBatchPrecompileFromConstructor public addr;
|
||||
|
||||
function simple(address to, bytes[] memory callData) external {
|
||||
addr = new CallBatchPrecompileFromConstructor(to, callData);
|
||||
}
|
||||
}
|
||||
47
test/datahaven/contracts/src/SimpleContractFactory.sol
Normal file
47
test/datahaven/contracts/src/SimpleContractFactory.sol
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.10;
|
||||
|
||||
contract SimpleContract {
|
||||
address public owner;
|
||||
|
||||
constructor() {
|
||||
owner = msg.sender;
|
||||
}
|
||||
}
|
||||
|
||||
contract SimpleContractFactory {
|
||||
SimpleContract[] public deployedWithCreate;
|
||||
SimpleContract[] public deployedWithCreate2;
|
||||
|
||||
constructor() {
|
||||
createSimpleContractWithCreate();
|
||||
createSimpleContractWithCreate2(0);
|
||||
}
|
||||
|
||||
function createSimpleContractWithCreate() public {
|
||||
SimpleContract newContract = new SimpleContract();
|
||||
deployedWithCreate.push(newContract);
|
||||
}
|
||||
|
||||
function createSimpleContractWithCreate2(uint256 salt) public returns (address) {
|
||||
bytes memory bytecode = type(SimpleContract).creationCode;
|
||||
|
||||
address addr;
|
||||
assembly {
|
||||
addr := create2(0, add(bytecode, 0x20), mload(bytecode), salt)
|
||||
if iszero(extcodesize(addr)) { revert(0, 0) }
|
||||
}
|
||||
|
||||
deployedWithCreate2.push(SimpleContract(addr));
|
||||
|
||||
return addr;
|
||||
}
|
||||
|
||||
function getDeployedWithCreate() public view returns (SimpleContract[] memory) {
|
||||
return deployedWithCreate;
|
||||
}
|
||||
|
||||
function getDeployedWithCreate2() public view returns (SimpleContract[] memory) {
|
||||
return deployedWithCreate2;
|
||||
}
|
||||
}
|
||||
335
test/datahaven/helpers/block.ts
Normal file
335
test/datahaven/helpers/block.ts
Normal file
|
|
@ -0,0 +1,335 @@
|
|||
import { type DevModeContext, expect } from "@moonwall/cli";
|
||||
import {
|
||||
type BlockRangeOption,
|
||||
EXTRINSIC_BASE_WEIGHT,
|
||||
mapExtrinsics,
|
||||
WEIGHT_PER_GAS
|
||||
} from "@moonwall/util";
|
||||
import type { ApiPromise } from "@polkadot/api";
|
||||
import type { TxWithEvent } from "@polkadot/api-derive/types";
|
||||
import type { u128 } from "@polkadot/types";
|
||||
import type { BlockHash, DispatchInfo, RuntimeDispatchInfo } from "@polkadot/types/interfaces";
|
||||
import type { RuntimeDispatchInfoV1 } from "@polkadot/types/interfaces/payment";
|
||||
import type { Block } from "@polkadot/types/interfaces/runtime/types";
|
||||
import Debug from "debug";
|
||||
import { calculateFeePortions } from "./fees";
|
||||
import { getFeesTreasuryProportion } from "./parameters";
|
||||
|
||||
const debug = Debug("test:blocks");
|
||||
|
||||
export interface TxWithEventAndFee extends TxWithEvent {
|
||||
fee: RuntimeDispatchInfo | RuntimeDispatchInfoV1;
|
||||
}
|
||||
|
||||
export interface BlockDetails {
|
||||
block: Block;
|
||||
txWithEvents: TxWithEventAndFee[];
|
||||
}
|
||||
|
||||
export const getBlockDetails = async (
|
||||
api: ApiPromise,
|
||||
blockHash: BlockHash | string | any
|
||||
): Promise<BlockDetails> => {
|
||||
debug(`Querying ${blockHash}`);
|
||||
|
||||
const [{ block }, records] = await Promise.all([
|
||||
api.rpc.chain.getBlock(blockHash),
|
||||
await (await api.at(blockHash)).query.system.events()
|
||||
]);
|
||||
|
||||
const fees = await Promise.all(
|
||||
block.extrinsics.map(async (ext) =>
|
||||
(await api.at(block.header.parentHash)).call.transactionPaymentApi.queryInfo(
|
||||
ext.toU8a(),
|
||||
ext.encodedLength
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const txWithEvents = mapExtrinsics(block.extrinsics, records, fees);
|
||||
|
||||
return {
|
||||
block,
|
||||
txWithEvents
|
||||
} as any as BlockDetails;
|
||||
};
|
||||
|
||||
// Explore all blocks for the given range and returns block information for each one
|
||||
// fromBlockNumber and toBlockNumber included
|
||||
export const exploreBlockRange = async (
|
||||
api: ApiPromise,
|
||||
{ from, to, concurrency = 1 }: BlockRangeOption,
|
||||
callBack: (blockDetails: BlockDetails) => Promise<void>
|
||||
) => {
|
||||
let current = from;
|
||||
while (current <= to) {
|
||||
const concurrentTasks: any[] = [];
|
||||
for (let i = 0; i < concurrency && current <= to; i++) {
|
||||
concurrentTasks.push(
|
||||
api.rpc.chain.getBlockHash(current++).then((hash) => getBlockDetails(api, hash))
|
||||
);
|
||||
}
|
||||
const blocksDetails = await Promise.all(concurrentTasks);
|
||||
for (const blockDetails of blocksDetails) {
|
||||
await callBack(blockDetails);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const verifyBlockFees = async (
|
||||
context: DevModeContext,
|
||||
fromBlockNumber: number,
|
||||
toBlockNumber: number,
|
||||
expectedBalanceDiff: bigint
|
||||
) => {
|
||||
const api = context.polkadotJs();
|
||||
debug(`========= Checking block ${fromBlockNumber}...${toBlockNumber}`);
|
||||
|
||||
// let sumBlockFees = 0n;
|
||||
let sumBlockBurnt = 0n;
|
||||
|
||||
// Get from block hash and totalSupply
|
||||
const fromPreBlockHash = (await api.rpc.chain.getBlockHash(fromBlockNumber - 1)).toString();
|
||||
const fromPreSupply = (await (
|
||||
await api.at(fromPreBlockHash)
|
||||
).query.balances.totalIssuance()) as any;
|
||||
let previousBlockHash = fromPreBlockHash;
|
||||
|
||||
// Get to block hash and totalSupply
|
||||
const toBlockHash = (await api.rpc.chain.getBlockHash(toBlockNumber)).toString();
|
||||
const toSupply = (await (await api.at(toBlockHash)).query.balances.totalIssuance()) as any;
|
||||
// fetch block information for all blocks in the range
|
||||
await exploreBlockRange(
|
||||
api,
|
||||
{ from: fromBlockNumber, to: toBlockNumber, concurrency: 5 },
|
||||
async (blockDetails) => {
|
||||
// let blockFees = 0n;
|
||||
let blockBurnt = 0n;
|
||||
|
||||
// iterate over every extrinsic
|
||||
for (const txWithEvents of blockDetails.txWithEvents) {
|
||||
const { events, extrinsic, fee } = txWithEvents;
|
||||
|
||||
// This hash will only exist if the transaction was executed through ethereum.
|
||||
let ethereumAddress = "";
|
||||
|
||||
if (extrinsic.method.section === "ethereum") {
|
||||
// Search for ethereum execution
|
||||
events.forEach((event) => {
|
||||
if (event.section === "ethereum" && event.method === "Executed") {
|
||||
ethereumAddress = event.data[0].toString();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Payment event is submitted for substrate transactions
|
||||
const paymentEvent = events.find(
|
||||
(event) => event.section === "transactionPayment" && event.method === "TransactionFeePaid"
|
||||
);
|
||||
|
||||
let txFees = 0n;
|
||||
let txBurnt = 0n;
|
||||
// For every extrinsic, iterate over every event
|
||||
// and search for ExtrinsicSuccess or ExtrinsicFailed
|
||||
for (const event of events) {
|
||||
if (
|
||||
api.events.system.ExtrinsicSuccess.is(event) ||
|
||||
api.events.system.ExtrinsicFailed.is(event)
|
||||
) {
|
||||
const dispatchInfo =
|
||||
event.method === "ExtrinsicSuccess"
|
||||
? (event.data[0] as DispatchInfo)
|
||||
: (event.data[1] as DispatchInfo);
|
||||
|
||||
const feesTreasuryProportion = await getFeesTreasuryProportion(context);
|
||||
|
||||
// We are only interested in fee paying extrinsics:
|
||||
// Either ethereum transactions or signed extrinsics with fees (substrate tx)
|
||||
if (
|
||||
(dispatchInfo.paysFee.isYes && !extrinsic.signer.isEmpty) ||
|
||||
extrinsic.method.section === "ethereum"
|
||||
) {
|
||||
if (extrinsic.method.section === "ethereum") {
|
||||
// For Ethereum tx we caluculate fee by first converting weight to gas
|
||||
const gasUsed = (dispatchInfo as any).weight.refTime.toBigInt() / WEIGHT_PER_GAS;
|
||||
const ethTxWrapper = extrinsic.method.args[0] as any;
|
||||
|
||||
const number = blockDetails.block.header.number.toNumber();
|
||||
// The on-chain base fee used by the transaction. Aka the parent block's base fee.
|
||||
//
|
||||
// Note on 1559 fees: no matter what the user was willing to pay (maxFeePerGas),
|
||||
// the transaction fee is ultimately computed using the onchain base fee. The
|
||||
// additional tip eventually paid by the user (maxPriorityFeePerGas) is purely a
|
||||
// prioritization component: the EVM is not aware of it and thus not part of the
|
||||
// weight cost of the extrinsic.
|
||||
|
||||
// let baseFeePerGas = BigInt(
|
||||
// (await context.web3().eth.getBlock(number - 1)).baseFeePerGas!
|
||||
// );
|
||||
const baseFeePerGas = (
|
||||
await context.viem().getBlock({ blockNumber: BigInt(number - 1) })
|
||||
).baseFeePerGas!;
|
||||
|
||||
let priorityFee: bigint;
|
||||
let gasFee: bigint;
|
||||
// Transaction is an enum now with as many variants as supported transaction types.
|
||||
if (ethTxWrapper.isLegacy) {
|
||||
priorityFee = ethTxWrapper.asLegacy.gasPrice.toBigInt();
|
||||
gasFee = priorityFee;
|
||||
} else if (ethTxWrapper.isEip2930) {
|
||||
priorityFee = ethTxWrapper.asEip2930.gasPrice.toBigInt();
|
||||
gasFee = priorityFee;
|
||||
} else if (ethTxWrapper.isEip1559) {
|
||||
priorityFee = ethTxWrapper.asEip1559.maxPriorityFeePerGas.toBigInt();
|
||||
gasFee = ethTxWrapper.asEip1559.maxFeePerGas.toBigInt();
|
||||
}
|
||||
|
||||
const hash = events
|
||||
.find((event) => event.section === "ethereum" && event.method === "Executed")!
|
||||
.data[2].toHex();
|
||||
await context.viem("public").getTransactionReceipt({ hash });
|
||||
let effectiveTipPerGas = gasFee - baseFeePerGas;
|
||||
if (effectiveTipPerGas > priorityFee) {
|
||||
effectiveTipPerGas = priorityFee;
|
||||
}
|
||||
|
||||
// Calculate the fees paid for the base fee and tip fee independently.
|
||||
// Only the base fee is subject to the split between burn and treasury.
|
||||
let baseFeesPaid = gasUsed * baseFeePerGas;
|
||||
let tipAsFeesPaid = gasUsed * effectiveTipPerGas;
|
||||
const actualPaidFees = (
|
||||
events.find(
|
||||
(event) => event.section === "balances" && event.method === "Withdraw"
|
||||
)!.data[1] as u128
|
||||
).toBigInt();
|
||||
if (actualPaidFees < baseFeesPaid + tipAsFeesPaid) {
|
||||
baseFeesPaid = actualPaidFees < baseFeesPaid ? actualPaidFees : baseFeesPaid;
|
||||
tipAsFeesPaid =
|
||||
actualPaidFees < baseFeesPaid ? 0n : actualPaidFees - baseFeesPaid;
|
||||
}
|
||||
|
||||
const { burnt: baseFeePortionsBurnt } = calculateFeePortions(
|
||||
feesTreasuryProportion,
|
||||
baseFeesPaid
|
||||
);
|
||||
|
||||
txFees += baseFeesPaid + tipAsFeesPaid;
|
||||
txBurnt += baseFeePortionsBurnt;
|
||||
} else {
|
||||
// For a regular substrate tx, we use the partialFee
|
||||
const feePortions = calculateFeePortions(
|
||||
feesTreasuryProportion,
|
||||
fee.partialFee.toBigInt()
|
||||
);
|
||||
|
||||
txFees += fee.partialFee.toBigInt() + extrinsic.tip.toBigInt();
|
||||
txBurnt += feePortions.burnt;
|
||||
|
||||
// verify entire substrate txn fee
|
||||
const apiAt = await context.polkadotJs().at(previousBlockHash);
|
||||
const lengthFee = (
|
||||
(await apiAt.call.transactionPaymentApi.queryLengthToFee(
|
||||
extrinsic.encodedLength
|
||||
)) as any
|
||||
).toBigInt();
|
||||
|
||||
const unadjustedWeightFee = (
|
||||
await apiAt.call.transactionPaymentApi.queryWeightToFee(
|
||||
"refTime" in fee.weight
|
||||
? fee.weight
|
||||
: {
|
||||
refTime: fee.weight,
|
||||
proofSize: 0n
|
||||
}
|
||||
)
|
||||
).toBigInt();
|
||||
const multiplier = await apiAt.query.transactionPayment.nextFeeMultiplier();
|
||||
const denominator = 1_000_000_000_000_000_000n;
|
||||
const weightFee = (unadjustedWeightFee * multiplier.toBigInt()) / denominator;
|
||||
|
||||
const baseFee = (
|
||||
(await apiAt.call.transactionPaymentApi.queryWeightToFee({
|
||||
refTime: EXTRINSIC_BASE_WEIGHT,
|
||||
proofSize: 0n
|
||||
})) as any
|
||||
).toBigInt();
|
||||
|
||||
const tip = extrinsic.tip.toBigInt();
|
||||
const expectedPartialFee = lengthFee + weightFee + baseFee;
|
||||
|
||||
// Verify the computed fees are equal to the actual fees + tip
|
||||
expect(expectedPartialFee + tip).to.eq((paymentEvent!.data[1] as u128).toBigInt());
|
||||
expect(tip).to.eq((paymentEvent!.data[2] as u128).toBigInt());
|
||||
|
||||
// Verify the computed fees are equal to the rpc computed fees
|
||||
expect(expectedPartialFee).to.eq(fee.partialFee.toBigInt());
|
||||
}
|
||||
|
||||
// blockFees += txFees;
|
||||
blockBurnt += txBurnt;
|
||||
|
||||
const origin = extrinsic.signer.isEmpty
|
||||
? ethereumAddress
|
||||
: extrinsic.signer.toString();
|
||||
|
||||
// Get balance of the origin account both before and after extrinsic execution
|
||||
const fromBalance = (await (
|
||||
await api.at(previousBlockHash)
|
||||
).query.system.account(origin)) as any;
|
||||
const toBalance = (await (
|
||||
await api.at(blockDetails.block.hash)
|
||||
).query.system.account(origin)) as any;
|
||||
|
||||
expect(txFees.toString()).to.eq(
|
||||
(
|
||||
(((fromBalance.data.free.toBigInt() as any) -
|
||||
toBalance.data.free.toBigInt()) as any) - expectedBalanceDiff
|
||||
).toString()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// sumBlockFees += blockFees;
|
||||
sumBlockBurnt += blockBurnt;
|
||||
previousBlockHash = blockDetails.block.hash.toString();
|
||||
}
|
||||
);
|
||||
|
||||
expect(fromPreSupply.toBigInt() - toSupply.toBigInt()).to.eq(sumBlockBurnt);
|
||||
|
||||
// Log difference in supply, we should be equal to the burnt fees
|
||||
// debug(
|
||||
// ` supply diff: ${(fromPreSupply.toBigInt() - toSupply.toBigInt())
|
||||
// .toString()
|
||||
// .padStart(30, " ")}`
|
||||
// );
|
||||
// debug(` burnt fees : ${sumBlockBurnt.toString().padStart(30, " ")}`);
|
||||
// debug(` total fees : ${sumBlockFees.toString().padStart(30, " ")}`);
|
||||
};
|
||||
|
||||
export const verifyLatestBlockFees = async (
|
||||
context: DevModeContext,
|
||||
expectedBalanceDiff: bigint = BigInt(0)
|
||||
) => {
|
||||
const signedBlock = await context.polkadotJs().rpc.chain.getBlock();
|
||||
const blockNumber = Number(signedBlock.block.header.number);
|
||||
return verifyBlockFees(context, blockNumber, blockNumber, expectedBalanceDiff);
|
||||
};
|
||||
|
||||
export async function jumpToRound(context: DevModeContext, round: number): Promise<string | null> {
|
||||
let lastBlockHash = "";
|
||||
for (;;) {
|
||||
const currentRound = (
|
||||
await context.polkadotJs().query.parachainStaking.round()
|
||||
).current.toNumber();
|
||||
if (currentRound === round) {
|
||||
return lastBlockHash;
|
||||
}
|
||||
if (currentRound > round) {
|
||||
return null;
|
||||
}
|
||||
|
||||
lastBlockHash = (await context.createBlock()).block.hash.toString();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,63 +1,55 @@
|
|||
/**
|
||||
* Runtime constants for DataHaven networks
|
||||
* Adapted from Moonbeam test helpers
|
||||
*/
|
||||
|
||||
import type { GenericContext } from "@moonwall/cli";
|
||||
|
||||
/**
|
||||
* Class allowing to store multiple value for a runtime constant based on the runtime version
|
||||
*/
|
||||
class RuntimeConstant<T> {
|
||||
private values: { [version: number]: T };
|
||||
private readonly values: Map<number, T>;
|
||||
|
||||
/*
|
||||
* Get the expected value for a given runtime version. Lookup for the closest smaller runtime
|
||||
*/
|
||||
get(runtimeVersion: number): T {
|
||||
const versions = Object.keys(this.values).map(Number); // slow but easier to maintain
|
||||
let value: T | undefined;
|
||||
for (let i = 0; i < versions.length; i++) {
|
||||
if (versions[i] > runtimeVersion) {
|
||||
break;
|
||||
constructor(valuesByVersion: Record<number, T>) {
|
||||
this.values = new Map(Object.entries(valuesByVersion).map(([k, v]) => [Number(k), v]));
|
||||
}
|
||||
|
||||
get(version: number): T {
|
||||
const sortedVersions = Array.from(this.values.keys()).sort((a, b) => b - a);
|
||||
for (const v of sortedVersions) {
|
||||
if (version >= v) {
|
||||
return this.values.get(v)!;
|
||||
}
|
||||
value = this.values[versions[i]];
|
||||
}
|
||||
return value as T;
|
||||
}
|
||||
|
||||
// Builds RuntimeConstant with single or multiple values
|
||||
constructor(values: { [version: number]: T } | T) {
|
||||
if (values instanceof Object) {
|
||||
this.values = values;
|
||||
} else {
|
||||
this.values = { 0: values };
|
||||
}
|
||||
return this.values.get(0)!;
|
||||
}
|
||||
}
|
||||
|
||||
// Fees and gas limits
|
||||
// Values derived from Rust runtime configuration in operator/runtime/*/src/lib.rs
|
||||
export const RUNTIME_CONSTANTS = {
|
||||
"DATAHAVEN-STAGENET": {
|
||||
GAS_LIMIT: new RuntimeConstant(60_000_000n),
|
||||
EXTRINSIC_GAS_LIMIT: new RuntimeConstant(52_000_000n),
|
||||
BLOCK_WEIGHT_LIMIT: new RuntimeConstant(2_000_000_000_000n),
|
||||
MAX_POV_SIZE: new RuntimeConstant(10_485_760n) // 10MB in bytes (matching Moonbeam)
|
||||
},
|
||||
"DATAHAVEN-MAINNET": {
|
||||
GAS_LIMIT: new RuntimeConstant(60_000_000n),
|
||||
EXTRINSIC_GAS_LIMIT: new RuntimeConstant(52_000_000n),
|
||||
BLOCK_WEIGHT_LIMIT: new RuntimeConstant(2_000_000_000_000n),
|
||||
MAX_POV_SIZE: new RuntimeConstant(10_485_760n) // 10MB in bytes (matching Moonbeam)
|
||||
},
|
||||
"DATAHAVEN-TESTNET": {
|
||||
GAS_LIMIT: new RuntimeConstant(60_000_000n),
|
||||
EXTRINSIC_GAS_LIMIT: new RuntimeConstant(52_000_000n),
|
||||
BLOCK_WEIGHT_LIMIT: new RuntimeConstant(2_000_000_000_000n),
|
||||
MAX_POV_SIZE: new RuntimeConstant(10_485_760n) // 10MB in bytes (matching Moonbeam)
|
||||
const DATAHAVEN_CONSTANTS = {
|
||||
BLOCK_WEIGHT_LIMIT: new RuntimeConstant({
|
||||
0: 2_000_000_000_000n
|
||||
}),
|
||||
GAS_LIMIT: new RuntimeConstant({
|
||||
0: 60_000_000n
|
||||
}),
|
||||
EXTRINSIC_GAS_LIMIT: new RuntimeConstant({
|
||||
0: 52_000_000n
|
||||
}),
|
||||
WEIGHT_TO_GAS_RATIO: 25_000n,
|
||||
STORAGE_READ_COST: 25_000_000n,
|
||||
STORAGE_WRITE_COST: 50_000_000n,
|
||||
SUPPLY_FACTOR: 1n,
|
||||
PRECOMPILE_ADDRESSES: {
|
||||
BATCH: "0x0000000000000000000000000000000000000808" as const,
|
||||
CALL_PERMIT: "0x000000000000000000000000000000000000080a" as const,
|
||||
PROXY: "0x000000000000000000000000000000000000080b" as const,
|
||||
ERC20_BALANCES: "0x0000000000000000000000000000000000000802" as const,
|
||||
PRECOMPILE_REGISTRY: "0x0000000000000000000000000000000000000815" as const
|
||||
}
|
||||
};
|
||||
} as const;
|
||||
|
||||
type ConstantStoreType = (typeof RUNTIME_CONSTANTS)["DATAHAVEN-STAGENET"];
|
||||
type ConstantStoreType = typeof DATAHAVEN_CONSTANTS;
|
||||
|
||||
export function ConstantStore(context: GenericContext): ConstantStoreType {
|
||||
const runtime = context.polkadotJs().consts.system.version.specName.toUpperCase();
|
||||
console.log("runtime", runtime);
|
||||
return RUNTIME_CONSTANTS[runtime];
|
||||
export function ConstantStore(_context: GenericContext): ConstantStoreType {
|
||||
return DATAHAVEN_CONSTANTS;
|
||||
}
|
||||
|
||||
export { RuntimeConstant };
|
||||
|
|
|
|||
|
|
@ -1,20 +1,17 @@
|
|||
import { readFile } from "node:fs/promises";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type { Abi } from "viem";
|
||||
|
||||
interface ArtifactEvmBytecode {
|
||||
object?: string;
|
||||
}
|
||||
|
||||
interface ArtifactEvm {
|
||||
bytecode?: ArtifactEvmBytecode;
|
||||
}
|
||||
/**
|
||||
* Contract-related helper utilities for DataHaven tests
|
||||
* Adapted from Moonbeam test helpers
|
||||
*/
|
||||
|
||||
interface ArtifactContract {
|
||||
abi?: Abi;
|
||||
evm?: ArtifactEvm;
|
||||
bytecode?: `0x${string}`;
|
||||
evm?: { bytecode?: { object?: string } };
|
||||
}
|
||||
|
||||
interface CompiledContractArtifactJson {
|
||||
|
|
@ -34,11 +31,28 @@ export interface CompiledContractArtifact {
|
|||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
export const fetchCompiledContract = async (
|
||||
contractName: string
|
||||
): Promise<CompiledContractArtifact> => {
|
||||
const artifactPath = path.join(__dirname, "../", "contracts", "out", `${contractName}.json`);
|
||||
const artifactContent = await readFile(artifactPath, "utf-8");
|
||||
export const fetchCompiledContract = (contractName: string): CompiledContractArtifact => {
|
||||
let artifactPath = path.join(__dirname, "../", "contracts", "out", `${contractName}.json`);
|
||||
if (!existsSync(artifactPath)) {
|
||||
const folder = contractName
|
||||
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
||||
.replace(/_/g, "-")
|
||||
.toLowerCase()
|
||||
.replace(/-+precompile$/, "");
|
||||
const candidate = path.join(
|
||||
__dirname,
|
||||
"../",
|
||||
"contracts",
|
||||
"out",
|
||||
"precompiles",
|
||||
folder,
|
||||
`${contractName}.json`
|
||||
);
|
||||
if (existsSync(candidate)) {
|
||||
artifactPath = candidate;
|
||||
}
|
||||
}
|
||||
const artifactContent = readFileSync(artifactPath, "utf-8");
|
||||
const artifactJson = JSON.parse(artifactContent) as CompiledContractArtifactJson;
|
||||
|
||||
const abi = artifactJson.abi ?? artifactJson.contract.abi;
|
||||
|
|
|
|||
|
|
@ -1,65 +0,0 @@
|
|||
import type { Abi } from "viem";
|
||||
import { fetchCompiledContract } from "./contracts";
|
||||
|
||||
export const TransactionTypes = ["legacy", "eip1559", "eip2930"] as const;
|
||||
|
||||
export interface DeployCompiledContractOptions {
|
||||
type?: (typeof TransactionTypes)[number];
|
||||
gas?: bigint;
|
||||
gasPrice?: bigint;
|
||||
maxFeePerGas?: bigint;
|
||||
maxPriorityFeePerGas?: bigint;
|
||||
value?: bigint;
|
||||
}
|
||||
|
||||
export interface DeployCompiledContractResult {
|
||||
hash: `0x${string}`;
|
||||
contractAddress: `0x${string}` | null;
|
||||
status: "success" | "reverted";
|
||||
abi: Abi;
|
||||
}
|
||||
|
||||
export const deployCompiledContract = async (
|
||||
context: any,
|
||||
contractName: string,
|
||||
options: DeployCompiledContractOptions = {}
|
||||
): Promise<DeployCompiledContractResult> => {
|
||||
const artifact = await fetchCompiledContract(contractName);
|
||||
|
||||
const tx: any = {
|
||||
data: artifact.bytecode,
|
||||
gas: options.gas,
|
||||
value: options.value ?? 0n
|
||||
};
|
||||
|
||||
switch (options.type) {
|
||||
case "legacy":
|
||||
tx.type = "legacy";
|
||||
if (options.gasPrice !== undefined) tx.gasPrice = options.gasPrice;
|
||||
break;
|
||||
case "eip2930":
|
||||
tx.type = "eip2930";
|
||||
if (options.gasPrice !== undefined) tx.gasPrice = options.gasPrice;
|
||||
tx.accessList = [];
|
||||
break;
|
||||
default:
|
||||
tx.type = "eip1559";
|
||||
if (options.maxFeePerGas !== undefined) tx.maxFeePerGas = options.maxFeePerGas;
|
||||
if (options.maxPriorityFeePerGas !== undefined)
|
||||
tx.maxPriorityFeePerGas = options.maxPriorityFeePerGas;
|
||||
break;
|
||||
}
|
||||
|
||||
const hash: `0x${string}` = await context.viem().sendTransaction(tx);
|
||||
if (typeof context.createBlock === "function") {
|
||||
await context.createBlock();
|
||||
}
|
||||
const receipt = await context.viem().waitForTransactionReceipt({ hash });
|
||||
|
||||
return {
|
||||
hash,
|
||||
contractAddress: receipt.contractAddress,
|
||||
status: receipt.status,
|
||||
abi: artifact.abi
|
||||
};
|
||||
};
|
||||
159
test/datahaven/helpers/evm.ts
Normal file
159
test/datahaven/helpers/evm.ts
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
/**
|
||||
* EVM-related helper utilities for DataHaven tests
|
||||
* Adapted from Moonbeam test helpers
|
||||
*/
|
||||
|
||||
import { type DevModeContext, expect } from "@moonwall/cli";
|
||||
import type { EventRecord } from "@polkadot/types/interfaces";
|
||||
import type {
|
||||
EvmCoreErrorExitError,
|
||||
EvmCoreErrorExitFatal,
|
||||
EvmCoreErrorExitReason,
|
||||
EvmCoreErrorExitRevert,
|
||||
EvmCoreErrorExitSucceed
|
||||
} from "@polkadot/types/lookup";
|
||||
|
||||
export type Errors = {
|
||||
Succeed: EvmCoreErrorExitSucceed["type"];
|
||||
Error: EvmCoreErrorExitError["type"];
|
||||
Revert: EvmCoreErrorExitRevert["type"];
|
||||
Fatal: EvmCoreErrorExitFatal["type"];
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate EVM execution result from Ethereum.Executed events
|
||||
*
|
||||
* @param events - Array of event records from block execution
|
||||
* @param resultType - Expected result type (Succeed, Error, Revert, Fatal)
|
||||
* @param reason - Optional specific reason within the result type
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* expectEVMResult(result.events, "Succeed");
|
||||
* expectEVMResult(result.events, "Revert", "Reverted");
|
||||
* ```
|
||||
*/
|
||||
export function expectEVMResult<T extends Errors, Type extends keyof T>(
|
||||
events: EventRecord[],
|
||||
resultType: Type,
|
||||
reason?: T[Type]
|
||||
) {
|
||||
expect(events, "Missing events, probably failed execution").toHaveLength;
|
||||
expect(events.length).toBeGreaterThan(0);
|
||||
|
||||
const ethereumExecuted = events.find(
|
||||
({ event: { section, method } }) => section === "ethereum" && method === "Executed"
|
||||
);
|
||||
|
||||
expect(ethereumExecuted, "Ethereum.Executed event not found").toBeDefined();
|
||||
|
||||
const ethereumResult = ethereumExecuted!.event.data[3] as EvmCoreErrorExitReason;
|
||||
|
||||
const _foundReason = ethereumResult.isError
|
||||
? ethereumResult.asError.type
|
||||
: ethereumResult.isFatal
|
||||
? ethereumResult.asFatal.type
|
||||
: ethereumResult.isRevert
|
||||
? ethereumResult.asRevert.type
|
||||
: ethereumResult.asSucceed.type;
|
||||
|
||||
expect(ethereumResult.type).toBe(resultType);
|
||||
|
||||
if (reason) {
|
||||
if (ethereumResult.isError) {
|
||||
expect(ethereumResult.asError.type).toBe(reason);
|
||||
} else if (ethereumResult.isFatal) {
|
||||
expect(ethereumResult.asFatal.type).toBe(reason);
|
||||
} else if (ethereumResult.isRevert) {
|
||||
expect(ethereumResult.asRevert.type).toBe(reason);
|
||||
} else {
|
||||
expect(ethereumResult.asSucceed.type).toBe(reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract signature parameters (r, s, v) from a hex signature string
|
||||
*
|
||||
* @param signature - Hex signature string
|
||||
* @returns Object containing r, s, v components
|
||||
*/
|
||||
export function getSignatureParameters(signature: string): {
|
||||
r: string;
|
||||
s: string;
|
||||
v: number;
|
||||
} {
|
||||
const r = signature.slice(0, 66); // 32 bytes
|
||||
const s = `0x${signature.slice(66, 130)}`; // 32 bytes
|
||||
let v = Number.parseInt(signature.slice(130, 132), 16); // 1 byte
|
||||
|
||||
if (![27, 28].includes(v)) {
|
||||
v += 27;
|
||||
}
|
||||
|
||||
return { r, s, v };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get transaction receipt with retry logic for async block production
|
||||
*
|
||||
* @param context - Moonwall dev context
|
||||
* @param hash - Transaction hash
|
||||
* @param options - Retry configuration
|
||||
* @returns Transaction receipt
|
||||
*/
|
||||
export async function getTransactionReceiptWithRetry(
|
||||
context: DevModeContext,
|
||||
hash: `0x${string}`,
|
||||
options?: {
|
||||
maxAttempts?: number;
|
||||
delayMs?: number;
|
||||
exponentialBackoff?: boolean;
|
||||
}
|
||||
) {
|
||||
const maxAttempts = options?.maxAttempts ?? 4;
|
||||
const delayMs = options?.delayMs ?? 2000;
|
||||
const exponentialBackoff = options?.exponentialBackoff ?? true;
|
||||
|
||||
let lastError: Error | undefined;
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
const receipt = await context.viem().getTransactionReceipt({ hash });
|
||||
return receipt;
|
||||
} catch (error: unknown) {
|
||||
lastError = error as Error;
|
||||
|
||||
// Check if it's the specific error we want to retry
|
||||
if (
|
||||
(error as Error).name === "TransactionReceiptNotFoundError" ||
|
||||
(error as Error).message?.includes("Transaction receipt with hash") ||
|
||||
(error as Error).message?.includes("could not be found")
|
||||
) {
|
||||
if (attempt < maxAttempts) {
|
||||
const delay = exponentialBackoff ? delayMs * 1.5 ** (attempt - 1) : delayMs;
|
||||
await new Promise((resolve) => setTimeout(resolve, Math.min(delay, 10000)));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// If it's a different error, throw immediately
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// If we've exhausted all attempts, throw the last error
|
||||
throw lastError || new Error(`Failed to get transaction receipt after ${maxAttempts} attempts`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate total transaction fees (gasUsed * effectiveGasPrice)
|
||||
*
|
||||
* @param context - Moonwall dev context
|
||||
* @param hash - Transaction hash
|
||||
* @returns Total fees in wei
|
||||
*/
|
||||
export async function getTransactionFees(context: DevModeContext, hash: string): Promise<bigint> {
|
||||
const receipt = await getTransactionReceiptWithRetry(context, hash as `0x${string}`);
|
||||
return receipt.gasUsed * receipt.effectiveGasPrice;
|
||||
}
|
||||
89
test/datahaven/helpers/fees.ts
Normal file
89
test/datahaven/helpers/fees.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import { BN } from "@polkadot/util";
|
||||
|
||||
// EIP-7623 Gas Cost Constants
|
||||
// These constants define the gas costs under EIP-7623's floor cost mechanism
|
||||
export const EIP7623_GAS_CONSTANTS = {
|
||||
// Base transaction cost
|
||||
BASE_TX_COST: 21000n,
|
||||
|
||||
// Token costs (EIP-7623 defines tokens = zero_bytes + nonzero_bytes * 4)
|
||||
TOKENS_PER_NONZERO_BYTE: 4n,
|
||||
TOKENS_PER_ZERO_BYTE: 1n,
|
||||
|
||||
// Floor costs per token
|
||||
TOTAL_COST_FLOOR_PER_TOKEN: 10n,
|
||||
|
||||
// Derived floor costs per byte type
|
||||
COST_FLOOR_PER_ZERO_BYTE: 10n, // 1 token * 10 gas/token
|
||||
COST_FLOOR_PER_NON_ZERO_BYTE: 40n, // 4 tokens * 10 gas/token
|
||||
|
||||
// Standard (pre-EIP-7623) costs
|
||||
STANDARD_COST_PER_ZERO_BYTE: 4n,
|
||||
STANDARD_COST_PER_NON_ZERO_BYTE: 16n
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Calculate the expected gas cost with EIP-7623 floor cost mechanism
|
||||
* @param numZeroBytes Number of zero bytes in calldata
|
||||
* @param numNonZeroBytes Number of non-zero bytes in calldata
|
||||
* @param executionGas Gas cost for the execution (e.g., contract execution, precompile)
|
||||
* @returns Expected total gas used (maximum of floor cost and standard cost + execution)
|
||||
*/
|
||||
export function calculateEIP7623Gas(
|
||||
numZeroBytes: number,
|
||||
numNonZeroBytes: number,
|
||||
executionGas = 0n
|
||||
): bigint {
|
||||
const {
|
||||
BASE_TX_COST,
|
||||
COST_FLOOR_PER_ZERO_BYTE,
|
||||
COST_FLOOR_PER_NON_ZERO_BYTE,
|
||||
STANDARD_COST_PER_ZERO_BYTE,
|
||||
STANDARD_COST_PER_NON_ZERO_BYTE
|
||||
} = EIP7623_GAS_CONSTANTS;
|
||||
|
||||
// Floor cost calculation
|
||||
const floorCost =
|
||||
BigInt(numNonZeroBytes) * COST_FLOOR_PER_NON_ZERO_BYTE +
|
||||
BigInt(numZeroBytes) * COST_FLOOR_PER_ZERO_BYTE +
|
||||
BASE_TX_COST;
|
||||
|
||||
// Standard cost + execution
|
||||
const standardCalldataCost =
|
||||
BigInt(numNonZeroBytes) * STANDARD_COST_PER_NON_ZERO_BYTE +
|
||||
BigInt(numZeroBytes) * STANDARD_COST_PER_ZERO_BYTE;
|
||||
const standardCostPlusExecution = standardCalldataCost + BASE_TX_COST + executionGas;
|
||||
|
||||
// Return the maximum of floor cost and standard cost + execution
|
||||
return floorCost > standardCostPlusExecution ? floorCost : standardCostPlusExecution;
|
||||
}
|
||||
|
||||
/// Recreation of fees.ration(burn_part, treasury_part)
|
||||
export const split = (value: BN, part1: BN, part2: BN): [BN, BN] => {
|
||||
const total = part1.add(part2);
|
||||
if (total.eq(new BN(0)) || value.eq(new BN(0))) {
|
||||
return [new BN(0), new BN(0)];
|
||||
}
|
||||
const part1BN = value.mul(part1).div(total);
|
||||
const part2BN = value.sub(part1BN);
|
||||
return [part1BN, part2BN];
|
||||
};
|
||||
|
||||
export const calculateFeePortions = (
|
||||
feesTreasuryProportion: bigint,
|
||||
fees: bigint
|
||||
): {
|
||||
burnt: bigint;
|
||||
treasury: bigint;
|
||||
} => {
|
||||
const feesBN = new BN(fees.toString());
|
||||
const treasuryPartBN = new BN(feesTreasuryProportion.toString());
|
||||
const burntPartBN = new BN(1e9).sub(treasuryPartBN);
|
||||
|
||||
const [burntBN, treasuryBN] = split(feesBN, burntPartBN, treasuryPartBN);
|
||||
|
||||
return {
|
||||
burnt: BigInt(burntBN.toString()),
|
||||
treasury: BigInt(treasuryBN.toString())
|
||||
};
|
||||
};
|
||||
|
|
@ -1,2 +1,14 @@
|
|||
/**
|
||||
* DataHaven test helpers
|
||||
*
|
||||
* This module exports helper utilities for writing Moonwall tests for DataHaven.
|
||||
* These helpers are adapted from Moonbeam's test suite to work with DataHaven's
|
||||
* runtime configuration.
|
||||
*/
|
||||
|
||||
export * from "./block";
|
||||
export * from "./constants";
|
||||
export * from "./deploy";
|
||||
export * from "./contracts";
|
||||
export * from "./evm";
|
||||
export * from "./fees";
|
||||
export * from "./parameters";
|
||||
|
|
|
|||
14
test/datahaven/helpers/parameters.ts
Normal file
14
test/datahaven/helpers/parameters.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import type { DevModeContext } from "@moonwall/cli";
|
||||
|
||||
export const getFeesTreasuryProportion = async (context: DevModeContext): Promise<bigint> => {
|
||||
const parameter = await context.polkadotJs().query.parameters.parameters({
|
||||
RuntimeConfig: "FeesTreasuryProportion"
|
||||
});
|
||||
|
||||
// 20% default value
|
||||
let feesTreasuryProportion = 200_000_000n;
|
||||
if (parameter.isSome) {
|
||||
feesTreasuryProportion = parameter.value.asRuntimeConfig.asFeesTreasuryProportion.toBigInt();
|
||||
}
|
||||
return feesTreasuryProportion;
|
||||
};
|
||||
|
|
@ -1,5 +1,11 @@
|
|||
import { beforeAll, describeSuite, expect } from "@moonwall/cli";
|
||||
import { ConstantStore, deployCompiledContract, TransactionTypes } from "../../../../helpers";
|
||||
import {
|
||||
beforeAll,
|
||||
deployCreateCompiledContract,
|
||||
describeSuite,
|
||||
expect,
|
||||
TransactionTypes
|
||||
} from "@moonwall/cli";
|
||||
import { ConstantStore } from "../../../../helpers";
|
||||
|
||||
describeSuite({
|
||||
id: "D010103",
|
||||
|
|
@ -17,7 +23,7 @@ describeSuite({
|
|||
id: `T0${TransactionTypes.indexOf(txnType) + 1}`,
|
||||
title: `${txnType} should be allowed to the max block gas`,
|
||||
test: async () => {
|
||||
const { hash, status } = await deployCompiledContract(context, "MultiplyBy7", {
|
||||
const { hash, status } = await deployCreateCompiledContract(context, "MultiplyBy7", {
|
||||
type: txnType,
|
||||
gas: ConstantStore(context).EXTRINSIC_GAS_LIMIT.get(specVersion)
|
||||
});
|
||||
|
|
@ -32,7 +38,7 @@ describeSuite({
|
|||
title: `${txnType} should fail setting it over the max block gas`,
|
||||
test: async () => {
|
||||
await expect(async () =>
|
||||
deployCompiledContract(context, "MultiplyBy7", {
|
||||
deployCreateCompiledContract(context, "MultiplyBy7", {
|
||||
type: txnType,
|
||||
gas: ConstantStore(context).EXTRINSIC_GAS_LIMIT.get(specVersion) + 1n
|
||||
})
|
||||
|
|
@ -45,9 +51,13 @@ describeSuite({
|
|||
id: "T07",
|
||||
title: "should be accessible within a contract",
|
||||
test: async () => {
|
||||
const { contractAddress, abi } = await deployCompiledContract(context, "BlockVariables", {
|
||||
gas: 500_000n
|
||||
});
|
||||
const { contractAddress, abi } = await deployCreateCompiledContract(
|
||||
context,
|
||||
"BlockVariables",
|
||||
{
|
||||
gas: 500_000n
|
||||
}
|
||||
);
|
||||
|
||||
if (!contractAddress) {
|
||||
throw new Error("Expected deployed contract to have an address");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,169 @@
|
|||
import {
|
||||
deployCreateCompiledContract,
|
||||
describeSuite,
|
||||
expect,
|
||||
fetchCompiledContract,
|
||||
TransactionTypes
|
||||
} from "@moonwall/cli";
|
||||
import { ALITH_ADDRESS } from "@moonwall/util";
|
||||
import { hexToU8a } from "@polkadot/util";
|
||||
import { encodeDeployData, keccak256, numberToHex, toRlp } from "viem";
|
||||
import { verifyLatestBlockFees } from "../../../../helpers";
|
||||
|
||||
describeSuite({
|
||||
id: "D010201",
|
||||
title: "Contract creation",
|
||||
foundationMethods: "dev",
|
||||
testCases: ({ context, it }) => {
|
||||
for (const txnType of TransactionTypes) {
|
||||
it({
|
||||
id: `T0-${TransactionTypes.indexOf(txnType) + 1}`,
|
||||
title: `should return the ${txnType} transaction hash`,
|
||||
test: async () => {
|
||||
const { hash } = await deployCreateCompiledContract(context, "MultiplyBy7", {
|
||||
txnType: txnType as any
|
||||
});
|
||||
await context.createBlock();
|
||||
expect(hash).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
it({
|
||||
id: `T0-${TransactionTypes.indexOf(txnType) + 4}`,
|
||||
title: `${txnType} should return the contract code`,
|
||||
test: async () => {
|
||||
const compiled = fetchCompiledContract("MultiplyBy7");
|
||||
const callCode = (await context.viem().call({ data: compiled.bytecode })).data;
|
||||
await context.createBlock();
|
||||
const { contractAddress } = await deployCreateCompiledContract(context, "MultiplyBy7", {
|
||||
txnType: txnType as any,
|
||||
gas: 5_000_000n
|
||||
});
|
||||
const deployedCode = await context.viem().getCode({ address: contractAddress! });
|
||||
expect(callCode).to.be.eq(deployedCode);
|
||||
}
|
||||
});
|
||||
|
||||
it({
|
||||
id: `T0-${TransactionTypes.indexOf(txnType) + 7}`,
|
||||
title: `should not contain ${txnType} contract at genesis`,
|
||||
test: async () => {
|
||||
const { contractAddress } = await deployCreateCompiledContract(context, "MultiplyBy7", {
|
||||
type: txnType as any,
|
||||
gas: 5_000_000n
|
||||
});
|
||||
expect(
|
||||
await context.viem().getCode({ address: contractAddress!, blockNumber: 0n })
|
||||
).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it({
|
||||
id: `T0-${TransactionTypes.indexOf(txnType) + 10}`,
|
||||
title: `${txnType} deployed contracts should store the code on chain`,
|
||||
test: async () => {
|
||||
// This is to enable pending tag support
|
||||
await context.createBlock();
|
||||
const compiled = fetchCompiledContract("MultiplyBy7");
|
||||
const callData = encodeDeployData({
|
||||
abi: compiled.abi,
|
||||
bytecode: compiled.bytecode,
|
||||
args: []
|
||||
}) as `0x${string}`;
|
||||
|
||||
const nonce = await context
|
||||
.viem("public")
|
||||
.getTransactionCount({ address: ALITH_ADDRESS });
|
||||
|
||||
await context.viem().sendTransaction({
|
||||
data: callData,
|
||||
nonce,
|
||||
txnType: txnType as any,
|
||||
gas: 5_000_000n
|
||||
});
|
||||
|
||||
const contractAddress = ("0x" +
|
||||
keccak256(hexToU8a(toRlp([ALITH_ADDRESS, numberToHex(nonce)])))
|
||||
.slice(12)
|
||||
.substring(14)) as `0x${string}`;
|
||||
|
||||
expect(
|
||||
await context.viem("public").getCode({ address: contractAddress, blockTag: "pending" })
|
||||
).to.deep.equal(compiled.deployedBytecode);
|
||||
|
||||
await context.createBlock();
|
||||
|
||||
expect(
|
||||
await context.viem("public").getCode({ address: contractAddress, blockTag: "latest" })
|
||||
).to.deep.equal(compiled.deployedBytecode);
|
||||
}
|
||||
});
|
||||
|
||||
it({
|
||||
id: `T0-${TransactionTypes.indexOf(txnType) + 13}`,
|
||||
title: `should check latest block fees for ${txnType}`,
|
||||
test: async () => {
|
||||
await context.createBlock();
|
||||
await deployCreateCompiledContract(context, "Fibonacci", {
|
||||
maxPriorityFeePerGas: 0n,
|
||||
txnType: txnType as any
|
||||
});
|
||||
await verifyLatestBlockFees(context);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
it({
|
||||
id: "T1",
|
||||
title: "Check smart-contract nonce increase when calling CREATE/CREATE2 opcodes",
|
||||
test: async () => {
|
||||
const factory = await context.deployContract!("SimpleContractFactory");
|
||||
|
||||
expect(await context.viem().getTransactionCount({ address: factory.contractAddress })).eq(
|
||||
3
|
||||
);
|
||||
|
||||
await context.writeContract!({
|
||||
contractName: "SimpleContractFactory",
|
||||
contractAddress: factory.contractAddress,
|
||||
functionName: "createSimpleContractWithCreate",
|
||||
value: 0n
|
||||
});
|
||||
await context.createBlock();
|
||||
|
||||
expect(await context.viem().getTransactionCount({ address: factory.contractAddress })).eq(
|
||||
4
|
||||
);
|
||||
|
||||
const deployedWithCreate = (await context.readContract!({
|
||||
contractName: "SimpleContractFactory",
|
||||
contractAddress: factory.contractAddress,
|
||||
functionName: "getDeployedWithCreate",
|
||||
args: []
|
||||
})) as string[];
|
||||
expect(deployedWithCreate.length).eq(2);
|
||||
|
||||
await context.writeContract!({
|
||||
contractName: "SimpleContractFactory",
|
||||
contractAddress: factory.contractAddress,
|
||||
functionName: "createSimpleContractWithCreate2",
|
||||
args: [1],
|
||||
value: 0n
|
||||
});
|
||||
await context.createBlock();
|
||||
|
||||
expect(await context.viem().getTransactionCount({ address: factory.contractAddress })).eq(
|
||||
5
|
||||
);
|
||||
|
||||
const deployedWithCreate2 = (await context.readContract!({
|
||||
contractName: "SimpleContractFactory",
|
||||
contractAddress: factory.contractAddress,
|
||||
functionName: "getDeployedWithCreate2",
|
||||
args: []
|
||||
})) as string[];
|
||||
expect(deployedWithCreate2.length).eq(2);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,272 @@
|
|||
import { describeSuite, expect, fetchCompiledContract } from "@moonwall/cli";
|
||||
import {
|
||||
ALITH_ADDRESS,
|
||||
ALITH_PRIVATE_KEY,
|
||||
BALTATHAR_ADDRESS,
|
||||
BALTATHAR_PRIVATE_KEY,
|
||||
CHARLETH_ADDRESS,
|
||||
createViemTransaction,
|
||||
sendRawTransaction
|
||||
} from "@moonwall/util";
|
||||
import { encodeFunctionData, fromHex } from "viem";
|
||||
import { privateKeyToAccount } from "viem/accounts";
|
||||
import { ConstantStore, expectEVMResult, getSignatureParameters } from "../../../../helpers";
|
||||
|
||||
const ALITH_ACCOUNT = privateKeyToAccount(ALITH_PRIVATE_KEY);
|
||||
|
||||
// Precompile addresses for DataHaven
|
||||
const PRECOMPILE_BATCH_ADDRESS = "0x0000000000000000000000000000000000000808";
|
||||
const PRECOMPILE_CALL_PERMIT_ADDRESS = "0x000000000000000000000000000000000000080a";
|
||||
|
||||
describeSuite({
|
||||
id: "D010312",
|
||||
title: "Batch Precompile",
|
||||
foundationMethods: "dev",
|
||||
testCases: ({ context, it }) => {
|
||||
it({
|
||||
id: "T01",
|
||||
title: "all batch functions should consume similar gas",
|
||||
test: async () => {
|
||||
const { abi: batchInterface } = fetchCompiledContract("Batch");
|
||||
|
||||
// each tx have a different gas limit to ensure it doesn't impact gas used
|
||||
const batchAllTx = await createViemTransaction(context, {
|
||||
to: PRECOMPILE_BATCH_ADDRESS,
|
||||
gas: 1114112n,
|
||||
data: encodeFunctionData({
|
||||
abi: batchInterface,
|
||||
functionName: "batchAll",
|
||||
args: [
|
||||
[BALTATHAR_ADDRESS, CHARLETH_ADDRESS],
|
||||
["1000000000000000000", "2000000000000000000"],
|
||||
[],
|
||||
[]
|
||||
]
|
||||
})
|
||||
});
|
||||
|
||||
const batchSomeTx = await createViemTransaction(context, {
|
||||
to: PRECOMPILE_BATCH_ADDRESS,
|
||||
gas: 1179648n,
|
||||
nonce: 1,
|
||||
data: encodeFunctionData({
|
||||
abi: batchInterface,
|
||||
functionName: "batchSome",
|
||||
args: [
|
||||
[BALTATHAR_ADDRESS, CHARLETH_ADDRESS],
|
||||
["1000000000000000000", "2000000000000000000"],
|
||||
[],
|
||||
[]
|
||||
]
|
||||
})
|
||||
});
|
||||
|
||||
const batchSomeUntilFailureTx = await createViemTransaction(context, {
|
||||
to: PRECOMPILE_BATCH_ADDRESS,
|
||||
gas: 1245184n,
|
||||
nonce: 2,
|
||||
data: encodeFunctionData({
|
||||
abi: batchInterface,
|
||||
functionName: "batchSomeUntilFailure",
|
||||
args: [
|
||||
[BALTATHAR_ADDRESS, CHARLETH_ADDRESS],
|
||||
["1000000000000000000", "2000000000000000000"],
|
||||
[],
|
||||
[]
|
||||
]
|
||||
})
|
||||
});
|
||||
|
||||
const batchAllResult = await sendRawTransaction(context, batchAllTx);
|
||||
const batchSomeResult = await sendRawTransaction(context, batchSomeTx);
|
||||
const batchSomeUntilFailureResult = await sendRawTransaction(
|
||||
context,
|
||||
batchSomeUntilFailureTx
|
||||
);
|
||||
|
||||
await context.createBlock();
|
||||
|
||||
const batchAllReceipt = await context
|
||||
.viem()
|
||||
.getTransactionReceipt({ hash: batchAllResult as `0x${string}` });
|
||||
const batchSomeReceipt = await context
|
||||
.viem()
|
||||
.getTransactionReceipt({ hash: batchSomeResult as `0x${string}` });
|
||||
const batchSomeUntilFailureReceipt = await context
|
||||
.viem()
|
||||
.getTransactionReceipt({ hash: batchSomeUntilFailureResult as `0x${string}` });
|
||||
|
||||
const STORAGE_READ_GAS_COST = // One storage read gas cost
|
||||
ConstantStore(context).STORAGE_READ_COST / ConstantStore(context).WEIGHT_TO_GAS_RATIO;
|
||||
|
||||
// All batch functions should use similar gas (within reasonable tolerance)
|
||||
// The actual gas used includes one storage read for balance check
|
||||
const expectedGas = 43932n + STORAGE_READ_GAS_COST;
|
||||
|
||||
expect(batchAllReceipt.gasUsed).toBe(expectedGas);
|
||||
expect(batchSomeReceipt.gasUsed).toBe(expectedGas);
|
||||
expect(batchSomeUntilFailureReceipt.gasUsed).toBe(expectedGas);
|
||||
}
|
||||
});
|
||||
|
||||
it({
|
||||
id: "T02",
|
||||
title: "batch should be able to call itself",
|
||||
test: async () => {
|
||||
const { abi: batchInterface } = fetchCompiledContract("Batch");
|
||||
|
||||
const batchAll = await context.writeContract!({
|
||||
contractAddress: PRECOMPILE_BATCH_ADDRESS,
|
||||
contractName: "Batch",
|
||||
functionName: "batchAll",
|
||||
args: [
|
||||
[PRECOMPILE_BATCH_ADDRESS],
|
||||
[],
|
||||
[
|
||||
encodeFunctionData({
|
||||
abi: batchInterface,
|
||||
functionName: "batchAll",
|
||||
args: [
|
||||
[BALTATHAR_ADDRESS, CHARLETH_ADDRESS],
|
||||
["1000000000000000000", "2000000000000000000"],
|
||||
[],
|
||||
[]
|
||||
]
|
||||
})
|
||||
],
|
||||
[]
|
||||
],
|
||||
rawTxOnly: true
|
||||
});
|
||||
|
||||
const { result } = await context.createBlock(batchAll);
|
||||
expectEVMResult(result!.events, "Succeed");
|
||||
}
|
||||
});
|
||||
|
||||
it({
|
||||
id: "T03",
|
||||
title: "batch should be callable from call permit precompile",
|
||||
test: async () => {
|
||||
const { abi: batchInterface } = fetchCompiledContract("Batch");
|
||||
const { abi: callPermitAbi } = fetchCompiledContract("CallPermit");
|
||||
|
||||
const alithNonceResult = (
|
||||
await context.viem().call({
|
||||
to: PRECOMPILE_CALL_PERMIT_ADDRESS,
|
||||
data: encodeFunctionData({
|
||||
abi: callPermitAbi,
|
||||
functionName: "nonces",
|
||||
args: [ALITH_ADDRESS]
|
||||
})
|
||||
})
|
||||
).data;
|
||||
|
||||
const batchData = encodeFunctionData({
|
||||
abi: batchInterface,
|
||||
functionName: "batchAll",
|
||||
args: [
|
||||
[BALTATHAR_ADDRESS, CHARLETH_ADDRESS],
|
||||
["1000000000000000000", "2000000000000000000"],
|
||||
[],
|
||||
[]
|
||||
]
|
||||
});
|
||||
|
||||
const chainId = await context.viem().getChainId();
|
||||
const signature = await ALITH_ACCOUNT.signTypedData({
|
||||
types: {
|
||||
EIP712Domain: [
|
||||
{
|
||||
name: "name",
|
||||
type: "string"
|
||||
},
|
||||
{
|
||||
name: "version",
|
||||
type: "string"
|
||||
},
|
||||
{
|
||||
name: "chainId",
|
||||
type: "uint256"
|
||||
},
|
||||
{
|
||||
name: "verifyingContract",
|
||||
type: "address"
|
||||
}
|
||||
],
|
||||
CallPermit: [
|
||||
{
|
||||
name: "from",
|
||||
type: "address"
|
||||
},
|
||||
{
|
||||
name: "to",
|
||||
type: "address"
|
||||
},
|
||||
{
|
||||
name: "value",
|
||||
type: "uint256"
|
||||
},
|
||||
{
|
||||
name: "data",
|
||||
type: "bytes"
|
||||
},
|
||||
{
|
||||
name: "gaslimit",
|
||||
type: "uint64"
|
||||
},
|
||||
{
|
||||
name: "nonce",
|
||||
type: "uint256"
|
||||
},
|
||||
{
|
||||
name: "deadline",
|
||||
type: "uint256"
|
||||
}
|
||||
]
|
||||
},
|
||||
primaryType: "CallPermit",
|
||||
domain: {
|
||||
name: "Call Permit Precompile",
|
||||
version: "1",
|
||||
chainId: Number(chainId),
|
||||
verifyingContract: PRECOMPILE_CALL_PERMIT_ADDRESS
|
||||
},
|
||||
message: {
|
||||
from: ALITH_ADDRESS,
|
||||
to: PRECOMPILE_BATCH_ADDRESS,
|
||||
value: 0n,
|
||||
data: batchData,
|
||||
gaslimit: 200_000n,
|
||||
nonce: fromHex(alithNonceResult!, "bigint"),
|
||||
deadline: 9999999999n
|
||||
}
|
||||
});
|
||||
const { v, r, s } = getSignatureParameters(signature);
|
||||
|
||||
const { result: baltatharForAlithResult } = await context.createBlock(
|
||||
await createViemTransaction(context, {
|
||||
privateKey: BALTATHAR_PRIVATE_KEY,
|
||||
to: PRECOMPILE_CALL_PERMIT_ADDRESS,
|
||||
data: encodeFunctionData({
|
||||
abi: callPermitAbi,
|
||||
functionName: "dispatch",
|
||||
args: [
|
||||
ALITH_ADDRESS,
|
||||
PRECOMPILE_BATCH_ADDRESS,
|
||||
0,
|
||||
batchData,
|
||||
200_000,
|
||||
9999999999,
|
||||
v,
|
||||
r,
|
||||
s
|
||||
]
|
||||
})
|
||||
})
|
||||
);
|
||||
expectEVMResult(baltatharForAlithResult!.events, "Succeed");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -7,21 +7,19 @@
|
|||
{
|
||||
"name": "dev_datahaven",
|
||||
"testFileDir": [
|
||||
"suites/dev"
|
||||
"datahaven/suites/dev"
|
||||
],
|
||||
"include": [
|
||||
"**/*test*.ts"
|
||||
],
|
||||
"timeout": 180000,
|
||||
"multiThreads": 4,
|
||||
"contracts": "contracts/",
|
||||
"contracts": "datahaven/contracts/",
|
||||
"runScripts": [
|
||||
"compile-contracts.sh compile"
|
||||
],
|
||||
"envVars": [
|
||||
"DEBUG_COLORS=1",
|
||||
"RUST_BACKTRACE=1",
|
||||
"RUST_LOG=info"
|
||||
"DEBUG_COLORS=1"
|
||||
],
|
||||
"reporters": [
|
||||
"basic",
|
||||
|
|
|
|||
Loading…
Reference in a new issue