test: Integrate moonwall (#185)

### Description

This PR introduces the **Moonwall** end-to-end (E2E) testing framework.
The primary motivation for this is to enable the porting of existing
Mobeam tests into the `DataHaven` repository.

### Key Changes

*   **Node Manual Sealing:**
* Introduced a `--sealing=manual` flag for the `datahaven-node`. When
enabled, blocks are only produced on demand via an RPC call. This is the
core mechanism that allows for deterministic tests.

*   **Moonwall Framework Integration:**
* Added `@moonwall/cli` and `@moonwall/util` dependencies to the
`test/package.json`.
* A new `test/moonwall.config.json` file configures the test
environment, defining how Moonwall should launch the `datahaven-node`
with the manual sealing flag.
* Added a `moonwall:test` script to `package.json` for running the
tests.

*   **CI Workflow:**
* A new reusable workflow, `.github/workflows/task-moonwall-tests.yml`,
has been created to handle the setup, execution, and reporting of
Moonwall tests.
* The main `CI.yml` now includes a `moonwall-tests` job that runs after
the `build-operator` job, ensuring it always tests the correct,
freshly-built binary.

*   **Example Test Suite:**
* A new test suite, `test/datahaven/suites/dev/test-block.ts`, had been
copied from moonbeam.

### How to Run Locally

1.  Navigate to the `test` directory.
2.  Install dependencies: `bun install`
3.  Run the tests: `bun run moonwall:test`

---------

Co-authored-by: undercover-cactus <lola@moonsonglabs.com>
This commit is contained in:
Ahmad Kaouk 2025-09-30 16:47:39 +02:00 committed by GitHub
parent 066a416349
commit 17c706dc64
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 2455 additions and 153 deletions

View file

@ -49,6 +49,12 @@ jobs:
with:
binary-hash: ${{ needs.build-operator.outputs.binary-hash }}
moonwall-tests:
needs: [build-operator]
uses: ./.github/workflows/task-moonwall-tests.yml
with:
binary-hash: ${{ needs.build-operator.outputs.binary-hash }}
# Third Tier - E2E tests depend on docker build
e2e-tests:
needs: [docker-build]

View file

@ -0,0 +1,70 @@
name: Moonwall Tests
on:
workflow_dispatch:
workflow_call:
inputs:
binary-hash:
description: "Hash of the built operator binary uploaded as artifact"
required: true
type: string
jobs:
moonwall:
runs-on: ubuntu-latest
defaults:
run:
working-directory: test
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Download operator binary artifact
uses: actions/download-artifact@v4
with:
name: datahaven-node-${{ inputs.binary-hash }}
path: operator/target/x86_64-unknown-linux-gnu/release
- name: Prepare operator binary in expected path
run: |
mkdir -p ../operator/target/release
cp ../operator/target/x86_64-unknown-linux-gnu/release/datahaven-node ../operator/target/release/datahaven-node
chmod +x ../operator/target/release/datahaven-node
timeout 20 ../operator/target/release/datahaven-node \
--dev \
--no-telemetry \
--unsafe-force-node-key-generation \
--reserved-only \
--no-grandpa \
--no-prometheus \
--sealing=manual 2>&1 | head -50 || echo "Node startup test completed"
- uses: oven-sh/setup-bun@v2
- uses: actions/setup-node@v4
with:
node-version: 22
- uses: actions/cache@v4
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }}
restore-keys: |
${{ runner.os }}-bun-
- name: Install dependencies
run: bun install
- name: Run Moonwall tests
run: bun run moonwall:test
- name: Upload Moonwall reports
if: always()
uses: actions/upload-artifact@v4
with:
name: moonwall-reports
path: |
test/tmp/testResults.json
test/reports/**
test/**/*.log
retention-days: 1

View file

@ -12,7 +12,8 @@
"!**/tmp/*",
"!**/*.spec.json",
"!**/.papi/descriptors/**/*",
"!**/contract-bindings/**/*"
"!**/contract-bindings/**/*",
"!**/html/**/*"
]
},
"assist": { "actions": { "source": { "organizeImports": "on" } } },

View file

@ -10,7 +10,7 @@ use crate::{
use datahaven_runtime_common::Block;
use frame_benchmarking_cli::{BenchmarkCmd, ExtrinsicFactory, SUBSTRATE_REFERENCE_HARDWARE};
use sc_cli::SubstrateCli;
use sc_service::DatabaseSource;
use sc_service::{ChainType, DatabaseSource};
use serde::Deserialize;
use shc_client::builder::{
BlockchainServiceOptions, BspChargeFeesOptions, BspMoveBucketOptions, BspSubmitProofOptions,
@ -331,6 +331,17 @@ pub fn run() -> sc_cli::Result<()> {
};
runner.run_node_until_exit(|config| async move {
let sealing_mode = match (cli.sealing, config.chain_spec.chain_type()) {
(Some(mode), ChainType::Development) => Some(mode),
(Some(_), _) => {
log::warn!(
"`--sealing` is only supported on development chains; ignoring."
);
None
}
(None, _) => None,
};
match config.network.network_backend {
// TODO: Litep2p becomes standard with Polkadot SDK stable2412-7 (should move None to other arm)
// cfr. https://github.com/paritytech/polkadot-sdk/releases/tag/polkadot-stable2412-7
@ -347,6 +358,7 @@ pub fn run() -> sc_cli::Result<()> {
provider_options,
indexer_options,
fisherman_options,
sealing_mode,
)
.await
}
@ -361,6 +373,7 @@ pub fn run() -> sc_cli::Result<()> {
provider_options,
indexer_options,
fisherman_options,
sealing_mode,
)
.await
}
@ -375,6 +388,7 @@ pub fn run() -> sc_cli::Result<()> {
provider_options,
indexer_options,
fisherman_options,
sealing_mode,
)
.await
}
@ -394,6 +408,7 @@ pub fn run() -> sc_cli::Result<()> {
provider_options,
indexer_options,
fisherman_options,
sealing_mode,
)
.await
}
@ -408,6 +423,7 @@ pub fn run() -> sc_cli::Result<()> {
provider_options,
indexer_options,
fisherman_options,
sealing_mode,
)
.await
}
@ -422,6 +438,7 @@ pub fn run() -> sc_cli::Result<()> {
provider_options,
indexer_options,
fisherman_options,
sealing_mode,
)
.await
}

View file

@ -1,7 +1,6 @@
//! Service and ServiceFactory implementation. Specialized wrapper over substrate service.
use crate::cli::ProviderType;
use crate::cli::StorageLayer;
use crate::cli::{ProviderType, Sealing, StorageLayer};
use crate::command::ProviderOptions;
use crate::eth::{
new_frontier_partial, spawn_frontier_tasks, BackendType, FrontierBackend,
@ -14,16 +13,22 @@ use datahaven_runtime_common::{AccountId, Balance, Block, BlockNumber, Hash, Non
use fc_consensus::FrontierBlockImport;
use fc_db::DatabaseSource;
use fc_storage::StorageOverride;
use futures::channel::mpsc;
use futures::FutureExt;
use sc_client_api::{AuxStore, Backend, BlockBackend, StateBackend, StorageProvider};
use sc_consensus_babe::ImportQueueParams;
use sc_consensus_grandpa::SharedVoterState;
use sc_consensus_manual_seal::consensus::babe::BabeConsensusDataProvider;
use sc_consensus_manual_seal::rpc::EngineCommand;
use sc_consensus_manual_seal::{self, InstantSealParams, ManualSealParams};
use sc_executor::{HeapAllocStrategy, WasmExecutor, DEFAULT_HEAP_ALLOC_STRATEGY};
use sc_network::request_responses::IncomingRequest;
use sc_network::service::traits::NetworkService;
use sc_network::ProtocolName;
use sc_service::RpcHandlers;
use sc_service::{error::Error as ServiceError, Configuration, TaskManager, WarpSyncConfig};
use sc_service::{
error::Error as ServiceError, ChainType, Configuration, TaskManager, WarpSyncConfig,
};
use sc_telemetry::{Telemetry, TelemetryWorker};
use sc_transaction_pool::BasicPool;
use sc_transaction_pool_api::OffchainTransactionPoolFactory;
@ -49,6 +54,7 @@ use sp_keystore::KeystorePtr;
use sp_runtime::traits::BlakeTwo256;
use sp_runtime::SaturatedConversion;
use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
use std::{default::Default, path::Path, sync::Arc, time::Duration};
pub(crate) type FullClient<RuntimeApi> = sc_service::TFullClient<
@ -78,6 +84,11 @@ type FullBeefyBlockImport<InnerBlockImport, AuthorityId, RuntimeApi> =
/// imported and generated.
const GRANDPA_JUSTIFICATION_PERIOD: u32 = 512;
// Mock timestamp used for manual/instant sealing in dev mode, similar to Moonbeam.
// Each new block will advance the timestamp by one slot duration to satisfy
// pallet_timestamp MinimumPeriod checks when sealing back-to-back.
static MOCK_TIMESTAMP: AtomicU64 = AtomicU64::new(0);
pub(crate) trait FullRuntimeApi:
sp_transaction_pool::runtime_api::TaggedTransactionQueue<Block>
+ sp_api::Metadata<Block>
@ -230,6 +241,27 @@ where
Ok(frontier_backend)
}
fn build_babe_inherent_providers(
slot_duration: sp_consensus_babe::SlotDuration,
) -> (
sp_consensus_babe::inherents::InherentDataProvider,
sp_timestamp::InherentDataProvider,
) {
// In manual/instant sealing we want to advance time deterministically per block
// to satisfy `pallet_timestamp` MinimumPeriod without sleeping. We increment a
// static counter by one slot each time and use that value as the timestamp.
let increment = slot_duration.as_millis();
let next_ts = MOCK_TIMESTAMP
.fetch_add(increment, Ordering::SeqCst)
.saturating_add(increment);
let timestamp = sp_timestamp::InherentDataProvider::new(sp_timestamp::Timestamp::new(next_ts));
let slot = sp_consensus_babe::inherents::InherentDataProvider::from_timestamp_and_slot_duration(
*timestamp,
slot_duration,
);
(slot, timestamp)
}
pub fn new_partial<Runtime, RuntimeApi>(
config: &Configuration,
eth_config: &mut EthConfiguration,
@ -330,16 +362,10 @@ where
justification_import: Some(Box::new(grandpa_block_import.clone())),
client: client.clone(),
select_chain: select_chain.clone(),
create_inherent_data_providers: move |_, ()| async move {
let timestamp = sp_timestamp::InherentDataProvider::from_system_time();
let slot =
sp_consensus_babe::inherents::InherentDataProvider::from_timestamp_and_slot_duration(
*timestamp,
slot_duration,
);
Ok((slot, timestamp))
create_inherent_data_providers: move |_, ()| {
std::future::ready(Ok::<_, Box<dyn std::error::Error + Send + Sync>>(
build_babe_inherent_providers(slot_duration),
))
},
spawner: &task_manager.spawn_essential_handle(),
registry: config.prometheus_registry(),
@ -385,6 +411,7 @@ pub async fn new_full_impl<
provider_options: Option<ProviderOptions>,
indexer_options: Option<IndexerOptions>,
fisherman_options: Option<FishermanOptions>,
sealing: Option<Sealing>,
) -> Result<TaskManager, ServiceError>
where
Runtime: shc_common::traits::StorageEnableRuntime<RuntimeApi = RuntimeApi>,
@ -415,6 +442,24 @@ where
),
} = new_partial::<Runtime, RuntimeApi>(&config, &mut eth_config)?;
let role = config.role;
let mut sealing = match sealing {
Some(_) if !matches!(config.chain_spec.chain_type(), ChainType::Development) => {
log::warn!("Manual sealing is only available for development chains; disabling.");
None
}
other => other,
};
if sealing.is_some() && !role.is_authority() {
log::warn!(
"Manual sealing requested but the node is not running as an authority; disabling."
);
sealing = None;
}
let is_authority = role.is_authority();
let FrontierPartialComponents {
filter_pool,
fee_history_cache,
@ -543,11 +588,10 @@ where
)
.await?;
let role = config.role;
let force_authoring = config.force_authoring;
let backoff_authoring_blocks: Option<()> = None;
let name = config.network.node_name.clone();
let enable_grandpa = !config.disable_grandpa;
let enable_grandpa = sealing.is_none() && !config.disable_grandpa;
let prometheus_registry = config.prometheus_registry().cloned();
let overrides = Arc::new(StorageOverrideHandler::new(client.clone()));
@ -559,6 +603,15 @@ where
prometheus_registry.clone(),
));
let mut manual_commands_stream: Option<mpsc::Receiver<EngineCommand<Hash>>> = None;
let command_sink = if matches!(sealing, Some(Sealing::Manual)) {
let (sink, stream) = mpsc::channel::<EngineCommand<Hash>>(1000);
manual_commands_stream = Some(stream);
Some(sink)
} else {
None
};
// Sinks for pubsub notifications.
// Everytime a new subscription is created, a new mpsc channel is added to the sink pool.
// The MappingSyncWorker sends through the channel on block import and the subscription emits a notification to the subscriber on receiving a message through this channel.
@ -619,8 +672,8 @@ where
filter_pool: filter_pool.clone(),
block_data_cache: block_data_cache.clone(),
overrides: overrides.clone(),
is_authority: false,
command_sink: None,
is_authority: is_authority.clone(),
command_sink: command_sink.clone(),
backend: backend.clone(),
frontier_backend: match &*frontier_backend {
fc_db::Backend::KeyValue(b) => b.clone(),
@ -648,47 +701,122 @@ where
telemetry: telemetry.as_mut(),
})?;
if role.is_authority() {
let proposer_factory = sc_basic_authorship::ProposerFactory::new(
task_manager.spawn_handle(),
client.clone(),
transaction_pool.clone(),
prometheus_registry.as_ref(),
telemetry.as_ref().map(|x| x.handle()),
);
if is_authority {
if let Some(mode) = sealing {
let proposer_factory = sc_basic_authorship::ProposerFactory::new(
task_manager.spawn_handle(),
client.clone(),
transaction_pool.clone(),
prometheus_registry.as_ref(),
telemetry.as_ref().map(|x| x.handle()),
);
let slot_duration = babe_link.clone().config().slot_duration();
let babe_config = sc_consensus_babe::BabeParams {
keystore: keystore_container.keystore(),
client: client.clone(),
select_chain,
env: proposer_factory,
block_import,
sync_oracle: sync_service.clone(),
justification_sync_link: sync_service.clone(),
create_inherent_data_providers: move |_, ()| async move {
let timestamp = sp_timestamp::InherentDataProvider::from_system_time();
let slot =
sp_consensus_babe::inherents::InherentDataProvider::from_timestamp_and_slot_duration(
*timestamp,
slot_duration,
);
Ok((slot, timestamp))
},
force_authoring,
backoff_authoring_blocks,
babe_link,
block_proposal_slot_portion: sc_consensus_babe::SlotProportion::new(0.5),
max_block_proposal_slot_portion: None,
telemetry: telemetry.as_ref().map(|x| x.handle()),
};
let slot_duration = babe_link.config().slot_duration();
let epoch_changes = babe_link.epoch_changes().clone();
let authorities = babe_link.config().authorities.clone();
let keystore = keystore_container.keystore();
let client_for_consensus = client.clone();
let consensus_data_provider = move || {
BabeConsensusDataProvider::new(
client_for_consensus.clone(),
keystore.clone(),
epoch_changes.clone(),
authorities.clone(),
)
.map(|provider| Box::new(provider) as _)
.map_err(|e| ServiceError::Other(e.to_string()))
};
let babe = sc_consensus_babe::start_babe(babe_config)?;
task_manager.spawn_essential_handle().spawn_blocking(
"babe-proposer",
Some("block-authoring"),
babe,
);
let create_inherent_data_providers = move |_, ()| {
std::future::ready(Ok::<_, Box<dyn std::error::Error + Send + Sync>>(
build_babe_inherent_providers(slot_duration),
))
};
match mode {
Sealing::Manual => {
let commands_stream = manual_commands_stream.take().ok_or_else(|| {
ServiceError::Other(
"Manual sealing requested but command channel is unavailable".into(),
)
})?;
let future = sc_consensus_manual_seal::run_manual_seal(ManualSealParams {
block_import,
env: proposer_factory,
client: client.clone(),
pool: transaction_pool.clone(),
commands_stream,
select_chain,
consensus_data_provider: Some(consensus_data_provider()?),
create_inherent_data_providers,
});
task_manager.spawn_essential_handle().spawn_blocking(
"manual-seal",
Some("block-authoring"),
future,
);
}
Sealing::Instant => {
let future = sc_consensus_manual_seal::run_instant_seal(InstantSealParams {
block_import,
env: proposer_factory,
client: client.clone(),
pool: transaction_pool.clone(),
select_chain,
consensus_data_provider: Some(consensus_data_provider()?),
create_inherent_data_providers,
});
task_manager.spawn_essential_handle().spawn_blocking(
"manual-seal",
Some("block-authoring"),
future,
);
}
}
log::info!("Manual sealing enabled (mode: {:?})", mode);
} else {
let proposer_factory = sc_basic_authorship::ProposerFactory::new(
task_manager.spawn_handle(),
client.clone(),
transaction_pool.clone(),
prometheus_registry.as_ref(),
telemetry.as_ref().map(|x| x.handle()),
);
let slot_duration = babe_link.clone().config().slot_duration();
let create_inherent_data_providers = move |_, ()| {
std::future::ready(Ok::<_, Box<dyn std::error::Error + Send + Sync>>(
build_babe_inherent_providers(slot_duration),
))
};
let babe_config = sc_consensus_babe::BabeParams {
keystore: keystore_container.keystore(),
client: client.clone(),
select_chain,
env: proposer_factory,
block_import,
sync_oracle: sync_service.clone(),
justification_sync_link: sync_service.clone(),
create_inherent_data_providers,
force_authoring,
backoff_authoring_blocks,
babe_link,
block_proposal_slot_portion: sc_consensus_babe::SlotProportion::new(0.5),
max_block_proposal_slot_portion: None,
telemetry: telemetry.as_ref().map(|x| x.handle()),
};
let babe = sc_consensus_babe::start_babe(babe_config)?;
task_manager.spawn_essential_handle().spawn_blocking(
"babe-proposer",
Some("block-authoring"),
babe,
);
}
}
if enable_grandpa {
@ -748,39 +876,43 @@ where
};
// beefy is enabled if its notification service exists
if let Some(notification_service) = beefy_notification_service {
let justifications_protocol_name = beefy_on_demand_justifications_handler.protocol_name();
let network_params = sc_consensus_beefy::BeefyNetworkParams {
network: Arc::new(network.clone()),
sync: sync_service.clone(),
gossip_protocol_name: beefy_gossip_proto_name,
justifications_protocol_name,
notification_service,
_phantom: core::marker::PhantomData::<Block>,
};
let payload_provider = sp_consensus_beefy::mmr::MmrRootProvider::new(client.clone());
let beefy_params = sc_consensus_beefy::BeefyParams {
client: client.clone(),
backend: backend.clone(),
payload_provider,
runtime: client.clone(),
key_store: keystore_opt.clone(),
network_params,
min_block_delta: 8,
prometheus_registry: prometheus_registry.clone(),
links: beefy_voter_links,
on_demand_justifications_handler: beefy_on_demand_justifications_handler,
is_authority: role.is_authority(),
};
if sealing.is_none() {
if let Some(notification_service) = beefy_notification_service {
let justifications_protocol_name =
beefy_on_demand_justifications_handler.protocol_name();
let network_params = sc_consensus_beefy::BeefyNetworkParams {
network: Arc::new(network.clone()),
sync: sync_service.clone(),
gossip_protocol_name: beefy_gossip_proto_name,
justifications_protocol_name,
notification_service,
_phantom: core::marker::PhantomData::<Block>,
};
let payload_provider = sp_consensus_beefy::mmr::MmrRootProvider::new(client.clone());
let beefy_params = sc_consensus_beefy::BeefyParams {
client: client.clone(),
backend: backend.clone(),
payload_provider,
runtime: client.clone(),
key_store: keystore_opt.clone(),
network_params,
min_block_delta: 8,
prometheus_registry: prometheus_registry.clone(),
links: beefy_voter_links,
on_demand_justifications_handler: beefy_on_demand_justifications_handler,
is_authority: role.is_authority(),
};
let gadget =
sc_consensus_beefy::start_beefy_gadget::<_, _, _, _, _, _, _, BeefyId>(beefy_params);
let gadget = sc_consensus_beefy::start_beefy_gadget::<_, _, _, _, _, _, _, BeefyId>(
beefy_params,
);
// BEEFY is part of consensus, if it fails we'll bring the node down with it to make sure it
// is noticed.
task_manager
.spawn_essential_handle()
.spawn_blocking("beefy-gadget", None, gadget);
// BEEFY is part of consensus, if it fails we'll bring the node down with it to make
// sure it is noticed.
task_manager
.spawn_essential_handle()
.spawn_blocking("beefy-gadget", None, gadget);
}
}
if let Some(_) = provider_options {
@ -822,6 +954,7 @@ pub async fn new_full<
provider_options: Option<ProviderOptions>,
indexer_options: Option<IndexerOptions>,
fisherman_options: Option<FishermanOptions>,
sealing: Option<Sealing>,
) -> Result<TaskManager, ServiceError>
where
Runtime: shc_common::traits::StorageEnableRuntime<RuntimeApi = RuntimeApi>,
@ -840,6 +973,7 @@ where
Some(provider_options),
indexer_options,
fisherman_options,
sealing,
)
.await;
}
@ -850,6 +984,7 @@ where
Some(provider_options),
indexer_options,
fisherman_options,
sealing,
)
.await;
}
@ -860,6 +995,7 @@ where
Some(provider_options),
indexer_options,
fisherman_options,
sealing,
)
.await;
}
@ -870,6 +1006,7 @@ where
Some(provider_options),
indexer_options,
fisherman_options,
sealing,
)
.await;
}
@ -881,6 +1018,7 @@ where
None,
indexer_options,
fisherman_options,
sealing,
)
.await;
};

View file

@ -92,8 +92,14 @@ pub fn development_config_genesis() -> Value {
endowed_accounts.sort();
testnet_genesis(
// Alice is the only authority in Dev mode
vec![authority_keys_from_seed("Alice")],
// Alith is the only authority in Dev mode (using Alice's session keys)
vec![(
alith(),
get_from_seed::<BabeId>("Alice"),
get_from_seed::<GrandpaId>("Alice"),
get_from_seed::<ImOnlineId>("Alice"),
get_from_seed::<BeefyId>("Alice"),
)],
// Alith is Sudo
alith(),
// Endowed: Alice, Bob, Charlie, Dave, Eve, Ferdie,

1
test/.gitignore vendored
View file

@ -36,6 +36,7 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Test files
tmp/*
html/
# Local CLAUDE configuration
CLAUDE.local.md

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,53 @@
import type { GenericContext } from "@moonwall/cli";
/**
* Class allowing to store multiple value for a runtime constant based on the runtime version
*/
class RuntimeConstant<T> {
private values: { [version: number]: T };
/*
* Get the expected value for a given runtime version. Lookup for the closest smaller runtime
*/
get(runtimeVersion: number): T {
const versions = Object.keys(this.values).map(Number); // slow but easier to maintain
let value: T | undefined;
for (let i = 0; i < versions.length; i++) {
if (versions[i] > runtimeVersion) {
break;
}
value = this.values[versions[i]];
}
return value as T;
}
// Builds RuntimeConstant with single or multiple values
constructor(values: { [version: number]: T } | T) {
if (values instanceof Object) {
this.values = values;
} else {
this.values = { 0: values };
}
}
}
// Fees and gas limits
export const RUNTIME_CONSTANTS = {
"DATAHAVEN-STAGENET": {
GAS_LIMIT: new RuntimeConstant(60_000_000n)
},
"DATAHAVEN-MAINNET": {
GAS_LIMIT: new RuntimeConstant(60_000_000n)
},
"DATAHAVEN-TESTNET": {
GAS_LIMIT: new RuntimeConstant(60_000_000n)
}
};
type ConstantStoreType = (typeof RUNTIME_CONSTANTS)["DATAHAVEN-STAGENET"];
export function ConstantStore(context: GenericContext): ConstantStoreType {
const runtime = context.polkadotJs().consts.system.version.specName.toUpperCase();
console.log("runtime", runtime);
return RUNTIME_CONSTANTS[runtime];
}

View file

@ -0,0 +1 @@
export * from "./constants";

View file

@ -0,0 +1,92 @@
import { beforeAll, describeSuite, expect } from "@moonwall/cli";
import { ALITH_ADDRESS } from "@moonwall/util";
import { ConstantStore } from "../../helpers";
describeSuite({
id: "D010101",
title: "Block 1",
foundationMethods: "dev",
testCases: ({ context, it }) => {
let specVersion: number;
beforeAll(async () => {
await context.createBlock();
specVersion = (await context.polkadotJs().runtimeVersion.specVersion).toNumber();
});
it({
id: "T01",
title: "should be at block 1",
test: async () => {
expect(await context.viem().getBlockNumber()).to.equal(1n);
}
});
it({
id: "T02",
title: "should have valid timestamp after block production",
test: async () => {
// Seal a new block manually
await context.createBlock();
// Originally, this test required the timestamp be in the last five minutes.
// This requirement doesn't make sense when we forge timestamps in manual seal.
const block = await context.viem().getBlock({ blockTag: "latest" });
const next5Minutes = BigInt(Math.floor(Date.now() / 1000 + 300));
expect(block.timestamp).toBeGreaterThan(0n);
expect(block.timestamp).toBeLessThan(next5Minutes);
}
});
it({
id: "T03",
title: "should contain block information",
test: async () => {
const block = await context.viem().getBlock({ blockTag: "latest" });
expect(block).to.include({
author: ALITH_ADDRESS.toLocaleLowerCase(),
difficulty: 0n,
extraData: "0x",
gasLimit: ConstantStore(context).GAS_LIMIT.get(specVersion),
gasUsed: 0n,
logsBloom: `0x${"0".repeat(512)}`,
miner: ALITH_ADDRESS.toLocaleLowerCase(),
number: 2n,
receiptsRoot: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
sha3Uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
totalDifficulty: 0n,
transactionsRoot: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"
});
expect(block.transactions).to.be.a("array").empty;
expect(block.uncles).to.be.a("array").empty;
expect(block.nonce).to.be.eq("0x0000000000000000");
expect(block.hash).to.be.a("string").lengthOf(66);
expect(block.parentHash).to.be.a("string").lengthOf(66);
expect(block.timestamp).to.be.a("bigint");
}
});
it({
id: "T04",
title: "should be accessible by hash",
test: async () => {
const latestBlock = await context.viem().getBlock({ blockTag: "latest" });
if (!latestBlock.hash) {
throw new Error("Latest block hash is null");
}
const block = await context.viem().getBlock({ blockHash: latestBlock.hash });
expect(block.hash).toBe(latestBlock.hash);
}
});
it({
id: "T05",
title: "should be accessible by number",
test: async () => {
const latestBlock = await context.viem().getBlock({ blockTag: "latest" });
const block = await context.viem().getBlock({ blockNumber: latestBlock.number });
expect(block.hash).toBe(latestBlock.hash);
}
});
}
});

77
test/moonwall.config.json Normal file
View file

@ -0,0 +1,77 @@
{
"$schema": "https://raw.githubusercontent.com/Moonsong-Labs/moonwall/main/packages/types/config_schema.json",
"label": "DataHaven Tests 🔷",
"defaultTestTimeout": 120000,
"environments": [
{
"name": "dev_datahaven",
"testFileDir": [
"suites/dev"
],
"include": [
"**/*test*.ts"
],
"timeout": 180000,
"multiThreads": 1,
"envVars": [
"DEBUG_COLORS=1",
"RUST_BACKTRACE=1",
"RUST_LOG=info"
],
"reporters": [
"basic",
"html",
"json"
],
"reportFile": {
"json": "./tmp/testResults.json"
},
"foundation": {
"type": "dev",
"launchSpec": [
{
"name": "datahaven",
"binPath": "../operator/target/release/datahaven-node",
"newRpcBehaviour": true,
"options": [
"--dev",
"--no-telemetry",
"--unsafe-force-node-key-generation",
"--reserved-only",
"--no-grandpa",
"--no-prometheus",
"--sealing=manual"
],
"ports": {
"p2pPort": 30333,
"rpcPort": 9944
}
}
]
},
"connections": [
{
"name": "ethers",
"type": "ethers",
"endpoints": [
"ws://127.0.0.1:9944"
]
},
{
"name": "viem",
"type": "viem",
"endpoints": [
"ws://127.0.0.1:9944"
]
},
{
"name": "solo",
"type": "polkadotJs",
"endpoints": [
"ws://127.0.0.1:9944"
]
}
]
}
]
}

View file

@ -26,6 +26,8 @@
"stop:engine": "bun cli stop --kurtosisEngine --no-datahaven --no-relayer --no-enclave",
"test:e2e": "bun test ./suites --timeout 900000",
"test:e2e:parallel": "bun scripts/test-parallel.ts",
"moonwall:test": "moonwall test dev_datahaven",
"moonwall:run": "moonwall run dev_datahaven",
"typecheck": "tsc --noEmit",
"tsgo": "tsgo tsc --noEmit --pretty --skipLibCheck",
"postinstall": "papi"
@ -42,6 +44,8 @@
"@commander-js/extra-typings": "^13.1.0",
"@dotenvx/dotenvx": "^1.44.2",
"@inquirer/prompts": "^7.5.3",
"@moonwall/cli": "^5.15.0",
"@moonwall/util": "^5.15.0",
"@noble/curves": "^1.9.2",
"@noble/hashes": "^1.8.0",
"@polkadot-api/descriptors": "file:.papi/descriptors",