mirror of
https://github.com/datahaven-xyz/datahaven
synced 2026-05-24 01:38:32 +00:00
test: port ethereum tests from moonbeam (#278)
Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com>
This commit is contained in:
parent
0fa701f900
commit
2cc1a4d3f0
71 changed files with 23624 additions and 3612 deletions
|
|
@ -13,7 +13,9 @@
|
|||
"!**/*.spec.json",
|
||||
"!**/.papi/descriptors/**/*",
|
||||
"!**/contract-bindings/**/*",
|
||||
"!**/html/**/*"
|
||||
"!**/html/**/*",
|
||||
"!**/datahaven/contracts/out/**/*",
|
||||
"!**/contracts/out/**/*"
|
||||
]
|
||||
},
|
||||
"assist": { "actions": { "source": { "organizeImports": "on" } } },
|
||||
|
|
|
|||
22215
test/bun.lock
22215
test/bun.lock
File diff suppressed because it is too large
Load diff
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
41
test/datahaven/contracts/src/AccessListHelper.sol
Normal file
41
test/datahaven/contracts/src/AccessListHelper.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
13
test/datahaven/contracts/src/BloatedContract.sol
Normal file
13
test/datahaven/contracts/src/BloatedContract.sol
Normal 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;
|
||||
}
|
||||
}
|
||||
26
test/datahaven/contracts/src/CallForwarder.sol
Normal file
26
test/datahaven/contracts/src/CallForwarder.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
10
test/datahaven/contracts/src/EventEmitter.sol
Normal file
10
test/datahaven/contracts/src/EventEmitter.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
19
test/datahaven/contracts/src/Incrementor.sol
Normal file
19
test/datahaven/contracts/src/Incrementor.sol
Normal 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;
|
||||
}
|
||||
}
|
||||
18
test/datahaven/contracts/src/Looper.sol
Normal file
18
test/datahaven/contracts/src/Looper.sol
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
53
test/datahaven/contracts/src/SelfDestruct.sol
Normal file
53
test/datahaven/contracts/src/SelfDestruct.sol
Normal 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");
|
||||
}
|
||||
}
|
||||
37
test/datahaven/contracts/src/StateOverrideTest.sol
Normal file
37
test/datahaven/contracts/src/StateOverrideTest.sol
Normal 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;
|
||||
}
|
||||
}
|
||||
33
test/datahaven/contracts/src/SubCallOOG.sol
Normal file
33
test/datahaven/contracts/src/SubCallOOG.sol
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
23
test/datahaven/contracts/src/dancun/ProxySuicide.sol
Normal file
23
test/datahaven/contracts/src/dancun/ProxySuicide.sol
Normal 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));
|
||||
}
|
||||
}
|
||||
57
test/datahaven/contracts/src/dancun/TransientStorage.sol
Normal file
57
test/datahaven/contracts/src/dancun/TransientStorage.sol
Normal 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
};
|
||||
|
|
|
|||
144
test/datahaven/helpers/eth-transactions.ts
Normal file
144
test/datahaven/helpers/eth-transactions.ts
Normal 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
|
||||
};
|
||||
}
|
||||
176
test/datahaven/helpers/expect.ts
Normal file
176
test/datahaven/helpers/expect.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
|
|
|||
124
test/datahaven/helpers/transactions.ts
Normal file
124
test/datahaven/helpers/transactions.ts
Normal 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;
|
||||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -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());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -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");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -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");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -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");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
153
test/datahaven/suites/dev/stagenet/contract/test-eip-6780.ts
Normal file
153
test/datahaven/suites/dev/stagenet/contract/test-eip-6780.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
36
test/datahaven/suites/dev/stagenet/contract/test-eip1153.ts
Normal file
36
test/datahaven/suites/dev/stagenet/contract/test-eip1153.ts
Normal 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");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -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");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -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");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -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`);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -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");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
167
test/datahaven/suites/dev/stagenet/eth-tx/test-eth-tx-types.ts
Normal file
167
test/datahaven/suites/dev/stagenet/eth-tx/test-eth-tx-types.ts
Normal 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"
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
173
test/datahaven/suites/dev/stagenet/eth-tx/test-test-tx-nonce.ts
Normal file
173
test/datahaven/suites/dev/stagenet/eth-tx/test-test-tx-nonce.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
82
test/datahaven/suites/dev/stagenet/ethers/test-ethers.ts
Normal file
82
test/datahaven/suites/dev/stagenet/ethers/test-ethers.ts
Normal 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");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -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");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -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");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -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");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -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");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
39
test/utils/service-mappings.ts
Normal file
39
test/utils/service-mappings.ts
Normal 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"
|
||||
}
|
||||
];
|
||||
Loading…
Reference in a new issue