feat: Enabling solochain relayer (#82)

This PR:
- Adds launching the new `solochain` relayer in the relayers' script,
with its config files and schemas.
- Updates the relayer launching to use switch-case logic since we are
now going to be running 4 relayers, making it cleaner.
- Minor CLI fixes (adding additional args to build command for Linux,
improving naming, deleting idle containers instead of only active ones).
- Deletes unused files `substrate-relay.json` (now
`solochain-relay.json`), `gen-snowbridge-cfgs.ts` and
`snowbridge-relayer.ts` (now Snowbridge binary and docker image
generation can be done exclusively from our [Snowbridge
repo](https://github.com/Moonsong-Labs/snowbridge))
- Updates the `UniversalLocation` of our stagenet runtime to be under
the global consensus for XCM instead of actually being the global
consensus. This makes it so we can actually use the Snowbridge System
pallets to queue up messages. For mainnet we are going to want to have
our own Network ID instead of using Polkadot's.

> [!WARNING]
> ~~All in all these changes allows us to run the solochain relayer, but
it won't work without the refactor that's being worked on in
https://github.com/Moonsong-Labs/snowbridge/pull/14. I'd advise waiting
for that PR to be merged before merging this one.~~ MERGED 
This commit is contained in:
Tobi Demeco 2025-05-29 15:14:46 +02:00 committed by GitHub
parent 1997c298a1
commit 9f55e10339
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 198 additions and 615 deletions

View file

@ -640,7 +640,9 @@ impl pallet_evm_chain_id::Config for Runtime {}
// --- Snowbridge Config Constants & Parameter Types ---
parameter_types! {
pub UniversalLocation: InteriorLocation = Here;
// TODO: Change this to the actual network ID of DataHaven
pub const ThisNetwork: NetworkId = NetworkId::Polkadot;
pub UniversalLocation: InteriorLocation = [GlobalConsensus(ThisNetwork::get())].into();
pub InboundDeliveryCost: BalanceOf<Runtime> = 0;
pub RootLocation: Location = Location::here();
pub Parameters: PricingParameters<u128> = PricingParameters {

View file

@ -1,5 +1,5 @@
{
"version": "0.1.0-autogenerated.7209825829100564812",
"version": "0.1.0-autogenerated.13919917606265561517",
"name": "@polkadot-api/descriptors",
"files": [
"dist"

Binary file not shown.

View file

@ -33,12 +33,14 @@ type RelayerSpec = {
type: RelayerType;
config: string;
pk: { type: "ethereum" | "substrate"; value: string };
secondaryPk?: { type: "ethereum" | "substrate"; value: string };
};
const RELAYER_CONFIG_DIR = "tmp/configs";
const RELAYER_CONFIG_PATHS = {
BEACON: path.join(RELAYER_CONFIG_DIR, "beacon-relay.json"),
BEEFY: path.join(RELAYER_CONFIG_DIR, "beefy-relay.json")
BEEFY: path.join(RELAYER_CONFIG_DIR, "beefy-relay.json"),
SOLOCHAIN: path.join(RELAYER_CONFIG_DIR, "solochain-relay.json")
};
const INITIAL_CHECKPOINT_FILE = "dump-initial-checkpoint.json";
const INITIAL_CHECKPOINT_DIR = "tmp/beacon-checkpoint";
@ -129,7 +131,20 @@ export const launchRelayers = async (options: LaunchOptions, launchedNetwork: La
config: RELAYER_CONFIG_PATHS.BEACON,
pk: {
type: "substrate",
value: SUBSTRATE_FUNDED_ACCOUNTS.ALITH.privateKey
value: SUBSTRATE_FUNDED_ACCOUNTS.BALTATHAR.privateKey
}
},
{
name: "relayer-⛓️",
type: "solochain",
config: RELAYER_CONFIG_PATHS.SOLOCHAIN,
pk: {
type: "ethereum",
value: ANVIL_FUNDED_ACCOUNTS[1].privateKey
},
secondaryPk: {
type: "substrate",
value: SUBSTRATE_FUNDED_ACCOUNTS.CHARLETH.privateKey
}
}
];
@ -163,23 +178,46 @@ export const launchRelayers = async (options: LaunchOptions, launchedNetwork: La
`Fetched ports: ETH WS=${ethWsPort}, ETH HTTP=${ethHttpPort}, Substrate WS=${substrateWsPort} (from DataHaven node)`
);
if (type === "beacon") {
const cfg = parseRelayConfig(json, type);
cfg.source.beacon.endpoint = `http://host.docker.internal:${ethHttpPort}`;
cfg.source.beacon.stateEndpoint = `http://host.docker.internal:${ethHttpPort}`;
cfg.source.beacon.datastore.location = "/data";
cfg.sink.parachain.endpoint = `ws://${substrateNodeId}:${substrateWsPort}`;
switch (type) {
case "beacon":
{
const cfg = parseRelayConfig(json, type);
cfg.source.beacon.endpoint = `http://host.docker.internal:${ethHttpPort}`;
cfg.source.beacon.stateEndpoint = `http://host.docker.internal:${ethHttpPort}`;
cfg.source.beacon.datastore.location = "/data";
cfg.sink.parachain.endpoint = `ws://${substrateNodeId}:${substrateWsPort}`;
await Bun.write(outputFilePath, JSON.stringify(cfg, null, 4));
logger.success(`Updated beacon config written to ${outputFilePath}`);
} else {
const cfg = parseRelayConfig(json, type);
cfg.source.polkadot.endpoint = `ws://${substrateNodeId}:${substrateWsPort}`;
cfg.sink.ethereum.endpoint = `ws://host.docker.internal:${ethWsPort}`;
cfg.sink.contracts.BeefyClient = beefyClientAddress;
cfg.sink.contracts.Gateway = gatewayAddress;
await Bun.write(outputFilePath, JSON.stringify(cfg, null, 4));
logger.success(`Updated beefy config written to ${outputFilePath}`);
await Bun.write(outputFilePath, JSON.stringify(cfg, null, 4));
logger.success(`Updated beacon config written to ${outputFilePath}`);
}
break;
case "beefy":
{
const cfg = parseRelayConfig(json, type);
cfg.source.polkadot.endpoint = `ws://${substrateNodeId}:${substrateWsPort}`;
cfg.sink.ethereum.endpoint = `ws://host.docker.internal:${ethWsPort}`;
cfg.sink.contracts.BeefyClient = beefyClientAddress;
cfg.sink.contracts.Gateway = gatewayAddress;
await Bun.write(outputFilePath, JSON.stringify(cfg, null, 4));
logger.success(`Updated beefy config written to ${outputFilePath}`);
}
break;
case "solochain":
{
const cfg = parseRelayConfig(json, type);
cfg.source.ethereum.endpoint = `ws://host.docker.internal:${ethWsPort}`;
cfg.source.solochain.endpoint = `ws://${substrateNodeId}:${substrateWsPort}`;
cfg.source.contracts.BeefyClient = beefyClientAddress;
cfg.source.contracts.Gateway = gatewayAddress;
cfg.source.beacon.endpoint = `http://host.docker.internal:${ethHttpPort}`;
cfg.source.beacon.stateEndpoint = `http://host.docker.internal:${ethHttpPort}`;
cfg.source.beacon.datastore.location = datastorePath;
cfg.sink.ethereum.endpoint = `ws://host.docker.internal:${ethWsPort}`;
cfg.sink.contracts.Gateway = gatewayAddress;
await Bun.write(outputFilePath, JSON.stringify(cfg, null, 4));
logger.success(`Updated solochain config written to ${outputFilePath}`);
}
break;
}
}
@ -187,7 +225,7 @@ export const launchRelayers = async (options: LaunchOptions, launchedNetwork: La
await initEthClientPallet(options, launchedNetwork);
for (const { config, name, type, pk } of relayersToStart) {
for (const { config, name, type, pk, secondaryPk } of relayersToStart) {
try {
const containerName = `snowbridge-${type}-relay`;
logger.info(`🚀 Starting relayer ${containerName} ...`);
@ -228,6 +266,10 @@ export const launchRelayers = async (options: LaunchOptions, launchedNetwork: La
pk.value
];
if (type === "solochain" && secondaryPk) {
relayerCommandArgs.push("--substrate.private-key", secondaryPk.value);
}
const command: string[] = [
...commandBase,
...volumeMounts,

View file

@ -50,16 +50,16 @@ export const stopDockerComponents = async (type: keyof typeof COMPONENTS, option
const name = COMPONENTS[type].componentName;
const imageName = COMPONENTS[type].imageName;
logger.debug(`Checking currently running ${name} ...`);
const relayers = await getContainersMatchingImage(imageName);
logger.info(`🔎 Found ${relayers.length} containers(s) running`);
if (relayers.length === 0) {
const components = await getContainersMatchingImage(imageName);
logger.info(`🔎 Found ${components.length} containers(s) running the ${name}`);
if (components.length === 0) {
logger.info(`🤷‍ No ${name} containers found running`);
return;
}
let shouldStopComponent = options.all || options[COMPONENTS[type].optionName];
if (shouldStopComponent === undefined) {
shouldStopComponent = await confirmWithTimeout(
`Do you want to stop the ${imageName} relayers?`,
`Do you want to stop the ${imageName} containers?`,
true,
10
);
@ -80,7 +80,7 @@ export const stopDockerComponents = async (type: keyof typeof COMPONENTS, option
remaining.length === 0,
`${remaining.length} containers are still running and have not been stopped.`
);
logger.info(`🪓 ${relayers.length} ${name} containers stopped successfully`);
logger.info(`🪓 ${components.length} ${name} containers stopped successfully`);
};
const removeDockerNetwork = async (networkName: string, options: StopOptions) => {

View file

@ -0,0 +1,49 @@
{
"source": {
"ethereum": {
"endpoint": ""
},
"solochain": {
"endpoint": ""
},
"contracts": {
"BeefyClient": "",
"Gateway": ""
},
"beacon": {
"endpoint": "http://127.0.0.1:33030",
"stateEndpoint": "http://127.0.0.1:33030",
"spec": {
"syncCommitteeSize": 512,
"slotsInEpoch": 32,
"epochsPerSyncCommitteePeriod": 256,
"forkVersions": {
"deneb": 0,
"electra": 0
}
},
"datastore": {
"location": "/Users/tdemeco/Desktop/Moonsong/datahaven/test/tmp/tobi-test",
"maxEntries": 100
}
}
},
"sink": {
"ethereum": {
"endpoint": ""
},
"contracts": {
"Gateway": ""
}
},
"schedule": {
"id": 0,
"totalRelayerCount": 1,
"sleepInterval": 10
},
"reward-address": "0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d",
"ofac": {
"enabled": false,
"apiKey": ""
}
}

View file

@ -1,28 +0,0 @@
{
"source": {
"ethereum": {
"endpoint": ""
},
"polkadot": {
"endpoint": ""
},
"contracts": {
"BeefyClient": "",
"Gateway": ""
},
"channel-id": ""
},
"sink": {
"ethereum": {
"endpoint": ""
},
"contracts": {
"Gateway": ""
}
},
"schedule": {
"id": 0,
"totalRelayerCount": 1,
"sleepInterval": 10
}
}

View file

@ -52,7 +52,11 @@ export const cargoCrossbuild = async (options: { datahavenBuildExtraArgs?: strin
const target = "x86_64-unknown-linux-gnu";
await addRustupTarget(target);
const command = `cargo build --target ${target} --release`;
// Get additional arguments from command line
const additionalArgs = options.datahavenBuildExtraArgs ?? "";
const command = `cargo build --target ${target} --release ${additionalArgs}`;
logger.debug(`Running build command: ${command}`);
if (LOG_LEVEL === "debug") {

View file

@ -1,453 +0,0 @@
import { mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { parseArgs } from "node:util";
import { spawn } from "bun";
import { logger } from "utils";
import { z } from "zod";
// ---- Zod Schemas for Validation ----
const beefyRelaySchema = z
.object({
sink: z.object({
contracts: z.object({
BeefyClient: z.string().optional(),
Gateway: z.string().optional()
}),
ethereum: z.object({
endpoint: z.string(),
"gas-limit": z.string()
})
}),
source: z.object({
polkadot: z.object({
endpoint: z.string()
})
})
})
.describe("Beefy Relay Configuration");
const beaconRelaySchema = z
.object({
source: z.object({
beacon: z.object({
endpoint: z.string(),
spec: z.object({
forkVersions: z.object({
electra: z.number()
})
}),
datastore: z.object({
location: z.string()
})
})
}),
sink: z.object({
parachain: z.object({
endpoint: z.string()
})
})
})
.describe("Beacon Relay Configuration");
const executionRelaySchema = z
.object({
source: z.object({
ethereum: z.object({
endpoint: z.string()
}),
contracts: z.object({
Gateway: z.string()
}),
"channel-id": z.string(),
beacon: z.object({
datastore: z.object({
location: z.string()
})
})
}),
sink: z.object({
parachain: z.object({
endpoint: z.string()
})
}),
schedule: z.object({
id: z.number()
})
})
.describe("Execution Layer Relay Configuration");
const substrateRelaySchema = z
.object({
source: z.object({
ethereum: z.object({
endpoint: z.string()
}),
polkadot: z.object({
endpoint: z.string()
}),
contracts: z.object({
BeefyClient: z.string(),
Gateway: z.string()
}),
"channel-id": z.string()
}),
sink: z.object({
contracts: z.object({
Gateway: z.string()
}),
ethereum: z.object({
endpoint: z.string()
})
})
})
.describe("Substrate Relay Configuration");
const beaconFinalitySchema = z
.object({
execution_optimistic: z.boolean(),
finalized: z.boolean(),
data: z.object({
previous_justified: z.object({
epoch: z.string(),
root: z.string()
}),
current_justified: z.object({
epoch: z.string(),
root: z.string()
}),
finalized: z.object({
epoch: z.string(),
root: z.string()
})
})
})
.describe("Beacon Finality Configuration");
// ---- Configuration Options ----
interface SnowbridgeConfigOptions {
outputDir: string;
assetsDir: string;
logsDir: string;
relayBin: string;
ethEndpointWs: string;
ethGasLimit: string;
relaychainEndpoint: string;
beaconEndpointHttp: string;
ethWriterEndpoint: string;
primaryGovernanceChannelId: string;
secondaryGovernanceChannelId: string;
dataStoreDir: string;
beaconWaitTimeoutSeconds: number;
beaconElectraForkVersion: number;
executionScheduleId: number;
}
const DEFAULT_OPTIONS = {
outputDir: "tmp/output",
assetsDir: "configs/snowbridge",
logsDir: "tmp/logs",
relayBin: "relay",
ethEndpointWs: "ws://localhost:8545",
ethGasLimit: "8000000",
relaychainEndpoint: "ws://localhost:9944",
beaconEndpointHttp: "http://localhost:5052",
ethWriterEndpoint: "",
primaryGovernanceChannelId: "0",
secondaryGovernanceChannelId: "1",
beaconWaitTimeoutSeconds: 300,
beaconElectraForkVersion: 0,
executionScheduleId: 0
};
/**
* Retrieves a Snowbridge contract address from environment variables.
*/
async function getSnowbridgeAddressFromEnv(name: string): Promise<string> {
const envVarName = `SNOWBRIDGE_${name.toUpperCase()}_ADDRESS`;
const address = process.env[envVarName];
if (!address) {
logger.warn(`Environment variable ${envVarName} not set. Using empty string.`);
}
return address || "";
}
/**
* Reads, validates, updates, and writes a JSON configuration file.
*/
async function updateJsonConfig<T>(
templateName: string,
outputName: string,
schema: z.ZodType<T>,
updateFn: (obj: T) => void | Promise<void>,
options: { assetsDir: string; outputDir: string }
): Promise<void> {
const templatePath = join(options.assetsDir, templateName);
const outputPath = join(options.outputDir, outputName);
try {
logger.trace({ templatePath, outputPath }, "Read config template");
const obj = await import(templatePath, { with: { type: "json" } });
logger.trace(
{ rawConfig: obj.default },
`Attempting to parse ${templateName} config with Zod schema`
);
const config = schema.parse(obj.default);
logger.debug(`Successfully parsed ${schema.description} `);
await updateFn(config);
logger.trace({ config }, "Updated config object");
await writeFile(outputPath, JSON.stringify(config, null, 2));
logger.debug(`Wrote configuration to ${outputPath}`);
} catch (error) {
logger.error(
{ err: error, templatePath, outputPath },
`Failed to update/write config ${outputName}`
);
throw error;
}
}
/**
* Configures all relayer components
*/
async function configRelayer(options: SnowbridgeConfigOptions): Promise<void> {
logger.info("Starting configuration generation...");
// Ensure all required directories exist
logger.debug("Ensuring all required directories exist");
for (const dir of [options.outputDir, options.assetsDir, options.logsDir, options.dataStoreDir]) {
await mkdir(dir, { recursive: true });
logger.debug(`Ensured directory exists: ${dir}`);
}
const commonOptions = {
assetsDir: options.assetsDir,
outputDir: options.outputDir
};
// Beefy relay
logger.debug("Configuring Beefy relay...");
await updateJsonConfig(
"beefy-relay.json",
"beefy-relay.json",
beefyRelaySchema,
async (obj) => {
obj.sink.contracts.BeefyClient = await getSnowbridgeAddressFromEnv("BeefyClient");
obj.sink.contracts.Gateway = await getSnowbridgeAddressFromEnv("GatewayProxy");
obj.sink.ethereum.endpoint = options.ethEndpointWs;
obj.sink.ethereum["gas-limit"] = options.ethGasLimit;
obj.source.polkadot.endpoint = options.relaychainEndpoint;
},
commonOptions
);
// Beacon relay
logger.debug("Configuring Beacon relay...");
await updateJsonConfig(
"beacon-relay.json",
"beacon-relay.json",
beaconRelaySchema,
(obj) => {
obj.source.beacon.endpoint = options.beaconEndpointHttp;
obj.source.beacon.spec.forkVersions.electra = options.beaconElectraForkVersion;
obj.source.beacon.datastore.location = options.dataStoreDir;
obj.sink.parachain.endpoint = options.relaychainEndpoint;
},
commonOptions
);
// Execution relay
logger.debug("Configuring Execution relay...");
await updateJsonConfig(
"execution-relay.json",
"execution-relay.json",
executionRelaySchema,
async (obj) => {
obj.source.ethereum.endpoint = options.ethEndpointWs;
obj.source.contracts.Gateway = await getSnowbridgeAddressFromEnv("GatewayProxy");
obj.source["channel-id"] = options.primaryGovernanceChannelId;
obj.source.beacon.datastore.location = options.dataStoreDir;
obj.sink.parachain.endpoint = options.relaychainEndpoint;
obj.schedule.id = options.executionScheduleId;
},
commonOptions
);
// Substrate relay - primary
logger.debug("Configuring Primary Substrate relay...");
await updateJsonConfig(
"substrate-relay.json",
"substrate-relay-primary.json",
substrateRelaySchema,
async (obj) => {
obj.source.ethereum.endpoint = options.ethEndpointWs;
obj.source.polkadot.endpoint = options.relaychainEndpoint;
obj.source.contracts.BeefyClient = await getSnowbridgeAddressFromEnv("BeefyClient");
obj.source.contracts.Gateway = await getSnowbridgeAddressFromEnv("GatewayProxy");
obj.source["channel-id"] = options.primaryGovernanceChannelId;
obj.sink.contracts.Gateway = await getSnowbridgeAddressFromEnv("GatewayProxy");
obj.sink.ethereum.endpoint = options.ethWriterEndpoint;
},
commonOptions
);
// Substrate relay - secondary
logger.debug("Configuring Secondary Substrate relay...");
await updateJsonConfig(
"substrate-relay.json",
"substrate-relay-secondary.json",
substrateRelaySchema,
async (obj) => {
obj.source.ethereum.endpoint = options.ethEndpointWs;
obj.source.polkadot.endpoint = options.relaychainEndpoint;
obj.source.contracts.BeefyClient = await getSnowbridgeAddressFromEnv("BeefyClient");
obj.source.contracts.Gateway = await getSnowbridgeAddressFromEnv("GatewayProxy");
obj.source["channel-id"] = options.secondaryGovernanceChannelId;
obj.sink.contracts.Gateway = await getSnowbridgeAddressFromEnv("GatewayProxy");
obj.sink.ethereum.endpoint = options.ethWriterEndpoint;
},
commonOptions
);
logger.info("Finished configuration generation.");
}
/**
* Waits for the Beacon chain to reach finality before proceeding
*/
async function waitBeaconChainReady(options: SnowbridgeConfigOptions): Promise<void> {
logger.info("Waiting for Beacon chain finality...");
let initialBeaconBlock = "";
const maxAttempts = options.beaconWaitTimeoutSeconds;
for (let i = 0; i < maxAttempts; i++) {
try {
const res = await fetch(
`${options.beaconEndpointHttp}/eth/v1/beacon/states/head/finality_checkpoints`
);
const json = await res.json();
const parsed = beaconFinalitySchema.parse(json);
initialBeaconBlock = parsed.data.finalized.root || "";
logger.trace({ attempt: i + 1, initialBeaconBlock }, "Checked beacon finality");
if (
initialBeaconBlock &&
initialBeaconBlock !== "0x0000000000000000000000000000000000000000000000000000000000000000"
) {
logger.info(`Beacon chain finalized. Finalized root: ${initialBeaconBlock}`);
return;
}
} catch (_error) {
logger.trace({ attempt: i + 1 }, "Beacon finality check failed or not ready, retrying...");
}
await new Promise((resolve) => setTimeout(resolve, 1000));
}
throw new Error(
`❌ Beacon chain not ready after ${options.beaconWaitTimeoutSeconds} seconds timeout`
);
}
/**
* Generates a beacon checkpoint using the relay binary
*/
async function writeBeaconCheckpoint(options: SnowbridgeConfigOptions): Promise<void> {
logger.info("Generating beacon checkpoint...");
const cmdArgs = [
options.relayBin,
"generate-beacon-checkpoint",
"--config",
join(options.outputDir, "beacon-relay.json"),
"--export-json"
];
logger.debug({ command: cmdArgs.join(" ") }, "Spawning process to generate beacon checkpoint");
const proc = spawn({
cmd: cmdArgs,
cwd: options.outputDir,
stdout: "pipe",
stderr: "pipe"
});
await proc.exited;
logger.info("Beacon checkpoint generated.");
}
/**
* Main function to generate Snowbridge configurations
*/
export async function generateSnowbridgeConfigs(
customOptions: Partial<Omit<SnowbridgeConfigOptions, "dataStoreDir">> = {}
): Promise<void> {
// Merge default options with custom options
const mergedOptions = { ...DEFAULT_OPTIONS, ...customOptions };
// Add derived options
const options: SnowbridgeConfigOptions = {
...mergedOptions,
ethWriterEndpoint: mergedOptions.ethWriterEndpoint || mergedOptions.ethEndpointWs,
dataStoreDir: join(mergedOptions.outputDir, "relayer_data")
};
logger.debug({ options }, "Resolved configuration values");
logger.info("Starting Snowbridge config generation script...");
try {
await configRelayer(options);
await waitBeaconChainReady(options);
await writeBeaconCheckpoint(options);
logger.info("Snowbridge config generation script finished successfully.");
} catch (error) {
logger.error({ err: error }, "Snowbridge config generation script failed");
throw error;
}
}
// Check if we're running this file directly
if (import.meta.url === `file://${process.argv[1]}`) {
logger.trace("Parsing command line arguments");
const { values } = parseArgs({
options: {
outputDir: { type: "string" },
assetsDir: { type: "string" },
logsDir: { type: "string" },
relayBin: { type: "string" },
ethEndpointWs: { type: "string" },
ethGasLimit: { type: "string" },
relaychainEndpoint: { type: "string" },
beaconEndpointHttp: { type: "string" },
ethWriterEndpoint: { type: "string" },
primaryGovernanceChannelId: { type: "string" },
secondaryGovernanceChannelId: { type: "string" }
},
args: process.argv.slice(2)
});
// Convert string arguments to appropriate types
const options: Partial<Omit<SnowbridgeConfigOptions, "dataStoreDir">> = {};
// Only add properties that were actually provided
if (values.outputDir) options.outputDir = values.outputDir;
if (values.assetsDir) options.assetsDir = values.assetsDir;
if (values.logsDir) options.logsDir = values.logsDir;
if (values.relayBin) options.relayBin = values.relayBin;
if (values.ethEndpointWs) options.ethEndpointWs = values.ethEndpointWs;
if (values.ethGasLimit) options.ethGasLimit = values.ethGasLimit;
if (values.relaychainEndpoint) options.relaychainEndpoint = values.relaychainEndpoint;
if (values.beaconEndpointHttp) options.beaconEndpointHttp = values.beaconEndpointHttp;
if (values.ethWriterEndpoint) options.ethWriterEndpoint = values.ethWriterEndpoint;
if (values.primaryGovernanceChannelId)
options.primaryGovernanceChannelId = values.primaryGovernanceChannelId;
if (values.secondaryGovernanceChannelId)
options.secondaryGovernanceChannelId = values.secondaryGovernanceChannelId;
generateSnowbridgeConfigs(options).catch((error) => {
console.error("Failed to generate Snowbridge configs:", error);
process.exit(1);
});
}

View file

@ -4,7 +4,6 @@ import { createClient } from "polkadot-api";
import { withPolkadotSdkCompat } from "polkadot-api/polkadot-sdk-compat";
import { getWsProvider } from "polkadot-api/ws-provider/web";
import invariant from "tiny-invariant";
import {
confirmWithTimeout,
getEvmEcdsaSigner,
@ -95,7 +94,8 @@ export const setDataHavenParameters = async (
try {
for (const param of parameters) {
logger.info(`Attempting to set parameter: ${String(param.name)} = ${String(param.value)}`);
// TODO: Add a graceful way to print the value of the parameter, since it won't always be representable as a hex string
logger.info(`Attempting to set parameter: ${String(param.name)} = ${param.value.asHex()}`);
const setParameterArgs: any = {
key_value: {

View file

@ -1,100 +0,0 @@
import fs from "node:fs";
import path from "node:path";
import { $ } from "bun";
import { Octokit } from "octokit";
import invariant from "tiny-invariant";
import { logger, printHeader } from "utils";
const IMAGE_NAME = "snowbridge-relay:local";
const RELATIVE_DOCKER_FILE_PATH = "../../docker/SnowbridgeRelayer.dockerfile";
const CONTEXT = "../..";
const TMP_DIR = path.resolve(__dirname, "../tmp");
const RELAY_BINARY_PATH = path.resolve(TMP_DIR, "snowbridge-relay");
//Downloads the latest snowbridge-relay binary from SnowFork's GitHub releases
async function downloadRelayBinary() {
printHeader("Downloading latest snowbridge-relay binary");
if (!fs.existsSync(TMP_DIR)) {
fs.mkdirSync(TMP_DIR, { recursive: true });
}
const octokit = new Octokit();
try {
logger.info("Fetching latest release info from Snowfork/snowbridge");
const latestRelease = await octokit.rest.repos.getLatestRelease({
owner: "Snowfork",
repo: "snowbridge"
});
const tagName = latestRelease.data.tag_name;
logger.info(`🔎 Found latest release: ${tagName}`);
const relayAsset = latestRelease.data.assets.find((asset) => asset.name === "snowbridge-relay");
if (!relayAsset) {
throw new Error("Could not find snowbridge-relay asset in the latest release");
}
logger.info(
`Downloading snowbridge-relay (${Math.round((relayAsset.size / 1024 / 1024) * 100) / 100} MB)`
);
const response = await fetch(relayAsset.browser_download_url);
if (!response.ok) {
throw new Error(`Failed to download: ${response.statusText}`);
}
const buffer = await response.arrayBuffer();
await Bun.write(RELAY_BINARY_PATH, buffer);
await $`chmod +x ${RELAY_BINARY_PATH}`;
logger.success(`Successfully downloaded snowbridge-relay ${tagName} to ${RELAY_BINARY_PATH}`);
return RELAY_BINARY_PATH;
} catch (error: any) {
logger.error(`Failed to download snowbridge-relay: ${error.message}`);
throw error;
}
}
// This can be run with `bun build:docker:relayer` or via a script by importing the below function
export default async function buildRelayer() {
await downloadRelayBinary();
printHeader(`Running docker-build at: ${__dirname}`);
const dockerfilePath = path.resolve(__dirname, RELATIVE_DOCKER_FILE_PATH);
const contextPath = path.resolve(__dirname, CONTEXT);
const file = Bun.file(dockerfilePath);
invariant(await file.exists(), `Dockerfile not found at ${dockerfilePath}`);
logger.debug(`Dockerfile found at ${dockerfilePath}`);
const dockerCommand = `docker build -t ${IMAGE_NAME} -f ${dockerfilePath} ${contextPath}`;
logger.debug(`Executing docker command: ${dockerCommand}`);
const { stdout, stderr, exitCode } = await $`sh -c ${dockerCommand}`.nothrow().quiet();
if (exitCode !== 0) {
logger.error(`Docker build failed with exit code ${exitCode}`);
logger.error(`stdout: ${stdout.toString()}`);
logger.error(`stderr: ${stderr.toString()}`);
process.exit(exitCode);
}
logger.info("Docker build action completed");
const {
exitCode: runExitCode,
stdout: runStdout,
stderr: runStderr
} = await $`sh -c docker run ${IMAGE_NAME}`.quiet().nothrow();
if (runExitCode !== 0) {
logger.error(`Docker run failed with exit code ${runExitCode}`);
logger.error(`stdout: ${runStdout.toString()}`);
logger.error(`stderr: ${runStderr.toString()}`);
process.exit(runExitCode);
}
logger.info("Docker run action completed");
logger.success("Docker image built successfully");
}

View file

@ -66,7 +66,7 @@ export const getServicesFromDocker = async (): Promise<ServiceInfo[]> => {
};
export const getContainersMatchingImage = async (imageName: string) => {
const containers = await docker.listContainers();
const containers = await docker.listContainers({ all: true });
const matches = containers.filter((container) => container.Image.includes(imageName));
return matches;
};

View file

@ -56,7 +56,58 @@ export const BeefyRelayConfigSchema = z.object({
});
export type BeefyRelayConfig = z.infer<typeof BeefyRelayConfigSchema>;
export type RelayerType = "beefy" | "beacon";
export const SolochainRelayConfigSchema = z.object({
source: z.object({
ethereum: z.object({
endpoint: z.string()
}),
solochain: z.object({
endpoint: z.string()
}),
contracts: z.object({
BeefyClient: z.string(),
Gateway: z.string()
}),
beacon: z.object({
endpoint: z.string(),
stateEndpoint: z.string(),
spec: z.object({
syncCommitteeSize: z.number(),
slotsInEpoch: z.number(),
epochsPerSyncCommitteePeriod: z.number(),
forkVersions: z.object({
deneb: z.number(),
electra: z.number()
})
}),
datastore: z.object({
location: z.string(),
maxEntries: z.number()
})
})
}),
sink: z.object({
contracts: z.object({
Gateway: z.string()
}),
ethereum: z.object({
endpoint: z.string()
})
}),
schedule: z.object({
id: z.number(),
totalRelayerCount: z.number(),
sleepInterval: z.number()
}),
"reward-address": z.string(),
ofac: z.object({
enabled: z.boolean(),
apiKey: z.string()
})
});
export type SolochainRelayConfig = z.infer<typeof SolochainRelayConfigSchema>;
export type RelayerType = "beefy" | "beacon" | "solochain";
/**
* Parse beacon relay configuration
@ -80,6 +131,17 @@ function parseBeefyConfig(config: unknown): BeefyRelayConfig {
throw new Error(`Failed to parse config as BeefyRelayConfig: ${result.error.message}`);
}
/**
* Parse solochain relay configuration
*/
function parseSolochainConfig(config: unknown): SolochainRelayConfig {
const result = SolochainRelayConfigSchema.safeParse(config);
if (result.success) {
return result.data;
}
throw new Error(`Failed to parse config as SolochainRelayConfig: ${result.error.message}`);
}
/**
* Type Guard to check if a config object is a BeaconRelayConfig
*/
@ -91,13 +153,18 @@ export function isBeaconConfig(
export function parseRelayConfig(config: unknown, type: "beacon"): BeaconRelayConfig;
export function parseRelayConfig(config: unknown, type: "beefy"): BeefyRelayConfig;
export function parseRelayConfig(config: unknown, type: "solochain"): SolochainRelayConfig;
export function parseRelayConfig(
config: unknown,
type: RelayerType
): BeaconRelayConfig | BeefyRelayConfig;
): BeaconRelayConfig | BeefyRelayConfig | SolochainRelayConfig;
export function parseRelayConfig(
config: unknown,
type: RelayerType
): BeaconRelayConfig | BeefyRelayConfig {
return type === "beacon" ? parseBeaconConfig(config) : parseBeefyConfig(config);
): BeaconRelayConfig | BeefyRelayConfig | SolochainRelayConfig {
return type === "beacon"
? parseBeaconConfig(config)
: type === "beefy"
? parseBeefyConfig(config)
: parseSolochainConfig(config);
}