@moonwall/cli
Version:
Testing framework for the Moon family of projects
1,067 lines (1,062 loc) • 43.4 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
var ChopsticksService_exports = {};
__export(ChopsticksService_exports, {
ChopsticksBlockError: () => ChopsticksBlockError,
ChopsticksCleanupError: () => ChopsticksCleanupError,
ChopsticksConfigTag: () => ChopsticksConfigTag,
ChopsticksExtrinsicError: () => ChopsticksExtrinsicError,
ChopsticksService: () => ChopsticksService,
ChopsticksSetupError: () => ChopsticksSetupError,
ChopsticksStorageError: () => ChopsticksStorageError,
ChopsticksXcmError: () => ChopsticksXcmError
});
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 BuildBlockModeValues, logger, resolveEnvVars, resolveEnvVarsDeep, parseBuildBlockMode, parseBlockField, parseChopsticksConfigFile;
var init_chopsticksConfigParser = __esm({
"src/internal/effect/chopsticksConfigParser.ts"() {
"use strict";
init_ChopsticksService();
BuildBlockModeValues = {
Batch: "Batch",
Manual: "Manual",
Instant: "Instant"
};
logger = createLogger({ name: "chopsticksConfigParser" });
resolveEnvVars = (value) => {
return value.replace(/\$\{env\.([^}]+)\}/g, (_, varName) => {
const envValue = process.env[varName];
if (envValue === void 0) {
logger.warn(`Environment variable ${varName} is not set`);
return "";
}
return envValue;
});
};
resolveEnvVarsDeep = (value) => {
if (typeof value === "string") {
return resolveEnvVars(value);
}
if (Array.isArray(value)) {
return value.map(resolveEnvVarsDeep);
}
if (value !== null && typeof value === "object") {
const result = {};
for (const [key, val] of Object.entries(value)) {
result[key] = resolveEnvVarsDeep(val);
}
return result;
}
return value;
};
parseBuildBlockMode = (mode) => {
switch (mode?.toLowerCase()) {
case "batch":
return BuildBlockModeValues.Batch;
case "instant":
return BuildBlockModeValues.Instant;
case "manual":
default:
return BuildBlockModeValues.Manual;
}
};
parseBlockField = (block) => {
if (block === void 0 || block === null) {
return block;
}
if (typeof block === "number") {
return block;
}
if (block === "") {
return void 0;
}
const blockNum = Number(block);
if (!Number.isNaN(blockNum)) {
return blockNum;
}
return block;
};
parseChopsticksConfigFile = (configPath, overrides) => Effect2.gen(function* () {
const fileContent = yield* Effect2.tryPromise({
try: async () => {
const content = await fs.promises.readFile(configPath, "utf-8");
return content;
},
catch: (cause) => new ChopsticksSetupError({
cause,
endpoint: `file://${configPath}`
})
});
const rawConfigUnresolved = yield* Effect2.try({
try: () => yaml.parse(fileContent),
catch: (cause) => new ChopsticksSetupError({
cause: new Error(`Failed to parse YAML config: ${cause}`),
endpoint: `file://${configPath}`
})
});
const rawConfig = resolveEnvVarsDeep(rawConfigUnresolved);
const rawEndpoint = rawConfig.endpoint;
const endpoint = typeof rawEndpoint === "string" ? rawEndpoint : Array.isArray(rawEndpoint) ? rawEndpoint[0] : "";
if (!endpoint) {
return yield* Effect2.fail(
new ChopsticksSetupError({
cause: new Error(
`Endpoint is required but not configured. Check that the environment variable in your chopsticks config is set. Raw value: "${rawConfigUnresolved.endpoint ?? ""}"`
),
endpoint: String(rawConfigUnresolved.endpoint) || "undefined"
})
);
}
if (!endpoint.startsWith("ws://") && !endpoint.startsWith("wss://")) {
return yield* Effect2.fail(
new ChopsticksSetupError({
cause: new Error(
`Invalid endpoint format: "${endpoint}" - must start with ws:// or wss://`
),
endpoint
})
);
}
const block = parseBlockField(rawConfig.block);
const rawBuildBlockMode = rawConfig["build-block-mode"];
const buildBlockMode = overrides?.buildBlockMode !== void 0 ? parseBuildBlockMode(overrides.buildBlockMode) : rawBuildBlockMode !== void 0 ? parseBuildBlockMode(rawBuildBlockMode) : BuildBlockModeValues.Manual;
const finalPort = overrides?.port ?? rawConfig.port ?? 8e3;
logger.debug(`Parsed chopsticks config from ${configPath}`);
logger.debug(` endpoint: ${endpoint}`);
logger.debug(` port: ${finalPort}`);
const config = {
...rawConfig,
block,
"build-block-mode": buildBlockMode,
port: finalPort,
...overrides?.host !== void 0 && { host: overrides.host },
...overrides?.wasmOverride !== void 0 && { "wasm-override": overrides.wasmOverride },
...overrides?.allowUnresolvedImports !== void 0 && {
"allow-unresolved-imports": overrides.allowUnresolvedImports
}
};
return config;
});
}
});
// src/internal/effect/launchChopsticksEffect.ts
var launchChopsticksEffect_exports = {};
__export(launchChopsticksEffect_exports, {
BuildBlockModeValues: () => BuildBlockModeValues2,
ChopsticksServiceLayer: () => ChopsticksServiceLayer,
ChopsticksServiceLive: () => ChopsticksServiceLive,
configureChopsticksLogger: () => configureChopsticksLogger,
launchChopsticksEffect: () => launchChopsticksEffect,
launchChopsticksEffectProgram: () => launchChopsticksEffectProgram,
launchChopsticksFromSpec: () => launchChopsticksFromSpec,
setChopsticksLogStream: () => setChopsticksLogStream
});
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;
};
}
}
function configureChopsticksLogger(level = "inherit") {
if (chopsticksLoggerConfigured) {
return;
}
chopsticksLoggerConfigured = true;
if (!chopsticksModuleCache) {
return;
}
const resolvedLevel = level === "inherit" ? process.env.LOG_LEVEL || "info" : level;
chopsticksModuleCache.pinoLogger.level = resolvedLevel;
logger2.debug(`Chopsticks internal logger level: ${resolvedLevel}`);
}
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)
};
}
async function launchChopsticksFromSpec(spec, options) {
const timeout = options?.timeout ?? 6e4;
const startTime = Date.now();
logger2.debug(`Launching chopsticks from spec: ${spec.configPath}`);
const parseEffect = parseChopsticksConfigFile(spec.configPath, {
port: spec.wsPort,
host: spec.address,
buildBlockMode: spec.buildBlockMode,
wasmOverride: spec.wasmOverride,
allowUnresolvedImports: spec.allowUnresolvedImports
});
let configResult;
try {
configResult = await Effect3.runPromise(
parseEffect.pipe(
Effect3.timeout(timeout),
Effect3.catchTag(
"TimeoutException",
() => Effect3.fail(
new ChopsticksSetupError({
cause: new Error(`Config parsing timed out after ${timeout}ms`),
endpoint: spec.configPath
})
)
)
)
);
} catch (error) {
const errorString = String(error);
const causeMatch = errorString.match(/\[cause\]:\s*Error:\s*(.+)/s);
if (causeMatch) {
throw new Error(`Chopsticks config validation failed: ${causeMatch[1].trim()}`);
}
throw new Error(`Chopsticks config validation failed: ${errorString}`);
}
logger2.debug(`Config parsed in ${Date.now() - startTime}ms`);
logger2.debug(` endpoint: ${configResult.endpoint}`);
logger2.debug(` port: ${configResult.port}`);
let service;
let cleanup;
try {
const result = await launchChopsticksEffect(configResult);
service = result.result;
cleanup = result.cleanup;
} catch (error) {
const errorString = String(error);
const causeMatch = errorString.match(/\[cause\]:\s*Error:\s*(.+)/s);
const causeMessage = causeMatch ? causeMatch[1].trim() : errorString;
throw new Error(
`Chopsticks failed to connect to endpoint '${configResult.endpoint}': ${causeMessage}`
);
}
logger2.debug(`Chopsticks launched in ${Date.now() - startTime}ms at ${service.addr}`);
return {
service,
cleanup,
port: service.port,
addr: service.addr
};
}
var BuildBlockModeValues2, chopsticksLoggerConfigured, chopsticksModuleCache, logger2, chopsticksLogStream, getEndpointString, prepareConfigForSetup, createServiceMethods, launchChopsticksEffectProgram, acquireChopsticks, ChopsticksServiceLayer, ChopsticksServiceLive;
var init_launchChopsticksEffect = __esm({
"src/internal/effect/launchChopsticksEffect.ts"() {
"use strict";
init_ChopsticksService();
init_chopsticksConfigParser();
BuildBlockModeValues2 = {
Batch: "Batch",
Manual: "Manual",
Instant: "Instant"
};
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
})
})
});
launchChopsticksEffectProgram = (config) => Effect3.gen(function* () {
const chopsticksModules = yield* Effect3.promise(() => getChopsticksModules("silent"));
const args = prepareConfigForSetup(config);
const context = yield* Effect3.tryPromise({
try: () => chopsticksModules.setupWithServer(args),
catch: (cause) => new ChopsticksSetupError({
cause,
endpoint: getEndpointString(config.endpoint),
block: config.block ?? void 0
})
});
const port = Number.parseInt(context.addr.split(":")[1], 10);
const cleanup = Effect3.tryPromise({
try: () => context.close(),
catch: (cause) => new ChopsticksCleanupError({ cause })
}).pipe(
Effect3.catchAll(
(error) => Effect3.sync(() => {
logger2.error(`Failed to cleanly close chopsticks: ${error}`);
})
)
);
const serviceMethods = createServiceMethods(context.chain);
const service = {
chain: context.chain,
addr: context.addr,
port,
...serviceMethods
};
return { result: service, cleanup };
});
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}`);
})
)
)
);
ChopsticksServiceLayer = (config) => Layer.scoped(
ChopsticksService,
Effect3.gen(function* () {
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
};
})
);
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/__tests__/launchChopsticksEffect.test.ts
init_ChopsticksService();
import { describe, it, expect } from "vitest";
import { Effect as Effect4, Layer as Layer2 } from "effect";
import { BuildBlockMode } from "@acala-network/chopsticks";
describe("launchChopsticksEffect - Phase 2: Module Structure", () => {
describe("Module Exports", () => {
it("should export launchChopsticksEffect function", async () => {
const module = await Promise.resolve().then(() => (init_launchChopsticksEffect(), launchChopsticksEffect_exports));
expect(module.launchChopsticksEffect).toBeDefined();
expect(typeof module.launchChopsticksEffect).toBe("function");
});
it("should export launchChopsticksEffectProgram function", async () => {
const module = await Promise.resolve().then(() => (init_launchChopsticksEffect(), launchChopsticksEffect_exports));
expect(module.launchChopsticksEffectProgram).toBeDefined();
expect(typeof module.launchChopsticksEffectProgram).toBe("function");
});
it("should export ChopsticksLaunchResult type", async () => {
const _typeCheck = {
chain: {},
addr: "127.0.0.1:8000",
port: 8e3
};
expect(_typeCheck.port).toBe(8e3);
});
it("should export ChopsticksServiceImpl type", async () => {
const _typeCheck = {
chain: {},
addr: "127.0.0.1:8000",
port: 8e3,
createBlock: () => Effect4.succeed({ block: { hash: "0x123", number: 1 } }),
setStorage: () => Effect4.void,
submitExtrinsic: () => Effect4.succeed("0x"),
dryRunExtrinsic: () => Effect4.succeed({ success: true, storageDiff: [] }),
getBlock: () => Effect4.succeed({ hash: "0x123", number: 1 }),
setHead: () => Effect4.void,
submitUpwardMessages: () => Effect4.void,
submitDownwardMessages: () => Effect4.void,
submitHorizontalMessages: () => Effect4.void
};
expect(_typeCheck.addr).toBe("127.0.0.1:8000");
});
});
describe("ChopsticksServiceImpl Interface", () => {
it("should have all required properties", () => {
const mockService = {
chain: {},
addr: "127.0.0.1:9000",
port: 9e3,
createBlock: () => Effect4.succeed({ block: { hash: "0xabc", number: 42 } }),
setStorage: () => Effect4.void,
submitExtrinsic: () => Effect4.succeed("0xhash"),
dryRunExtrinsic: () => Effect4.succeed({ success: true, storageDiff: [] }),
getBlock: () => Effect4.succeed({ hash: "0xdef", number: 100 }),
setHead: () => Effect4.void,
submitUpwardMessages: () => Effect4.void,
submitDownwardMessages: () => Effect4.void,
submitHorizontalMessages: () => Effect4.void
};
expect(mockService.addr).toBe("127.0.0.1:9000");
expect(mockService.port).toBe(9e3);
expect(mockService.chain).toBeDefined();
});
it("should allow createBlock to return BlockCreationResult", async () => {
const mockService = {
chain: {},
addr: "127.0.0.1:8000",
port: 8e3,
createBlock: () => Effect4.succeed({
block: { hash: "0x123456", number: 999 }
}),
setStorage: () => Effect4.void,
submitExtrinsic: () => Effect4.succeed("0x"),
dryRunExtrinsic: () => Effect4.succeed({ success: true, storageDiff: [] }),
getBlock: () => Effect4.succeed(void 0),
setHead: () => Effect4.void,
submitUpwardMessages: () => Effect4.void,
submitDownwardMessages: () => Effect4.void,
submitHorizontalMessages: () => Effect4.void
};
const result = await Effect4.runPromise(mockService.createBlock());
expect(result.block.hash).toBe("0x123456");
expect(result.block.number).toBe(999);
});
it("should allow createBlock to fail with ChopsticksBlockError", async () => {
const mockService = {
chain: {},
addr: "127.0.0.1:8000",
port: 8e3,
createBlock: () => Effect4.fail(
new ChopsticksBlockError({
cause: new Error("Block creation failed"),
operation: "newBlock"
})
),
setStorage: () => Effect4.void,
submitExtrinsic: () => Effect4.succeed("0x"),
dryRunExtrinsic: () => Effect4.succeed({ success: true, storageDiff: [] }),
getBlock: () => Effect4.succeed(void 0),
setHead: () => Effect4.void,
submitUpwardMessages: () => Effect4.void,
submitDownwardMessages: () => Effect4.void,
submitHorizontalMessages: () => Effect4.void
};
const result = await Effect4.runPromise(
mockService.createBlock().pipe(
Effect4.catchTag(
"ChopsticksBlockError",
(error) => Effect4.succeed({ caught: true, operation: error.operation })
)
)
);
expect(result).toEqual({ caught: true, operation: "newBlock" });
});
it("should allow setStorage to fail with ChopsticksStorageError", async () => {
const mockService = {
chain: {},
addr: "127.0.0.1:8000",
port: 8e3,
createBlock: () => Effect4.succeed({ block: { hash: "0x", number: 1 } }),
setStorage: () => Effect4.fail(
new ChopsticksStorageError({
cause: new Error("Storage write failed"),
module: "System",
method: "Account"
})
),
submitExtrinsic: () => Effect4.succeed("0x"),
dryRunExtrinsic: () => Effect4.succeed({ success: true, storageDiff: [] }),
getBlock: () => Effect4.succeed(void 0),
setHead: () => Effect4.void,
submitUpwardMessages: () => Effect4.void,
submitDownwardMessages: () => Effect4.void,
submitHorizontalMessages: () => Effect4.void
};
const result = await Effect4.runPromise(
mockService.setStorage({ module: "System", method: "Account", params: [] }).pipe(
Effect4.catchTag(
"ChopsticksStorageError",
(error) => Effect4.succeed({ caught: true, module: error.module, method: error.method })
)
)
);
expect(result).toEqual({ caught: true, module: "System", method: "Account" });
});
});
describe("Config Conversion (kebab-case)", () => {
it("should accept config with required fields", () => {
const config = {
endpoint: "wss://rpc.polkadot.io",
port: 8e3,
"build-block-mode": BuildBlockMode.Manual
};
expect(config.endpoint).toBe("wss://rpc.polkadot.io");
expect(config.port).toBe(8e3);
expect(config["build-block-mode"]).toBe(BuildBlockMode.Manual);
});
it("should accept full config with all options using kebab-case", () => {
const config = {
endpoint: "wss://rpc.polkadot.io",
block: 12345,
port: 9e3,
host: "0.0.0.0",
"build-block-mode": BuildBlockMode.Manual,
"wasm-override": "/path/to/wasm",
"allow-unresolved-imports": true,
"mock-signature-host": true,
db: "./chopsticks.db",
"import-storage": { System: { Account: {} } },
"runtime-log-level": 3,
"rpc-timeout": 3e4
// New field supported via chopsticks type
};
expect(config.endpoint).toBe("wss://rpc.polkadot.io");
expect(config.block).toBe(12345);
expect(config.port).toBe(9e3);
expect(config.host).toBe("0.0.0.0");
expect(config["build-block-mode"]).toBe(BuildBlockMode.Manual);
expect(config["wasm-override"]).toBe("/path/to/wasm");
expect(config["allow-unresolved-imports"]).toBe(true);
expect(config["mock-signature-host"]).toBe(true);
expect(config.db).toBe("./chopsticks.db");
expect(config["runtime-log-level"]).toBe(3);
expect(config["rpc-timeout"]).toBe(3e4);
});
it("should accept config with block as hash string", () => {
const config = {
endpoint: "wss://rpc.polkadot.io",
port: 8e3,
"build-block-mode": BuildBlockMode.Manual,
block: "0x1234567890abcdef"
};
expect(config.block).toBe("0x1234567890abcdef");
});
it("should accept config with block as null for latest", () => {
const config = {
endpoint: "wss://rpc.polkadot.io",
port: 8e3,
"build-block-mode": BuildBlockMode.Manual,
block: null
};
expect(config.block).toBeNull();
});
});
describe("launchChopsticksEffectProgram Effect Type", () => {
it("should return an Effect that requires no context when config is provided inline", async () => {
const { launchChopsticksEffectProgram: launchChopsticksEffectProgram2 } = await Promise.resolve().then(() => (init_launchChopsticksEffect(), launchChopsticksEffect_exports));
const program = launchChopsticksEffectProgram2({
endpoint: "wss://test.io",
port: 8e3,
"build-block-mode": BuildBlockMode.Manual
});
expect(typeof program.pipe).toBe("function");
});
it("should produce ChopsticksSetupError on failure", async () => {
const { launchChopsticksEffectProgram: launchChopsticksEffectProgram2 } = await Promise.resolve().then(() => (init_launchChopsticksEffect(), launchChopsticksEffect_exports));
const program = launchChopsticksEffectProgram2({
endpoint: "wss://nonexistent.invalid",
port: 8e3,
"build-block-mode": BuildBlockMode.Manual
}).pipe(
Effect4.catchTag(
"ChopsticksSetupError",
(error) => Effect4.succeed({ caught: true, endpoint: error.endpoint })
)
);
expect(typeof program.pipe).toBe("function");
});
});
describe("Return Value Structure", () => {
it("should return object with result and cleanup when successful", () => {
const mockReturn = {
result: {
chain: {},
addr: "127.0.0.1:8000",
port: 8e3,
createBlock: () => Effect4.succeed({ block: { hash: "0x", number: 1 } }),
setStorage: () => Effect4.void,
submitExtrinsic: () => Effect4.succeed("0x"),
dryRunExtrinsic: () => Effect4.succeed({ success: true, storageDiff: [] }),
getBlock: () => Effect4.succeed(void 0),
setHead: () => Effect4.void,
submitUpwardMessages: () => Effect4.void,
submitDownwardMessages: () => Effect4.void,
submitHorizontalMessages: () => Effect4.void
},
cleanup: async () => {
}
};
expect(mockReturn.result).toBeDefined();
expect(mockReturn.cleanup).toBeDefined();
expect(typeof mockReturn.cleanup).toBe("function");
});
});
});
describe("ChopsticksServiceLayer - Phase 3: Layer.scoped", () => {
describe("Module Exports", () => {
it("should export ChopsticksServiceLayer function", async () => {
const module = await Promise.resolve().then(() => (init_launchChopsticksEffect(), launchChopsticksEffect_exports));
expect(module.ChopsticksServiceLayer).toBeDefined();
expect(typeof module.ChopsticksServiceLayer).toBe("function");
});
it("should export ChopsticksServiceLive Layer", async () => {
const module = await Promise.resolve().then(() => (init_launchChopsticksEffect(), launchChopsticksEffect_exports));
expect(module.ChopsticksServiceLive).toBeDefined();
});
});
describe("ChopsticksServiceLayer Type", () => {
it("should return a Layer when called with config", async () => {
const { ChopsticksServiceLayer: ChopsticksServiceLayer2 } = await Promise.resolve().then(() => (init_launchChopsticksEffect(), launchChopsticksEffect_exports));
const layer = ChopsticksServiceLayer2({
endpoint: "wss://test.io",
port: 8e3,
"build-block-mode": BuildBlockMode.Manual
});
expect(layer).toBeDefined();
});
it("should create Layer that provides ChopsticksService", async () => {
const { ChopsticksServiceLayer: ChopsticksServiceLayer2 } = await Promise.resolve().then(() => (init_launchChopsticksEffect(), launchChopsticksEffect_exports));
const { ChopsticksService: ChopsticksService2 } = await Promise.resolve().then(() => (init_ChopsticksService(), ChopsticksService_exports));
const layer = ChopsticksServiceLayer2({
endpoint: "wss://test.io",
port: 8e3,
"build-block-mode": BuildBlockMode.Manual
});
const program = Effect4.gen(function* () {
const service = yield* ChopsticksService2;
return service.addr;
});
const providedProgram = program.pipe(Effect4.provide(layer));
expect(typeof providedProgram.pipe).toBe("function");
});
it("should accept all config options using kebab-case", async () => {
const { ChopsticksServiceLayer: ChopsticksServiceLayer2 } = await Promise.resolve().then(() => (init_launchChopsticksEffect(), launchChopsticksEffect_exports));
const { BuildBlockMode: BuildBlockMode2 } = await import("@acala-network/chopsticks");
const layer = ChopsticksServiceLayer2({
endpoint: "wss://rpc.polkadot.io",
block: 12345,
port: 9e3,
host: "0.0.0.0",
"build-block-mode": BuildBlockMode2.Manual,
"wasm-override": "/path/to/wasm",
"allow-unresolved-imports": true,
"mock-signature-host": true,
db: "./chopsticks.db",
"runtime-log-level": 3,
"rpc-timeout": 3e4
});
expect(layer).toBeDefined();
});
});
describe("ChopsticksServiceLive Type", () => {
it("should require ChopsticksConfigTag in context", async () => {
const { ChopsticksServiceLive: ChopsticksServiceLive2 } = await Promise.resolve().then(() => (init_launchChopsticksEffect(), launchChopsticksEffect_exports));
const { ChopsticksService: ChopsticksService2, ChopsticksConfigTag: ChopsticksConfigTag2 } = await Promise.resolve().then(() => (init_ChopsticksService(), ChopsticksService_exports));
const configLayer = Layer2.succeed(ChopsticksConfigTag2, {
endpoint: "wss://test.io",
port: 8e3,
"build-block-mode": BuildBlockMode.Manual
});
const program = Effect4.gen(function* () {
const service = yield* ChopsticksService2;
return service.addr;
});
const fullLayer = ChopsticksServiceLive2.pipe(Layer2.provide(configLayer));
const providedProgram = program.pipe(Effect4.provide(fullLayer));
expect(typeof providedProgram.pipe).toBe("function");
});
it("should allow Layer composition patterns", async () => {
const { ChopsticksServiceLive: ChopsticksServiceLive2 } = await Promise.resolve().then(() => (init_launchChopsticksEffect(), launchChopsticksEffect_exports));
const { ChopsticksConfigTag: ChopsticksConfigTag2 } = await Promise.resolve().then(() => (init_ChopsticksService(), ChopsticksService_exports));
const configLayer = Layer2.succeed(ChopsticksConfigTag2, {
endpoint: "wss://test.io",
port: 8e3,
"build-block-mode": BuildBlockMode.Manual
});
const serviceLayer = ChopsticksServiceLive2.pipe(Layer2.provide(configLayer));
expect(serviceLayer).toBeDefined();
});
});
describe("Layer Error Handling", () => {
it("should produce ChopsticksSetupError on failure via ChopsticksServiceLayer", async () => {
const { ChopsticksServiceLayer: ChopsticksServiceLayer2 } = await Promise.resolve().then(() => (init_launchChopsticksEffect(), launchChopsticksEffect_exports));
const { ChopsticksService: ChopsticksService2, ChopsticksSetupError: ChopsticksSetupError3 } = await Promise.resolve().then(() => (init_ChopsticksService(), ChopsticksService_exports));
const layer = ChopsticksServiceLayer2({
endpoint: "wss://nonexistent.invalid",
port: 8e3,
"build-block-mode": BuildBlockMode.Manual
});
const program = Effect4.gen(function* () {
const service = yield* ChopsticksService2;
return service.addr;
}).pipe(
Effect4.provide(layer),
Effect4.catchTag(
"ChopsticksSetupError",
(error) => Effect4.succeed({ caught: true, endpoint: error.endpoint })
)
);
expect(typeof program.pipe).toBe("function");
});
it("should produce ChopsticksSetupError on failure via ChopsticksServiceLive", async () => {
const { ChopsticksServiceLive: ChopsticksServiceLive2 } = await Promise.resolve().then(() => (init_launchChopsticksEffect(), launchChopsticksEffect_exports));
const { ChopsticksService: ChopsticksService2, ChopsticksConfigTag: ChopsticksConfigTag2 } = await Promise.resolve().then(() => (init_ChopsticksService(), ChopsticksService_exports));
const configLayer = Layer2.succeed(ChopsticksConfigTag2, {
endpoint: "wss://nonexistent.invalid",
port: 8e3,
"build-block-mode": BuildBlockMode.Manual
});
const fullLayer = ChopsticksServiceLive2.pipe(Layer2.provide(configLayer));
const program = Effect4.gen(function* () {
const service = yield* ChopsticksService2;
return service.addr;
}).pipe(
Effect4.provide(fullLayer),
Effect4.catchTag(
"ChopsticksSetupError",
(error) => Effect4.succeed({ caught: true, endpoint: error.endpoint })
)
);
expect(typeof program.pipe).toBe("function");
});
});
describe("Scope Management", () => {
it("should be usable with Effect.scoped for manual scope control", async () => {
const { ChopsticksServiceLayer: ChopsticksServiceLayer2 } = await Promise.resolve().then(() => (init_launchChopsticksEffect(), launchChopsticksEffect_exports));
const { ChopsticksService: ChopsticksService2 } = await Promise.resolve().then(() => (init_ChopsticksService(), ChopsticksService_exports));
const layer = ChopsticksServiceLayer2({
endpoint: "wss://test.io",
port: 8e3,
"build-block-mode": BuildBlockMode.Manual
});
const program = Effect4.scoped(
Effect4.gen(function* () {
const service = yield* ChopsticksService2;
return service.addr;
}).pipe(Effect4.provide(layer))
);
expect(typeof program.pipe).toBe("function");
});
});
});
describe.skip("launchChopsticksEffect - Integration Tests", () => {
it("should launch chopsticks and return a working service", async () => {
const { launchChopsticksEffect: launchChopsticksEffect2 } = await Promise.resolve().then(() => (init_launchChopsticksEffect(), launchChopsticksEffect_exports));
const { result, cleanup } = await launchChopsticksEffect2({
endpoint: "wss://rpc.polkadot.io",
port: 8e3,
"build-block-mode": BuildBlockMode.Manual
});
try {
expect(result.addr).toMatch(/127\.0\.0\.1:\d+/);
expect(result.port).toBe(8e3);
expect(result.chain).toBeDefined();
} finally {
await cleanup();
}
});
it("should allow creating blocks after launch", async () => {
const { launchChopsticksEffect: launchChopsticksEffect2 } = await Promise.resolve().then(() => (init_launchChopsticksEffect(), launchChopsticksEffect_exports));
const { result, cleanup } = await launchChopsticksEffect2({
endpoint: "wss://rpc.polkadot.io",
port: 8001,
"build-block-mode": BuildBlockMode.Manual
});
try {
const blockResult = await Effect4.runPromise(result.createBlock());
expect(blockResult.block.number).toBeGreaterThan(0);
expect(blockResult.block.hash).toMatch(/^0x/);
} finally {
await cleanup();
}
});
});