Skip to content

Commit

Permalink
ready for real-world testing I guess
Browse files Browse the repository at this point in the history
  • Loading branch information
root authored and signorecello committed Jan 16, 2025
1 parent e1959ab commit 68291ba
Show file tree
Hide file tree
Showing 20 changed files with 301 additions and 51 deletions.
2 changes: 2 additions & 0 deletions tooling/sparta/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.env
node_modules
bun.lockb
.vercel
.dist
8 changes: 7 additions & 1 deletion tooling/sparta/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
FROM oven/bun:latest

RUN apt update && apt install -y curl
RUN curl -fsSL https://get.docker.com | bash

WORKDIR /app
COPY package.json ./
COPY bun.lockb ./
COPY src ./
COPY src ./src
COPY .env ./

RUN bun install
CMD ["bun", "run", "start"]
36 changes: 36 additions & 0 deletions tooling/sparta/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Sparta

Welcome to Sparta, the Discord bot. It's like having a virtual assistant, but with less features and less judgment.

Here's a quick rundown of what this codebase is all about:

## What is Sparta?

Sparta is a Discord bot that lives to serve (and occasionally sass) testnet participants.

## Features (WIP)

- **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.

- **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...

- **Check Validators**: ... `/validator check`, which tells you if you're in the validator set (also tells you if you're in the committee)

## Getting Started

To get Sparta up and running, you'll need to:

1. Clone the repo.
2. Install the dependencies with `bun install`.
3. Copy .env.example and set up with your environment stuff
4. Start the bot with `bun run start`.

And just like that, you're ready to unleash Sparta on your Discord server!

## Contributing

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).

## License

This project is licensed under the MIT License. Because sharing is caring.
71 changes: 71 additions & 0 deletions tooling/sparta/dist/commands/addValidator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { SlashCommandBuilder } from "discord.js";
import { ValidatorService } from "../services/validator-service.js";
import { ChainInfoService } from "../services/chaininfo-service.js";
export default {
data: new SlashCommandBuilder()
.setName("validator")
.setDescription("Manage validator addresses")
.addSubcommand((subcommand) => subcommand
.setName("add")
.setDescription("Add yourself to the validator set")
.addStringOption((option) => option
.setName("address")
.setDescription("Your validator address")))
.addSubcommand((subcommand) => subcommand
.setName("check")
.setDescription("Check if you are a validator")
.addStringOption((option) => option
.setName("address")
.setDescription("The validator address to check"))),
execute: async (interaction) => {
const address = interaction.options.getString("address");
if (!address) {
return interaction.reply({
content: "Address is required.",
ephemeral: true,
});
}
// Basic address validation
if (!address.match(/^0x[a-fA-F0-9]{40}$/)) {
return interaction.reply({
content: "Please provide a valid Ethereum address.",
ephemeral: true,
});
}
await interaction.deferReply();
if (interaction.options.getSubcommand() === "add") {
try {
await ValidatorService.addValidator(address);
await interaction.editReply({
content: `Successfully added validator address: ${address}`,
});
}
catch (error) {
await interaction.editReply({
content: `Failed to add validator address: ${error instanceof Error ? error.message : String(error)}`,
});
}
}
else if (interaction.options.getSubcommand() === "check") {
try {
const info = await ChainInfoService.getInfo();
const { validators, committee } = info;
let reply = "";
if (validators.includes(address)) {
reply += "You are a validator\n";
}
if (committee.includes(address)) {
reply += "You are a committee member\n";
}
await interaction.editReply({
content: reply,
});
}
catch (error) {
await interaction.editReply({
content: `Failed to check validator address: ${error instanceof Error ? error.message : String(error)}`,
});
}
}
},
};
22 changes: 22 additions & 0 deletions tooling/sparta/dist/commands/getChainInfo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { SlashCommandBuilder } from "discord.js";
import { ChainInfoService } from "../services/chaininfo-service.js";
export default {
data: new SlashCommandBuilder()
.setName("get-info")
.setDescription("Get chain info"),
execute: async (interaction) => {
await interaction.deferReply();
try {
const { pendingBlockNum, provenBlockNum, currentEpoch, currentSlot, proposerNow, } = await ChainInfoService.getInfo();
await interaction.editReply({
content: `Pending block: ${pendingBlockNum}\nProven block: ${provenBlockNum}\nCurrent epoch: ${currentEpoch}\nCurrent slot: ${currentSlot}\nProposer now: ${proposerNow}`,
});
}
catch (error) {
console.error("Error in get-info command:", error);
await interaction.editReply({
content: `Failed to get chain info`,
});
}
},
};
6 changes: 6 additions & 0 deletions tooling/sparta/dist/commands/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import addValidator from "./addValidator.js";
import getChainInfo from "./getChainInfo.js";
export default {
addValidator,
getChainInfo,
};
17 changes: 17 additions & 0 deletions tooling/sparta/dist/deploy-commands.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { REST, Routes } from "discord.js";
import commands from "./commands/index.js";
import { BOT_TOKEN, BOT_CLIENT_ID, GUILD_ID } from "./env.js";
export const deployCommands = async () => {
const rest = new REST({ version: "10" }).setToken(BOT_TOKEN);
try {
console.log("Started refreshing application (/) commands.");
const commandsData = Object.values(commands).map((command) => command.data.toJSON());
await rest.put(Routes.applicationGuildCommands(BOT_CLIENT_ID, GUILD_ID), {
body: commandsData,
});
console.log("Successfully reloaded application (/) commands.");
}
catch (error) {
console.error(error);
}
};
3 changes: 3 additions & 0 deletions tooling/sparta/dist/env.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import dotenv from "dotenv";
dotenv.config();
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;
46 changes: 46 additions & 0 deletions tooling/sparta/dist/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Client, GatewayIntentBits, Collection, MessageFlags, } from "discord.js";
import { deployCommands } from "./deploy-commands.js";
import commands from "./commands/index.js";
import { BOT_TOKEN, PRODUCTION_CHANNEL_ID, DEV_CHANNEL_ID, ENVIRONMENT, PRODUCTION_CHANNEL_NAME, DEV_CHANNEL_NAME, } from "./env.js";
const client = new Client({
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages],
});
client.commands = new Collection();
for (const command of Object.values(commands)) {
client.commands.set(command.data.name, command);
}
client.once("ready", () => {
console.log("Sparta bot is ready!");
deployCommands();
});
client.on("interactionCreate", async (interaction) => {
if (!interaction.isChatInputCommand())
return;
// Determine which channel to use based on environment
const targetChannelId = ENVIRONMENT === "production" ? PRODUCTION_CHANNEL_ID : DEV_CHANNEL_ID;
// Check if the command is in the correct channel
if (interaction.channelId !== targetChannelId) {
const channelName = ENVIRONMENT === "production"
? PRODUCTION_CHANNEL_NAME
: DEV_CHANNEL_NAME;
return interaction.reply({
content: `This command can only be used in the ${channelName} channel.`,
flags: MessageFlags.Ephemeral,
});
}
const command = client.commands.get(interaction.commandName);
if (!command)
return;
try {
console.log("Executing command:", command.data.name);
const response = await command.execute(interaction);
}
catch (error) {
console.error(error);
await interaction.reply({
content: "There was an error executing this command!",
flags: MessageFlags.Ephemeral,
});
}
});
client.login(BOT_TOKEN);
34 changes: 34 additions & 0 deletions tooling/sparta/dist/services/chaininfo-service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { exec } from "child_process";
import { promisify } from "util";
import { ETHEREUM_HOST, ETHEREUM_ROLLUP_ADDRESS, ETHEREUM_CHAIN_ID, } from "../env.js";
const execAsync = promisify(exec);
export class ChainInfoService {
static async getInfo() {
try {
// Add validator to the set
const command = `docker run --rm aztecprotocol/aztec:unhinged-unicorn debug-rollup -u ${ETHEREUM_HOST} --rollup ${ETHEREUM_ROLLUP_ADDRESS} --l1-chain-id ${ETHEREUM_CHAIN_ID} `;
const { stdout, stderr } = await execAsync(command);
if (stderr) {
throw new Error(stderr);
}
// looks like hell, but it just parses the output of the command
// into a key-value object
const info = stdout
.split("\n")
.map((line) => line.trim())
.filter(Boolean)
.reduce((acc, s) => {
const [key, value] = s.split(": ");
const sanitizedKey = key
.toLowerCase()
.replace(/\s+(.)/g, (_, c) => c.toUpperCase());
return { ...acc, [sanitizedKey]: value };
}, {});
return info;
}
catch (error) {
console.error("Error getting chain info:", error);
throw error;
}
}
}
37 changes: 37 additions & 0 deletions tooling/sparta/dist/services/validator-service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { exec } from "child_process";
import { promisify } from "util";
import { ETHEREUM_HOST, ETHEREUM_ROLLUP_ADDRESS, ETHEREUM_ADMIN_ADDRESS, ETHEREUM_CHAIN_ID, ETHEREUM_MNEMONIC, ETHEREUM_PRIVATE_KEY, ETHEREUM_VALUE, } from "../env.js";
const execAsync = promisify(exec);
export class ValidatorService {
static async addValidator(address) {
try {
// Send ETH to the validator address
await this.fundValidator(address);
// Add validator to the set
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}"`;
const { stdout, stderr } = await execAsync(command);
if (stderr) {
throw new Error(stderr);
}
return stdout;
}
catch (error) {
console.error("Error adding validator:", error);
throw error;
}
}
static async fundValidator(address) {
try {
const command = `cast send --value ${ETHEREUM_VALUE} --rpc-url ${ETHEREUM_HOST} --chain-id ${ETHEREUM_CHAIN_ID} --private-key ${ETHEREUM_PRIVATE_KEY} ${address}`;
const { stdout, stderr } = await execAsync(command);
if (stderr) {
throw new Error(stderr);
}
return stdout;
}
catch (error) {
console.error("Error funding validator:", error);
throw error;
}
}
}
7 changes: 7 additions & 0 deletions tooling/sparta/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
name: Sparta
services:
sparta:
volumes:
- /var/run/docker.sock:/var/run/docker.sock
build:
context: .
7 changes: 2 additions & 5 deletions tooling/sparta/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@
"type": "module",
"scripts": {
"build": "tsc",
"start": "bun run src/index.ts",
"dev": "bun run --watch src/index.ts",
"deploy": "bun run src/deploy-commands.ts"
"start": "bun run src/index.ts"
},
"dependencies": {
"discord.js": "^14.14.1",
Expand All @@ -15,7 +13,6 @@
"devDependencies": {
"typescript": "^5.3.3",
"@types/node": "^20.10.5",
"ts-node": "^10.9.2",
"bun-types": "latest"
"ts-node": "^10.9.2"
}
}
4 changes: 2 additions & 2 deletions tooling/sparta/src/commands/addValidator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from "discord.js";
import { ValidatorService } from "../services/validator-service";
import { ChainInfoService } from "../services/chaininfo-service";
import { ValidatorService } from "../services/validator-service.js";
import { ChainInfoService } from "../services/chaininfo-service.js";

export default {
data: new SlashCommandBuilder()
Expand Down
2 changes: 1 addition & 1 deletion tooling/sparta/src/commands/getChainInfo.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from "discord.js";
import { ChainInfoService } from "../services/chaininfo-service";
import { ChainInfoService } from "../services/chaininfo-service.js";

export default {
data: new SlashCommandBuilder()
Expand Down
8 changes: 4 additions & 4 deletions tooling/sparta/src/commands/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import address from "./addValidator";
import chainInfo from "./getChainInfo";
import addValidator from "./addValidator.js";
import getChainInfo from "./getChainInfo.js";

export default {
address,
chainInfo,
addValidator,
getChainInfo,
};
Loading

0 comments on commit 68291ba

Please sign in to comment.