Skip to content

Commit f62eff7

Browse files
committed
fly bun, fly
1 parent d9e6ee8 commit f62eff7

10 files changed

+676
-6
lines changed

Diff for: tooling/sparta/dist/commands/addValidator.js

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { SlashCommandBuilder } from "discord.js";
2+
import { ValidatorService } from "../services/validator-service.js";
3+
import { ChainInfoService } from "../services/chaininfo-service.js";
4+
export default {
5+
data: new SlashCommandBuilder()
6+
.setName("validator")
7+
.setDescription("Manage validator addresses")
8+
.addSubcommand((subcommand) => subcommand
9+
.setName("add")
10+
.setDescription("Add yourself to the validator set")
11+
.addStringOption((option) => option
12+
.setName("address")
13+
.setDescription("Your validator address")))
14+
.addSubcommand((subcommand) => subcommand
15+
.setName("check")
16+
.setDescription("Check if you are a validator")
17+
.addStringOption((option) => option
18+
.setName("address")
19+
.setDescription("The validator address to check"))),
20+
execute: async (interaction) => {
21+
const address = interaction.options.getString("address");
22+
if (!address) {
23+
return interaction.reply({
24+
content: "Address is required.",
25+
ephemeral: true,
26+
});
27+
}
28+
// Basic address validation
29+
if (!address.match(/^0x[a-fA-F0-9]{40}$/)) {
30+
return interaction.reply({
31+
content: "Please provide a valid Ethereum address.",
32+
ephemeral: true,
33+
});
34+
}
35+
await interaction.deferReply();
36+
if (interaction.options.getSubcommand() === "add") {
37+
try {
38+
await ValidatorService.addValidator(address);
39+
await interaction.editReply({
40+
content: `Successfully added validator address: ${address}`,
41+
});
42+
}
43+
catch (error) {
44+
await interaction.editReply({
45+
content: `Failed to add validator address: ${error instanceof Error ? error.message : String(error)}`,
46+
});
47+
}
48+
}
49+
else if (interaction.options.getSubcommand() === "check") {
50+
try {
51+
const info = await ChainInfoService.getInfo();
52+
const { validators, committee } = info;
53+
let reply = "";
54+
if (validators.includes(address)) {
55+
reply += "You are a validator\n";
56+
}
57+
if (committee.includes(address)) {
58+
reply += "You are a committee member\n";
59+
}
60+
await interaction.editReply({
61+
content: reply,
62+
});
63+
}
64+
catch (error) {
65+
await interaction.editReply({
66+
content: `Failed to check validator address: ${error instanceof Error ? error.message : String(error)}`,
67+
});
68+
}
69+
}
70+
},
71+
};

Diff for: tooling/sparta/dist/commands/getChainInfo.js

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { SlashCommandBuilder } from "discord.js";
2+
import { ChainInfoService } from "../services/chaininfo-service.js";
3+
export default {
4+
data: new SlashCommandBuilder()
5+
.setName("get-info")
6+
.setDescription("Get chain info"),
7+
execute: async (interaction) => {
8+
await interaction.deferReply();
9+
try {
10+
const { pendingBlockNum, provenBlockNum, currentEpoch, currentSlot, proposerNow, } = await ChainInfoService.getInfo();
11+
await interaction.editReply({
12+
content: `Pending block: ${pendingBlockNum}\nProven block: ${provenBlockNum}\nCurrent epoch: ${currentEpoch}\nCurrent slot: ${currentSlot}\nProposer now: ${proposerNow}`,
13+
});
14+
}
15+
catch (error) {
16+
console.error("Error in get-info command:", error);
17+
await interaction.editReply({
18+
content: `Failed to get chain info`,
19+
});
20+
}
21+
},
22+
};

Diff for: tooling/sparta/dist/commands/index.js

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import addValidator from "./addValidator.js";
2+
import getChainInfo from "./getChainInfo.js";
3+
export default {
4+
addValidator,
5+
getChainInfo,
6+
};

Diff for: tooling/sparta/dist/deploy-commands.js

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { REST, Routes } from "discord.js";
2+
import commands from "./commands/index.js";
3+
import { BOT_TOKEN, BOT_CLIENT_ID, GUILD_ID } from "./env.js";
4+
export const deployCommands = async () => {
5+
const rest = new REST({ version: "10" }).setToken(BOT_TOKEN);
6+
try {
7+
console.log("Started refreshing application (/) commands.");
8+
const commandsData = Object.values(commands).map((command) => command.data.toJSON());
9+
await rest.put(Routes.applicationGuildCommands(BOT_CLIENT_ID, GUILD_ID), {
10+
body: commandsData,
11+
});
12+
console.log("Successfully reloaded application (/) commands.");
13+
}
14+
catch (error) {
15+
console.error(error);
16+
}
17+
};

Diff for: tooling/sparta/dist/env.js

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import dotenv from "dotenv";
2+
dotenv.config();
3+
export const { TOKEN, CLIENT_ID, GUILD_ID, PRODUCTION_CHANNEL_NAME, DEV_CHANNEL_NAME, PRODUCTION_CHANNEL_ID, DEV_CHANNEL_ID, ETHEREUM_HOST, ETHEREUM_ROLLUP_ADDRESS, ETHEREUM_ADMIN_ADDRESS, ETHEREUM_CHAIN_ID, ETHEREUM_MNEMONIC, ETHEREUM_PRIVATE_KEY, ETHEREUM_VALUE, BOT_TOKEN, BOT_CLIENT_ID, ENVIRONMENT, } = process.env;

Diff for: tooling/sparta/dist/index.js

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { Client, GatewayIntentBits, Collection, MessageFlags, } from "discord.js";
2+
import { deployCommands } from "./deploy-commands.js";
3+
import commands from "./commands/index.js";
4+
import { BOT_TOKEN, PRODUCTION_CHANNEL_ID, DEV_CHANNEL_ID, ENVIRONMENT, PRODUCTION_CHANNEL_NAME, DEV_CHANNEL_NAME, } from "./env.js";
5+
const client = new Client({
6+
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages],
7+
});
8+
client.commands = new Collection();
9+
for (const command of Object.values(commands)) {
10+
client.commands.set(command.data.name, command);
11+
}
12+
client.once("ready", () => {
13+
console.log("Sparta bot is ready!");
14+
deployCommands();
15+
});
16+
client.on("interactionCreate", async (interaction) => {
17+
if (!interaction.isChatInputCommand())
18+
return;
19+
// Determine which channel to use based on environment
20+
const targetChannelId = ENVIRONMENT === "production" ? PRODUCTION_CHANNEL_ID : DEV_CHANNEL_ID;
21+
// Check if the command is in the correct channel
22+
if (interaction.channelId !== targetChannelId) {
23+
const channelName = ENVIRONMENT === "production"
24+
? PRODUCTION_CHANNEL_NAME
25+
: DEV_CHANNEL_NAME;
26+
return interaction.reply({
27+
content: `This command can only be used in the ${channelName} channel.`,
28+
flags: MessageFlags.Ephemeral,
29+
});
30+
}
31+
const command = client.commands.get(interaction.commandName);
32+
if (!command)
33+
return;
34+
try {
35+
console.log("Executing command:", command.data.name);
36+
const response = await command.execute(interaction);
37+
}
38+
catch (error) {
39+
console.error(error);
40+
await interaction.reply({
41+
content: "There was an error executing this command!",
42+
flags: MessageFlags.Ephemeral,
43+
});
44+
}
45+
});
46+
client.login(BOT_TOKEN);

Diff for: tooling/sparta/dist/services/chaininfo-service.js

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { exec } from "child_process";
2+
import { promisify } from "util";
3+
import { ETHEREUM_HOST, ETHEREUM_ROLLUP_ADDRESS, ETHEREUM_CHAIN_ID, } from "../env.js";
4+
const execAsync = promisify(exec);
5+
export class ChainInfoService {
6+
static async getInfo() {
7+
try {
8+
// Add validator to the set
9+
const command = `docker run --rm aztecprotocol/aztec:unhinged-unicorn debug-rollup -u ${ETHEREUM_HOST} --rollup ${ETHEREUM_ROLLUP_ADDRESS} --l1-chain-id ${ETHEREUM_CHAIN_ID} `;
10+
const { stdout, stderr } = await execAsync(command);
11+
if (stderr) {
12+
throw new Error(stderr);
13+
}
14+
// looks like hell, but it just parses the output of the command
15+
// into a key-value object
16+
const info = stdout
17+
.split("\n")
18+
.map((line) => line.trim())
19+
.filter(Boolean)
20+
.reduce((acc, s) => {
21+
const [key, value] = s.split(": ");
22+
const sanitizedKey = key
23+
.toLowerCase()
24+
.replace(/\s+(.)/g, (_, c) => c.toUpperCase());
25+
return { ...acc, [sanitizedKey]: value };
26+
}, {});
27+
return info;
28+
}
29+
catch (error) {
30+
console.error("Error getting chain info:", error);
31+
throw error;
32+
}
33+
}
34+
}

Diff for: tooling/sparta/dist/services/validator-service.js

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { exec } from "child_process";
2+
import { promisify } from "util";
3+
import { ETHEREUM_HOST, ETHEREUM_ROLLUP_ADDRESS, ETHEREUM_ADMIN_ADDRESS, ETHEREUM_CHAIN_ID, ETHEREUM_MNEMONIC, ETHEREUM_PRIVATE_KEY, ETHEREUM_VALUE, } from "../env.js";
4+
const execAsync = promisify(exec);
5+
export class ValidatorService {
6+
static async addValidator(address) {
7+
try {
8+
// Send ETH to the validator address
9+
await this.fundValidator(address);
10+
// Add validator to the set
11+
const command = `docker run --rm aztecprotocol/aztec:unhinged-unicorn add-l1-validator -u ${ETHEREUM_HOST} --validator ${address} --rollup ${ETHEREUM_ROLLUP_ADDRESS} --withdrawer ${ETHEREUM_ADMIN_ADDRESS} --l1-chain-id ${ETHEREUM_CHAIN_ID} --mnemonic "${ETHEREUM_MNEMONIC}"`;
12+
const { stdout, stderr } = await execAsync(command);
13+
if (stderr) {
14+
throw new Error(stderr);
15+
}
16+
return stdout;
17+
}
18+
catch (error) {
19+
console.error("Error adding validator:", error);
20+
throw error;
21+
}
22+
}
23+
static async fundValidator(address) {
24+
try {
25+
const command = `cast send --value ${ETHEREUM_VALUE} --rpc-url ${ETHEREUM_HOST} --chain-id ${ETHEREUM_CHAIN_ID} --private-key ${ETHEREUM_PRIVATE_KEY} ${address}`;
26+
const { stdout, stderr } = await execAsync(command);
27+
if (stderr) {
28+
throw new Error(stderr);
29+
}
30+
return stdout;
31+
}
32+
catch (error) {
33+
console.error("Error funding validator:", error);
34+
throw error;
35+
}
36+
}
37+
}

0 commit comments

Comments
 (0)