Skip to content

Commit e1959ab

Browse files
author
root
committed
sparta bot initial commit
1 parent 23f2680 commit e1959ab

File tree

13 files changed

+470
-0
lines changed

13 files changed

+470
-0
lines changed

tooling/sparta/.env.example

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
ENVIRONMENT=
2+
3+
DEV_CHANNEL_ID=
4+
DEV_CHANNEL_NAME=
5+
6+
PRODUCTION_CHANNEL_ID=
7+
PRODUCTION_CHANNEL_NAME=
8+
9+
BOT_TOKEN=
10+
BOT_CLIENT_ID=
11+
GUILD_ID=
12+
13+
ETHEREUM_HOST=
14+
ETHEREUM_MNEMONIC=
15+
ETHEREUM_PRIVATE_KEY=
16+
ETHEREUM_ROLLUP_ADDRESS=
17+
ETHEREUM_CHAIN_ID=
18+
ETHEREUM_VALUE=
19+
ETHEREUM_ADMIN_ADDRESS=

tooling/sparta/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.env
2+
node_modules
3+
bun.lockb

tooling/sparta/Dockerfile

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
FROM oven/bun:latest
2+
3+
COPY package.json ./
4+
COPY bun.lockb ./
5+
COPY src ./
6+
7+
RUN bun install

tooling/sparta/package.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "sparta-bot",
3+
"version": "1.0.0",
4+
"type": "module",
5+
"scripts": {
6+
"build": "tsc",
7+
"start": "bun run src/index.ts",
8+
"dev": "bun run --watch src/index.ts",
9+
"deploy": "bun run src/deploy-commands.ts"
10+
},
11+
"dependencies": {
12+
"discord.js": "^14.14.1",
13+
"dotenv": "^16.3.1"
14+
},
15+
"devDependencies": {
16+
"typescript": "^5.3.3",
17+
"@types/node": "^20.10.5",
18+
"ts-node": "^10.9.2",
19+
"bun-types": "latest"
20+
}
21+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { SlashCommandBuilder, ChatInputCommandInteraction } from "discord.js";
2+
import { ValidatorService } from "../services/validator-service";
3+
import { ChainInfoService } from "../services/chaininfo-service";
4+
5+
export default {
6+
data: new SlashCommandBuilder()
7+
.setName("validator")
8+
.setDescription("Manage validator addresses")
9+
.addSubcommand((subcommand) =>
10+
subcommand
11+
.setName("add")
12+
.setDescription("Add yourself to the validator set")
13+
.addStringOption((option) =>
14+
option
15+
.setName("address")
16+
.setDescription("Your validator address")
17+
)
18+
)
19+
.addSubcommand((subcommand) =>
20+
subcommand
21+
.setName("check")
22+
.setDescription("Check if you are a validator")
23+
.addStringOption((option) =>
24+
option
25+
.setName("address")
26+
.setDescription("The validator address to check")
27+
)
28+
),
29+
30+
execute: async (interaction: ChatInputCommandInteraction) => {
31+
const address = interaction.options.getString("address");
32+
if (!address) {
33+
return interaction.reply({
34+
content: "Address is required.",
35+
ephemeral: true,
36+
});
37+
}
38+
39+
// Basic address validation
40+
if (!address.match(/^0x[a-fA-F0-9]{40}$/)) {
41+
return interaction.reply({
42+
content: "Please provide a valid Ethereum address.",
43+
ephemeral: true,
44+
});
45+
}
46+
47+
await interaction.deferReply();
48+
49+
if (interaction.options.getSubcommand() === "add") {
50+
try {
51+
await ValidatorService.addValidator(address);
52+
53+
await interaction.editReply({
54+
content: `Successfully added validator address: ${address}`,
55+
});
56+
} catch (error) {
57+
await interaction.editReply({
58+
content: `Failed to add validator address: ${
59+
error instanceof Error ? error.message : String(error)
60+
}`,
61+
});
62+
}
63+
} else if (interaction.options.getSubcommand() === "check") {
64+
try {
65+
const info = await ChainInfoService.getInfo();
66+
const { validators, committee } = info;
67+
68+
let reply = "";
69+
if (validators.includes(address)) {
70+
reply += "You are a validator\n";
71+
}
72+
if (committee.includes(address)) {
73+
reply += "You are a committee member\n";
74+
}
75+
76+
await interaction.editReply({
77+
content: reply,
78+
});
79+
} catch (error) {
80+
await interaction.editReply({
81+
content: `Failed to check validator address: ${
82+
error instanceof Error ? error.message : String(error)
83+
}`,
84+
});
85+
}
86+
}
87+
},
88+
};
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { SlashCommandBuilder, ChatInputCommandInteraction } from "discord.js";
2+
import { ChainInfoService } from "../services/chaininfo-service";
3+
4+
export default {
5+
data: new SlashCommandBuilder()
6+
.setName("get-info")
7+
.setDescription("Get chain info"),
8+
9+
execute: async (interaction: ChatInputCommandInteraction) => {
10+
await interaction.deferReply();
11+
12+
try {
13+
const {
14+
pendingBlockNum,
15+
provenBlockNum,
16+
currentEpoch,
17+
currentSlot,
18+
proposerNow,
19+
} = await ChainInfoService.getInfo();
20+
21+
await interaction.editReply({
22+
content: `Pending block: ${pendingBlockNum}\nProven block: ${provenBlockNum}\nCurrent epoch: ${currentEpoch}\nCurrent slot: ${currentSlot}\nProposer now: ${proposerNow}`,
23+
});
24+
} catch (error) {
25+
console.error("Error in get-info command:", error);
26+
await interaction.editReply({
27+
content: `Failed to get chain info`,
28+
});
29+
}
30+
},
31+
};

tooling/sparta/src/commands/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import address from "./addValidator";
2+
import chainInfo from "./getChainInfo";
3+
4+
export default {
5+
address,
6+
chainInfo,
7+
};

tooling/sparta/src/deploy-commands.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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+
5+
export const deployCommands = async (): Promise<void> => {
6+
const rest = new REST({ version: "10" }).setToken(BOT_TOKEN as string);
7+
8+
try {
9+
console.log("Started refreshing application (/) commands.");
10+
11+
const commandsData = Object.values(commands).map((command) =>
12+
command.data.toJSON()
13+
);
14+
15+
await rest.put(
16+
Routes.applicationGuildCommands(BOT_CLIENT_ID, GUILD_ID),
17+
{
18+
body: commandsData,
19+
}
20+
);
21+
22+
console.log("Successfully reloaded application (/) commands.");
23+
} catch (error) {
24+
console.error(error);
25+
}
26+
};

tooling/sparta/src/env.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import dotenv from "dotenv";
2+
dotenv.config();
3+
4+
export const {
5+
TOKEN,
6+
CLIENT_ID,
7+
GUILD_ID,
8+
PRODUCTION_CHANNEL_NAME,
9+
DEV_CHANNEL_NAME,
10+
PRODUCTION_CHANNEL_ID,
11+
DEV_CHANNEL_ID,
12+
ETHEREUM_HOST,
13+
ETHEREUM_ROLLUP_ADDRESS,
14+
ETHEREUM_ADMIN_ADDRESS,
15+
ETHEREUM_CHAIN_ID,
16+
ETHEREUM_MNEMONIC,
17+
ETHEREUM_PRIVATE_KEY,
18+
ETHEREUM_VALUE,
19+
BOT_TOKEN,
20+
BOT_CLIENT_ID,
21+
ENVIRONMENT,
22+
} = process.env as {
23+
TOKEN: string;
24+
CLIENT_ID: string;
25+
GUILD_ID: string;
26+
PRODUCTION_CHANNEL_NAME: string;
27+
DEV_CHANNEL_NAME: string;
28+
ETHEREUM_HOST: string;
29+
ETHEREUM_ROLLUP_ADDRESS: string;
30+
ETHEREUM_ADMIN_ADDRESS: string;
31+
ETHEREUM_CHAIN_ID: string;
32+
ETHEREUM_MNEMONIC: string;
33+
ETHEREUM_PRIVATE_KEY: string;
34+
ETHEREUM_VALUE: string;
35+
BOT_TOKEN: string;
36+
PRODUCTION_CHANNEL_ID: string;
37+
DEV_CHANNEL_ID: string;
38+
BOT_CLIENT_ID: string;
39+
ENVIRONMENT: string;
40+
};

tooling/sparta/src/index.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import {
2+
Client,
3+
GatewayIntentBits,
4+
Collection,
5+
Interaction,
6+
MessageFlags,
7+
} from "discord.js";
8+
import { deployCommands } from "./deploy-commands";
9+
import commands from "./commands/index.js";
10+
import {
11+
BOT_TOKEN,
12+
PRODUCTION_CHANNEL_ID,
13+
DEV_CHANNEL_ID,
14+
ENVIRONMENT,
15+
PRODUCTION_CHANNEL_NAME,
16+
DEV_CHANNEL_NAME,
17+
} from "./env.js";
18+
19+
// Extend the Client class to include the commands property
20+
interface ExtendedClient extends Client {
21+
commands: Collection<string, any>;
22+
}
23+
24+
const client = new Client({
25+
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages],
26+
}) as ExtendedClient;
27+
28+
client.commands = new Collection();
29+
30+
for (const command of Object.values(commands)) {
31+
client.commands.set(command.data.name, command);
32+
}
33+
34+
client.once("ready", () => {
35+
console.log("Sparta bot is ready!");
36+
deployCommands();
37+
});
38+
39+
client.on("interactionCreate", async (interaction: Interaction) => {
40+
if (!interaction.isChatInputCommand()) return;
41+
42+
// Determine which channel to use based on environment
43+
const targetChannelId =
44+
ENVIRONMENT === "production" ? PRODUCTION_CHANNEL_ID : DEV_CHANNEL_ID;
45+
46+
// Check if the command is in the correct channel
47+
if (interaction.channelId !== targetChannelId) {
48+
const channelName =
49+
ENVIRONMENT === "production"
50+
? PRODUCTION_CHANNEL_NAME
51+
: DEV_CHANNEL_NAME;
52+
return interaction.reply({
53+
content: `This command can only be used in the ${channelName} channel.`,
54+
flags: MessageFlags.Ephemeral,
55+
});
56+
}
57+
58+
const command = client.commands.get(interaction.commandName);
59+
if (!command) return;
60+
61+
console.log(JSON.stringify(command.execute, null, 2));
62+
63+
try {
64+
console.log("Executing command:", command.data.name);
65+
const response = await command.execute(interaction);
66+
67+
if (typeof response === "string") {
68+
const [
69+
pendingBlock,
70+
provenBlock,
71+
validators,
72+
committee,
73+
archive,
74+
currentEpoch,
75+
currentSlot,
76+
proposerPrevious,
77+
proposerNow,
78+
] = response
79+
.split("\n")
80+
.map((line) => line.trim())
81+
.filter(Boolean);
82+
83+
console.log({
84+
pendingBlock,
85+
provenBlock,
86+
validators,
87+
committee,
88+
archive,
89+
currentEpoch,
90+
currentSlot,
91+
proposerPrevious,
92+
proposerNow,
93+
});
94+
}
95+
} catch (error) {
96+
console.error(error);
97+
await interaction.reply({
98+
content: "There was an error executing this command!",
99+
flags: MessageFlags.Ephemeral,
100+
});
101+
}
102+
});
103+
104+
client.login(BOT_TOKEN);

0 commit comments

Comments
 (0)