Skip to content

Commit 68291ba

Browse files
rootsignorecello
authored andcommitted
ready for real-world testing I guess
1 parent e1959ab commit 68291ba

20 files changed

+301
-51
lines changed

tooling/sparta/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
.env
22
node_modules
33
bun.lockb
4+
.vercel
5+
.dist

tooling/sparta/Dockerfile

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
FROM oven/bun:latest
22

3+
RUN apt update && apt install -y curl
4+
RUN curl -fsSL https://get.docker.com | bash
5+
6+
WORKDIR /app
37
COPY package.json ./
48
COPY bun.lockb ./
5-
COPY src ./
9+
COPY src ./src
10+
COPY .env ./
611

712
RUN bun install
13+
CMD ["bun", "run", "start"]

tooling/sparta/README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Sparta
2+
3+
Welcome to Sparta, the Discord bot. It's like having a virtual assistant, but with less features and less judgment.
4+
5+
Here's a quick rundown of what this codebase is all about:
6+
7+
## What is Sparta?
8+
9+
Sparta is a Discord bot that lives to serve (and occasionally sass) testnet participants.
10+
11+
## Features (WIP)
12+
13+
- **Chain Info**: Need to know the latest on your blockchain? Sparta's got you covered with the `/get-info` command. It's like having a blockchain oracle, but without the cryptic riddles.
14+
15+
- **Add Validators**: Are you an S&P Participant and want to be added to the validator set? Just go `/validator add` and send your address, you can then query it with...
16+
17+
- **Check Validators**: ... `/validator check`, which tells you if you're in the validator set (also tells you if you're in the committee)
18+
19+
## Getting Started
20+
21+
To get Sparta up and running, you'll need to:
22+
23+
1. Clone the repo.
24+
2. Install the dependencies with `bun install`.
25+
3. Copy .env.example and set up with your environment stuff
26+
4. Start the bot with `bun run start`.
27+
28+
And just like that, you're ready to unleash Sparta on your Discord server!
29+
30+
## Contributing
31+
32+
Want to make Sparta even better? Feel free to fork the repo and submit a pull request. Just remember, with great power comes great responsibility (and maybe a few more memes).
33+
34+
## License
35+
36+
This project is licensed under the MIT License. Because sharing is caring.
Lines changed: 71 additions & 0 deletions
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+
};
Lines changed: 22 additions & 0 deletions
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+
};

tooling/sparta/dist/commands/index.js

Lines changed: 6 additions & 0 deletions
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+
};
Lines changed: 17 additions & 0 deletions
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+
};

tooling/sparta/dist/env.js

Lines changed: 3 additions & 0 deletions
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;

tooling/sparta/dist/index.js

Lines changed: 46 additions & 0 deletions
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);
Lines changed: 34 additions & 0 deletions
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+
}

0 commit comments

Comments
 (0)