UNPKG

create-midnight-app

Version:

🌙 Scaffold for building Midnight smart contracts with dynamic type detection and automated CLI generation

203 lines (183 loc) • 7.06 kB
// This file is part of midnightntwrk/example-counter. // Copyright (C) 2025 Midnight Foundation // SPDX-License-Identifier: Apache-2.0 // Licensed under the Apache License, Version 2.0 (the "License"); // You may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { type Config, StandaloneConfig, currentDir, TestnetRemoteConfig } from '../config'; import { DockerComposeEnvironment, GenericContainer, type StartedDockerComposeEnvironment, type StartedTestContainer, Wait, } from 'testcontainers'; import path from 'path'; import * as api from '../api'; import * as Rx from 'rxjs'; import { nativeToken } from '@midnight-ntwrk/ledger'; import type { Logger } from 'pino'; import type { Wallet } from '@midnight-ntwrk/wallet-api'; import type { Resource } from '@midnight-ntwrk/wallet'; import { expect } from 'vitest'; const GENESIS_MINT_WALLET_SEED = '0000000000000000000000000000000000000000000000000000000000000001'; export interface TestConfiguration { seed: string; entrypoint: string; dappConfig: Config; psMode: string; cacheFileName: string; } export class LocalTestConfig implements TestConfiguration { seed = GENESIS_MINT_WALLET_SEED; entrypoint = 'dist/standalone.js'; psMode = 'undeployed'; cacheFileName = ''; dappConfig = new StandaloneConfig(); } export function parseArgs(required: string[]): TestConfiguration { let entry = ''; if (required.includes('entry')) { if (process.env.TEST_ENTRYPOINT !== undefined) { entry = process.env.TEST_ENTRYPOINT; } else { throw new Error('TEST_ENTRYPOINT environment variable is not defined.'); } } let seed = ''; if (required.includes('seed')) { if (process.env.TEST_WALLET_SEED !== undefined) { seed = process.env.TEST_WALLET_SEED; } else { throw new Error('TEST_WALLET_SEED environment variable is not defined.'); } } let cfg: Config = new TestnetRemoteConfig(); let env = ''; let psMode = 'undeployed'; let cacheFileName = ''; if (required.includes('env')) { if (process.env.TEST_ENV !== undefined) { env = process.env.TEST_ENV; } else { throw new Error('TEST_ENV environment variable is not defined.'); } switch (env) { case 'testnet': cfg = new TestnetRemoteConfig(); psMode = 'testnet'; cacheFileName = `${seed.substring(0, 7)}-${psMode}.state`; break; default: throw new Error(`Unknown env value=${env}`); } } return { seed, entrypoint: entry, dappConfig: cfg, psMode, cacheFileName, }; } export class TestEnvironment { private readonly logger: Logger; private env: StartedDockerComposeEnvironment | undefined; private dockerEnv: DockerComposeEnvironment | undefined; private container: StartedTestContainer | undefined; private wallet: (Wallet & Resource) | undefined; private testConfig: TestConfiguration; constructor(logger: Logger) { this.logger = logger; this.testConfig = new LocalTestConfig(); } start = async (): Promise<TestConfiguration> => { if (process.env.RUN_ENV_TESTS === 'true') { this.testConfig = parseArgs(['seed', 'env']); this.logger.info(`Test wallet seed: ${this.testConfig.seed}`); this.logger.info('Proof server starting...'); this.container = await TestEnvironment.getProofServerContainer(this.testConfig.psMode); this.testConfig.dappConfig = { ...this.testConfig.dappConfig, proofServer: `http://${this.container.getHost()}:${this.container.getMappedPort(6300).toString()}`, }; } else { this.testConfig = new LocalTestConfig(); this.logger.info('Test containers starting...'); const composeFile = process.env.COMPOSE_FILE ?? 'standalone.yml'; this.logger.info(`Using compose file: ${composeFile}`); this.dockerEnv = new DockerComposeEnvironment(path.resolve(currentDir, '..'), composeFile) .withWaitStrategy( 'counter-proof-server', Wait.forLogMessage('Actix runtime found; starting in Actix runtime', 1), ) .withWaitStrategy('counter-indexer', Wait.forLogMessage(/starting indexing/, 1)); this.env = await this.dockerEnv.up(); this.testConfig.dappConfig = { ...this.testConfig.dappConfig, indexer: TestEnvironment.mapContainerPort(this.env, this.testConfig.dappConfig.indexer, 'counter-indexer'), indexerWS: TestEnvironment.mapContainerPort(this.env, this.testConfig.dappConfig.indexerWS, 'counter-indexer'), node: TestEnvironment.mapContainerPort(this.env, this.testConfig.dappConfig.node, 'counter-node'), proofServer: TestEnvironment.mapContainerPort( this.env, this.testConfig.dappConfig.proofServer, 'counter-proof-server', ), }; } this.logger.info(`Configuration:${JSON.stringify(this.testConfig)}`); this.logger.info('Test containers started'); return this.testConfig; }; static mapContainerPort = (env: StartedDockerComposeEnvironment, url: string, containerName: string) => { const mappedUrl = new URL(url); const container = env.getContainer(containerName); mappedUrl.port = String(container.getFirstMappedPort()); return mappedUrl.toString().replace(/\/+$/, ''); }; static getProofServerContainer = async (env: string) => await new GenericContainer('midnightnetwork/proof-server:4.0.0') .withExposedPorts(6300) .withCommand([`midnight-proof-server --network ${env}`]) .withEnvironment({ RUST_BACKTRACE: 'full' }) .withWaitStrategy(Wait.forLogMessage('Actix runtime found; starting in Actix runtime', 1)) .start(); shutdown = async () => { if (this.wallet !== undefined) { await this.wallet.close(); } if (this.env !== undefined) { this.logger.info('Test containers closing'); await this.env.down(); } if (this.container !== undefined) { this.logger.info('Test container closing'); await this.container.stop(); } }; getWallet = async () => { this.logger.info('Setting up wallet'); this.wallet = await api.buildWalletAndWaitForFunds( this.testConfig.dappConfig, this.testConfig.seed, this.testConfig.cacheFileName, ); expect(this.wallet).not.toBeNull(); const state = await Rx.firstValueFrom(this.wallet.state()); expect(state.balances[nativeToken()].valueOf()).toBeGreaterThan(BigInt(0)); return this.wallet; }; saveWalletCache = async () => { if (this.wallet !== undefined) { await api.saveState(this.wallet, this.testConfig.cacheFileName); } }; }