test: port ethereum tests from moonbeam (#278)

Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com>
This commit is contained in:
Ahmad Kaouk 2025-11-22 10:02:05 +01:00 committed by GitHub
parent 0fa701f900
commit 2cc1a4d3f0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
71 changed files with 23624 additions and 3612 deletions

View file

@ -13,7 +13,9 @@
"!**/*.spec.json",
"!**/.papi/descriptors/**/*",
"!**/contract-bindings/**/*",
"!**/html/**/*"
"!**/html/**/*",
"!**/datahaven/contracts/out/**/*",
"!**/contracts/out/**/*"
]
},
"assist": { "actions": { "source": { "organizeImports": "on" } } },

File diff suppressed because it is too large Load diff

View file

@ -88,7 +88,9 @@ const showDatahavenContractStatus = async (chain: string, rpcUrl: string) => {
const exists = await deploymentsFile.exists();
if (!exists) {
contracts.forEach(({ name }) => logger.info(`${name}: Not deployed`));
contracts.forEach(({ name }) => {
logger.info(`${name}: Not deployed`);
});
return;
}

View file

@ -0,0 +1,41 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract AccessListHelper {
uint256 private storedValue;
// Function to set the stored value
function setValue(uint256 _value) public {
storedValue = _value;
}
// Function to load the stored value multiple times using assembly and sload
function loadValueMultipleTimes(uint256 times) public view returns (uint256 total) {
total = 0;
uint256 loaded;
for (uint256 i = 0; i < times; i++) {
assembly {
loaded := sload(storedValue.slot)
}
total += loaded;
}
return total;
}
}
// pragma solidity ^0.8.0;
// import "./AccessListHelper.sol";
contract AccessListHelperProxy {
AccessListHelper accessListHelper;
constructor(address helper) {
accessListHelper = AccessListHelper(helper);
}
function callHelper(uint256 times) public view returns (uint256 value) {
value = accessListHelper.loadValueMultipleTimes(times);
}
}

View file

@ -0,0 +1,13 @@
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.24;
contract BloatedContract {
string public constant HUGE =
"0009029190000120068200810084000002900009444026015071899001591001260845208240017000684720039550000028098850000070600000600003552005936017007053807100000041771580000244664000088007500007700000400000096706200270000267100001000969750000067700056830092002978180072930092092021000644480200750000008400830389708783012663109400001023405538004655400240000045404920000006600110094000113481000525400601812000000056007306626044000042720008605282710425473000027800233400300002500009102903167660730097009598200220053180002052169775049488200700750005660079099422811952047000025490610000097000342400467600000770830040000980026218502900078170000000620450000054000081000003552008290356000000000800003250415214008770723805244741000088000000000592636380007300084062950000018000000008777510000420180307006800190005410046470000416500004200291992003400000111189020270874000700000009900079000040000006000000010006712810006797000500210000067655604000004308300005800111313000032039850411100369100031870182209200019820120611924838009009678000920000000001855000308001341017097640019016027860099820094005600000330202920000615900060680004007000003612660000024005355620550005065000540002100041072059005000003907461062035012000096000028720026611364106886000000999800000001476288954000075200296302389837609535878931960004309600800000290000039000949095033429466000669329005000019420460820075940423086032007000361302000000060627242000320032000000002104000000950780090025075000000075250000990410503708408505037000000700000024090000083063002900144304000004200037007762004203857170057020062273802992604900120068910000750008806400093095005947000990000420088566045090000000015360410000081117690150530002403100036011830000000042051081800105000001066087053027074000009425610442298002698000156609707815450000439406500026330071080007653001100550630637749000000178000005304485000890000052110954284820000000035130";
string store = "";
function doSomething() public {
store = HUGE;
}
}

View file

@ -0,0 +1,26 @@
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.3;
contract CallForwarder {
function call(
address target,
bytes memory data
) public returns (bool, bytes memory) {
return target.call(data);
}
function callRange(address first, address last) public {
require(first < last, "invalid range");
while (first < last) {
first.call("");
first = address(uint160(first) + 1);
}
}
function delegateCall(
address target,
bytes memory data
) public returns (bool, bytes memory) {
return target.delegatecall(data);
}
}

View file

@ -0,0 +1,10 @@
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.3;
contract EventEmitter {
event Constructed(address indexed owner);
constructor() {
emit Constructed(msg.sender);
}
}

View file

@ -0,0 +1,19 @@
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.3;
contract Incrementor {
uint256 public count;
constructor() {
count = 0;
}
function incr() public {
count = count + 1;
}
function incr(uint256 num) public returns (uint256) {
count = count + num;
return count;
}
}

View file

@ -0,0 +1,18 @@
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.3;
contract Looper {
uint256 public count;
function infinite() public pure {
while (true) {}
}
function incrementalLoop(uint256 n) public {
uint256 i = 0;
while (i < n) {
count = count + 1;
i += 1;
}
}
}

View file

@ -0,0 +1,53 @@
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.24;
contract SelfDestructable {
constructor() {
selfdestruct(payable(address(0)));
}
}
contract SelfDestructAfterCreate2 {
uint constant SALT = 1;
address public deployed1;
address public deployed2;
function step1() public {
bytes memory bytecode = type(SelfDestructable).creationCode;
address contractAddress;
uint contractSize;
assembly {
contractAddress := create2(0, add(bytecode, 32), mload(bytecode), SALT)
contractSize := extcodesize(contractAddress)
}
require(contractSize == 0, "Contract size should be zero");
deployed1 = contractAddress;
}
function step2() public {
bytes memory bytecode = type(SelfDestructable).creationCode;
address contractAddress;
uint contractSize;
assembly {
contractAddress := create2(0, add(bytecode, 32), mload(bytecode), SALT)
contractSize := extcodesize(contractAddress)
}
require(contractSize == 0, "Contract size should be zero");
deployed2 = contractAddress;
require(deployed1 == deployed2, "Addresses not equal");
}
function cannotRecreateInTheSameCall() public {
bytes memory bytecode = type(SelfDestructable).creationCode;
address contractAddress1;
address contractAddress2;
assembly {
contractAddress1 := create2(0, add(bytecode, 32), mload(bytecode), SALT)
contractAddress2 := create2(0, add(bytecode, 32), mload(bytecode), SALT)
}
require(contractAddress1 != address(0), "First address must not be null");
require(contractAddress2 == address(0), "Second address must be null");
}
}

View file

@ -0,0 +1,37 @@
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.0;
/// @notice Smart contract to help test state override
contract StateOverrideTest {
/// @notice The maxmium allowed value
uint256 public MAX_ALLOWED = 3;
uint256 public availableFunds;
mapping(address => mapping(address => uint256)) public allowance;
address owner;
constructor(uint256 intialAmount) payable {
owner = msg.sender;
availableFunds = intialAmount;
}
function getBalance() external view returns (uint256) {
return address(this).balance;
}
function getSenderBalance() external view returns (uint256) {
return address(msg.sender).balance;
}
function getAllowance(address from, address who)
external
view
returns (uint256)
{
return allowance[from][who];
}
function setAllowance(address who, uint256 amount) external {
allowance[address(msg.sender)][who] = amount;
}
}

View file

@ -0,0 +1,33 @@
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.3;
interface IBloatedContract {
function doSomething() external;
}
interface ILooper {
function incrementalLoop(uint256 n) external;
}
contract SubCallOOG {
event SubCallSucceed();
event SubCallFail();
function subCallPov(address[] memory addresses) public {
for (uint256 i = 0; i < addresses.length; i++) {
try IBloatedContract(addresses[i]).doSomething() {
emit SubCallSucceed();
} catch (bytes memory) {
emit SubCallFail();
}
}
}
function subCallLooper(address target, uint256 n) public {
try ILooper(target).incrementalLoop(n) {
emit SubCallSucceed();
} catch (bytes memory) {
emit SubCallFail();
}
}
}

View file

@ -0,0 +1,23 @@
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.24;
contract ProxyDeployer {
event ContractDestroyed(address destroyedAddress);
// Function to deploy a new Suicide contract
function deployAndDestroy(address target) public {
Suicide newContract = new Suicide();
newContract.destroy(target);
emit ContractDestroyed(address(newContract));
}
}
contract Suicide {
constructor() payable {
}
function destroy(address target) public {
selfdestruct(payable(target));
}
}

View file

@ -0,0 +1,57 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract ReentrancyProtected {
// A constant key for the reentrancy guard stored in Transient Storage.
// This acts as a unique identifier for the reentrancy lock.
bytes32 constant REENTRANCY_GUARD = keccak256("REENTRANCY_GUARD");
// Modifier to prevent reentrant calls.
// It checks if the reentrancy guard is set (indicating an ongoing execution)
// and sets the guard before proceeding with the function execution.
// After the function executes, it resets the guard to allow future calls.
modifier nonReentrant() {
// Ensure the guard is not set (i.e., no ongoing execution).
require(tload(REENTRANCY_GUARD) == 0, "Reentrant call detected.");
// Set the guard to block reentrant calls.
tstore(REENTRANCY_GUARD, 1);
_; // Execute the function body.
// Reset the guard after execution to allow future calls.
tstore(REENTRANCY_GUARD, 0);
}
// Uses inline assembly to access the Transient Storage's tstore operation.
function tstore(bytes32 location, uint value) private {
assembly {
tstore(location, value)
}
}
// Uses inline assembly to access the Transient Storage's tload operation.
// Returns the value stored at the given location.
function tload(bytes32 location) private returns (uint value) {
assembly {
value := tload(location)
}
}
function nonReentrantMethod() public nonReentrant {
(bool success, bytes memory result) = msg.sender.call("");
if (!success) {
assembly {
revert(add(32, result), mload(result))
}
}
}
function test() external {
this.nonReentrantMethod();
}
receive() external payable {
this.nonReentrantMethod();
}
}

View file

@ -16,7 +16,6 @@ import { calculateFeePortions } from "./fees";
import { getFeesTreasuryProportion } from "./parameters";
const debug = Debug("test:blocks");
export interface TxWithEventAndFee extends TxWithEvent {
fee: RuntimeDispatchInfo | RuntimeDispatchInfoV1;
}
@ -182,6 +181,8 @@ export const verifyBlockFees = async (
} else if (ethTxWrapper.isEip1559) {
priorityFee = ethTxWrapper.asEip1559.maxPriorityFeePerGas.toBigInt();
gasFee = ethTxWrapper.asEip1559.maxFeePerGas.toBigInt();
} else {
throw new Error("Unsupported Ethereum transaction type");
}
const hash = events

View file

@ -4,6 +4,18 @@
*/
import type { GenericContext } from "@moonwall/cli";
import {
ALITH_GENESIS_FREE_BALANCE,
ALITH_GENESIS_LOCK_BALANCE,
ALITH_GENESIS_RESERVE_BALANCE
} from "@moonwall/util";
export const ALITH_GENESIS_TRANSFERABLE_COUNT =
ALITH_GENESIS_FREE_BALANCE + ALITH_GENESIS_RESERVE_BALANCE - ALITH_GENESIS_LOCK_BALANCE;
export const ALITH_GENESIS_TRANSFERABLE_BALANCE =
ALITH_GENESIS_FREE_BALANCE > ALITH_GENESIS_TRANSFERABLE_COUNT
? ALITH_GENESIS_TRANSFERABLE_COUNT
: ALITH_GENESIS_FREE_BALANCE;
class RuntimeConstant<T> {
private readonly values: Map<number, T>;
@ -33,6 +45,9 @@ const DATAHAVEN_CONSTANTS = {
EXTRINSIC_GAS_LIMIT: new RuntimeConstant({
0: 52_000_000n
}),
GENESIS_BASE_FEE: new RuntimeConstant({
0: 312_500_000n
}),
WEIGHT_TO_GAS_RATIO: 25_000n,
STORAGE_READ_COST: 25_000_000n,
STORAGE_WRITE_COST: 50_000_000n,

View file

@ -1,7 +1,10 @@
import { existsSync, readFileSync } from "node:fs";
import path from "node:path";
import { setTimeout as delay } from "node:timers/promises";
import { fileURLToPath } from "node:url";
import type { Abi } from "viem";
import { ALITH_PRIVATE_KEY } from "@moonwall/util";
import { type Abi, createWalletClient, type Hex, http, type Log } from "viem";
import { privateKeyToAccount } from "viem/accounts";
/**
* Contract-related helper utilities for DataHaven tests
@ -28,6 +31,22 @@ export interface CompiledContractArtifact {
sourceCode: string;
}
export interface DeployContractOptions {
args?: readonly unknown[];
gasLimit?: bigint | number;
txnType?: "legacy" | "eip2930" | "eip1559";
value?: bigint | number;
privateKey?: `0x${string}`;
poolSettleDelayMs?: number;
}
export interface DeployedContractResult extends CompiledContractArtifact {
contractAddress: `0x${string}`;
hash: Hex;
logs: readonly Log[];
status: "success" | "reverted";
}
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@ -41,7 +60,7 @@ export const fetchCompiledContract = (contractName: string): CompiledContractArt
.replace(/-+precompile$/, "");
const candidate = path.join(
__dirname,
"../",
"../../",
"contracts",
"out",
"precompiles",
@ -52,6 +71,9 @@ export const fetchCompiledContract = (contractName: string): CompiledContractArt
artifactPath = candidate;
}
}
if (!existsSync(artifactPath)) {
throw new Error(`Contract artifact not found: ${contractName} (searched: ${artifactPath})`);
}
const artifactContent = readFileSync(artifactPath, "utf-8");
const artifactJson = JSON.parse(artifactContent) as CompiledContractArtifactJson;
@ -76,3 +98,82 @@ export const fetchCompiledContract = (contractName: string): CompiledContractArt
sourceCode: artifactJson.sourceCode
} satisfies CompiledContractArtifact;
};
/**
* Deploys a compiled contract using walletClient.deployContract and creates
* blocks while waiting for the receipt.
*/
export const deployContract = async (
context: {
createBlock: (...args: any[]) => Promise<any>;
viem: () => {
getTransactionReceipt: (params: { hash: Hex }) => Promise<{
contractAddress?: `0x${string}` | null;
logs: readonly Log[];
status: "success" | "reverted";
}>;
transport: { url?: string };
chain: unknown;
};
},
contractName: string,
options?: DeployContractOptions
): Promise<DeployedContractResult> => {
const compiled = fetchCompiledContract(contractName);
const { abi, bytecode } = compiled;
const transport = context.viem().transport as { url?: string };
if (!transport?.url) {
throw new Error("Missing viem transport url for contract deployment");
}
const signerKey = options?.privateKey ?? ALITH_PRIVATE_KEY;
const walletClient = createWalletClient({
account: privateKeyToAccount(signerKey),
transport: http(transport.url),
chain: context.viem().chain as any
});
const deployOptions: {
abi: Abi;
bytecode: `0x${string}`;
args?: readonly unknown[];
gas?: bigint;
value?: bigint;
} = {
abi,
bytecode
};
if (options?.args) {
deployOptions.args = options.args;
}
if (options?.gasLimit !== undefined) {
deployOptions.gas = BigInt(options.gasLimit);
}
if (options?.value !== undefined) {
deployOptions.value = BigInt(options.value);
}
const hash = await walletClient.deployContract(deployOptions as any);
for (let attempt = 0; attempt < 12; attempt++) {
await context.createBlock();
try {
const receipt = await context.viem().getTransactionReceipt({ hash });
if (!receipt.contractAddress) {
throw new Error("Missing contract address in deployment receipt");
}
return {
...compiled,
contractAddress: receipt.contractAddress,
hash,
logs: receipt.logs,
status: receipt.status
};
} catch (error) {
if (attempt === 11) {
throw error;
}
await delay(100 * (attempt + 1));
}
}
throw new Error(`Timed out deploying ${contractName}`);
};

View file

@ -0,0 +1,144 @@
import "@moonbeam-network/api-augment";
import assert from "node:assert";
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";
import { fromHex } from "viem";
export type Errors = {
Succeed: EvmCoreErrorExitSucceed["type"];
Error: EvmCoreErrorExitError["type"];
Revert: EvmCoreErrorExitRevert["type"];
Fatal: EvmCoreErrorExitFatal["type"];
};
export async function extractRevertReason(context: DevModeContext, responseHash: string) {
const tx = await context.ethers().provider?.getTransaction(responseHash);
assert(tx, "Transaction not found");
try {
await context.ethers().call({ to: tx.to, data: tx.data, gasLimit: tx.gasLimit });
return null;
} catch (e: any) {
const errorMessage = e.info.error.message;
return errorMessage.split("VM Exception while processing transaction: revert ")[1];
}
}
export function expectEVMResult<T extends Errors, Type extends keyof T>(
events: EventRecord[],
resultType: Type,
reason?: T[Type]
) {
expect(events, "Missing events, probably failed execution").to.be.length.at.least(1);
const ethereumResult = events.find(
({ event: { section, method } }) => section === "ethereum" && method === "Executed"
)!.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,
`Invalid EVM Execution - (${ethereumResult.type}.${foundReason})`
).to.equal(resultType);
if (reason) {
if (ethereumResult.isError) {
expect(
ethereumResult.asError.type,
`Invalid EVM Execution ${ethereumResult.type} Reason`
).to.equal(reason);
} else if (ethereumResult.isFatal) {
expect(
ethereumResult.asFatal.type,
`Invalid EVM Execution ${ethereumResult.type} Reason`
).to.equal(reason);
} else if (ethereumResult.isRevert) {
expect(
ethereumResult.asRevert.type,
`Invalid EVM Execution ${ethereumResult.type} Reason`
).to.equal(reason);
} else
expect(
ethereumResult.asSucceed.type,
`Invalid EVM Execution ${ethereumResult.type} Reason`
).to.equal(reason);
}
}
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: any) {
lastError = error;
// Check if it's the specific error we want to retry
if (
error.name === "TransactionReceiptNotFoundError" ||
error.message?.includes("Transaction receipt with hash") ||
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`);
}
export async function getTransactionFees(context: DevModeContext, hash: string): Promise<bigint> {
const receipt = await getTransactionReceiptWithRetry(context, hash as `0x${string}`);
return receipt.gasUsed * receipt.effectiveGasPrice;
}
export function getSignatureParameters(signature: string) {
const r = signature.slice(0, 66); // 32 bytes
const s = `0x${signature.slice(66, 130)}`; // 32 bytes
let v = fromHex(`0x${signature.slice(130, 132)}`, "number"); // 1 byte
if (![27, 28].includes(v)) v += 27; // not sure why we coerce 27
return {
r,
s,
v
};
}

View file

@ -0,0 +1,176 @@
import { type BlockCreationResponse, type DevModeContext, expect } from "@moonwall/cli";
import type {
ApiTypes,
AugmentedEvent,
AugmentedEvents,
SubmittableExtrinsic
} from "@polkadot/api/types";
import type { EventRecord } from "@polkadot/types/interfaces";
import type { IEvent } from "@polkadot/types/types";
export type ExtractTuple<P> = P extends AugmentedEvent<"rxjs", infer T> ? T : never;
export async function expectOk<
ApiType extends ApiTypes,
Call extends
| SubmittableExtrinsic<ApiType>
| Promise<SubmittableExtrinsic<ApiType>>
| string
| Promise<string>,
Calls extends Call | Call[],
BlockCreation extends BlockCreationResponse<
ApiType,
// @ts-expect-error TODO: fix this
Calls extends Call[] ? Awaited<Call>[] : Awaited<Call>
>
>(call: Promise<BlockCreation>): Promise<BlockCreation> {
const block = await call;
if (Array.isArray(block.result)) {
block.result.forEach((r, idx) => {
expect(
r.successful,
`tx[${idx}] - ${r.error?.name}${
r.extrinsic
? `\n\t\t${r.extrinsic.method.section}.${r.extrinsic.method.method}(${r.extrinsic.args
.map((d) => d.toHuman())
.join("; ")})`
: ""
}`
).to.be.true;
});
} else {
// @ts-expect-error TODO: fix this
expect(block.result!.successful, block.result!.error?.name).to.be.true;
}
return block;
}
export function expectSubstrateEvent<
ApiType extends ApiTypes,
Call extends
| SubmittableExtrinsic<ApiType>
| Promise<SubmittableExtrinsic<ApiType>>
| string
| Promise<string>,
Calls extends Call | Call[],
Event extends AugmentedEvents<ApiType>,
Section extends keyof Event,
Method extends keyof Event[Section],
Tuple extends ExtractTuple<Event[Section][Method]>
>(
//@ts-expect-error TODO: fix this
block: BlockCreationResponse<ApiType, Calls extends Call[] ? Awaited<Call>[] : Awaited<Call>>,
section: Section,
method: Method
): IEvent<Tuple> {
let event: EventRecord | undefined;
if (Array.isArray(block.result)) {
block.result.forEach((r) => {
const foundEvents = r.events.filter(
({ event }) => event.section.toString() === section && event.method.toString() === method
);
if (foundEvents.length > 0) {
expect(
event,
`Event ${section.toString()}.${method.toString()} appeared multiple times`
).toBeUndefined();
expect(
foundEvents,
`Event ${section.toString()}.${method.toString()} appeared multiple times`
).to.be.length(1);
event = foundEvents[0];
}
});
} else {
const foundEvents = (block.result! as any).events!.filter(
(item: any) =>
item.event.section.toString() === section && item.event.method.toString() === method
);
if (foundEvents.length > 0) {
expect(
foundEvents,
`Event ${section.toString()}.${method.toString()} appeared multiple times`
).to.be.length(1);
event = foundEvents[0];
}
}
expect(
event,
`Event ${section.toString()}.${method.toString()} not found:\n${(Array.isArray(block.result)
? block.result.flatMap((r) => r.events)
: block.result
? block.result.events
: []
)
.map(({ event }) => ` - ${event.section.toString()}.${event.method.toString()}\n`)
.join("")}`
).to.not.be.undefined;
return event!.event as any;
}
export function expectSubstrateEvents<
ApiType extends ApiTypes,
Call extends
| SubmittableExtrinsic<ApiType>
| Promise<SubmittableExtrinsic<ApiType>>
| string
| Promise<string>,
Calls extends Call | Call[],
Event extends AugmentedEvents<ApiType>,
Section extends keyof Event,
Method extends keyof Event[Section],
Tuple extends ExtractTuple<Event[Section][Method]>
>(
//@ts-expect-error TODO: fix this
block: BlockCreationResponse<ApiType, Calls extends Call[] ? Awaited<Call>[] : Awaited<Call>>,
section: Section,
method: Method
): IEvent<Tuple>[] {
const events: EventRecord[] = [];
if (Array.isArray(block.result)) {
block.result.forEach((r) => {
const foundEvents = r.events.filter(
({ event }) => event.section.toString() === section && event.method.toString() === method
);
if (foundEvents.length > 0) {
events.push(...foundEvents);
}
});
} else {
const foundEvents = (block.result! as any).events.filter(
(item: any) =>
item.event.section.toString() === section && item.event.method.toString() === method
);
if (foundEvents.length > 0) {
events.push(...foundEvents);
}
}
expect(events.length > 0).to.not.be.null;
return events.map(({ event }) => event) as any;
}
export async function expectSystemEvent(
blockHash: string,
section: string,
method: string,
context: DevModeContext
): Promise<EventRecord> {
const events = await getAllBlockEvents(blockHash, context);
const foundEvents = events.filter(
({ event }) => event.section.toString() === section && event.method.toString() === method
);
const event = foundEvents[0];
expect(
foundEvents,
`Event ${section.toString()}.${method.toString()} appeared multiple times`
).to.be.length(1);
expect(event, `Event ${section.toString()}.${method.toString()} not found in block ${blockHash}`)
.to.not.be.undefined;
return event;
}
async function getAllBlockEvents(hash: string, context: DevModeContext): Promise<EventRecord[]> {
const apiAt = await context.polkadotJs().at(hash);
const events = await apiAt.query.system.events();
return events;
}

View file

@ -9,6 +9,10 @@
export * from "./block";
export * from "./constants";
export * from "./contracts";
// Export unique functions from eth-transactions that aren't in evm.ts
export { extractRevertReason } from "./eth-transactions";
export * from "./evm";
export * from "./expect";
export * from "./fees";
export * from "./parameters";
export * from "./transactions";

View file

@ -0,0 +1,124 @@
// Ethers is used to handle post-london transactions
import type { DevModeContext } from "@moonwall/cli";
import { createViemTransaction } from "@moonwall/util";
import type { ApiPromise } from "@polkadot/api";
import type { SubmittableExtrinsic } from "@polkadot/api/promise/types";
export const DEFAULT_TXN_MAX_BASE_FEE = 10_000_000_000;
/**
* Send a JSONRPC request to the node at http://localhost:9944.
*
* @param method - The JSONRPC request method.
* @param params - The JSONRPC request params.
*/
export async function rpcToLocalNode(
rpcPort: number,
method: string,
params: any[] = []
): Promise<any> {
return fetch(`http://localhost:${rpcPort}`, {
body: JSON.stringify({
id: 1,
jsonrpc: "2.0",
method,
params
}),
headers: {
"Content-Type": "application/json"
},
method: "POST"
})
.then((response) => response.json())
.then((data: any) => {
if ("error" in data && "result" in data) {
const { error, result } = data;
if (error) {
throw new Error(`${error.code} ${error.message}: ${JSON.stringify(error.data)}`);
}
return result;
}
throw new Error("Unexpected response format");
});
}
export const sendAllStreamAndWaitLast = async (
api: ApiPromise,
extrinsics: SubmittableExtrinsic[],
{ threshold = 500, batch = 200, timeout = 120000 } = {
threshold: 500,
batch: 200,
timeout: 120000
}
) => {
const promises: any[] = [];
while (extrinsics.length > 0) {
const pending = await api.rpc.author.pendingExtrinsics();
if (pending.length < threshold) {
const chunk = extrinsics.splice(0, Math.min(threshold - pending.length, batch));
// console.log(`Sending ${chunk.length}tx (${extrinsics.length} left)`);
promises.push(
Promise.all(
chunk.map((tx) => {
return new Promise(async (resolve, reject) => {
const timer = setTimeout(() => {
reject("timed out");
unsub();
}, timeout);
const unsub = await tx.send((result) => {
// reset the timer
if (result.isError) {
console.log(result.toHuman());
clearTimeout(timer);
reject(result.toHuman());
}
if (result.isInBlock) {
unsub();
clearTimeout(timer);
resolve(null);
}
});
}).catch(() => {});
})
)
);
}
await new Promise((resolve) => setTimeout(resolve, 2000));
}
await Promise.all(promises);
};
// The parameters passed to the function are assumed to have all been converted to hexadecimal
export async function sendPrecompileTx(
context: DevModeContext,
precompileContractAddress: `0x${string}`,
selectors: { [key: string]: string },
from: string,
privateKey: `0x${string}`,
selector: string,
parameters: string[]
) {
let data: `0x${string}`;
if (selectors[selector]) {
data = `0x${selectors[selector]}`;
} else {
throw new Error(`selector doesn't exist on the precompile contract`);
}
for (const param of parameters) {
data += param.slice(2).padStart(64, "0");
}
return context.createBlock(
createViemTransaction(context, {
from,
privateKey,
value: 0n,
gas: 200_000n,
to: precompileContractAddress,
data
})
);
}
export const ERC20_TOTAL_SUPPLY = 1_000_000_000n;

View file

@ -0,0 +1,112 @@
import { beforeAll, deployCreateCompiledContract, describeSuite, expect } from "@moonwall/cli";
import { ALITH_ADDRESS } from "@moonwall/util";
import { type Abi, encodeFunctionData } from "viem";
const PRECOMPILE_PREFIXES = [1, 2, 3, 4, 5, 6, 7, 8, 9, 1024, 1026, 2050, 2056, 2058, 2059];
// Ethereum precompile 1-9 are pure and allowed to be called through DELEGATECALL
const ALLOWED_PRECOMPILE_PREFIXES = PRECOMPILE_PREFIXES.filter((add) => add <= 9);
const FORBIDDEN_PRECOMPILE_PREFIXES = PRECOMPILE_PREFIXES.filter((add) => add > 9);
const DELEGATECALL_FORDIDDEN_MESSAGE =
// contract call result (boolean). False === failed.
"0x0000000000000000000000000000000000000000000000000000000000000000" +
"0000000000000000000000000000000000000000000000000000000000000040" + // result offset
"0000000000000000000000000000000000000000000000000000000000000084" + // result length
"08c379a0" + // revert selector
"0000000000000000000000000000000000000000000000000000000000000020" + // revert offset
"000000000000000000000000000000000000000000000000000000000000002e" + // revert message length
"43616e6e6f742062652063616c6c656420" + // Cannot be called
"776974682044454c454741544543414c4c20" + // with DELEGATECALL
"6f722043414c4c434f4445" + // or CALLCODE
"0000000000000000000000000000" + // padding
"0000000000000000000000000000000000000000000000000000000000000000"; // padding;
describeSuite({
id: "D020501",
title: "Delegate Call",
foundationMethods: "dev",
testCases: ({ context, it }) => {
let forwardAddress: `0x${string}`;
let forwardAbi: Abi;
beforeAll(async () => {
const { contractAddress, abi } = await deployCreateCompiledContract(context, "CallForwarder");
forwardAddress = contractAddress;
forwardAbi = abi;
});
it({
id: "T01",
timeout: 10000,
title: "should work for normal smart contract",
test: async () => {
const { contractAddress: dummyAddress, abi: dummyAbi } = await deployCreateCompiledContract(
context,
"MultiplyBy7"
);
const txCall = await context.viem().call({
account: ALITH_ADDRESS as `0x${string}`,
to: forwardAddress,
data: encodeFunctionData({
abi: forwardAbi,
functionName: "delegateCall",
args: [
dummyAddress,
encodeFunctionData({ abi: dummyAbi, functionName: "multiply", args: [42] })
]
})
});
expect(txCall.data).to.equal(
"0x0000000000000000000000000000000000000000000000000000000000000001" +
"0000000000000000000000000000000000000000000000000000000000000040" +
"0000000000000000000000000000000000000000000000000000000000000020" +
"0000000000000000000000000000000000000000000000000000000000000126"
);
}
});
for (const precompilePrefix of ALLOWED_PRECOMPILE_PREFIXES) {
it({
id: `T${ALLOWED_PRECOMPILE_PREFIXES.indexOf(precompilePrefix) + 1}`,
title: `should succeed for standard precompile ${precompilePrefix}`,
test: async () => {
const precompileAddress = `0x${precompilePrefix.toString(16).padStart(40, "0")}`;
const txCall = await context.viem().call({
account: ALITH_ADDRESS as `0x${string}`,
to: forwardAddress,
data: encodeFunctionData({
abi: forwardAbi,
functionName: "delegateCall",
args: [precompileAddress, "0x00"]
})
});
expect(txCall.data).to.not.equal(DELEGATECALL_FORDIDDEN_MESSAGE);
}
});
}
for (const precompilePrefix of FORBIDDEN_PRECOMPILE_PREFIXES) {
it({
id: `T${ALLOWED_PRECOMPILE_PREFIXES.indexOf(precompilePrefix) * 2 + 1}`,
title: `should fail for non-standard precompile ${precompilePrefix}`,
test: async () => {
const precompileAddress = `0x${precompilePrefix.toString(16).padStart(40, "0")}`;
const txCall = await context.viem().call({
account: ALITH_ADDRESS as `0x${string}`,
to: forwardAddress,
data: encodeFunctionData({
abi: forwardAbi,
functionName: "delegateCall",
args: [precompileAddress, "0x00"]
})
});
expect(txCall.data, "Call should be forbidden").to.equal(DELEGATECALL_FORDIDDEN_MESSAGE);
}
});
}
}
});

View file

@ -0,0 +1,97 @@
import {
beforeAll,
deployCreateCompiledContract,
describeSuite,
expect,
TransactionTypes
} from "@moonwall/cli";
import { CHARLETH_ADDRESS, CHARLETH_PRIVATE_KEY, createEthersTransaction } from "@moonwall/util";
import { type Abi, encodeFunctionData } from "viem";
import { verifyLatestBlockFees } from "../../../../helpers";
// TODO: expand these tests to do multiple txn types when added to viem
describeSuite({
id: "D020502",
title: "Contract loop error",
foundationMethods: "dev",
testCases: ({ context, it }) => {
let looperAddress: `0x${string}`;
let looperAbi: Abi;
beforeAll(async () => {
const { contractAddress, abi } = await deployCreateCompiledContract(context, "Looper");
looperAddress = contractAddress;
looperAbi = abi;
});
for (const txnType of TransactionTypes) {
it({
id: `T0${TransactionTypes.indexOf(txnType) + 1}`,
title: `"should return OutOfGas on inifinite loop ${txnType} call`,
test: async () => {
await expect(
async () =>
await context.viem().call({
account: CHARLETH_ADDRESS,
to: looperAddress,
data: encodeFunctionData({ abi: looperAbi, functionName: "infinite", args: [] }),
gas: 12_000_000n
}),
"Execution succeeded but should have failed"
).rejects.toThrowError("out of gas");
}
});
it({
id: `T0${TransactionTypes.indexOf(txnType) + 1 + TransactionTypes.length}`,
title: `should fail with OutOfGas on infinite loop ${txnType} transaction`,
test: async () => {
const nonce = await context.viem().getTransactionCount({ address: CHARLETH_ADDRESS });
const rawSigned = await createEthersTransaction(context, {
to: looperAddress,
data: encodeFunctionData({ abi: looperAbi, functionName: "infinite", args: [] }),
txnType,
nonce,
privateKey: CHARLETH_PRIVATE_KEY
});
const { result } = await context.createBlock(rawSigned, {
signer: { type: "ethereum", privateKey: CHARLETH_PRIVATE_KEY }
});
expect(result.successful).to.be.true;
const receipt = await context
.viem("public")
.getTransactionReceipt({ hash: result!.hash as `0x${string}` });
expect(receipt.status).toBe("reverted");
}
});
it({
id: `T0${TransactionTypes.indexOf(txnType) + 1 + TransactionTypes.length * 2}`,
title: `should fail with OutOfGas on infinite loop ${txnType} transaction - check fees`,
test: async () => {
const nonce = await context.viem().getTransactionCount({ address: CHARLETH_ADDRESS });
const rawSigned = await createEthersTransaction(context, {
to: looperAddress,
data: encodeFunctionData({ abi: looperAbi, functionName: "infinite", args: [] }),
txnType,
nonce,
privateKey: CHARLETH_PRIVATE_KEY
});
const { result } = await context.createBlock(rawSigned, {
signer: { type: "ethereum", privateKey: CHARLETH_PRIVATE_KEY }
});
expect(result.successful).to.be.true;
await verifyLatestBlockFees(context);
}
});
}
}
});

View file

@ -0,0 +1,38 @@
import { describeSuite, expect, fetchCompiledContract, TransactionTypes } from "@moonwall/cli";
import { ALITH_ADDRESS, createEthersTransaction } from "@moonwall/util";
import { encodeDeployData } from "viem";
describeSuite({
id: "D020503",
title: "Contract event",
foundationMethods: "dev",
testCases: ({ context, it }) => {
for (const txnType of TransactionTypes) {
it({
id: `T0${TransactionTypes.indexOf(txnType) + 1}`,
title: "should contain event",
test: async () => {
const { abi, bytecode } = fetchCompiledContract("EventEmitter");
const rawSigned = await createEthersTransaction(context, {
data: encodeDeployData({ abi, bytecode, args: [] }),
txnType,
gasLimit: 10_000_000
});
const { result } = await context.createBlock(rawSigned);
expect(result?.successful, "Unsuccessful deploy").toBe(true);
const receipt = await context
.viem("public")
.getTransactionReceipt({ hash: result?.hash as `0x${string}` });
expect(receipt.logs.length).toBe(1);
expect(
`0x${receipt.logs[0].topics[1]!.substring(26, receipt.logs[0].topics[1]!.length + 1)}`
).toBe(ALITH_ADDRESS.toLowerCase());
}
});
}
}
});

View file

@ -0,0 +1,42 @@
import { describeSuite, expect } from "@moonwall/cli";
import { ALITH_ADDRESS, createEthersTransaction } from "@moonwall/util";
describeSuite({
id: "D020504",
title: "Contract - Excessive memory allocation",
foundationMethods: "dev",
testCases: ({ context, it }) => {
// this tests a security vulnerability in our EVM which was patched in May 2021 or so.
// The vulnerability allowed contract code to request an extremely large amount of memory,
// causing a node to crash.
//
// fixed by:
// https://github.com/rust-blockchain/evm/commit/19ade858c430ab13eb562764a870ac9f8506f8dd
it({
id: "T01",
title: "should fail with out of gas",
test: async () => {
const value = `0x${993452714685890559n.toString(16)}`;
const rawSigned = await createEthersTransaction(context, {
from: ALITH_ADDRESS,
to: null,
value,
gasLimit: 0x100000,
gasPrice: 10_000_000_000,
data:
"0x4141046159864141414141343933343346" +
"460100000028F900E06F01000000F71E01000000000000"
});
const { result } = await context.createBlock(rawSigned);
const receipt = await context
.viem("public")
.getTransactionReceipt({ hash: result?.hash as `0x${string}` });
expect(receipt.status).toBe("reverted");
}
});
}
});

View file

@ -0,0 +1,146 @@
import { describeSuite, expect, fetchCompiledContract, TransactionTypes } from "@moonwall/cli";
import { encodeDeployData } from "viem";
import { getTransactionReceiptWithRetry } from "../../../../helpers/eth-transactions";
describeSuite({
id: "D020505",
title: "Fibonacci",
foundationMethods: "dev",
testCases: ({ context, it }) => {
for (const txnType of TransactionTypes) {
it({
id: `T0${TransactionTypes.indexOf(txnType) + 1}`,
title: "should be able to call fibonacci",
test: async () => {
//TODO: replace this with txnType deploy fn when available
const { abi, bytecode } = fetchCompiledContract("Fibonacci");
const data = encodeDeployData({
abi,
bytecode
});
const { result } = await context.createBlock(
context.createTxn!({
data,
txnType,
libraryType: "ethers",
gasLimit: 260_000n
})
);
const contractAddress = (
await context.viem().getTransactionReceipt({ hash: result!.hash as `0x${string}` })
).contractAddress!;
expect(
await context.readContract!({
contractName: "Fibonacci",
contractAddress,
functionName: "fib2",
args: [0]
})
).toBe(0n);
expect(
await context.readContract!({
contractName: "Fibonacci",
contractAddress,
functionName: "fib2",
args: [1]
})
).toBe(1n);
expect(
await context.readContract!({
contractName: "Fibonacci",
contractAddress,
functionName: "fib2",
args: [2]
})
).toBe(1n);
expect(
await context.readContract!({
contractName: "Fibonacci",
contractAddress,
functionName: "fib2",
args: [3]
})
).toBe(2n);
expect(
await context.readContract!({
contractName: "Fibonacci",
contractAddress,
functionName: "fib2",
args: [4]
})
).toBe(3n);
expect(
await context.readContract!({
contractName: "Fibonacci",
contractAddress,
functionName: "fib2",
args: [5]
})
).toBe(5n);
expect(
await context.readContract!({
contractName: "Fibonacci",
contractAddress,
functionName: "fib2",
args: [20]
})
).toBe(6765n);
// the largest Fib number supportable by a uint256 is 370.
// actual value:
// 94611056096305838013295371573764256526437182762229865607320618320601813254535
expect(
await context.readContract!({
contractName: "Fibonacci",
contractAddress,
functionName: "fib2",
args: [370]
})
).toBe(94611056096305838013295371573764256526437182762229865607320618320601813254535n);
}
});
it({
id: `T0${TransactionTypes.indexOf(txnType) + 4}`,
title: "should be able to call fibonacci[370] in txn",
test: async () => {
//TODO: replace this with txnType deploy fn when available
const { abi, bytecode } = fetchCompiledContract("Fibonacci");
const data = encodeDeployData({
abi,
bytecode
});
const { result } = await context.createBlock(
context.createTxn!({
data,
txnType,
libraryType: "ethers"
})
);
const receipt1 = await getTransactionReceiptWithRetry(
context,
result!.hash as `0x${string}`
);
const contractAddress = receipt1.contractAddress!;
const hash = await context.writeContract!({
contractName: "Fibonacci",
contractAddress,
functionName: "fib2",
args: [370],
value: 0n
});
await context.createBlock();
const receipt2 = await getTransactionReceiptWithRetry(context, hash as `0x${string}`);
expect(receipt2.status).toBe("success");
}
});
}
}
});

View file

@ -0,0 +1,84 @@
import { beforeEach, describeSuite, expect, fetchCompiledContract } from "@moonwall/cli";
import { encodeFunctionData } from "viem";
import { verifyLatestBlockFees } from "../../../../helpers";
describeSuite({
id: "D020506",
title: "Contract loop",
foundationMethods: "dev",
testCases: ({ context, it }) => {
// let incrementorAbi: Abi;
let incrementorAddress: `0x${string}`;
beforeEach(async () => {
// const {
// // contract: incContract,
// contractAddress: incAddress,
// abi: incAbi,
// } = await deployCreateCompiledContract(context, "Incrementor");
const { contractAddress } = await context.deployContract!("Incrementor");
// incrementorContract = incContract;
incrementorAddress = contractAddress;
});
it({
id: "T01",
title: "creation be initialized at 0",
test: async () => {
expect(
await context.readContract!({
contractName: "Incrementor",
contractAddress: incrementorAddress,
functionName: "count"
})
).toBe(0n);
}
});
it({
id: "T02",
title: "should increment contract state",
test: async () => {
await context.writeContract!({
contractName: "Incrementor",
contractAddress: incrementorAddress,
functionName: "incr",
value: 0n
});
await context.createBlock();
expect(
await context.readContract!({
contractName: "Incrementor",
contractAddress: incrementorAddress,
functionName: "count"
})
).toBe(1n);
}
});
it({
id: "T03",
title: "should increment contract state (check fees)",
test: async () => {
const data = encodeFunctionData({
abi: fetchCompiledContract("Incrementor").abi,
functionName: "incr"
});
await context.createBlock(
context.createTxn!({
data,
to: incrementorAddress,
value: 0n,
maxPriorityFeePerGas: 0n,
txnType: "eip1559"
})
);
await verifyLatestBlockFees(context);
}
});
}
});

View file

@ -0,0 +1,60 @@
import { describeSuite, expect, TransactionTypes } from "@moonwall/cli";
import { createEthersTransaction } from "@moonwall/util";
import { encodeFunctionData } from "viem";
import { deployContract } from "../../../../helpers/contracts";
describeSuite({
id: "D020507",
title: "Contract loop",
foundationMethods: "dev",
testCases: ({ context, it }) => {
let testNumber = 0;
const TestParameters = [
{
loop: 1n,
gas: 43_774n
},
{
loop: 500n,
gas: 241_390n
},
{
loop: 600n,
gas: 280_990n
}
];
TestParameters.forEach(({ loop, gas }) => {
for (const txnType of TransactionTypes) {
testNumber++;
it({
id: `T${testNumber > 9 ? testNumber : `0${testNumber}`}`,
title: `should consume ${gas} for ${loop} loop for ${txnType}`,
test: async () => {
const { abi, contractAddress } = await deployContract(context as any, "Looper");
const rawSigned = await createEthersTransaction(context, {
to: contractAddress,
data: encodeFunctionData({ abi, functionName: "incrementalLoop", args: [loop] }),
gasLimit: 10_000_000,
txnType
});
await context.createBlock(rawSigned);
expect(
await context.readContract!({
contractName: "Looper",
contractAddress,
functionName: "count"
})
).toBe(loop);
const block = await context.viem().getBlock();
expect(block.gasUsed).toBe(gas);
}
});
}
});
}
});

View file

@ -0,0 +1,139 @@
import { beforeAll, describeSuite, expect } from "@moonwall/cli";
import { ALITH_ADDRESS } from "@moonwall/util";
import { type Abi, encodeFunctionData } from "viem";
describeSuite({
id: "D020508",
title: "Contract creation",
foundationMethods: "dev",
testCases: ({ context, it }) => {
let multiplyAddress: `0x${string}`;
let multiplyAbi: Abi;
let deployHash: `0x${string}`;
beforeAll(async () => {
const { contractAddress, abi, hash } = await context.deployContract!("MultiplyBy7");
multiplyAddress = contractAddress;
multiplyAbi = abi;
deployHash = hash;
});
// TODO: Re-enable when viem add txntype support for call method
// for (const txnType of TransactionTypes) {
it({
id: "T01",
title: "should appear in the block transaction list",
test: async () => {
const block = await context.viem().getBlock();
const txHash = block.transactions[0];
expect(txHash).toBe(deployHash);
}
});
it({
id: "T02",
title: "should be in the transaction list",
test: async () => {
const tx = await context.viem().getTransaction({ hash: deployHash });
expect(tx.hash).to.equal(deployHash);
}
});
it({
id: "T03",
title: "should provide callable methods",
test: async () => {
expect(
await context.readContract!({
contractName: "MultiplyBy7",
contractAddress: multiplyAddress,
functionName: "multiply",
args: [3]
})
// multiplyContract.read.multiply([3])
).toBe(21n);
}
});
it({
id: "T04",
title: "should fail for call method with missing parameters",
test: async () => {
await expect(
async () =>
await context.viem().call({
account: ALITH_ADDRESS as `0x${string}`,
to: multiplyAddress,
data: encodeFunctionData({
abi: [{ ...multiplyAbi[0], inputs: [] }],
functionName: "multiply",
args: []
})
}),
"Execution succeeded but should have failed"
).rejects.toThrowError("VM Exception while processing transaction: revert");
}
});
it({
id: "T05",
title: "should fail for too many parameters",
test: async () => {
await expect(
async () =>
await context.viem().call({
account: ALITH_ADDRESS as `0x${string}`,
to: multiplyAddress,
data: encodeFunctionData({
abi: [
{
...multiplyAbi[0],
inputs: [
{ internalType: "uint256", name: "a", type: "uint256" },
{ internalType: "uint256", name: "b", type: "uint256" }
]
}
],
functionName: "multiply",
args: [3, 4]
})
}),
"Execution succeeded but should have failed"
).rejects.toThrowError("VM Exception while processing transaction: revert");
}
});
it({
id: "T06",
title: "should fail for invalid parameters",
test: async () => {
await expect(
async () =>
await context.viem().call({
account: ALITH_ADDRESS as `0x${string}`,
to: multiplyAddress,
data: encodeFunctionData({
abi: [
{
...multiplyAbi[0],
inputs: [
{
internalType: "address",
name: "a",
type: "address"
}
]
}
],
functionName: "multiply",
args: ["0x0123456789012345678901234567890123456789"]
})
}),
"Execution succeeded but should have failed"
).rejects.toThrowError("VM Exception while processing transaction: revert");
}
});
}
});

View file

@ -0,0 +1,67 @@
import { beforeAll, describeSuite, expect } from "@moonwall/cli";
describeSuite({
id: "D020509",
title: "Block Contract - Block variables",
foundationMethods: "dev",
testCases: ({ context, it }) => {
let blockContract: `0x${string}`;
beforeAll(async () => {
const { contractAddress } = await context.deployContract!("BlockVariables", {
gas: 1000000n
});
blockContract = contractAddress;
});
it({
id: "T01",
title: "should store the valid block number at creation",
test: async () => {
expect(
await context.readContract!({
contractName: "BlockVariables",
contractAddress: blockContract,
functionName: "initialnumber"
})
).toBe(1n);
}
});
it({
id: "T02",
title: "should return parent block number + 1 when accessed by RPC call",
test: async () => {
const block = await context.viem().getBlock();
expect(
await context.readContract!({
contractName: "BlockVariables",
contractAddress: blockContract,
functionName: "getNumber"
})
).toBe(1n);
expect(
await context.readContract!({
contractName: "BlockVariables",
contractAddress: blockContract,
functionName: "getNumber"
})
).toBe(block.number);
}
});
it({
id: "T03",
title: "should store the valid chain id at creation",
test: async () => {
expect(
await context.readContract!({
contractName: "BlockVariables",
contractAddress: blockContract,
functionName: "initialchainid"
})
).toBe(55932n);
}
});
}
});

View file

@ -0,0 +1,153 @@
import { beforeEach, describeSuite, expect, fetchCompiledContract } from "@moonwall/cli";
import { BALTATHAR_ADDRESS, GLMR } from "@moonwall/util";
import { decodeEventLog } from "viem";
import { expectEVMResult, expectSubstrateEvent } from "../../../../helpers";
describeSuite({
id: "D020510",
title: "EIP-6780 - Self Destruct",
foundationMethods: "dev",
testCases: ({ context, it }) => {
let contract: `0x${string}`;
beforeEach(async () => {
const { contractAddress } = await context.deployContract!("Suicide", {
gas: 45_000_000n
});
contract = contractAddress;
});
it({
id: "T01",
title:
"Should not delete contract when self-destruct is not called in the same " +
"transaction that created the contract",
test: async () => {
// Get Code
const code = (await context.polkadotJs().query.evm.accountCodes(contract)).toHex();
// transfer some tokens to the contract
await context.createBlock(
context.polkadotJs().tx.balances.transferAllowDeath(contract, 10n * GLMR)
);
const balanceBaltatharBefore = (
await context.polkadotJs().query.system.account(BALTATHAR_ADDRESS)
).data.free.toBigInt();
const rawTx = await context.writeContract!({
contractName: "Suicide",
contractAddress: contract,
functionName: "destroy",
args: [BALTATHAR_ADDRESS],
rawTxOnly: true
});
const { result } = await context.createBlock(rawTx);
expectEVMResult(result!.events, "Succeed", "Suicided");
// Code should not be deleted
const postSuicideCode = (
await context.polkadotJs().query.evm.accountCodes(contract)
).toHex();
expect(postSuicideCode).to.eq(code);
// Nonce should be one
expect((await context.polkadotJs().query.system.account(contract)).nonce.toBigInt()).to.eq(
1n
);
// Balance should be zero
expect(
(await context.polkadotJs().query.system.account(contract)).data.free.toBigInt()
).to.eq(0n);
// Check funds are transmitted to Baltathar
const balanceBaltatharAfter = (
await context.polkadotJs().query.system.account(BALTATHAR_ADDRESS)
).data.free.toBigInt();
expect(balanceBaltatharAfter).to.be.eq(balanceBaltatharBefore + 10n * GLMR);
}
});
it({
id: "T02",
title:
"Should not burn funds if contract is not deleted in the same create tx and" +
"funds are sent to deleted contract",
test: async () => {
// transfer some tokens to the contract
await context.createBlock(
context.polkadotJs().tx.balances.transferAllowDeath(contract, 10n * GLMR)
);
const rawTx = await context.writeContract!({
contractName: "Suicide",
contractAddress: contract,
functionName: "destroy",
args: [contract],
rawTxOnly: true
});
const { result } = await context.createBlock(rawTx);
expectEVMResult(result!.events, "Succeed", "Suicided");
expect(
(await context.polkadotJs().query.system.account(contract)).data.free.toBigInt()
).to.eq(10n * GLMR);
}
});
it({
id: "T03",
title:
"Should delete contract when self-destruct is called in the same transaction" +
"that created the contract",
test: async () => {
const { contractAddress } = await context.deployContract!("ProxyDeployer", {
gas: 1000000n
});
const block = await context.createBlock(
await context.writeContract!({
contractName: "ProxyDeployer",
contractAddress,
functionName: "deployAndDestroy",
rawTxOnly: true,
args: [BALTATHAR_ADDRESS]
})
);
const { data } = expectSubstrateEvent(block, "evm", "Log");
const evmLog = decodeEventLog({
abi: fetchCompiledContract("ProxyDeployer").abi,
topics: data[0].topics.map((t) => t.toHex()) as any,
data: data[0].data.toHex()
}) as any;
const suicideAddress: `0x${string}` = evmLog.args.destroyedAddress.toLowerCase();
// Code should be deleted
expect((await context.polkadotJs().query.evm.accountCodes(suicideAddress)).toHex()).to.eq(
"0x"
);
// Balance should be zero
expect(
(await context.polkadotJs().query.system.account(suicideAddress)).data.free.toBigInt()
).to.eq(0n);
// Sufficients should be zero
expect(
(await context.polkadotJs().query.system.account(suicideAddress)).sufficients.toBigInt()
).to.eq(0n);
// Nonce should be zero
expect(
(await context.polkadotJs().query.system.account(suicideAddress)).nonce.toBigInt()
).to.eq(0n);
}
});
}
});

View file

@ -0,0 +1,36 @@
import { beforeAll, describeSuite, expect } from "@moonwall/cli";
describeSuite({
id: "D020511",
title: "EIP-1153 - Transient storage",
foundationMethods: "dev",
testCases: ({ context, it }) => {
let contract: `0x${string}`;
beforeAll(async () => {
const { contractAddress } = await context.deployContract!("ReentrancyProtected", {
gas: 1000000n
});
contract = contractAddress;
});
it({
id: "T01",
title: "should detect reentrant call and revert",
test: async () => {
try {
await context.writeContract!({
contractName: "ReentrancyProtected",
contractAddress: contract,
functionName: "test"
});
} catch (error) {
return expect(error.details).to.be.eq(
"VM Exception while processing transaction: revert Reentrant call detected."
);
}
expect.fail("Expected the contract call to fail");
}
});
}
});

View file

@ -0,0 +1,281 @@
import {
beforeAll,
customDevRpcRequest,
deployCreateCompiledContract,
describeSuite,
expect,
fetchCompiledContract
} from "@moonwall/cli";
import { ALITH_ADDRESS, baltathar, createEthersTransaction, GLMR } from "@moonwall/util";
import { hexToBigInt, nToHex } from "@polkadot/util";
import { type Abi, encodeFunctionData, encodePacked, keccak256, pad, parseEther } from "viem";
import { expectOk } from "../../../../helpers";
describeSuite({
id: "D020901",
title: "Call - State Override",
foundationMethods: "dev",
testCases: ({ context, it }) => {
let stateOverrideAddress: string;
let contractAbi: Abi;
beforeAll(async () => {
const { contractAddress, abi, status } = await deployCreateCompiledContract(
context,
"StateOverrideTest",
{ args: [100n], value: parseEther("1") }
);
expect(status).to.equal("success");
const rawSigned = await createEthersTransaction(context, {
to: contractAddress,
data: encodeFunctionData({
abi,
functionName: "setAllowance",
args: [baltathar.address, 10n]
}),
gasLimit: 10_000_000
});
await expectOk(context.createBlock(rawSigned));
contractAbi = abi;
stateOverrideAddress = contractAddress;
});
it({
id: "T01",
title: "should have a balance of > 100 GLMR without state override",
test: async () => {
const { data } = await context.viem().call({
account: baltathar.address,
to: stateOverrideAddress as `0x${string}`,
data: encodeFunctionData({ abi: contractAbi, functionName: "getSenderBalance" })
});
expect(hexToBigInt(data) > 100n * GLMR).to.be.true;
}
});
it({
id: "T02",
title: "should have a balance of 50 GLMR with state override",
test: async () => {
const result = await customDevRpcRequest("eth_call", [
{
from: baltathar.address,
to: stateOverrideAddress,
data: encodeFunctionData({ abi: contractAbi, functionName: "getSenderBalance" })
},
"latest",
{
[baltathar.address]: {
balance: nToHex(50n * GLMR)
}
}
]);
expect(hexToBigInt(result)).to.equal(50n * GLMR);
}
});
it({
id: "T03",
title: "should have availableFunds of 100 without state override",
test: async () => {
const result = await customDevRpcRequest("eth_call", [
{
from: ALITH_ADDRESS,
to: stateOverrideAddress,
data: encodeFunctionData({ abi: contractAbi, functionName: "availableFunds" })
}
]);
expect(hexToBigInt(result)).to.equal(100n);
}
});
it({
id: "T04",
title: "should have availableFunds of 500 with state override",
test: async () => {
const availableFundsKey = pad(nToHex(1)); // slot 1
const newValue = pad(nToHex(500));
const result = await customDevRpcRequest("eth_call", [
{
from: ALITH_ADDRESS,
to: stateOverrideAddress,
data: encodeFunctionData({ abi: contractAbi, functionName: "availableFunds" })
},
"latest",
{
[stateOverrideAddress]: {
stateDiff: {
[availableFundsKey]: newValue
}
}
}
]);
expect(hexToBigInt(result)).to.equal(500n);
}
});
it({
id: "T05",
title: "should have allowance of 10 without state override",
test: async () => {
const result = await customDevRpcRequest("eth_call", [
{
from: ALITH_ADDRESS,
to: stateOverrideAddress,
data: encodeFunctionData({
abi: contractAbi,
functionName: "allowance",
args: [ALITH_ADDRESS, baltathar.address]
})
}
]);
expect(hexToBigInt(result)).to.equal(10n);
}
});
it({
id: "T06",
title: "should have allowance of 50 with state override",
test: async () => {
const allowanceKey = keccak256(
encodePacked(
["uint256", "uint256"],
[
baltathar.address,
keccak256(
encodePacked(
["uint256", "uint256"],
[
ALITH_ADDRESS as any,
2n // slot 2
]
)
) as unknown as bigint
]
)
);
const newValue = pad(nToHex(50));
const result = await customDevRpcRequest("eth_call", [
{
from: ALITH_ADDRESS,
to: stateOverrideAddress,
data: encodeFunctionData({
abi: contractAbi,
functionName: "allowance",
args: [ALITH_ADDRESS, baltathar.address]
})
},
"latest",
{
[stateOverrideAddress]: {
stateDiff: {
[allowanceKey]: newValue
}
}
}
]);
expect(hexToBigInt(result)).to.equal(50n);
}
});
it({
id: "T07",
title: "should have allowance 50 but availableFunds 0 with full state override",
test: async () => {
const allowanceKey = keccak256(
encodePacked(
["uint256", "uint256"],
[
baltathar.address,
keccak256(
encodePacked(
["uint256", "uint256"],
[
ALITH_ADDRESS as any,
2n // slot 2
]
)
) as unknown as bigint
]
)
);
const newValue = pad(nToHex(50));
const result = await customDevRpcRequest("eth_call", [
{
from: ALITH_ADDRESS,
to: stateOverrideAddress,
data: encodeFunctionData({
abi: contractAbi,
functionName: "allowance",
args: [ALITH_ADDRESS, baltathar.address]
})
},
"latest",
{
[stateOverrideAddress]: {
state: {
[allowanceKey]: newValue
}
}
}
]);
expect(hexToBigInt(result)).to.equal(50n);
const result2 = await customDevRpcRequest("eth_call", [
{
from: ALITH_ADDRESS,
to: stateOverrideAddress,
data: encodeFunctionData({
abi: contractAbi,
functionName: "availableFunds"
})
},
"latest",
{
[stateOverrideAddress]: {
state: {
[allowanceKey]: newValue
}
}
}
]);
expect(hexToBigInt(result2)).to.equal(0n);
}
});
it({
id: "T08",
title: "should set MultiplyBy7 deployedBytecode with state override",
test: async () => {
const { abi, deployedBytecode } = fetchCompiledContract("MultiplyBy7");
const result = await customDevRpcRequest("eth_call", [
{
from: ALITH_ADDRESS,
to: stateOverrideAddress,
data: encodeFunctionData({
abi,
functionName: "multiply",
args: [5n]
})
},
"latest",
{
[stateOverrideAddress]: {
code: deployedBytecode
}
}
]);
expect(hexToBigInt(result)).to.equal(35n);
}
});
}
});

View file

@ -0,0 +1,214 @@
import { customDevRpcRequest, describeSuite, expect, fetchCompiledContract } from "@moonwall/cli";
import { ALITH_ADDRESS } from "@moonwall/util";
import { hexToNumber, numberToHex } from "@polkadot/util";
import { CHAIN_ID } from "utils/constants";
import { parseGwei } from "viem";
// We use ethers library in this test as apparently web3js's types are not fully EIP-1559
// compliant yet.
describeSuite({
id: "D021001",
title: "Fee History",
foundationMethods: "dev",
testCases: ({ context, it }) => {
interface FeeHistory {
oldestBlock: string;
baseFeePerGas: string[];
gasUsedRatio: number[];
reward: string[][];
}
async function createBlocks(
block_count: number,
priority_fees: number[],
max_fee_per_gas: string
) {
let nonce = await context.viem().getTransactionCount({ address: ALITH_ADDRESS });
const contractData = fetchCompiledContract("MultiplyBy7");
for (let b = 0; b < block_count; b++) {
for (let p = 0; p < priority_fees.length; p++) {
await context.ethers().sendTransaction({
from: ALITH_ADDRESS,
data: contractData.bytecode,
value: "0x00",
maxFeePerGas: max_fee_per_gas,
maxPriorityFeePerGas: numberToHex(priority_fees[p]),
accessList: [],
nonce: nonce,
gasLimit: "0x100000",
chainId: CHAIN_ID
});
nonce++;
}
await context.createBlock();
}
}
function getPercentile(percentile: number, array: number[]) {
array.sort((a, b) => a - b);
const index = (percentile / 100) * array.length - 1;
if (Math.floor(index) === index) {
return array[index];
}
return Math.ceil((array[Math.floor(index)] + array[Math.ceil(index)]) / 2);
}
function matchExpectations(
feeResults: FeeHistory,
block_count: number,
reward_percentiles: number[]
) {
expect(
feeResults.baseFeePerGas.length,
"baseFeePerGas should always the requested block range + 1 (the next derived base fee)"
).toBe(block_count + 1);
expect(feeResults.gasUsedRatio).to.be.deep.eq(Array(block_count).fill(0.0105225));
expect(
feeResults.reward.length,
"should return two-dimensional reward list for the requested block range"
).to.be.eq(block_count);
const failures = feeResults.reward.filter(
(item) => item.length !== reward_percentiles.length
);
expect(
failures.length,
"each block has a reward list which's size is the requested percentile list"
).toBe(0);
}
it({
id: "T01",
title: "result length should match spec",
timeout: 40_000,
test: async () => {
const block_count = 2;
const reward_percentiles = [20, 50, 70];
const priority_fees = [1, 2, 3];
const startingBlock = await context.viem().getBlockNumber();
const feeHistory = new Promise<FeeHistory>((resolve, _reject) => {
const unwatch = context.viem().watchBlocks({
onBlock: async (block) => {
if (Number(block.number! - startingBlock) === block_count) {
const result = (await customDevRpcRequest("eth_feeHistory", [
"0x2",
"latest",
reward_percentiles
])) as FeeHistory;
unwatch();
resolve(result);
}
}
});
});
await createBlocks(block_count, priority_fees, parseGwei("10").toString());
matchExpectations(await feeHistory, block_count, reward_percentiles);
}
});
it({
id: "T02",
title: "should calculate percentiles",
timeout: 40_000,
test: async () => {
const max_fee_per_gas = parseGwei("10").toString();
const block_count = 11;
const reward_percentiles = [20, 50, 70, 85, 100];
const priority_fees = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const startingBlock = await context.viem().getBlockNumber();
const feeHistory = new Promise<FeeHistory>((resolve, _reject) => {
const unwatch = context.viem().watchBlocks({
onBlock: async (block) => {
if (Number(block.number! - startingBlock) === block_count) {
const result = (await customDevRpcRequest("eth_feeHistory", [
"0xA",
"latest",
reward_percentiles
])) as FeeHistory;
unwatch();
resolve(result);
}
}
});
});
await createBlocks(block_count, priority_fees, max_fee_per_gas);
const feeResults = await feeHistory;
const localRewards = reward_percentiles
.map((percentile) => getPercentile(percentile, priority_fees))
.map((reward) => numberToHex(reward));
// We only test if BaseFee update is enabled.
//
// If BaseFee is a constant 1GWEI, that means that there is no effective reward using
// the specs formula MIN(tx.maxPriorityFeePerGas, tx.maxFeePerGas-block.baseFee).
//
// In other words, for this tip oracle there would be no need to provide a priority fee
// when the block fullness is considered ideal in an EIP-1559 chain.
const mismatchDetails = feeResults.reward
.map((item, index) => {
const isEmptyBlock =
feeResults.gasUsedRatio[index] === 0 || item.every((val) => BigInt(val) === 0n);
const checkAgainstLocal =
!isEmptyBlock &&
hexToNumber(max_fee_per_gas) - hexToNumber(feeResults.baseFeePerGas[index]) > 0 &&
(item.length !== localRewards.length ||
!item.every((val, idx) => BigInt(val) === BigInt(localRewards[idx])));
return {
index,
baseFee: feeResults.baseFeePerGas[index],
actual: item,
expectedUncapped: localRewards,
checkAgainstLocal
};
})
.filter(({ checkAgainstLocal }) => checkAgainstLocal);
const failures = mismatchDetails.filter(({ checkAgainstLocal }) => checkAgainstLocal);
expect(
failures.length,
"each block should have rewards matching the requested percentile list"
).toBe(0);
}
});
it({
id: "T03",
title: "result length should match spec using an integer block count",
timeout: 40_000,
test: async () => {
const block_count = 2;
const reward_percentiles = [20, 50, 70];
const priority_fees = [1, 2, 3];
const startingBlock = await context.viem().getBlockNumber();
const feeHistory = new Promise<FeeHistory>((resolve, _reject) => {
const unwatch = context.viem().watchBlocks({
onBlock: async (block) => {
if (Number(block.number! - startingBlock) === block_count) {
const result = (await customDevRpcRequest("eth_feeHistory", [
block_count,
"latest",
reward_percentiles
])) as FeeHistory;
unwatch();
resolve(result);
}
}
});
});
await createBlocks(block_count, priority_fees, parseGwei("10").toString());
matchExpectations(await feeHistory, block_count, reward_percentiles);
}
});
}
});

View file

@ -0,0 +1,26 @@
import { describeSuite, expect, extractInfo, TransactionTypes } from "@moonwall/cli";
import { BALTATHAR_ADDRESS, createRawTransfer, GLMR } from "@moonwall/util";
// We use ethers library in this test as apparently web3js's types are not fully EIP-1559
// compliant yet.
describeSuite({
id: "D021002",
title: "Ethereum - PaysFee",
foundationMethods: "dev",
testCases: ({ context, it }) => {
for (const txnType of TransactionTypes) {
it({
id: `T0${TransactionTypes.indexOf(txnType) + 1}`,
title: `should be false for successful ethereum ${txnType} transactions`,
test: async () => {
const { result } = await context.createBlock(
await createRawTransfer(context, BALTATHAR_ADDRESS, GLMR, { type: txnType })
);
const info = extractInfo(result?.events)!;
expect(info).to.not.be.empty;
expect(info.paysFee.isYes, "Transaction should be marked as paysFees === no").to.be.false;
}
});
}
}
});

View file

@ -0,0 +1,101 @@
import { describeSuite, expect } from "@moonwall/cli";
import {
ALITH_ADDRESS,
BALTATHAR_ADDRESS,
BALTATHAR_PRIVATE_KEY,
baltathar,
CHARLETH_ADDRESS,
CHARLETH_PRIVATE_KEY,
createRawTransfer,
createViemTransaction,
EXTRINSIC_GAS_LIMIT,
GLMR,
WEIGHT_PER_GAS
} from "@moonwall/util";
// This tests an issue where pallet Ethereum in Frontier does not properly account for weight after
// transaction application. Specifically, it accounts for weight before a transaction by multiplying
// GasToWeight by gas_price, but does not adjust this afterwards. This leads to accounting for too
// much weight in a block.
describeSuite({
id: "D021003",
title: "Ethereum Weight Accounting",
foundationMethods: "dev",
testCases: ({ context, it }) => {
it({
id: "T01",
title: "should account for weight used",
timeout: 10000,
test: async () => {
const { block, result } = await context.createBlock(
await createViemTransaction(context, {
gas: BigInt(EXTRINSIC_GAS_LIMIT),
maxFeePerGas: 10_000_000_000n,
maxPriorityFeePerGas: 0n,
to: baltathar.address
})
);
const EXPECTED_GAS_USED = 21_000n;
const EXPECTED_WEIGHT = EXPECTED_GAS_USED * WEIGHT_PER_GAS;
const receipt = await context
.viem("public")
.getTransactionReceipt({ hash: result?.hash as `0x${string}` });
expect(BigInt(receipt.gasUsed)).to.equal(EXPECTED_GAS_USED);
const apiAt = await context.polkadotJs().at(block.hash);
const blockWeightsUsed = await apiAt.query.system.blockWeight();
const normalWeight = blockWeightsUsed.normal.refTime.toBigInt();
expect(normalWeight, "Block's Normal category should reflect this txn").to.equal(
EXPECTED_WEIGHT
);
const wholeBlock = await context.polkadotJs().rpc.chain.getBlock(block.hash);
const index = wholeBlock.block.extrinsics.findIndex(
(ext) => ext.method.method === "transact" && ext.method.section === "ethereum"
);
const extSuccessEvent = result?.events
.filter(({ phase }) => phase.isApplyExtrinsic && phase.asApplyExtrinsic.eq(index))
.find(({ event }) => context.polkadotJs().events.system.ExtrinsicSuccess.is(event));
expect(extSuccessEvent).to.not.be.eq(null);
const eventWeight = extSuccessEvent.event.data.dispatchInfo.weight.refTime.toBigInt();
expect(eventWeight).to.eq(EXPECTED_WEIGHT);
}
});
it({
id: "T02",
title: "should correctly refund weight from excess gas_limit supplied",
test: async () => {
const gasAmount = (EXTRINSIC_GAS_LIMIT * 8n) / 10n;
const tx1 = await createRawTransfer(context, BALTATHAR_ADDRESS, GLMR, {
gas: BigInt(gasAmount),
maxFeePerGas: 10_000_000_000n,
maxPriorityFeePerGas: 0n
});
const tx2 = await createRawTransfer(context, CHARLETH_ADDRESS, GLMR, {
privateKey: BALTATHAR_PRIVATE_KEY,
gas: BigInt(gasAmount),
maxFeePerGas: 10_000_000_000n,
maxPriorityFeePerGas: 0n
});
const tx3 = await createRawTransfer(context, ALITH_ADDRESS, GLMR, {
privateKey: CHARLETH_PRIVATE_KEY,
gas: BigInt(gasAmount),
maxFeePerGas: 10_000_000_000n,
maxPriorityFeePerGas: 0n
});
const { result } = await context.createBlock([tx1, tx2, tx3]);
const fails = result!.filter((a) => !a.successful);
expect(
fails,
`Transactions ${fails.map((a) => a.hash).join(", ")} have failed to be included`
).to.be.empty;
}
});
}
});

View file

@ -0,0 +1,31 @@
import { customDevRpcRequest, describeSuite, expect } from "@moonwall/cli";
describeSuite({
id: "D021101",
title: "Transaction Cost discards",
foundationMethods: "dev",
testCases: ({ it }) => {
it({
id: "T01",
title: "should take transaction cost into account and not submit it to the pool",
test: async () => {
// This is a contract deployment signed by Alith but that doesn't have a high enough
// gaslimit. Since the standard helpers reject such transactions, we need to use
// the customDevRpcRequest helper to send it directly to the node.
const txString =
"0xf9011b80843b9aca008252088080b8c960806040526000805534801561001457600080fd5b5060005b6064\
81101561003557806000819055508080600101915050610018565b506085806100446000396000f3fe6080604\
052348015600f57600080fd5b506004361060285760003560e01c80631572821714602d575b600080fd5b6033\
6049565b6040518082815260200191505060405180910390f35b6000548156fea264697066735822122015105\
f2e5f98d0c6e61fe09f704e2a86dd1cbf55424720229297a0fff65fe04064736f6c63430007000033820a26a0\
8ac98ea04dec8017ebddd1e87cc108d1df1ef1bf69ba35606efad4df2dfdbae2a07ac9edffaa0fd7c91fa5688\
b5e36a1944944bca22b8ff367e4094be21f7d85a3";
await expect(
async () => await customDevRpcRequest("eth_sendRawTransaction", [txString])
).rejects.toThrowError("intrinsic gas too low");
}
});
}
});

View file

@ -0,0 +1,188 @@
import { afterEach, beforeAll, customDevRpcRequest, describeSuite, expect } from "@moonwall/cli";
import {
ALITH_ADDRESS,
BALTATHAR_ADDRESS,
CHARLETH_ADDRESS,
CHARLETH_PRIVATE_KEY,
createEthersTransaction,
createRawTransfer,
DOROTHY_ADDRESS,
GLMR,
GOLIATH_ADDRESS,
GOLIATH_PRIVATE_KEY,
sendRawTransaction
} from "@moonwall/util";
import { parseGwei } from "viem";
import { ALITH_GENESIS_TRANSFERABLE_BALANCE, ConstantStore } from "../../../../helpers";
describeSuite({
id: "D021102",
title: "Ethereum Rpc pool errors",
foundationMethods: "dev",
testCases: ({ context, it }) => {
beforeAll(async () => {
await context.createBlock(await createRawTransfer(context, BALTATHAR_ADDRESS, 3n));
});
afterEach(async () => {
await context.createBlock();
});
it({
id: "T01",
title: "already known #1",
test: async () => {
const tx = (await createRawTransfer(context, BALTATHAR_ADDRESS, 1)) as `0x${string}`;
await sendRawTransaction(context, tx);
await expect(async () => await sendRawTransaction(context, tx)).rejects.toThrowError(
"already known"
);
}
});
it({
id: "T02",
title: "replacement transaction underpriced",
test: async () => {
const nonce = await context.viem().getTransactionCount({ address: ALITH_ADDRESS });
const tx1 = await createEthersTransaction(context, {
to: CHARLETH_ADDRESS,
nonce,
gasPrice: parseGwei("15"),
value: 100,
txnType: "legacy"
});
await customDevRpcRequest("eth_sendRawTransaction", [tx1]);
const tx2 = await createEthersTransaction(context, {
to: DOROTHY_ADDRESS,
nonce,
value: 200,
gasPrice: parseGwei("10"),
txnType: "legacy"
});
await expect(
async () => await customDevRpcRequest("eth_sendRawTransaction", [tx2])
).rejects.toThrowError("replacement transaction underpriced");
}
});
it({
id: "T03",
title: "nonce too low",
test: async () => {
const nonce = await context.viem().getTransactionCount({ address: CHARLETH_ADDRESS });
const tx1 = await context.createTxn!({
to: BALTATHAR_ADDRESS,
value: 1n,
nonce,
privateKey: CHARLETH_PRIVATE_KEY
});
await context.createBlock(tx1);
const tx2 = await context.createTxn!({
to: DOROTHY_ADDRESS,
value: 2n,
nonce: Math.max(nonce - 1, 0),
privateKey: CHARLETH_PRIVATE_KEY
});
await expect(
async () => await customDevRpcRequest("eth_sendRawTransaction", [tx2]),
"tx should be rejected for duplicate nonce"
).rejects.toThrowError("nonce too low");
}
});
it({
id: "T04",
title: "already known #2",
test: async () => {
const { specVersion } = await context.polkadotJs().consts.system.version;
const GENESIS_BASE_FEE = ConstantStore(context).GENESIS_BASE_FEE.get(
specVersion.toNumber()
);
const nonce = await context
.viem("public")
.getTransactionCount({ address: GOLIATH_ADDRESS });
const tx1 = await createRawTransfer(context, BALTATHAR_ADDRESS, 1, {
nonce: nonce + 1,
gasPrice: GENESIS_BASE_FEE,
privateKey: GOLIATH_PRIVATE_KEY
});
await context.createBlock(tx1);
await expect(
async () => await customDevRpcRequest("eth_sendRawTransaction", [tx1])
).rejects.toThrowError("already known");
}
});
it({
id: "T05",
title: "insufficient funds for gas * price + value",
test: async () => {
const ZEROED_PKEY = "0xbf2a9f29a7631116a1128e34fcf8817581fb3ec159ef2be004b459bc33f2ed2d";
const tx = await createRawTransfer(context, BALTATHAR_ADDRESS, 1, {
privateKey: ZEROED_PKEY
});
await expect(
async () => await customDevRpcRequest("eth_sendRawTransaction", [tx])
).rejects.toThrowError("insufficient funds for gas * price + value");
}
});
it({
id: "T06",
title: "exceeds block gas limit",
test: async () => {
const tx = await createRawTransfer(context, BALTATHAR_ADDRESS, 1, {
gas: 1_000_000_0000n
});
await expect(
async () => await customDevRpcRequest("eth_sendRawTransaction", [tx])
).rejects.toThrowError("exceeds block gas limit");
}
});
it({
id: "T07",
title: "insufficient funds for gas * price + value",
test: async () => {
const CHARLETH_GENESIS_TRANSFERABLE_BALANCE =
ALITH_GENESIS_TRANSFERABLE_BALANCE + 1000n * GLMR + 10n * 100_000_000_000_000n;
const amount = CHARLETH_GENESIS_TRANSFERABLE_BALANCE - 21000n * 10_000_000_000n + 1n;
const tx = await createRawTransfer(context, BALTATHAR_ADDRESS, amount, {
privateKey: CHARLETH_PRIVATE_KEY
});
await expect(
async () => await customDevRpcRequest("eth_sendRawTransaction", [tx])
).rejects.toThrowError("insufficient funds for gas * price + value");
}
});
it({
id: "T08",
title: "max priority fee per gas higher than max fee per gast",
modifier: "skip", // client libraries block invalid txns like this
test: async () => {
const tx = await createRawTransfer(context, BALTATHAR_ADDRESS, 1n, {
maxFeePerGas: 100_000_000_000n,
maxPriorityFeePerGas: 200_000_000_000n
});
await expect(
async () => await customDevRpcRequest("eth_sendRawTransaction", [tx])
).rejects.toThrowError("max priority fee per gas higher than max fee per gas");
}
});
}
});

View file

@ -0,0 +1,85 @@
import { beforeAll, describeSuite, expect, fetchCompiledContract } from "@moonwall/cli";
import { encodeDeployData } from "viem";
/*
At rpc-level, there is no interface for retrieving emulated pending transactions - emulated
transactions that exist in the Substrate's pending transaction pool. Instead they are added to a
shared collection (Mutex) with get/set locking to serve requests that ask for this transactions
information before they are included in a block.
We want to test that:
- We resolve multiple promises in parallel that will write in this collection on the rpc-side
- We resolve multiple promises in parallel that will read from this collection on the rpc-side
- We can get the final transaction data once it leaves the pending collection
*/
describeSuite({
id: "D021103",
title: "EthPool - Multiple pending transactions",
foundationMethods: "dev",
testCases: ({ context, it }) => {
let txHashes: string[];
beforeAll(async () => {
const { bytecode, abi } = fetchCompiledContract("MultiplyBy7");
const callData = encodeDeployData({
abi,
bytecode,
args: []
});
txHashes = await Promise.all(
new Array(10).fill(0).map(async (_, i) => {
return await context.viem().sendTransaction({ nonce: i, data: callData, gas: 200000n });
})
);
});
it({
id: "T01",
title: "should all be available by hash",
test: async () => {
const transactions = await Promise.all(
txHashes.map((txHash) => context.viem().getTransaction({ hash: txHash as `0x${string}` }))
);
expect(transactions.length).toBe(10);
expect(
transactions.every((transaction, index) => transaction.hash === txHashes[index])
).toBe(true);
}
});
it({
id: "T02",
title: "should all be marked as pending",
test: async () => {
const transactions = await Promise.all(
txHashes.map((txHash) => context.viem().getTransaction({ hash: txHash as `0x${string}` }))
);
expect(transactions.length).toBe(10);
expect(transactions.every((transaction) => transaction.blockNumber === null)).toBe(true);
expect(transactions.every((transaction) => transaction.transactionIndex === null)).toBe(
true
);
}
});
it({
id: "T03",
title: "should all be populated when included in a block",
test: async () => {
await context.createBlock();
const transactions = await Promise.all(
txHashes.map((txHash) => context.viem().getTransaction({ hash: txHash as `0x${string}` }))
);
expect(transactions.length).toBe(10);
expect(transactions.every((transaction) => transaction.blockNumber === 1n)).toBe(true);
expect(
transactions.every((transaction, index) => transaction.transactionIndex === index)
).toBe(true);
}
});
}
});

View file

@ -0,0 +1,68 @@
import { describeSuite, expect, fetchCompiledContract, TransactionTypes } from "@moonwall/cli";
import {
ALITH_ADDRESS,
BALTATHAR_ADDRESS,
createEthersTransaction,
createRawTransfer,
sendRawTransaction
} from "@moonwall/util";
import { encodeDeployData } from "viem";
describeSuite({
id: "D021104",
title: "EthPool - Future Ethereum transaction",
foundationMethods: "dev",
testCases: ({ context, it }) => {
for (const txnType of TransactionTypes) {
it({
id: `T0${TransactionTypes.indexOf(txnType) + 1}`,
title: "should not be executed until condition is met",
test: async () => {
const { bytecode, abi } = fetchCompiledContract("MultiplyBy7");
const callData = encodeDeployData({
abi,
bytecode,
args: []
});
const rawSigned = await createEthersTransaction(context, {
data: callData,
txnType
});
const txHash = await sendRawTransaction(context, rawSigned);
const transaction = await context.viem().getTransaction({ hash: txHash });
expect(transaction.blockNumber).to.be.null;
await context.createBlock();
}
});
// TODO: Add txpool_content once implemented
it({
id: `T0${TransactionTypes.indexOf(txnType) + 4}`,
title: "should be executed after condition is met",
test: async () => {
const { bytecode, abi } = fetchCompiledContract("MultiplyBy7");
const callData = encodeDeployData({
abi,
bytecode,
args: []
});
const nonce = await context
.viem("public")
.getTransactionCount({ address: ALITH_ADDRESS });
const rawSigned = await createEthersTransaction(context, {
data: callData,
txnType,
nonce: nonce + 1
});
const txHash = await sendRawTransaction(context, rawSigned);
await context.createBlock(
await createRawTransfer(context, BALTATHAR_ADDRESS, 512, { nonce })
);
const transaction = await context.viem().getTransaction({ hash: txHash });
expect(transaction.blockNumber! > 0n).toBe(true);
}
});
}
}
});

View file

@ -0,0 +1,156 @@
import { beforeEach, describeSuite, expect } from "@moonwall/cli";
import {
CHARLETH_ADDRESS,
CHARLETH_PRIVATE_KEY,
createRawTransfer,
GLMR,
sendRawTransaction
} from "@moonwall/util";
import { parseGwei } from "viem";
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
describeSuite({
id: "D021105",
title: "Resubmit transations",
foundationMethods: "dev",
testCases: ({ context, it }) => {
let randomAddress: `0x${string}`;
let currentNonce: number;
let actorPrivateKey: `0x${string}`;
let actorAddress: `0x${string}`;
beforeEach(async () => {
actorPrivateKey = CHARLETH_PRIVATE_KEY;
actorAddress = CHARLETH_ADDRESS;
randomAddress = privateKeyToAccount(generatePrivateKey()).address;
currentNonce = await context.viem().getTransactionCount({ address: actorAddress });
});
it({
id: "T01",
title: "should allow resubmitting with higher gas (implying higher tip)",
test: async () => {
await context.createBlock([
await createRawTransfer(context, randomAddress, 1, {
nonce: currentNonce,
maxFeePerGas: parseGwei("300"),
maxPriorityFeePerGas: parseGwei("300"),
privateKey: actorPrivateKey
}),
await createRawTransfer(context, randomAddress, 2, {
nonce: currentNonce,
maxFeePerGas: parseGwei("400"),
maxPriorityFeePerGas: parseGwei("300"),
privateKey: actorPrivateKey
// same priority fee but higher max fee so higher tip
})
]);
expect(await context.viem().getBalance({ address: randomAddress })).to.equal(2n);
}
});
it({
id: "T02",
title: "should ignore resubmitting with lower gas",
test: async () => {
await context.createBlock([
await createRawTransfer(context, randomAddress, 1, {
nonce: currentNonce,
maxFeePerGas: parseGwei("20"),
privateKey: actorPrivateKey
}),
await createRawTransfer(context, randomAddress, 2, {
nonce: currentNonce,
maxFeePerGas: parseGwei("10"),
privateKey: actorPrivateKey
})
]);
expect(await context.viem().getBalance({ address: randomAddress })).to.equal(1n);
}
});
it({
id: "T03",
title: "should allow cancelling transaction by reducing limit",
test: async () => {
// gas price should trump limit
await context.createBlock([
await createRawTransfer(context, randomAddress, 1, {
nonce: currentNonce,
maxFeePerGas: parseGwei("10"),
gas: 1048575n,
privateKey: actorPrivateKey
}),
await createRawTransfer(context, randomAddress, 2, {
nonce: currentNonce,
maxFeePerGas: parseGwei("20"),
gas: 65536n,
privateKey: actorPrivateKey
})
]);
expect(await context.viem().getBalance({ address: randomAddress })).to.equal(1n);
}
});
it({
id: "T04",
title: "should prioritize higher gas tips",
test: async () => {
// GasFee are using very high value to ensure gasPrice is not impacting
const addressGLMR = (await context.viem().getBalance({ address: actorAddress })) / GLMR;
await sendRawTransaction(
context,
await createRawTransfer(context, randomAddress, 66, {
nonce: currentNonce,
maxFeePerGas: 1n * GLMR,
maxPriorityFeePerGas: 1n * GLMR,
privateKey: actorPrivateKey
})
);
const testParameters = [1n * GLMR, 2n * GLMR, 20n * GLMR, 4n * GLMR, 10n * GLMR];
const txns: string[] = await Promise.all(
testParameters.map(
async (gasPrice) =>
await createRawTransfer(context, randomAddress, 77, {
nonce: currentNonce,
maxFeePerGas: gasPrice,
maxPriorityFeePerGas: gasPrice,
privateKey: actorPrivateKey
})
)
);
await context.createBlock(txns);
expect((await context.viem().getBalance({ address: actorAddress })) / GLMR).to.equal(
addressGLMR - 21000n * 20n
);
expect(await context.viem().getBalance({ address: randomAddress })).to.equal(77n);
}
});
it({
id: "T05",
title: "should not allow resubmitting with higher gas (implying same tip)",
test: async () => {
await context.createBlock([
await createRawTransfer(context, randomAddress, 1, {
nonce: currentNonce,
maxFeePerGas: parseGwei("300"),
maxPriorityFeePerGas: parseGwei("10"),
privateKey: actorPrivateKey
}),
await createRawTransfer(context, randomAddress, 2, {
nonce: currentNonce,
maxFeePerGas: parseGwei("400"),
maxPriorityFeePerGas: parseGwei("10"),
privateKey: actorPrivateKey
})
]);
expect(await context.viem().getBalance({ address: randomAddress })).to.equal(1n);
}
});
}
});

View file

@ -0,0 +1,46 @@
import { customDevRpcRequest, describeSuite, expect } from "@moonwall/cli";
import { CHAIN_ID } from "utils/constants";
describeSuite({
id: "D021201",
title: "RPC Constants",
foundationMethods: "dev",
testCases: ({ it }) => {
const DATAHAVEN_CHAIN_ID = BigInt(CHAIN_ID);
it({
id: "T01",
title: "should have 0 hashrate",
test: async () => {
expect(BigInt(await customDevRpcRequest("eth_hashrate"))).toBe(0n);
}
});
it({
id: "T02",
title: `should have chainId ${CHAIN_ID}`,
test: async () => {
expect(BigInt(await customDevRpcRequest("eth_chainId"))).toBe(DATAHAVEN_CHAIN_ID);
}
});
it({
id: "T03",
title: "should have no account",
test: async () => {
expect(await customDevRpcRequest("eth_accounts")).toStrictEqual([]);
}
});
it({
id: "T04",
title: "block author should be 0x0000000000000000000000000000000000000000",
test: async () => {
// This address `0x1234567890` is hardcoded into the runtime find_author
// as we are running manual sealing consensus.
expect(await customDevRpcRequest("eth_coinbase")).toBe(
"0x0000000000000000000000000000000000000000"
);
}
});
}
});

View file

@ -0,0 +1,30 @@
import { customDevRpcRequest, describeSuite, expect } from "@moonwall/cli";
describeSuite({
id: "D021202",
title: "Deprecated RPC",
foundationMethods: "dev",
testCases: ({ it }) => {
const deprecatedMethods = [
{ method: "eth_getCompilers", params: [] },
{ method: "eth_compileLLL", params: ["(returnlll (suicide (caller)))"] },
{
method: "eth_compileSolidity",
params: ["contract test { function multiply(uint a) returns(uint d) {return a * 7;}}"]
},
{ method: "eth_compileSerpent", params: ["/* some serpent 🐍🐍🐍 */"] }
];
for (const { method, params } of deprecatedMethods) {
it({
id: `T0${deprecatedMethods.findIndex((item) => item.method === method) + 1}`,
title: `${method} should be mark as not found`,
test: async () => {
await expect(async () => await customDevRpcRequest(method, params)).rejects.toThrowError(
"Method not found"
);
}
});
}
}
});

View file

@ -0,0 +1,79 @@
import {
beforeAll,
customDevRpcRequest,
deployCreateCompiledContract,
describeSuite,
expect
} from "@moonwall/cli";
import type { TransactionReceipt } from "viem";
describeSuite({
id: "D021203",
title: "Ethereum RPC - Filtering non-matching logs",
foundationMethods: "dev",
testCases: ({ context, it }) => {
let nonMatchingCases: ReturnType<typeof getNonMatchingCases>;
const getNonMatchingCases = (receipt: TransactionReceipt) => {
return [
// Non-existant address.
{
fromBlock: "0x0",
toBlock: "latest",
address: "0x0000000000000000000000000000000000000000"
},
// Non-existant topic.
{
fromBlock: "0x0",
toBlock: "latest",
topics: ["0x0000000000000000000000000000000000000000000000000000000000000000"]
},
// Existant address + non-existant topic.
{
fromBlock: "0x0",
toBlock: "latest",
address: receipt.contractAddress,
topics: ["0x0000000000000000000000000000000000000000000000000000000000000000"]
},
// Non-existant address + existant topic.
{
fromBlock: "0x0",
toBlock: "latest",
address: "0x0000000000000000000000000000000000000000",
topics: receipt.logs[0].topics
}
];
};
beforeAll(async () => {
const { hash } = await deployCreateCompiledContract(context, "EventEmitter");
const receipt = await context.viem().getTransactionReceipt({ hash });
nonMatchingCases = getNonMatchingCases(receipt);
});
it({
id: "T01",
title: "EthFilterApi::getFilterLogs - should filter out non-matching cases.",
test: async () => {
const filterLogs = await Promise.all(
nonMatchingCases.map(async (item) => {
const filter = await customDevRpcRequest("eth_newFilter", [item]);
return await customDevRpcRequest("eth_getFilterLogs", [filter]);
})
);
expect(filterLogs.flat(1).length).toBe(0);
}
});
it({
id: "T02",
title: "EthApi::getLogs - should filter out non-matching cases.",
test: async () => {
const logs = await Promise.all(
nonMatchingCases.map(async (item) => await customDevRpcRequest("eth_getLogs", [item]))
);
expect(logs.flat(1).length).toBe(0);
}
});
}
});

View file

@ -0,0 +1,54 @@
import { beforeAll, describeSuite, expect } from "@moonwall/cli";
import { BALTATHAR_ADDRESS, createViemTransaction, extractFee } from "@moonwall/util";
import type { ApiPromise } from "@polkadot/api";
describeSuite({
id: "D021205",
title: "Ethereum RPC - eth_getTransactionReceipt",
foundationMethods: "dev",
testCases: ({ it, context }) => {
let polkadotJs: ApiPromise;
beforeAll(() => {
polkadotJs = context.polkadotJs();
});
it({
id: "T01",
title:
"should have correct effectiveGasPrice when fee multiplier changes in consecutive blocks",
test: async () => {
const prevBlockNextFeeMultiplier = (
await polkadotJs.query.transactionPayment.nextFeeMultiplier()
).toBigInt();
const { result } = await context.createBlock(
await createViemTransaction(context, {
gas: 21_000n,
maxFeePerGas: 1_000_000_000_000_000n,
maxPriorityFeePerGas: 1n,
type: "eip1559",
to: BALTATHAR_ADDRESS
})
);
const txHash = result?.hash;
const txFee = extractFee(result?.events)!.amount.toBigInt();
const txReceipt = await context.viem().getTransactionReceipt({ hash: txHash });
const txReceiptFee = txReceipt.effectiveGasPrice * txReceipt.gasUsed;
const txBlockNextFeeMultiplier = (
await polkadotJs.query.transactionPayment.nextFeeMultiplier()
).toBigInt();
// NOTE: fee multiplier needs to be different to ensure the test does not
// yield a false positive. If some conditions make these values equal, some
// extra transactions need to be added to the second block to make the
// values differ.
expect(prevBlockNextFeeMultiplier).not.toEqual(txBlockNextFeeMultiplier);
expect(txReceiptFee).toEqual(txFee);
}
});
}
});

View file

@ -0,0 +1,37 @@
import { beforeAll, describeSuite, expect } from "@moonwall/cli";
import { BALTATHAR_ADDRESS, createRawTransfer } from "@moonwall/util";
describeSuite({
id: "D021206",
title: "Transaction Index",
foundationMethods: "dev",
testCases: ({ context, it }) => {
beforeAll(async () => {
await context.createBlock(createRawTransfer(context, BALTATHAR_ADDRESS, 0));
});
it({
id: "T01",
title: "should get transaction by index",
test: async () => {
const block = 1n;
const index = 0;
const result = await context.viem().getTransaction({ blockNumber: block, index });
expect(result.transactionIndex).to.equal(index);
}
});
it({
id: "T02",
title: "should return out of bounds message",
test: async () => {
const block = 0n;
const index = 0;
await expect(
async () => await context.viem().getTransaction({ blockNumber: block, index })
).rejects.toThrowError(`${index} is out of bounds`);
}
});
}
});

View file

@ -0,0 +1,42 @@
import { customDevRpcRequest, describeSuite, expect } from "@moonwall/cli";
import { CHAIN_ID } from "utils/constants";
describeSuite({
id: "D021207",
title: "Version RPC",
foundationMethods: "dev",
testCases: ({ context, it }) => {
const DATAHAVEN_CHAIN_ID = BigInt(CHAIN_ID);
it({
id: "T01",
title: `should return ${CHAIN_ID} for eth_chainId`,
test: async () => {
expect(await customDevRpcRequest("eth_chainId")).to.equal(
`0x${DATAHAVEN_CHAIN_ID.toString(16)}`
);
}
});
it({
id: "T02",
title: `should return ${CHAIN_ID} for net_version`,
test: async () => {
expect(await customDevRpcRequest("net_version")).to.equal(DATAHAVEN_CHAIN_ID.toString());
}
});
it({
id: "T03",
title: "should include client version",
test: async () => {
const version = await customDevRpcRequest("web3_clientVersion");
const specName = context.polkadotJs().runtimeVersion.specName.toString();
const specVersion = context.polkadotJs().runtimeVersion.specVersion.toString();
const implVersion = context.polkadotJs().runtimeVersion.implVersion.toString();
const expectedString = `${specName}/v${specVersion}.${implVersion}/fc-rpc-2.0.0-dev`;
expect(version).toContain(expectedString);
}
});
}
});

View file

@ -0,0 +1,189 @@
import { error } from "node:console";
import { beforeAll, deployCreateCompiledContract, describeSuite, expect } from "@moonwall/cli";
import { createViemTransaction } from "@moonwall/util";
describeSuite({
id: "D021301",
title: "Ethereum Transaction - Access List",
foundationMethods: "dev",
testCases: ({ context, it }) => {
type DeployResult = Awaited<ReturnType<typeof deployCreateCompiledContract>>;
const data: `0x${string}` = "0x";
let helper: DeployResult;
let helperProxy: DeployResult;
beforeAll(async () => {
helper = await deployCreateCompiledContract(context, "AccessListHelper");
helperProxy = await deployCreateCompiledContract(context, "AccessListHelperProxy", {
args: [helper.contractAddress]
});
});
it({
id: "T01",
title: "after the 4th one, additional storage keys should cost 1900 gas",
test: async () => {
const keys = generateSequentialStorageKeys(100);
interface Results {
keys: number;
size: number;
gasWithAL: bigint;
}
const cases = Array.from({ length: 100 }, (_, i) => i + 1);
const results: Results[] = [];
for (const n of cases) {
const txWithAL = await createViemTransaction(context, {
to: helperProxy.contractAddress,
data: data,
gas: 1000000n,
accessList: [
{
address: helper.contractAddress,
storageKeys: keys.slice(0, n)
}
]
});
const { result } = await context.createBlock(txWithAL);
await context.createBlock();
const receipt = await context
.viem()
.getTransactionReceipt({ hash: result!.hash as `0x${string}` });
const gasCostWithAL = receipt.gasUsed;
const txSize = txWithAL.length;
results.push({
keys: n,
size: txSize,
gasWithAL: gasCostWithAL
});
}
results.forEach((result, index) => {
const diff = index === 0 ? "" : result.gasWithAL - results[index - 1].gasWithAL;
if (result.keys > 4) {
expect(
diff,
`Expected gas did not match when including ${result.keys} storage keys`
).toBe(1900n);
}
});
}
});
it({
id: "T02",
title: "after the 4th one, additional addresses should cost 2400 gas",
test: async () => {
const addresses = randomAddresses(100);
interface Results {
addresses: number;
size: number;
gasWithAL: bigint;
}
interface Address {
address: `0x${string}`;
storageKeys: `0x${string}`[];
}
const cases = Array.from({ length: 100 }, (_, i) => i + 1);
const results: Results[] = [];
for (const n of cases) {
const accessList: Address[] = [];
for (let i = 0; i < n; i++) {
accessList.push({
address: addresses[i],
storageKeys: []
});
}
const txWithAL = await createViemTransaction(context, {
to: helperProxy.contractAddress,
data: data,
gas: 1000000n,
accessList
});
const { result } = await context.createBlock(txWithAL);
await context.createBlock();
const receipt = await context
.viem()
.getTransactionReceipt({ hash: result!.hash as `0x${string}` });
const gasCostWithAL = receipt.gasUsed;
const txSize = txWithAL.length;
results.push({
addresses: n,
size: txSize,
gasWithAL: gasCostWithAL
});
}
results.forEach((result, index) => {
const diff = index === 0 ? 0n : result.gasWithAL - results[index - 1].gasWithAL;
if (result.addresses > 4) {
expect(
diff,
`Expected gas did not match when including ${result.addresses} addresses`
).toBe(2400n);
}
});
}
});
it({
id: "T03",
title: "transaction should not be gossiped if it exceeds the gas limit",
test: async () => {
const keys = generateSequentialStorageKeys(100);
const bigTxWithAL = await createViemTransaction(context, {
to: helperProxy.contractAddress,
data: data,
gas: 100000000n,
accessList: [
{
address: helper.contractAddress,
storageKeys: keys
}
]
});
try {
await context.viem().sendRawTransaction({ serializedTransaction: bigTxWithAL });
error("Transaction should not have been gossiped");
} catch (e) {
expect(e.message).toContain("exceeds block gas limit");
}
}
});
}
});
function generateSequentialStorageKeys(n: number): `0x${string}`[] {
const keys: `0x${string}`[] = [];
for (let i = 0; i < n; i++) {
keys.push(`0x${i.toString().padStart(64, "0")}`);
}
return keys;
}
function randomAddresses(n: number): `0x${string}`[] {
const addresses: `0x${string}`[] = [];
for (let i = 0; i < n; i++) {
let current = "0x";
for (let j = 0; j < 40; j++) {
current += Math.floor(Math.random() * 16).toString(16);
}
addresses.push(current as `0x${string}`);
}
return addresses;
}

View file

@ -0,0 +1,75 @@
import { beforeEach, describeSuite, expect } from "@moonwall/cli";
import {
ALITH_ADDRESS,
BALTATHAR_ADDRESS,
checkBalance,
createViemTransaction,
GLMR
} from "@moonwall/util";
describeSuite({
id: "D021302",
title: "Native Token Transfer Test",
foundationMethods: "dev",
testCases: ({ context, it }) => {
let initialAlithBalance: bigint;
let initialBaltatharBalance: bigint;
beforeEach(async () => {
initialAlithBalance = await checkBalance(context, ALITH_ADDRESS);
initialBaltatharBalance = await checkBalance(context, BALTATHAR_ADDRESS);
});
it({
id: "T01",
title: "Native transfer with fixed gas limit (21000) should succeed",
test: async () => {
const amountToTransfer = 1n * GLMR;
const gasLimit = 21000n;
// Create and send the transaction with fixed gas limit
const { result } = await context.createBlock(
createViemTransaction(context, {
from: ALITH_ADDRESS,
to: BALTATHAR_ADDRESS,
value: amountToTransfer,
gas: gasLimit
})
);
expect(result?.successful).to.be.true;
// Check balances after transfer
const alithBalanceAfter = await checkBalance(context, ALITH_ADDRESS);
const baltatharBalanceAfter = await checkBalance(context, BALTATHAR_ADDRESS);
// Calculate gas cost
const receipt = await context
.viem()
.getTransactionReceipt({ hash: result!.hash as `0x${string}` });
const gasCost = gasLimit * receipt.effectiveGasPrice;
// Verify balances
expect(alithBalanceAfter).to.equal(initialAlithBalance - amountToTransfer - gasCost);
expect(baltatharBalanceAfter).to.equal(initialBaltatharBalance + amountToTransfer);
// Verify gas used matches our fixed gas limit
expect(receipt.gasUsed).to.equal(gasLimit);
}
});
it({
id: "T02",
title: "should estimate 21000 gas for native transfer",
test: async () => {
const estimatedGas = await context.viem().estimateGas({
account: ALITH_ADDRESS,
to: BALTATHAR_ADDRESS,
value: 1n * GLMR
});
expect(estimatedGas).to.equal(21000n);
}
});
}
});

View file

@ -0,0 +1,56 @@
import { customDevRpcRequest, describeSuite, expect } from "@moonwall/cli";
import { createEthersTransaction, EXTRINSIC_GAS_LIMIT } from "@moonwall/util";
describeSuite({
id: "D021303",
title: "Ethereum Transaction - Large Transaction",
foundationMethods: "dev",
testCases: ({ context, it }) => {
// TODO: I'm not sure where this 2000 came from...
const maxSize = (BigInt(EXTRINSIC_GAS_LIMIT) - 21000n) / 16n - 2000n;
it({
id: "T01",
title: "should accept txns up to known size",
test: async () => {
expect(maxSize).to.equal(809187n); // max Ethereum TXN size with EIP-7623 floor cost
// max_size - shanghai init cost - create cost
const maxSizeShanghai = maxSize - 6474n;
const data = `0x${"FF".repeat(Number(maxSizeShanghai))}` as `0x${string}`;
const rawSigned = await createEthersTransaction(context, {
value: 0n,
data,
gasLimit: EXTRINSIC_GAS_LIMIT
});
const { result } = await context.createBlock(rawSigned);
const receipt = await context
.viem("public")
.getTransactionReceipt({ hash: result!.hash as `0x${string}` });
expect(receipt.status, "Junk txn should be accepted by RPC but reverted").toBe("reverted");
}
});
it({
id: "T02",
title: "should reject txns which are too large to pay for",
test: async () => {
// Use exactMaxSize + 1 to ensure we exceed the gas limit
const data = `0x${"FF".repeat(Number(maxSize) + 1)}` as `0x${string}`;
const rawSigned = await createEthersTransaction(context, {
value: 0n,
data,
gasLimit: EXTRINSIC_GAS_LIMIT
});
await expect(
async () => await customDevRpcRequest("eth_sendRawTransaction", [rawSigned]),
"RPC must reject before gossiping to prevent spam"
).rejects.toThrowError("intrinsic gas too low");
}
});
}
});

View file

@ -0,0 +1,167 @@
import { describeSuite, expect } from "@moonwall/cli";
import { ALITH_ADDRESS, BALTATHAR_ADDRESS, createEthersTransaction } from "@moonwall/util";
import type { EthereumTransactionTransactionV2 } from "@polkadot/types/lookup";
import { CHAIN_ID } from "utils/constants";
import { DEFAULT_TXN_MAX_BASE_FEE } from "../../../../helpers";
describeSuite({
id: "D021304",
title: "Ethereum Transaction - Legacy",
foundationMethods: "dev",
testCases: ({ context, it }) => {
const DATAHAVEN_CHAIN_ID = CHAIN_ID;
it({
id: "T01",
title: "should contain valid legacy Ethereum data",
test: async () => {
await context.createBlock(
await createEthersTransaction(context, {
to: BALTATHAR_ADDRESS,
gasLimit: 12_000_000,
gasPrice: 10_000_000_000,
value: 512,
txnType: "legacy"
})
);
const signedBlock = await context.polkadotJs().rpc.chain.getBlock();
const extrinsic = signedBlock.block.extrinsics.find(
(ex) => ex.method.section === "ethereum"
)!.args[0] as EthereumTransactionTransactionV2;
expect(extrinsic.isLegacy).to.be.true;
const { gasLimit, gasPrice, nonce, action, value, input, signature } = extrinsic.asLegacy;
expect(gasPrice.toNumber()).to.equal(DEFAULT_TXN_MAX_BASE_FEE);
expect(gasLimit.toBigInt()).to.equal(12_000_000n);
expect(nonce.toNumber()).to.equal(0);
expect(action.asCall.toHex()).to.equal(BALTATHAR_ADDRESS.toLowerCase());
expect(value.toBigInt()).to.equal(512n);
expect(input.toHex()).to.equal("0x");
const actualV = signature.v.toNumber();
const expectedVBase = DATAHAVEN_CHAIN_ID * 2 + 35;
expect([expectedVBase, expectedVBase + 1]).to.include(actualV);
expect(signature.r.toHex()).to.equal(
"0xbb5b5f596668edaeeba96caf66b361ca2bbbe8e325e75abd7aee7f8399cb1679"
);
expect(signature.s.toHex()).to.equal(
"0x5a010be3c9f198c9e2f6681e0b95a66a741aa1e9ea63cbb2d57f02885d9beefc"
);
}
});
it({
id: "T02",
title: "should contain valid EIP2930 Ethereum data",
test: async () => {
const currentNonce = await context
.viem("public")
.getTransactionCount({ address: ALITH_ADDRESS });
await context.createBlock(
await createEthersTransaction(context, {
to: BALTATHAR_ADDRESS,
accessList: [],
value: 512,
gasLimit: 21000,
txnType: "eip2930"
})
);
const signedBlock = await context.polkadotJs().rpc.chain.getBlock();
const extrinsic = signedBlock.block.extrinsics.find(
(ex) => ex.method.section === "ethereum"
)!.args[0] as EthereumTransactionTransactionV2;
expect(extrinsic.isEip2930).to.be.true;
const {
chainId,
nonce,
gasPrice,
gasLimit,
action,
value,
input,
accessList,
oddYParity,
r,
s
} = extrinsic.asEip2930;
expect(chainId.toNumber()).to.equal(DATAHAVEN_CHAIN_ID);
expect(nonce.toNumber()).to.equal(currentNonce);
expect(gasPrice.toNumber()).to.equal(DEFAULT_TXN_MAX_BASE_FEE);
expect(gasLimit.toBigInt()).to.equal(21000n);
expect(action.asCall.toHex()).to.equal(BALTATHAR_ADDRESS.toLowerCase());
expect(value.toBigInt()).to.equal(512n);
expect(input.toHex()).to.equal("0x");
expect(accessList.toString()).toBe("[]");
expect(oddYParity.isFalse).to.be.true;
expect(r.toHex()).to.equal(
"0x1a703ae78b4f5bd48b04e848a0e261c195e037f39a4e1e2b2637edfe7bdf5328"
);
expect(s.toHex()).to.equal(
"0x772b2d95acc14739bdd57565a87ce4e51fb7457724e4c42b148c544e4ae3e968"
);
}
});
it({
id: "T03",
title: "should contain valid EIP1559 Ethereum data",
test: async () => {
const currentNonce = await context
.viem("public")
.getTransactionCount({ address: ALITH_ADDRESS });
await context.createBlock(
await createEthersTransaction(context, {
to: BALTATHAR_ADDRESS,
accessList: [],
value: 512,
gasLimit: 21000,
txnType: "eip1559"
})
);
const signedBlock = await context.polkadotJs().rpc.chain.getBlock();
const extrinsic = signedBlock.block.extrinsics.find(
(ex) => ex.method.section === "ethereum"
)!.args[0] as EthereumTransactionTransactionV2;
expect(extrinsic.isEip1559).to.be.true;
const {
chainId,
nonce,
maxFeePerGas,
maxPriorityFeePerGas,
gasLimit,
action,
value,
input,
accessList,
oddYParity,
r,
s
} = extrinsic.asEip1559;
expect(chainId.toNumber()).to.equal(DATAHAVEN_CHAIN_ID);
expect(nonce.toNumber()).to.equal(currentNonce);
expect(maxPriorityFeePerGas.toNumber()).to.equal(0);
expect(maxFeePerGas.toNumber()).to.equal(DEFAULT_TXN_MAX_BASE_FEE);
expect(gasLimit.toBigInt()).to.equal(21000n);
expect(action.asCall.toHex()).to.equal(BALTATHAR_ADDRESS.toLowerCase());
expect(value.toBigInt()).to.equal(512n);
expect(input.toHex()).to.equal("0x");
expect(accessList.toString()).toBe("[]");
expect(oddYParity.isFalse).to.be.true;
expect(r.toHex()).to.equal(
"0x07a83a8cea51ecfc21533dbec98de47b37d7f54110395b2b9fd514a9216bb741"
);
expect(s.toHex()).to.equal(
"0x6448665043b9a23baa7d58c3f26c8a291f0db6c38a36a7df21bcc26091f1c5aa"
);
}
});
}
});

View file

@ -0,0 +1,173 @@
import {
beforeAll,
customDevRpcRequest,
describeSuite,
expect,
fetchCompiledContract
} from "@moonwall/cli";
import {
ALITH_ADDRESS,
BALTATHAR_ADDRESS,
BALTATHAR_PRIVATE_KEY,
CHARLETH_ADDRESS,
createRawTransfer
} from "@moonwall/util";
import { encodeFunctionData } from "viem";
describeSuite({
id: "D021305",
title: "Ethereum Transaction - Nonce",
foundationMethods: "dev",
testCases: ({ context, it }) => {
it({
id: "T01",
title: "should be at 0 before using it",
test: async () => {
expect(await context.viem().getTransactionCount({ address: BALTATHAR_ADDRESS })).toBe(0);
}
});
it({
id: "T02",
title: "should be at 0 for genesis account",
test: async () => {
expect(await context.viem().getTransactionCount({ address: ALITH_ADDRESS })).toBe(0);
}
});
it({
id: "T03",
title: "should stay at 0 before block is created",
test: async () => {
await customDevRpcRequest("eth_sendRawTransaction", [
await createRawTransfer(context, ALITH_ADDRESS, 512)
]);
expect(await context.viem().getTransactionCount({ address: ALITH_ADDRESS })).toBe(0);
await context.createBlock();
}
});
it({
id: "T04",
title: "should stay at previous before block is created",
test: async () => {
const blockNumber = await context.viem().getBlockNumber();
const nonce = await context.viem().getTransactionCount({ address: ALITH_ADDRESS });
await context.createBlock(await createRawTransfer(context, ALITH_ADDRESS, 512));
expect(
await context.viem().getTransactionCount({ address: ALITH_ADDRESS, blockNumber })
).toBe(nonce);
}
});
it({
id: "T05",
title: "pending transaction nonce",
test: async () => {
const nonce = await context.viem().getTransactionCount({ address: ALITH_ADDRESS });
await customDevRpcRequest("eth_sendRawTransaction", [
await createRawTransfer(context, CHARLETH_ADDRESS, 512)
]);
expect(
await context.viem().getTransactionCount({ address: ALITH_ADDRESS }),
"should not increase transaction count"
).toBe(nonce);
expect(
await context
.viem("public")
.getTransactionCount({ address: ALITH_ADDRESS, blockTag: "latest" }),
"should not increase transaction count in latest block"
).toBe(nonce);
expect(
await context
.viem("public")
.getTransactionCount({ address: ALITH_ADDRESS, blockTag: "pending" }),
"should increase transaction count in pending block"
).toBe(nonce + 1);
await context.createBlock();
}
});
it({
id: "T06",
title: "transferring Nonce",
test: async () => {
const nonce = await context.viem().getTransactionCount({ address: ALITH_ADDRESS });
await context.createBlock([await createRawTransfer(context, BALTATHAR_ADDRESS, 512)]);
expect(
await context.viem().getTransactionCount({ address: ALITH_ADDRESS }),
"should increase the sender nonce"
).toBe(nonce + 1);
expect(
await context.viem().getTransactionCount({ address: BALTATHAR_ADDRESS }),
"should not increase the receiver nonce"
).toBe(0);
await context.createBlock();
}
});
}
});
describeSuite({
id: "D011304",
title: "Ethereum Transaction - Nonce #2",
foundationMethods: "dev",
testCases: ({ context, it }) => {
let incrementorAddress: `0x${string}`;
beforeAll(async () => {
// const {
// // contract: incContract,
// contractAddress: incAddress,
// abi: incAbi,
// } = await deployCreateCompiledContract(context, "Incrementor");
const { contractAddress } = await context.deployContract!("Incrementor");
// incrementorContract = incContract;
incrementorAddress = contractAddress;
});
it({
id: "T01",
title: "should be at 0 before using it",
test: async () => {
expect(await context.viem().getTransactionCount({ address: BALTATHAR_ADDRESS })).toBe(0);
}
});
it({
id: "T01",
title: "should increment to 1",
test: async () => {
const data = encodeFunctionData({
abi: fetchCompiledContract("Incrementor").abi,
functionName: "incr"
});
await context.createBlock(
context.createTxn!({
privateKey: BALTATHAR_PRIVATE_KEY,
to: incrementorAddress,
data,
value: 0n,
gasLimit: 21000,
txnType: "legacy"
})
);
const block = await context.viem().getBlock({ blockTag: "latest" });
expect(block.transactions.length, "should include the transaction in the block").to.be.eq(
1
);
expect(
await context.viem().getTransactionCount({ address: BALTATHAR_ADDRESS }),
"should increase the sender nonce"
).toBe(1);
}
});
}
});

View file

@ -0,0 +1,82 @@
import { describeSuite, expect, fetchCompiledContract } from "@moonwall/cli";
import { ethers } from "ethers";
describeSuite({
id: "D021401",
title: "Ethers.js",
foundationMethods: "dev",
testCases: ({ context, it, log }) => {
it({
id: "T01",
title: "should get correct network ids",
test: async () => {
expect((await context.ethers().provider!.getNetwork()).chainId).to.equal(55932n);
}
});
it({
id: "T02",
title: "should be deployable",
test: async () => {
const { abi, bytecode } = fetchCompiledContract("MultiplyBy7");
const contractFactory = new ethers.ContractFactory(
abi as ethers.InterfaceAbi,
bytecode,
context.ethers()
);
const contract = await contractFactory.deploy({
gasLimit: 1_000_000,
gasPrice: 10_000_000_000
});
await context.createBlock();
log("Contract address: ", await contract.getAddress());
expect((await contract.getAddress()).length).toBeGreaterThan(3);
expect(await context.ethers().provider?.getCode(await contract.getAddress())).to.be.string;
}
});
it({
id: "T03",
title: "should be callable",
test: async () => {
const contractData = fetchCompiledContract("MultiplyBy7");
const contractFactory = new ethers.ContractFactory(
contractData.abi as ethers.InterfaceAbi,
contractData.bytecode,
context.ethers()
);
const deployed = await contractFactory.deploy({
gasLimit: 1_000_000,
gasPrice: 10_000_000_000,
nonce: await context.ethers().getNonce()
});
await context.createBlock();
// @ts-expect-error It doesn't know what functions are available
const contractCallResult = await deployed.multiply(3, {
gasLimit: 1_000_000,
gasPrice: 10_000_000_000
});
await context.createBlock();
expect(contractCallResult.toString()).to.equal("21");
// Instantiate contract from address
const contractFromAddress = new ethers.Contract(
await deployed.getAddress(),
contractData.abi as ethers.InterfaceAbi,
context.ethers()
);
await context.createBlock();
expect(
(
await contractFromAddress.multiply(3, { gasLimit: 1_000_000, gasPrice: 10_000_000_000 })
).toString()
).to.equal("21");
}
});
}
});

View file

@ -0,0 +1,50 @@
import { describeSuite, expect } from "@moonwall/cli";
import { ALITH_ADDRESS, GLMR, generateKeyringPair } from "@moonwall/util";
// A call from root (sudo) can make a transfer directly in pallet_evm
// A signed call cannot make a transfer directly in pallet_evm
describeSuite({
id: "D021501",
title: "Pallet EVM - Transfering",
foundationMethods: "dev",
testCases: ({ context, it }) => {
const randomAddress = generateKeyringPair().address as string;
it({
id: "T01",
title: "should not overflow",
test: async () => {
const { result } = await context.createBlock(
context
.polkadotJs()
.tx.sudo.sudo(
context
.polkadotJs()
.tx.evm.call(
ALITH_ADDRESS,
randomAddress,
"0x0",
`0x${(5n * GLMR + 2n ** 128n).toString(16)}`,
"0x5209",
1_000_000_000n,
"0",
null,
[]
)
)
);
expect(
result?.events.find(
({ event: { section, method } }) =>
section === "system" && method === "ExtrinsicSuccess"
)
).to.exist;
const account = await context.polkadotJs().query.system.account(randomAddress);
expect(account.data.free.toBigInt()).to.equal(0n);
expect(account.data.reserved.toBigInt()).to.equal(0n);
}
});
}
});

View file

@ -0,0 +1,83 @@
import { describeSuite, expect } from "@moonwall/cli";
import {
ALITH_ADDRESS,
BALTATHAR_ADDRESS,
baltathar,
DEFAULT_GENESIS_BALANCE
} from "@moonwall/util";
// A call from root (sudo) can make a transfer directly in pallet_evm
// A signed call cannot make a transfer directly in pallet_evm
describeSuite({
id: "D021502",
title: "Pallet EVM - call",
foundationMethods: "dev",
testCases: ({ context, it }) => {
it({
id: "T01",
title: "should fail without sudo",
test: async () => {
expect(
await context
.createBlock(
context
.polkadotJs()
.tx.evm.call(
ALITH_ADDRESS,
BALTATHAR_ADDRESS,
"0x0",
100_000_000_000_000_000_000n,
12_000_000n,
1_000_000_000n,
"0",
null,
[]
)
)
.catch((e) => e.toString())
).to.equal("RpcError: 1010: Invalid Transaction: Transaction call is not expected");
expect(await context.viem().getBalance({ address: baltathar.address })).to.equal(
DEFAULT_GENESIS_BALANCE
);
}
});
it({
id: "T02",
title: "should succeed with sudo",
test: async () => {
const { result } = await context.createBlock(
context
.polkadotJs()
.tx.sudo.sudo(
context
.polkadotJs()
.tx.evm.call(
ALITH_ADDRESS,
baltathar.address,
"0x0",
100_000_000_000_000_000_000n,
12_000_000n,
100_000_000_000_000n,
"0",
null,
[]
)
)
);
expect(
result?.events.find(
({ event: { section, method } }) =>
section === "system" && method === "ExtrinsicSuccess"
)
).to.exist;
expect(await context.viem().getBalance({ address: baltathar.address })).to.equal(
DEFAULT_GENESIS_BALANCE + 100_000_000_000_000_000_000n
);
}
});
}
});

View file

@ -0,0 +1,66 @@
import {
customDevRpcRequest,
deployCreateCompiledContract,
describeSuite,
expect
} from "@moonwall/cli";
import { fromHex, toHex } from "viem";
describeSuite({
id: "D021701",
title: "Filter API",
foundationMethods: "dev",
testCases: ({ context, it }) => {
it({
id: "T01",
title: "should be able to create a Log filter",
test: async () => {
const { contractAddress } = await deployCreateCompiledContract(context, "EventEmitter");
const createFilter = await customDevRpcRequest("eth_newFilter", [
{
fromBlock: "0x0",
toBlock: "latest",
address: [contractAddress, "0x970951a12F975E6762482ACA81E57D5A2A4e73F4"],
topics: ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"]
}
]);
expect(createFilter).toBe(toHex(1));
}
});
it({
id: "T02",
title: "should increment filter id",
test: async () => {
const createFilter = await customDevRpcRequest("eth_newFilter", [
{
fromBlock: "0x1",
toBlock: "0x2",
address: "0xc01Ee7f10EA4aF4673cFff62710E1D7792aBa8f3",
topics: ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"]
}
]);
const createFilter2 = await customDevRpcRequest("eth_newFilter", [
{
fromBlock: "0x1",
toBlock: "0x2",
address: "0xc01Ee7f10EA4aF4673cFff62710E1D7792aBa8f3",
topics: ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"]
}
]);
expect(fromHex(createFilter2, "bigint")).toBeGreaterThan(fromHex(createFilter, "bigint"));
expect(fromHex(createFilter2, "bigint") - fromHex(createFilter, "bigint")).toBe(1n);
}
});
it({
id: "T03",
title: "should be able to create a Block Log filter",
test: async () => {
const createFilter = await customDevRpcRequest("eth_newBlockFilter", []);
expect(createFilter).toBeTruthy();
}
});
}
});

View file

@ -0,0 +1,19 @@
import { customDevRpcRequest, describeSuite, expect } from "@moonwall/cli";
import { fromHex } from "viem";
describeSuite({
id: "D021702",
title: "Filter Pending Transaction API",
foundationMethods: "dev",
testCases: ({ it }) => {
it({
id: "T01",
title: "should not be supported",
// Looks like this is now supported 🎉
test: async () => {
const resp = await customDevRpcRequest("eth_newPendingTransactionFilter", []);
expect(fromHex(resp, "bigint")).toBeGreaterThanOrEqual(0n);
}
});
}
});

View file

@ -0,0 +1,88 @@
import {
customDevRpcRequest,
deployCreateCompiledContract,
describeSuite,
expect
} from "@moonwall/cli";
describeSuite({
id: "D021703",
title: "Filter Block API",
foundationMethods: "dev",
testCases: ({ context, it }) => {
it({
id: "T01",
title: "should return block information",
test: async () => {
const createFilter = await customDevRpcRequest("eth_newBlockFilter", []);
const block = await context.viem().getBlock();
const poll = await customDevRpcRequest("eth_getFilterChanges", [createFilter]);
expect(poll.length).to.be.eq(1);
expect(poll[0]).to.be.eq(block.hash);
}
});
it({
id: "T02",
title: "should not retrieve previously polled",
test: async () => {
const filterId = await customDevRpcRequest("eth_newBlockFilter", []);
await context.createBlock();
await customDevRpcRequest("eth_getFilterChanges", [filterId]);
await context.createBlock();
await context.createBlock();
const poll = await customDevRpcRequest("eth_getFilterChanges", [filterId]);
const block2 = await context.viem().getBlock({ blockNumber: 2n });
const block3 = await context.viem().getBlock({ blockNumber: 3n });
expect(poll.length).to.be.eq(2);
expect(poll[0]).to.be.eq(block2.hash);
expect(poll[1]).to.be.eq(block3.hash);
}
});
it({
id: "T03",
title: "should be empty after already polling",
test: async () => {
const filterId = await customDevRpcRequest("eth_newBlockFilter", []);
await context.createBlock();
await customDevRpcRequest("eth_getFilterChanges", [filterId]);
const poll = await customDevRpcRequest("eth_getFilterChanges", [filterId]);
expect(poll.length).to.be.eq(0);
}
});
it({
id: "T04",
title: "should support filtering created contract",
test: async () => {
const { contractAddress, hash } = await deployCreateCompiledContract(
context,
"EventEmitter"
);
const receipt = await context.viem().getTransactionReceipt({ hash });
const filterId = await customDevRpcRequest("eth_newFilter", [
{
fromBlock: "0x0",
toBlock: "latest",
address: contractAddress,
topics: receipt.logs[0].topics
}
]);
const poll = await customDevRpcRequest("eth_getFilterChanges", [filterId]);
expect(poll.length).to.be.eq(1);
expect(poll[0].address).to.be.eq(contractAddress);
expect(poll[0].topics).to.be.deep.eq(receipt.logs[0].topics);
}
});
}
});

View file

@ -0,0 +1,23 @@
import { describeSuite, expect, fetchCompiledContract } from "@moonwall/cli";
import { ALITH_ADDRESS } from "@moonwall/util";
describeSuite({
id: "D021801",
title: "Estimate Gas - Contract creation",
foundationMethods: "dev",
testCases: ({ context, it }) => {
it({
id: "T01",
title: "should return contract creation gas cost",
test: async () => {
const { bytecode } = fetchCompiledContract("MultiplyBy7");
expect(
await context.viem().estimateGas({
account: ALITH_ADDRESS,
data: bytecode
})
).to.equal(210541n);
}
});
}
});

View file

@ -0,0 +1,134 @@
import {
beforeAll,
customDevRpcRequest,
describeSuite,
type EthTransactionType,
expect,
fetchCompiledContract,
TransactionTypes
} from "@moonwall/cli";
import {
ALITH_ADDRESS,
createEthersTransaction,
faith,
getAllCompiledContracts
} from "@moonwall/util";
import { randomBytes } from "ethers";
import { encodeDeployData } from "viem";
import { expectEVMResult } from "../../../../helpers";
describeSuite({
id: "D021802",
title: "Estimate Gas - Multiply",
foundationMethods: "dev",
testCases: ({ context, it }) => {
const contractNames = getAllCompiledContracts("datahaven/contracts/out", true);
beforeAll(async () => {
// Estimation for storage need to happen in a block > than genesis.
// Otherwise contracts that uses block number as storage will remove instead of storing
// (as block.number === H256::default).
await context.createBlock();
});
it({
id: "T01",
title: "should have at least 1 contract to estimate",
test: async () => {
expect(contractNames).length.to.be.at.least(1);
}
});
const calculateTestCaseNumber = (contractName: string, txnType: EthTransactionType) =>
contractNames.indexOf(contractName) * TransactionTypes.length +
TransactionTypes.indexOf(txnType) +
2;
for (const contractName of contractNames) {
for (const txnType of TransactionTypes) {
it({
id: `T${calculateTestCaseNumber(contractName, txnType).toString().padStart(2, "0")}`,
title: `should be enough for contract ${contractName} via ${txnType}`,
test: async () => {
const { bytecode, abi } = fetchCompiledContract(contractName);
const constructorAbi = abi.find(
(call) => call.type === "constructor"
) as AbiConstructor;
// ask RPC for an gas estimate of deploying this contract
const args = constructorAbi
? constructorAbi.inputs.map((input: { type: string }) => {
if (input.type === "bool") {
return true;
}
if (input.type === "address") {
return faith.address;
}
if (input.type.startsWith("uint")) {
const rest = input.type.split("uint")[1];
if (rest === "[]") {
return [];
}
const length = Number(rest) || 256;
return `0x${Buffer.from(randomBytes(length / 8)).toString("hex")}`;
}
if (input.type.startsWith("bytes")) {
const rest = input.type.split("bytes")[1];
if (rest === "[]") {
return [];
}
const length = Number(rest) || 1;
return `0x${Buffer.from(randomBytes(length)).toString("hex")}`;
}
return undefined;
})
: [];
const callData = encodeDeployData({
abi,
args,
bytecode
});
let estimate: bigint;
let creationResult: "Revert" | "Succeed";
try {
estimate = await customDevRpcRequest("eth_estimateGas", [
{
from: ALITH_ADDRESS,
data: callData
}
]);
creationResult = "Succeed";
} catch (e: any) {
if (e.message === "VM Exception while processing transaction: revert") {
estimate = 12_000_000n;
creationResult = "Revert";
} else {
throw e;
}
}
// attempt a transaction with our estimated gas
const rawSigned = await createEthersTransaction(context, {
data: callData,
gasLimit: estimate,
txnType
});
const { result } = await context.createBlock(rawSigned);
await context.createBlock();
const receipt = await context
.viem("public")
.getTransactionReceipt({ hash: result!.hash as `0x${string}` });
expectEVMResult(result!.events, creationResult);
expect(receipt.status === "success").to.equal(creationResult === "Succeed");
}
});
}
}
}
});

View file

@ -0,0 +1,160 @@
import {
customDevRpcRequest,
deployCreateCompiledContract,
describeSuite,
expect,
fetchCompiledContract
} from "@moonwall/cli";
import { ALITH_ADDRESS, PRECOMPILE_BATCH_ADDRESS } from "@moonwall/util";
import { encodeFunctionData } from "viem";
describeSuite({
id: "D021803",
title: "Estimate Gas - Contract estimation",
foundationMethods: "dev",
testCases: ({ context, it }) => {
it({
id: "T01",
title: "evm should return invalid opcode",
test: async () => {
await expect(
async () =>
await customDevRpcRequest("eth_estimateGas", [
{
from: ALITH_ADDRESS,
data: "0xe4"
}
])
).rejects.toThrowError("evm error: InvalidCode(Opcode(228))");
}
});
it({
id: "T02",
title: "eth_estimateGas 0x0 gasPrice is equivalent to not setting one",
test: async () => {
const { bytecode } = fetchCompiledContract("Incrementor");
const result = await context.viem().estimateGas({
account: ALITH_ADDRESS,
data: bytecode,
gasPrice: 0n
});
expect(result).to.equal(255341n);
const result2 = await context.viem().estimateGas({
account: ALITH_ADDRESS,
data: bytecode
});
expect(result2).to.equal(255341n);
}
});
it({
id: "T03",
title: "all batch functions should estimate the same cost",
test: async () => {
const { contractAddress: proxyAddress, abi: proxyAbi } = await deployCreateCompiledContract(
context,
"CallForwarder"
);
const { contractAddress: multiAddress, abi: multiAbi } = await deployCreateCompiledContract(
context,
"MultiplyBy7"
);
const batchAbi = fetchCompiledContract("Batch").abi;
const callParameters = [
[proxyAddress],
[],
[
encodeFunctionData({
abi: proxyAbi,
functionName: "call",
args: [
multiAddress,
encodeFunctionData({
abi: multiAbi,
functionName: "multiply",
args: [42]
})
]
})
],
[]
];
const batchSomeGas = await context.viem().estimateGas({
account: ALITH_ADDRESS,
to: PRECOMPILE_BATCH_ADDRESS,
data: encodeFunctionData({
abi: batchAbi,
functionName: "batchSome",
args: callParameters
})
});
const batchSomeUntilFailureGas = await context.viem().estimateGas({
account: ALITH_ADDRESS,
to: PRECOMPILE_BATCH_ADDRESS,
data: encodeFunctionData({
abi: batchAbi,
functionName: "batchSomeUntilFailure",
args: callParameters
})
});
const batchAllGas = await context.viem().estimateGas({
account: ALITH_ADDRESS,
to: PRECOMPILE_BATCH_ADDRESS,
data: encodeFunctionData({
abi: batchAbi,
functionName: "batchAll",
args: callParameters
})
});
expect(batchSomeGas).to.be.eq(batchAllGas);
expect(batchSomeUntilFailureGas).to.be.eq(batchAllGas);
}
});
it({
id: "T04",
title: "Non-transactional calls allowed from e.g. precompile address",
test: async () => {
const { bytecode } = fetchCompiledContract("MultiplyBy7");
expect(
await context.viem().estimateGas({
account: PRECOMPILE_BATCH_ADDRESS,
data: bytecode
})
).toBe(210541n);
}
});
it({
id: "T05",
title: "Should be able to estimate gas of infinite loop call",
timeout: 60000,
test: async () => {
const { contractAddress, abi } = await deployCreateCompiledContract(context, "Looper");
await expect(
async () =>
await customDevRpcRequest("eth_estimateGas", [
{
from: ALITH_ADDRESS,
to: contractAddress,
data: encodeFunctionData({
abi: abi,
functionName: "infinite",
args: []
})
}
])
).rejects.toThrowError("gas required exceeds allowance 6000000");
}
});
}
});

View file

@ -0,0 +1,120 @@
import { beforeAll, deployCreateCompiledContract, describeSuite, expect } from "@moonwall/cli";
import { ALITH_ADDRESS } from "@moonwall/util";
import type { Abi } from "viem";
describeSuite({
id: "D021804",
title: "Estimate Gas - Multiply",
foundationMethods: "dev",
testCases: ({ context, it }) => {
let multiAbi: Abi;
let multiAddress: `0x${string}`;
beforeAll(async () => {
const { abi, contractAddress } = await deployCreateCompiledContract(context, "MultiplyBy7");
multiAbi = abi;
multiAddress = contractAddress;
});
it({
id: "T01",
title: "should return correct gas estimation",
test: async () => {
const estimatedGas = await context.viem().estimateContractGas({
account: ALITH_ADDRESS,
abi: multiAbi,
address: multiAddress,
functionName: "multiply",
maxPriorityFeePerGas: 0n,
args: [3],
value: 0n
});
// Snapshot estimated gas
expect(estimatedGas).toMatchInlineSnapshot("22363n");
}
});
it({
id: "T02",
title: "should work without gas limit",
test: async () => {
const estimatedGas = await context.viem().estimateContractGas({
account: ALITH_ADDRESS,
abi: multiAbi,
address: multiAddress,
functionName: "multiply",
maxPriorityFeePerGas: 0n,
args: [3],
//@ts-expect-error expected
gasLimit: undefined,
value: 0n
});
// Snapshot estimated gas
expect(estimatedGas).toMatchInlineSnapshot("22363n");
}
});
it({
id: "T03",
title: "should work with gas limit",
test: async () => {
const estimatedGas = await context.viem().estimateContractGas({
account: ALITH_ADDRESS,
abi: multiAbi,
address: multiAddress,
functionName: "multiply",
args: [3],
// @ts-expect-error expected
gasLimit: 22363n,
value: 0n
});
expect(estimatedGas).toMatchInlineSnapshot("22363n");
}
});
it({
id: "T04",
title: "should ignore from balance (?)",
test: async () => {
const estimatedGas = await context.viem().estimateContractGas({
account: "0x0000000000000000000000000000000000000000",
abi: multiAbi,
address: multiAddress,
functionName: "multiply",
maxPriorityFeePerGas: 0n,
args: [3],
// @ts-expect-error expected
gasLimit: 22363n,
value: 0n
});
// Snapshot estimated gas
expect(estimatedGas).toMatchInlineSnapshot("22363n");
}
});
it({
id: "T05",
title: "should not work with a lower gas limit",
test: async () => {
await expect(
async () =>
await context.viem().estimateContractGas({
account: "0x0000000000000000000000000000000000000000",
abi: multiAbi,
address: multiAddress,
functionName: "multiply",
maxPriorityFeePerGas: 0n,
args: [3],
gas: 21000n,
value: 0n
})
).rejects.toThrowError("gas required exceeds allowance 21000");
}
});
}
});

View file

@ -0,0 +1,118 @@
import { beforeAll, deployCreateCompiledContract, describeSuite, expect } from "@moonwall/cli";
import { ALITH_ADDRESS } from "@moonwall/util";
import { type Abi, decodeEventLog, encodeFunctionData } from "viem";
import { deployContract } from "../../../../helpers/contracts";
describeSuite({
id: "D021805",
title: "Estimate Gas - subCall",
foundationMethods: "dev",
testCases: ({ context, it, log }) => {
let looperAddress: `0x${string}`;
let subCallOogAbi: Abi;
let subCallOogAddress: `0x${string}`;
const bloatedContracts: string[] = [];
const MAX_BLOATED_CONTRACTS = 15;
beforeAll(async () => {
const { contractAddress: contractAddress2 } = await deployCreateCompiledContract(
context,
"Looper"
);
looperAddress = contractAddress2;
const { abi, contractAddress: contractAddress3 } = await deployCreateCompiledContract(
context,
"SubCallOOG"
);
subCallOogAbi = abi;
subCallOogAddress = contractAddress3;
// Deploy bloated contracts (test won't use more than what is needed for reaching max pov)
for (let i = 0; i <= MAX_BLOATED_CONTRACTS; i++) {
const { contractAddress } = await deployContract(context as any, "BloatedContract");
bloatedContracts.push(contractAddress);
await context.createBlock();
}
});
it({
id: "T01",
title: "gas estimation should make subcall OOG",
test: async () => {
const estimatedGas = await context.viem().estimateContractGas({
account: ALITH_ADDRESS,
abi: subCallOogAbi,
address: subCallOogAddress,
functionName: "subCallLooper",
maxPriorityFeePerGas: 0n,
args: [looperAddress, 999],
value: 0n
});
const txHash = await context.viem().sendTransaction({
to: subCallOogAddress,
data: encodeFunctionData({
abi: subCallOogAbi,
functionName: "subCallLooper",
args: [looperAddress, 999]
}),
txnType: "eip1559",
gasLimit: estimatedGas
});
await context.createBlock();
const receipt = await context.viem().getTransactionReceipt({ hash: txHash });
const decoded = decodeEventLog({
abi: subCallOogAbi,
data: receipt.logs[0].data,
topics: receipt.logs[0].topics
}) as any;
expect(decoded.eventName).to.equal("SubCallFail");
}
});
it({
id: "T02",
title: "gas estimation should make pov-consuming subcall succeed",
test: async () => {
const estimatedGas = await context.viem().estimateContractGas({
account: ALITH_ADDRESS,
abi: subCallOogAbi,
address: subCallOogAddress,
functionName: "subCallPov",
maxPriorityFeePerGas: 0n,
args: [bloatedContracts],
value: 0n
});
log(`Estimated gas: ${estimatedGas}`);
const txHash = await context.viem().sendTransaction({
to: subCallOogAddress,
data: encodeFunctionData({
abi: subCallOogAbi,
functionName: "subCallPov",
args: [bloatedContracts]
}),
txnType: "eip1559",
gasLimit: estimatedGas
});
await context.createBlock();
const receipt = await context.viem().getTransactionReceipt({ hash: txHash });
const decoded = decodeEventLog({
abi: subCallOogAbi,
data: receipt.logs[bloatedContracts.length - 1].data,
topics: receipt.logs[bloatedContracts.length - 1].topics
}) as any;
expect(decoded.eventName).to.equal("SubCallSucceed");
}
});
}
});

View file

@ -59,6 +59,7 @@
"commander": "^13.1.0",
"dockerode": "^4.0.7",
"dotenv": "^16.5.0",
"ethers": "^6.13.4",
"octokit": "^4.1.4",
"ora": "^8.2.0",
"pino": "^9.7.0",

View file

@ -62,7 +62,7 @@ async function killAllProcesses() {
.split("\n")
.filter((p) => p)
]
.map((p) => Number.parseInt(p.toString()))
.map((p) => Number.parseInt(p.toString(), 10))
.filter((p) => !Number.isNaN(p));
logger.info(`Found PIDs to kill: ${allPids.join(", ")}`);
@ -278,7 +278,9 @@ async function runTestsWithConcurrencyLimit() {
// Get all test files dynamically
const testFiles = await getTestFiles();
logger.info(`📋 Found ${testFiles.length} test files:`);
testFiles.forEach((file) => logger.info(` - ${file}`));
testFiles.forEach((file) => {
logger.info(` - ${file}`);
});
// Create a queue of test files
const testQueue = [...testFiles];

View file

@ -32,7 +32,8 @@
"assumeChangesOnlyAffectDirectDependencies": true,
"paths": {
"utils": ["./utils/index.ts"],
"utils/types": ["./utils/types.ts"]
"utils/types": ["./utils/types.ts"],
"utils/constants": ["./utils/constants.ts"]
}
},
"include": [

View file

@ -45,7 +45,7 @@ export const ANVIL_FUNDED_ACCOUNTS = {
derivationPath: "m/44'/60'/0'/0/"
} as const;
export const CHAIN_ID = 3151908;
export const CHAIN_ID = 55932;
export const CONTAINER_NAMES = {
EL1: "el-1-reth-lodestar",

View file

@ -2,7 +2,8 @@ import { existsSync } from "node:fs";
import { type Duplex, PassThrough, Transform } from "node:stream";
import Docker from "dockerode";
import invariant from "tiny-invariant";
import { logger, type ServiceInfo, StandardServiceMappings } from "utils";
import { logger } from "./logger";
import { type ServiceInfo, StandardServiceMappings } from "./service-mappings";
function createDockerConnection(): Docker {
const dockerHost = process.env.DOCKER_HOST;
@ -16,7 +17,7 @@ function createDockerConnection(): Docker {
const url = new URL(dockerHost);
return new Docker({
host: url.hostname,
port: Number.parseInt(url.port) || 2375,
port: Number.parseInt(url.port, 10) || 2375,
protocol: "http"
});
}

View file

@ -10,6 +10,7 @@ export * from "./papi";
export * from "./parameters";
export * from "./parser";
export * from "./rpc";
export * from "./service-mappings";
export * from "./shell";
export * from "./validators";
export * from "./viem";

View file

@ -2,19 +2,6 @@ import { $ } from "bun";
import { z } from "zod";
import { logger } from "./logger";
export interface ServiceMapping {
service: string;
containerPattern: string;
internalPort: number;
protocol: string;
}
export interface ServiceInfo {
service: string;
port: string;
url: string;
}
export type KurtosisServiceInfo = {
name: string;
portType: string;
@ -29,33 +16,6 @@ export const standardKurtosisServices = [
"dora"
];
export const StandardServiceMappings: ServiceMapping[] = [
{
service: "reth-1-rpc",
containerPattern: "el-1-reth-lodestar",
internalPort: 8545,
protocol: "tcp"
},
{
service: "reth-2-rpc",
containerPattern: "el-2-reth-lodestar",
internalPort: 8545,
protocol: "tcp"
},
{
service: "blockscout-backend",
containerPattern: "blockscout--",
internalPort: 4000,
protocol: "tcp"
},
{
service: "dora",
containerPattern: "dora--",
internalPort: 8080,
protocol: "tcp"
}
];
const portDetailSchema = z.object({
number: z.number(),
transport: z.number(), // Consider z.literal(0) | z.literal(2) if these are the only values

View file

@ -0,0 +1,39 @@
export interface ServiceMapping {
service: string;
containerPattern: string;
internalPort: number;
protocol: string;
}
export interface ServiceInfo {
service: string;
port: string;
url: string;
}
export const StandardServiceMappings: ServiceMapping[] = [
{
service: "reth-1-rpc",
containerPattern: "el-1-reth-lodestar",
internalPort: 8545,
protocol: "tcp"
},
{
service: "reth-2-rpc",
containerPattern: "el-2-reth-lodestar",
internalPort: 8545,
protocol: "tcp"
},
{
service: "blockscout-backend",
containerPattern: "blockscout--",
internalPort: 4000,
protocol: "tcp"
},
{
service: "dora",
containerPattern: "dora--",
internalPort: 8080,
protocol: "tcp"
}
];