UNPKG

@moonwall/cli

Version:

Testing framework for the Moon family of projects

987 lines (981 loc) 37.9 kB
var __defProp = Object.defineProperty; var __getOwnPropNames = Object.getOwnPropertyNames; var __esm = (fn, res) => function __init() { return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; }; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; // src/internal/effect/ChopsticksService.ts import { Context, Data } from "effect"; var ChopsticksSetupError, ChopsticksBlockError, ChopsticksStorageError, ChopsticksExtrinsicError, ChopsticksXcmError, ChopsticksCleanupError, ChopsticksService, ChopsticksConfigTag; var init_ChopsticksService = __esm({ "src/internal/effect/ChopsticksService.ts"() { "use strict"; ChopsticksSetupError = class extends Data.TaggedError("ChopsticksSetupError") { }; ChopsticksBlockError = class extends Data.TaggedError("ChopsticksBlockError") { }; ChopsticksStorageError = class extends Data.TaggedError("ChopsticksStorageError") { }; ChopsticksExtrinsicError = class extends Data.TaggedError("ChopsticksExtrinsicError") { }; ChopsticksXcmError = class extends Data.TaggedError("ChopsticksXcmError") { }; ChopsticksCleanupError = class extends Data.TaggedError("ChopsticksCleanupError") { }; ChopsticksService = class extends Context.Tag("ChopsticksService")() { }; ChopsticksConfigTag = class extends Context.Tag("ChopsticksConfig")() { }; } }); // src/internal/effect/chopsticksConfigParser.ts import { Effect as Effect2 } from "effect"; import * as fs from "fs"; import * as yaml from "yaml"; import { createLogger } from "@moonwall/util"; var logger; var init_chopsticksConfigParser = __esm({ "src/internal/effect/chopsticksConfigParser.ts"() { "use strict"; init_ChopsticksService(); logger = createLogger({ name: "chopsticksConfigParser" }); } }); // src/internal/effect/launchChopsticksEffect.ts import { Effect as Effect3, Layer } from "effect"; import { createLogger as createLogger2 } from "@moonwall/util"; import * as fs2 from "fs"; import * as path from "path"; function createLogFile(port) { const dirPath = path.join(process.cwd(), "tmp", "node_logs"); if (!fs2.existsSync(dirPath)) { fs2.mkdirSync(dirPath, { recursive: true }); } const logPath = path.join(dirPath, `chopsticks_${port}_${Date.now()}.log`); const writeStream = fs2.createWriteStream(logPath); process.env.MOON_LOG_LOCATION = logPath; return { logPath, writeStream }; } function writeLog(stream, level, message) { const timestamp = (/* @__PURE__ */ new Date()).toISOString(); stream.write(`[${timestamp}] [${level.toUpperCase()}] ${message} `); } function setChopsticksLogStream(stream) { chopsticksLogStream = stream; } function hookLoggerToFile(pinoInstance, loggerName) { const wrapMethod = (level, originalMethod) => { return function(...args) { originalMethod.apply(this, args); if (chopsticksLogStream && args.length > 0) { const timestamp = (/* @__PURE__ */ new Date()).toISOString(); const message = typeof args[0] === "string" ? args[0] : JSON.stringify(args[0]); chopsticksLogStream.write( `[${timestamp}] [${level.toUpperCase()}] (${loggerName}) ${message} ` ); } }; }; const originalInfo = pinoInstance.info.bind(pinoInstance); const originalWarn = pinoInstance.warn.bind(pinoInstance); const originalError = pinoInstance.error.bind(pinoInstance); const originalDebug = pinoInstance.debug.bind(pinoInstance); pinoInstance.info = wrapMethod("info", originalInfo); pinoInstance.warn = wrapMethod("warn", originalWarn); pinoInstance.error = wrapMethod("error", originalError); pinoInstance.debug = wrapMethod("debug", originalDebug); const originalChild = pinoInstance.child?.bind(pinoInstance); if (originalChild) { pinoInstance.child = (bindings) => { const child = originalChild(bindings); const childName = bindings.name || bindings.child || loggerName; hookLoggerToFile(child, childName); return child; }; } } async function getChopsticksModules(logLevel = "inherit") { if (chopsticksModuleCache) { return chopsticksModuleCache; } const chopsticks = await import("@acala-network/chopsticks"); chopsticksModuleCache = { setupWithServer: chopsticks.setupWithServer, setStorage: chopsticks.setStorage, pinoLogger: chopsticks.pinoLogger, defaultLogger: chopsticks.defaultLogger }; const resolvedLevel = logLevel === "inherit" ? process.env.LOG_LEVEL || "info" : logLevel; chopsticksModuleCache.pinoLogger.level = resolvedLevel; hookLoggerToFile(chopsticksModuleCache.defaultLogger, "chopsticks"); chopsticksLoggerConfigured = true; logger2.debug(`Chopsticks internal logger level: ${resolvedLevel}`); return chopsticksModuleCache; } async function launchChopsticksEffect(config) { const startTime = Date.now(); logger2.debug(`[T+0ms] Starting chopsticks with endpoint: ${config.endpoint}`); const port = config.port ?? 8e3; const { logPath, writeStream } = createLogFile(port); setChopsticksLogStream(writeStream); const program = Effect3.gen(function* () { const chopsticksModules = yield* Effect3.promise(() => getChopsticksModules("inherit")); const args = prepareConfigForSetup(config); logger2.debug(`[T+${Date.now() - startTime}ms] Calling setupWithServer...`); const context = yield* Effect3.tryPromise({ try: () => chopsticksModules.setupWithServer(args), catch: (cause) => new ChopsticksSetupError({ cause, endpoint: getEndpointString(config.endpoint), block: config.block ?? void 0 }) }); const actualPort = Number.parseInt(context.addr.split(":")[1], 10); const chainName = yield* Effect3.promise(() => context.chain.api.getSystemChain()); logger2.info(`${chainName} RPC listening on ws://${context.addr}`); logger2.debug(`[T+${Date.now() - startTime}ms] Chopsticks started at ${context.addr}`); logger2.debug(`Log file: ${logPath}`); writeLog(writeStream, "info", `Chopsticks started for ${chainName}`); writeLog(writeStream, "info", `RPC listening on ws://${context.addr}`); writeLog(writeStream, "info", `Endpoint: ${config.endpoint}`); if (config.block) { writeLog(writeStream, "info", `Block: ${config.block}`); } const cleanup2 = Effect3.tryPromise({ try: async () => { logger2.debug("Closing chopsticks..."); writeLog(writeStream, "info", "Shutting down chopsticks..."); await context.close(); writeLog(writeStream, "info", "Chopsticks closed"); setChopsticksLogStream(null); writeStream.end(); logger2.debug("Chopsticks closed"); }, catch: (cause) => new ChopsticksCleanupError({ cause }) }).pipe( Effect3.catchAll( (error) => Effect3.sync(() => { logger2.error(`Failed to cleanly close chopsticks: ${error}`); writeLog(writeStream, "error", `Failed to close: ${error}`); setChopsticksLogStream(null); writeStream.end(); }) ) ); const serviceMethods = createServiceMethods(context.chain); const service2 = { chain: context.chain, addr: context.addr, port: actualPort, ...serviceMethods }; return { service: service2, cleanup: cleanup2 }; }); const { service, cleanup } = await Effect3.runPromise(program); return { result: service, cleanup: () => Effect3.runPromise(cleanup) }; } var chopsticksLoggerConfigured, chopsticksModuleCache, logger2, chopsticksLogStream, getEndpointString, prepareConfigForSetup, createServiceMethods, acquireChopsticks, ChopsticksServiceLive; var init_launchChopsticksEffect = __esm({ "src/internal/effect/launchChopsticksEffect.ts"() { "use strict"; init_ChopsticksService(); init_chopsticksConfigParser(); chopsticksLoggerConfigured = false; chopsticksModuleCache = null; logger2 = createLogger2({ name: "launchChopsticksEffect" }); chopsticksLogStream = null; getEndpointString = (endpoint) => { if (typeof endpoint === "string") return endpoint; if (Array.isArray(endpoint)) return endpoint[0]; return void 0; }; prepareConfigForSetup = (config) => ({ ...config, port: config.port ?? 8e3, host: config.host ?? "127.0.0.1", "build-block-mode": config["build-block-mode"] ?? "Manual" }); createServiceMethods = (chain) => ({ createBlock: (params) => Effect3.tryPromise({ try: async () => { const block = await chain.newBlock({ transactions: params?.transactions ?? [], upwardMessages: params?.ump ?? {}, downwardMessages: params?.dmp ?? [], horizontalMessages: params?.hrmp ?? {} }); return { block: { hash: block.hash, number: block.number } }; }, catch: (cause) => new ChopsticksBlockError({ cause, operation: "newBlock" }) }), setStorage: (params) => Effect3.tryPromise({ try: async () => { const storage = { [params.module]: { [params.method]: params.params } }; const modules = await getChopsticksModules(); await modules.setStorage(chain, storage); }, catch: (cause) => new ChopsticksStorageError({ cause, module: params.module, method: params.method }) }), submitExtrinsic: (extrinsic) => Effect3.tryPromise({ try: () => chain.submitExtrinsic(extrinsic), catch: (cause) => new ChopsticksExtrinsicError({ cause, operation: "submit", extrinsic }) }), dryRunExtrinsic: (extrinsic, at) => Effect3.tryPromise({ try: async () => { const result = await chain.dryRunExtrinsic(extrinsic, at); const isOk = result.outcome.isOk; return { success: isOk, storageDiff: result.storageDiff, error: isOk ? void 0 : result.outcome.asErr?.toString() }; }, catch: (cause) => new ChopsticksExtrinsicError({ cause, operation: "dryRun", extrinsic: typeof extrinsic === "string" ? extrinsic : extrinsic.call }) }), getBlock: (hashOrNumber) => Effect3.tryPromise({ try: async () => { const block = hashOrNumber === void 0 ? chain.head : typeof hashOrNumber === "number" ? await chain.getBlockAt(hashOrNumber) : await chain.getBlock(hashOrNumber); if (!block) return void 0; return { hash: block.hash, number: block.number }; }, catch: (cause) => new ChopsticksBlockError({ cause, operation: "getBlock", blockIdentifier: hashOrNumber }) }), setHead: (hashOrNumber) => Effect3.tryPromise({ try: async () => { const block = typeof hashOrNumber === "number" ? await chain.getBlockAt(hashOrNumber) : await chain.getBlock(hashOrNumber); if (!block) { throw new Error(`Block not found: ${hashOrNumber}`); } await chain.setHead(block); }, catch: (cause) => new ChopsticksBlockError({ cause, operation: "setHead", blockIdentifier: hashOrNumber }) }), submitUpwardMessages: (paraId, messages) => Effect3.try({ try: () => chain.submitUpwardMessages(paraId, messages), catch: (cause) => new ChopsticksXcmError({ cause, messageType: "ump", paraId }) }), submitDownwardMessages: (messages) => Effect3.try({ try: () => chain.submitDownwardMessages(messages), catch: (cause) => new ChopsticksXcmError({ cause, messageType: "dmp" }) }), submitHorizontalMessages: (paraId, messages) => Effect3.try({ try: () => chain.submitHorizontalMessages(paraId, messages), catch: (cause) => new ChopsticksXcmError({ cause, messageType: "hrmp", paraId }) }) }); acquireChopsticks = (config) => Effect3.acquireRelease( // Acquire: Setup chopsticks (first get modules, then setup) Effect3.promise(() => getChopsticksModules("silent")).pipe( Effect3.flatMap( (modules) => Effect3.tryPromise({ try: () => modules.setupWithServer(prepareConfigForSetup(config)), catch: (cause) => new ChopsticksSetupError({ cause, endpoint: getEndpointString(config.endpoint), block: config.block ?? void 0 }) }) ), Effect3.tap( (context) => Effect3.sync(() => logger2.debug(`Chopsticks started at ${context.addr}`)) ) ), // Release: Cleanup chopsticks (context) => Effect3.tryPromise({ try: async () => { logger2.debug("Closing chopsticks..."); await context.close(); logger2.debug("Chopsticks closed"); }, catch: (cause) => new ChopsticksCleanupError({ cause }) }).pipe( Effect3.catchAll( (error) => Effect3.sync(() => { logger2.error(`Failed to cleanly close chopsticks: ${error}`); }) ) ) ); ChopsticksServiceLive = Layer.scoped( ChopsticksService, Effect3.gen(function* () { const config = yield* ChopsticksConfigTag; const context = yield* acquireChopsticks(config); const port = Number.parseInt(context.addr.split(":")[1], 10); const serviceMethods = createServiceMethods(context.chain); return { chain: context.chain, addr: context.addr, port, ...serviceMethods }; }) ); } }); // src/internal/effect/ChopsticksMultiChain.ts var ChopsticksMultiChain_exports = {}; __export(ChopsticksMultiChain_exports, { ChopsticksMultiChainLayer: () => ChopsticksMultiChainLayer, ChopsticksMultiChainService: () => ChopsticksMultiChainService, ChopsticksOrchestrationError: () => ChopsticksOrchestrationError, createKusamaMoonriverConfig: () => createKusamaMoonriverConfig, createPolkadotMoonbeamConfig: () => createPolkadotMoonbeamConfig, launchMultiChainEffect: () => launchMultiChainEffect }); import { Context as Context2, Data as Data2, Effect as Effect4, Layer as Layer2 } from "effect"; import { createLogger as createLogger3 } from "@moonwall/util"; async function launchMultiChainEffect(config) { const cleanups = []; try { logger3.debug(`Launching relay chain on port ${config.relay.port}`); const relayResult = await launchChopsticksEffect(config.relay); cleanups.push(relayResult.cleanup); const parachains = /* @__PURE__ */ new Map(); for (const paraConfig of config.parachains) { logger3.debug(`Launching parachain ${paraConfig.paraId} on port ${paraConfig.port}`); const paraResult = await launchChopsticksEffect(paraConfig); cleanups.push(paraResult.cleanup); parachains.set(paraConfig.paraId, paraResult.result); } const service = createMultiChainService(relayResult.result, parachains); return { service, cleanup: async () => { logger3.debug("Cleaning up multi-chain setup..."); for (const cleanupFn of cleanups.reverse()) { await cleanupFn(); } logger3.debug("Multi-chain cleanup complete"); } }; } catch (error) { for (const cleanupFn of cleanups.reverse()) { try { await cleanupFn(); } catch { } } throw error; } } var logger3, getEndpointString2, ChopsticksOrchestrationError, ChopsticksMultiChainService, createMultiChainService, ChopsticksMultiChainLayer, createPolkadotMoonbeamConfig, createKusamaMoonriverConfig; var init_ChopsticksMultiChain = __esm({ "src/internal/effect/ChopsticksMultiChain.ts"() { "use strict"; init_ChopsticksService(); init_launchChopsticksEffect(); logger3 = createLogger3({ name: "ChopsticksMultiChain" }); getEndpointString2 = (endpoint) => { if (typeof endpoint === "string") return endpoint; if (Array.isArray(endpoint)) return endpoint[0]; return void 0; }; ChopsticksOrchestrationError = class extends Data2.TaggedError("ChopsticksOrchestrationError") { }; ChopsticksMultiChainService = class extends Context2.Tag("ChopsticksMultiChainService")() { }; createMultiChainService = (relay, parachains) => { const chains = /* @__PURE__ */ new Map(); chains.set("relay", { service: relay, type: "relay" }); for (const [paraId, service] of parachains) { chains.set(`para-${paraId}`, { service, type: "parachain", paraId }); } return { relay, parachain: (paraId) => parachains.get(paraId), chains, createBlocksAll: () => Effect4.gen(function* () { const results = /* @__PURE__ */ new Map(); const blockEffects = Array.from(chains.entries()).map( ([id, chain]) => chain.service.createBlock().pipe(Effect4.map((result) => [id, result])) ); const blockResults = yield* Effect4.all(blockEffects, { concurrency: "unbounded" }); for (const [id, result] of blockResults) { results.set(id, result); } return results; }), sendUmp: (paraId, messages) => Effect4.gen(function* () { yield* relay.submitUpwardMessages(paraId, messages); logger3.debug(`Sent ${messages.length} UMP messages from para ${paraId} to relay`); }), sendDmp: (paraId, messages) => Effect4.gen(function* () { const para = parachains.get(paraId); if (!para) { yield* Effect4.fail( new ChopsticksXcmError({ cause: new Error(`Parachain ${paraId} not found`), messageType: "dmp", paraId }) ); return; } yield* para.submitDownwardMessages(messages); logger3.debug(`Sent ${messages.length} DMP messages from relay to para ${paraId}`); }), sendHrmp: (fromParaId, toParaId, messages) => Effect4.gen(function* () { const toPara = parachains.get(toParaId); if (!toPara) { yield* Effect4.fail( new ChopsticksXcmError({ cause: new Error(`Target parachain ${toParaId} not found`), messageType: "hrmp", paraId: toParaId }) ); return; } yield* toPara.submitHorizontalMessages(fromParaId, messages); logger3.debug( `Sent ${messages.length} HRMP messages from para ${fromParaId} to para ${toParaId}` ); }), processXcm: () => Effect4.gen(function* () { yield* relay.createBlock(); for (const para of parachains.values()) { yield* para.createBlock(); } logger3.debug("Processed XCM messages across all chains"); }) }; }; ChopsticksMultiChainLayer = (config) => Layer2.scoped( ChopsticksMultiChainService, Effect4.gen(function* () { const cleanups = []; const relayResult = yield* Effect4.tryPromise({ try: () => launchChopsticksEffect(config.relay), catch: (cause) => new ChopsticksSetupError({ cause, endpoint: getEndpointString2(config.relay.endpoint) }) }); cleanups.push( Effect4.tryPromise({ try: () => relayResult.cleanup(), catch: () => void 0 }).pipe(Effect4.ignore) ); const parachains = /* @__PURE__ */ new Map(); for (const paraConfig of config.parachains) { const paraResult = yield* Effect4.tryPromise({ try: () => launchChopsticksEffect(paraConfig), catch: (cause) => new ChopsticksSetupError({ cause, endpoint: getEndpointString2(paraConfig.endpoint) }) }); cleanups.push( Effect4.tryPromise({ try: () => paraResult.cleanup(), catch: () => void 0 }).pipe(Effect4.ignore) ); parachains.set(paraConfig.paraId, paraResult.result); } yield* Effect4.addFinalizer( () => Effect4.gen(function* () { logger3.debug("Finalizing multi-chain setup..."); for (const cleanup of cleanups.reverse()) { yield* cleanup; } logger3.debug("Multi-chain finalization complete"); }) ); return createMultiChainService(relayResult.result, parachains); }) ); createPolkadotMoonbeamConfig = (relayPort = 8e3, moonbeamPort = 8001) => ({ relay: { type: "relay", endpoint: "wss://rpc.polkadot.io", port: relayPort, "build-block-mode": "Manual" }, parachains: [ { type: "parachain", paraId: 2004, // Moonbeam on Polkadot endpoint: "wss://wss.api.moonbeam.network", port: moonbeamPort, "build-block-mode": "Manual" } ] }); createKusamaMoonriverConfig = (relayPort = 8e3, moonriverPort = 8001) => ({ relay: { type: "relay", endpoint: "wss://kusama-rpc.polkadot.io", port: relayPort, "build-block-mode": "Manual" }, parachains: [ { type: "parachain", paraId: 2023, // Moonriver on Kusama endpoint: "wss://wss.api.moonriver.moonbeam.network", port: moonriverPort, "build-block-mode": "Manual" } ] }); } }); // src/internal/effect/__tests__/ChopsticksMultiChain.test.ts init_ChopsticksMultiChain(); init_ChopsticksService(); import { describe, it, expect } from "vitest"; import { Effect as Effect5, Layer as Layer3 } from "effect"; import { BuildBlockMode } from "@acala-network/chopsticks"; describe("ChopsticksMultiChain - Phase 4: Multi-chain XCM Support", () => { describe("Module Exports", () => { it("should export ChopsticksOrchestrationError", async () => { const module = await Promise.resolve().then(() => (init_ChopsticksMultiChain(), ChopsticksMultiChain_exports)); expect(module.ChopsticksOrchestrationError).toBeDefined(); }); it("should export ChopsticksMultiChainService tag", async () => { const module = await Promise.resolve().then(() => (init_ChopsticksMultiChain(), ChopsticksMultiChain_exports)); expect(module.ChopsticksMultiChainService).toBeDefined(); expect(module.ChopsticksMultiChainService.key).toBe("ChopsticksMultiChainService"); }); it("should export launchMultiChainEffect function", async () => { const module = await Promise.resolve().then(() => (init_ChopsticksMultiChain(), ChopsticksMultiChain_exports)); expect(module.launchMultiChainEffect).toBeDefined(); expect(typeof module.launchMultiChainEffect).toBe("function"); }); it("should export ChopsticksMultiChainLayer function", async () => { const module = await Promise.resolve().then(() => (init_ChopsticksMultiChain(), ChopsticksMultiChain_exports)); expect(module.ChopsticksMultiChainLayer).toBeDefined(); expect(typeof module.ChopsticksMultiChainLayer).toBe("function"); }); it("should export helper config functions", async () => { const module = await Promise.resolve().then(() => (init_ChopsticksMultiChain(), ChopsticksMultiChain_exports)); expect(module.createPolkadotMoonbeamConfig).toBeDefined(); expect(module.createKusamaMoonriverConfig).toBeDefined(); }); }); describe("ChopsticksOrchestrationError", () => { it("should create error with correct tag and properties", () => { const error = new ChopsticksOrchestrationError({ cause: new Error("Setup failed"), chains: ["relay", "para-2000"], operation: "setup" }); expect(error._tag).toBe("ChopsticksOrchestrationError"); expect(error.chains).toEqual(["relay", "para-2000"]); expect(error.operation).toBe("setup"); }); it("should allow pattern matching with catchTag", async () => { const program = Effect5.gen(function* () { yield* Effect5.fail( new ChopsticksOrchestrationError({ cause: new Error("XCM failed"), chains: ["relay"], operation: "xcm" }) ); return "success"; }).pipe( Effect5.catchTag( "ChopsticksOrchestrationError", (error) => Effect5.succeed(`Caught: ${error.operation} on ${error.chains.join(", ")}`) ) ); const result = await Effect5.runPromise(program); expect(result).toBe("Caught: xcm on relay"); }); }); describe("Configuration Types", () => { it("should create valid RelayChainConfig with kebab-case keys", () => { const config = { type: "relay", endpoint: "wss://rpc.polkadot.io", port: 8e3, "build-block-mode": BuildBlockMode.Manual }; expect(config.type).toBe("relay"); expect(config.endpoint).toBe("wss://rpc.polkadot.io"); }); it("should create valid ParachainConfig with kebab-case keys", () => { const config = { type: "parachain", paraId: 2e3, endpoint: "wss://moonbeam.rpc.io", port: 8001, "build-block-mode": BuildBlockMode.Manual }; expect(config.type).toBe("parachain"); expect(config.paraId).toBe(2e3); }); it("should create valid MultiChainConfig with kebab-case keys", () => { const config = { relay: { type: "relay", endpoint: "wss://rpc.polkadot.io", port: 8e3, "build-block-mode": BuildBlockMode.Manual }, parachains: [ { type: "parachain", paraId: 2e3, endpoint: "wss://moonbeam.rpc.io", port: 8001, "build-block-mode": BuildBlockMode.Manual }, { type: "parachain", paraId: 2001, endpoint: "wss://acala.rpc.io", port: 8002, "build-block-mode": BuildBlockMode.Manual } ] }; expect(config.relay.type).toBe("relay"); expect(config.parachains).toHaveLength(2); expect(config.parachains[0].paraId).toBe(2e3); expect(config.parachains[1].paraId).toBe(2001); }); }); describe("Helper Config Functions", () => { it("should create Polkadot + Moonbeam config with defaults", () => { const config = createPolkadotMoonbeamConfig(); expect(config.relay.type).toBe("relay"); expect(config.relay.endpoint).toBe("wss://rpc.polkadot.io"); expect(config.relay.port).toBe(8e3); expect(config.parachains).toHaveLength(1); expect(config.parachains[0].paraId).toBe(2004); expect(config.parachains[0].port).toBe(8001); }); it("should create Polkadot + Moonbeam config with custom ports", () => { const config = createPolkadotMoonbeamConfig(9e3, 9001); expect(config.relay.port).toBe(9e3); expect(config.parachains[0].port).toBe(9001); }); it("should create Kusama + Moonriver config with defaults", () => { const config = createKusamaMoonriverConfig(); expect(config.relay.type).toBe("relay"); expect(config.relay.endpoint).toBe("wss://kusama-rpc.polkadot.io"); expect(config.relay.port).toBe(8e3); expect(config.parachains).toHaveLength(1); expect(config.parachains[0].paraId).toBe(2023); expect(config.parachains[0].port).toBe(8001); }); it("should create Kusama + Moonriver config with custom ports", () => { const config = createKusamaMoonriverConfig(9e3, 9001); expect(config.relay.port).toBe(9e3); expect(config.parachains[0].port).toBe(9001); }); }); describe("MultiChainService Interface", () => { it("should define all required methods", () => { const mockService = { relay: {}, parachain: () => void 0, chains: /* @__PURE__ */ new Map(), createBlocksAll: () => Effect5.succeed(/* @__PURE__ */ new Map()), sendUmp: () => Effect5.void, sendDmp: () => Effect5.void, sendHrmp: () => Effect5.void, processXcm: () => Effect5.void }; expect(mockService.relay).toBeDefined(); expect(typeof mockService.parachain).toBe("function"); expect(mockService.chains).toBeDefined(); expect(typeof mockService.createBlocksAll).toBe("function"); expect(typeof mockService.sendUmp).toBe("function"); expect(typeof mockService.sendDmp).toBe("function"); expect(typeof mockService.sendHrmp).toBe("function"); expect(typeof mockService.processXcm).toBe("function"); }); it("should have sendUmp return Effect with correct error type", async () => { const mockService = { relay: {}, parachain: () => void 0, chains: /* @__PURE__ */ new Map(), createBlocksAll: () => Effect5.succeed(/* @__PURE__ */ new Map()), sendUmp: (paraId, messages) => Effect5.fail( new ChopsticksXcmError({ cause: new Error("UMP failed"), messageType: "ump", paraId }) ), sendDmp: () => Effect5.void, sendHrmp: () => Effect5.void, processXcm: () => Effect5.void }; const result = await Effect5.runPromise( mockService.sendUmp(2e3, ["0x1234"]).pipe( Effect5.catchTag( "ChopsticksXcmError", (error) => Effect5.succeed({ caught: true, type: error.messageType }) ) ) ); expect(result).toEqual({ caught: true, type: "ump" }); }); it("should have sendDmp return Effect with correct error type", async () => { const mockService = { relay: {}, parachain: () => void 0, chains: /* @__PURE__ */ new Map(), createBlocksAll: () => Effect5.succeed(/* @__PURE__ */ new Map()), sendUmp: () => Effect5.void, sendDmp: (paraId, messages) => Effect5.fail( new ChopsticksXcmError({ cause: new Error("DMP failed"), messageType: "dmp", paraId }) ), sendHrmp: () => Effect5.void, processXcm: () => Effect5.void }; const result = await Effect5.runPromise( mockService.sendDmp(2e3, [{ sentAt: 1, msg: "0x1234" }]).pipe( Effect5.catchTag( "ChopsticksXcmError", (error) => Effect5.succeed({ caught: true, type: error.messageType }) ) ) ); expect(result).toEqual({ caught: true, type: "dmp" }); }); it("should have sendHrmp return Effect with correct error type", async () => { const mockService = { relay: {}, parachain: () => void 0, chains: /* @__PURE__ */ new Map(), createBlocksAll: () => Effect5.succeed(/* @__PURE__ */ new Map()), sendUmp: () => Effect5.void, sendDmp: () => Effect5.void, sendHrmp: (fromParaId, toParaId, messages) => Effect5.fail( new ChopsticksXcmError({ cause: new Error("HRMP failed"), messageType: "hrmp", paraId: toParaId }) ), processXcm: () => Effect5.void }; const result = await Effect5.runPromise( mockService.sendHrmp(2e3, 2001, [{ sentAt: 1, data: "0x1234" }]).pipe( Effect5.catchTag( "ChopsticksXcmError", (error) => Effect5.succeed({ caught: true, type: error.messageType }) ) ) ); expect(result).toEqual({ caught: true, type: "hrmp" }); }); it("should have createBlocksAll return Effect with correct error type", async () => { const mockService = { relay: {}, parachain: () => void 0, chains: /* @__PURE__ */ new Map(), createBlocksAll: () => Effect5.fail( new ChopsticksBlockError({ cause: new Error("Block creation failed"), operation: "newBlock" }) ), sendUmp: () => Effect5.void, sendDmp: () => Effect5.void, sendHrmp: () => Effect5.void, processXcm: () => Effect5.void }; const result = await Effect5.runPromise( mockService.createBlocksAll().pipe( Effect5.catchTag( "ChopsticksBlockError", (error) => Effect5.succeed({ caught: true, op: error.operation }) ) ) ); expect(result).toEqual({ caught: true, op: "newBlock" }); }); }); describe("ChopsticksMultiChainService Tag", () => { it("should have correct service key", () => { expect(ChopsticksMultiChainService.key).toBe("ChopsticksMultiChainService"); }); it("should allow providing mock service via Layer", async () => { const mockService = { relay: { addr: "127.0.0.1:8000" }, parachain: () => void 0, chains: /* @__PURE__ */ new Map(), createBlocksAll: () => Effect5.succeed(/* @__PURE__ */ new Map()), sendUmp: () => Effect5.void, sendDmp: () => Effect5.void, sendHrmp: () => Effect5.void, processXcm: () => Effect5.void }; const mockLayer = Layer3.succeed(ChopsticksMultiChainService, mockService); const program = Effect5.gen(function* () { const service = yield* ChopsticksMultiChainService; return service.relay.addr; }).pipe(Effect5.provide(mockLayer)); const result = await Effect5.runPromise(program); expect(result).toBe("127.0.0.1:8000"); }); }); describe("ChopsticksMultiChainLayer Type", () => { it("should create a Layer when called with config", async () => { const { ChopsticksMultiChainLayer: ChopsticksMultiChainLayer2 } = await Promise.resolve().then(() => (init_ChopsticksMultiChain(), ChopsticksMultiChain_exports)); const config = { relay: { type: "relay", endpoint: "wss://test.io", port: 8e3, "build-block-mode": BuildBlockMode.Manual }, parachains: [ { type: "parachain", paraId: 2e3, endpoint: "wss://para.test.io", port: 8001, "build-block-mode": BuildBlockMode.Manual } ] }; const layer = ChopsticksMultiChainLayer2(config); expect(layer).toBeDefined(); }); it("should be providable to programs using ChopsticksMultiChainService", async () => { const { ChopsticksMultiChainLayer: ChopsticksMultiChainLayer2, ChopsticksMultiChainService: ChopsticksMultiChainService2 } = await Promise.resolve().then(() => (init_ChopsticksMultiChain(), ChopsticksMultiChain_exports)); const config = { relay: { type: "relay", endpoint: "wss://test.io", port: 8e3, "build-block-mode": BuildBlockMode.Manual }, parachains: [] }; const layer = ChopsticksMultiChainLayer2(config); const program = Effect5.gen(function* () { const service = yield* ChopsticksMultiChainService2; return service.relay; }).pipe(Effect5.provide(layer)); expect(typeof program.pipe).toBe("function"); }); }); describe("XCM Message Flow Types", () => { it("should support UMP message format (HexString[])", () => { const umpMessages = ["0x1234", "0x5678"]; expect(umpMessages).toHaveLength(2); }); it("should support DMP message format", () => { const dmpMessages = [ { sentAt: 100, msg: "0x1234" }, { sentAt: 101, msg: "0x5678" } ]; expect(dmpMessages).toHaveLength(2); expect(dmpMessages[0].sentAt).toBe(100); }); it("should support HRMP message format", () => { const hrmpMessages = [ { sentAt: 100, data: "0x1234" }, { sentAt: 101, data: "0x5678" } ]; expect(hrmpMessages).toHaveLength(2); expect(hrmpMessages[0].data).toBe("0x1234"); }); }); }); describe.skip("ChopsticksMultiChain - Integration Tests", () => { it("should launch multi-chain setup with relay and parachains", async () => { const { launchMultiChainEffect: launchMultiChainEffect2 } = await Promise.resolve().then(() => (init_ChopsticksMultiChain(), ChopsticksMultiChain_exports)); const { service, cleanup } = await launchMultiChainEffect2({ relay: { type: "relay", endpoint: "wss://rpc.polkadot.io", port: 8e3, "build-block-mode": BuildBlockMode.Manual }, parachains: [ { type: "parachain", paraId: 2004, endpoint: "wss://wss.api.moonbeam.network", port: 8001, "build-block-mode": BuildBlockMode.Manual } ] }); try { expect(service.relay).toBeDefined(); expect(service.parachain(2004)).toBeDefined(); expect(service.chains.size).toBe(2); } finally { await cleanup(); } }); it("should send UMP from parachain to relay", async () => { const { launchMultiChainEffect: launchMultiChainEffect2 } = await Promise.resolve().then(() => (init_ChopsticksMultiChain(), ChopsticksMultiChain_exports)); const { service, cleanup } = await launchMultiChainEffect2(createPolkadotMoonbeamConfig()); try { await Effect5.runPromise(service.sendUmp(2004, ["0x1234"])); await Effect5.runPromise(service.processXcm()); } finally { await cleanup(); } }); });