@moonwall/cli
Version:
Testing framework for the Moon family of projects
987 lines (981 loc) • 37.9 kB
JavaScript
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();
}
});
});