Skip to content

Commit 985e2d5

Browse files
committed
api refac
1 parent aaf9156 commit 985e2d5

File tree

12 files changed

+1151
-396
lines changed

12 files changed

+1151
-396
lines changed

.github/workflows/sparta.yml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,6 @@ jobs:
2626
TF_VAR_environment: "production"
2727
TF_VAR_bot_client_id: "1329079356785688616"
2828
TF_VAR_guild_id: "1144692727120937080"
29-
TF_VAR_passport_verified_role_id: "1364982673604345886"
30-
TF_VAR_minimum_score: "10"
31-
TF_VAR_passport_scorer_id: "11493"
32-
TF_VAR_vite_reown_project_id: "d037e9da5c5c9b24cfcd94c509d88dce"
3329
TF_VAR_staking_asset_handler_address: "0xF739D03e98e23A7B65940848aBA8921fF3bAc4b2"
3430
TF_VAR_l1_chain_id: "11155111"
3531
TF_VAR_local_dynamo_db: "false"

tooling/sparta/packages/discord/src/slashCommands/moderators/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { isOperatorInSet } from "./operator-in-set.js";
1313
import { isOperatorAttesting } from "./operator-attesting.js";
1414
import { showModeratorHelp } from "./help.js";
1515
import { checkModeratorPermissions } from "../../utils/index.js";
16+
import { approveUser } from "./operator-approve.js";
1617

1718
export default {
1819
data: new SlashCommandBuilder()
@@ -42,6 +43,17 @@ export default {
4243
.setRequired(true)
4344
)
4445
)
46+
.addSubcommand((subcommand) =>
47+
subcommand
48+
.setName(ModeratorSubcommands.Approve)
49+
.setDescription("Approve a user to join the validator set")
50+
.addStringOption((option) =>
51+
option
52+
.setName("user")
53+
.setDescription("The Discord username of the user to approve")
54+
.setRequired(true)
55+
)
56+
)
4557
.addSubcommand((subcommand) =>
4658
subcommand
4759
.setName(ModeratorSubcommands.Help)
@@ -68,6 +80,9 @@ export default {
6880
case ModeratorSubcommands.IsAttesting:
6981
await isOperatorAttesting(interaction);
7082
break;
83+
case ModeratorSubcommands.Approve:
84+
await approveUser(interaction);
85+
break;
7186
case ModeratorSubcommands.Help:
7287
await showModeratorHelp(interaction);
7388
break;
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { ChatInputCommandInteraction, EmbedBuilder } from "discord.js";
2+
import { logger } from "@sparta/utils";
3+
import * as dotenv from "dotenv";
4+
import { clientPromise } from "../../api/axios";
5+
6+
// Load environment variables
7+
dotenv.config();
8+
9+
/**
10+
* Approves a node operator by setting their isApproved flag to true
11+
*/
12+
export async function approveUser(
13+
interaction: ChatInputCommandInteraction
14+
) {
15+
try {
16+
// Get Discord username from options
17+
const discordUsername = interaction.options.getString("user");
18+
19+
if (!discordUsername) {
20+
await interaction.editReply("Please provide a Discord username.");
21+
return;
22+
}
23+
24+
try {
25+
const client = await clientPromise;
26+
27+
// Call the approve endpoint with the Discord username as query parameter
28+
try {
29+
await client.approveOperator({
30+
discordUsername: discordUsername
31+
});
32+
33+
const embed = new EmbedBuilder()
34+
.setTitle("✅ OPERATOR APPROVED")
35+
.setColor(0x00ff00) // Green for success
36+
.setDescription(`Discord Username: \`${discordUsername}\``)
37+
.addFields([
38+
{
39+
name: "Status",
40+
value: "Operator has been successfully approved.",
41+
}
42+
]);
43+
44+
await interaction.editReply({ embeds: [embed] });
45+
return "APPROVED";
46+
} catch (approvalError: any) {
47+
logger.error("Error approving operator:", approvalError);
48+
49+
// If 404, operator not found
50+
if (approvalError.response && approvalError.response.status === 404) {
51+
const embed = new EmbedBuilder()
52+
.setTitle("❌ APPROVAL FAILED")
53+
.setColor(0xff0000) // Red for failure
54+
.setDescription(`No node operator found with Discord username: \`${discordUsername}\``)
55+
.addFields([
56+
{
57+
name: "Error",
58+
value: "This Discord username is not registered in our database.",
59+
}
60+
]);
61+
62+
await interaction.editReply({ embeds: [embed] });
63+
return "NOT_FOUND";
64+
}
65+
66+
// Other errors
67+
const embed = new EmbedBuilder()
68+
.setTitle("❌ APPROVAL FAILED")
69+
.setColor(0xff0000) // Red for failure
70+
.setDescription(`Error approving operator with Discord username: \`${discordUsername}\``)
71+
.addFields([
72+
{
73+
name: "Error",
74+
value: "There was an error with the approval process.",
75+
}
76+
]);
77+
78+
await interaction.editReply({ embeds: [embed] });
79+
return "APPROVAL_ERROR";
80+
}
81+
82+
} catch (apiError) {
83+
logger.error("Error with operator API:", apiError);
84+
85+
const embed = new EmbedBuilder()
86+
.setTitle("❌ APPROVAL FAILED")
87+
.setColor(0xff0000) // Red for failure
88+
.setDescription(`API service error when approving Discord username: \`${discordUsername}\``)
89+
.addFields([
90+
{
91+
name: "Error",
92+
value: "The operator service is currently unavailable.",
93+
}
94+
]);
95+
96+
await interaction.editReply({ embeds: [embed] });
97+
return "API_ERROR";
98+
}
99+
} catch (error) {
100+
logger.error("Error executing approve user command:", error);
101+
await interaction.editReply("Error approving operator.");
102+
throw error;
103+
}
104+
}

tooling/sparta/packages/discord/src/slashCommands/operators/register.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export async function registerValidator(
6969
try {
7070
// Step 1: Check if operator exists with this discordId
7171
const { data: operatorData } =
72-
await client.getOperatorByDiscordId({
72+
await client.getOperator({
7373
discordId,
7474
});
7575

@@ -78,7 +78,7 @@ export async function registerValidator(
7878
operatorData.walletAddress !== address
7979
) {
8080
// Update wallet if it's a different address
81-
await client.updateOperatorWallet(
81+
await client.updateOperator(
8282
{
8383
discordId: operatorData.discordId,
8484
},
@@ -96,6 +96,7 @@ export async function registerValidator(
9696
await client.createOperator(null, {
9797
discordId,
9898
walletAddress: address,
99+
discordUsername: interaction.user.username,
99100
});
100101
operationResult = "created";
101102
} else {

tooling/sparta/packages/discord/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export enum ModeratorSubcommandGroups {
1212
export enum ModeratorSubcommands {
1313
IsInSet = "is-in-set",
1414
IsAttesting = "is-attesting",
15+
Approve = "approve",
1516
Help = "help",
1617
}
1718

tooling/sparta/packages/express/src/db/nodeOperatorRepository.ts

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,35 @@ export class NodeOperatorRepository {
9696
}
9797
}
9898

99+
async findByDiscordUsername(
100+
discordUsername: string
101+
): Promise<NodeOperator | undefined> {
102+
try {
103+
// Scan the table with a filter on the discordUsername field
104+
const command = new ScanCommand({
105+
TableName: this.tableName,
106+
FilterExpression: "discordUsername = :discordUsername",
107+
ExpressionAttributeValues: {
108+
":discordUsername": discordUsername,
109+
},
110+
});
111+
const response = await this.client.send(command);
112+
113+
// Return the first matching item, if any
114+
return response.Items && response.Items.length > 0
115+
? (response.Items[0] as NodeOperator)
116+
: undefined;
117+
} catch (error) {
118+
logger.error(
119+
{ error, discordUsername, tableName: this.tableName },
120+
"Error retrieving NodeOperator by Discord username in repository"
121+
);
122+
throw new Error(
123+
"Repository failed to retrieve node operator by Discord username."
124+
);
125+
}
126+
}
127+
99128
async findByWalletAddress(
100129
walletAddress: string
101130
): Promise<NodeOperator | undefined> {
@@ -144,12 +173,16 @@ export class NodeOperatorRepository {
144173

145174
async create(
146175
discordId: string,
147-
walletAddress: string
176+
walletAddress: string,
177+
discordUsername?: string,
178+
isApproved?: boolean
148179
): Promise<NodeOperator> {
149180
const now = Date.now();
150181
const newOperator: NodeOperator = {
151182
discordId,
152183
walletAddress, // Consider normalizing address before saving
184+
...(discordUsername && { discordUsername }),
185+
...(isApproved !== undefined && { isApproved }),
153186
createdAt: now,
154187
updatedAt: now,
155188
};
@@ -162,13 +195,13 @@ export class NodeOperatorRepository {
162195
});
163196
await this.client.send(command);
164197
logger.info(
165-
{ discordId, walletAddress, tableName: this.tableName },
198+
{ discordId, walletAddress, discordUsername, tableName: this.tableName },
166199
"Created new NodeOperator in repository"
167200
);
168201
return newOperator;
169202
} catch (error: any) {
170203
logger.error(
171-
{ error, discordId, walletAddress, tableName: this.tableName },
204+
{ error, discordId, walletAddress, discordUsername, tableName: this.tableName },
172205
"Error creating NodeOperator in repository"
173206
);
174207
// Re-throw specific error types if needed for service layer handling
@@ -223,6 +256,48 @@ export class NodeOperatorRepository {
223256
}
224257
}
225258

259+
async updateApprovalStatus(
260+
discordId: string,
261+
isApproved: boolean
262+
): Promise<boolean> {
263+
try {
264+
const command = new UpdateCommand({
265+
TableName: this.tableName,
266+
Key: { discordId },
267+
UpdateExpression:
268+
"SET isApproved = :isApproved, updatedAt = :updatedAt",
269+
ConditionExpression: "attribute_exists(discordId)",
270+
ExpressionAttributeValues: {
271+
":isApproved": isApproved,
272+
":updatedAt": Date.now(),
273+
},
274+
ReturnValues: "NONE",
275+
});
276+
await this.client.send(command);
277+
logger.info(
278+
{ discordId, isApproved, tableName: this.tableName },
279+
"Updated NodeOperator approval status in repository"
280+
);
281+
return true;
282+
} catch (error: any) {
283+
logger.error(
284+
{
285+
error,
286+
discordId,
287+
isApproved,
288+
tableName: this.tableName,
289+
},
290+
"Error updating NodeOperator approval status in repository"
291+
);
292+
if (error.name === "ConditionalCheckFailedException") {
293+
return false; // Operator not found
294+
}
295+
throw new Error(
296+
"Repository failed to update node operator approval status."
297+
);
298+
}
299+
}
300+
226301
async deleteByDiscordId(discordId: string): Promise<boolean> {
227302
try {
228303
const command = new DeleteCommand({

tooling/sparta/packages/express/src/domain/operators/service.ts

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
export interface NodeOperator {
88
discordId: string; // Primary Key
99
walletAddress: string; // GSI Partition Key: WalletAddressIndex
10+
discordUsername?: string; // Optional Discord username
1011
createdAt: number;
1112
updatedAt: number;
1213
}
@@ -67,6 +68,25 @@ class NodeOperatorService {
6768
}
6869
}
6970

71+
/**
72+
* Retrieves a node operator by their Discord username.
73+
* @param discordUsername The Discord username.
74+
* @returns The NodeOperator object or undefined if not found.
75+
*/
76+
public async getOperatorByDiscordUsername(
77+
discordUsername: string
78+
): Promise<NodeOperator | undefined> {
79+
try {
80+
return await this.repository.findByDiscordUsername(discordUsername);
81+
} catch (error) {
82+
logger.error(
83+
{ error, discordUsername },
84+
"Service error getting operator by Discord username"
85+
);
86+
throw error;
87+
}
88+
}
89+
7090
/**
7191
* Retrieves a node operator by their wallet address.
7292
* @param walletAddress The wallet address.
@@ -91,18 +111,21 @@ class NodeOperatorService {
91111
* Creates a new node operator.
92112
* @param discordId The Discord ID.
93113
* @param walletAddress The wallet address.
114+
* @param discordUsername The Discord username.
94115
* @returns The created NodeOperator object or undefined if creation failed (e.g., duplicate discordId).
95116
*/
96117
public async createOperator(
97118
discordId: string,
98-
walletAddress: string
119+
walletAddress: string,
120+
discordUsername?: string,
121+
isApproved?: boolean
99122
): Promise<NodeOperator | undefined> {
100123
try {
101124
// Add any service-level validation or transformation here
102-
return await this.repository.create(discordId, walletAddress);
125+
return await this.repository.create(discordId, walletAddress, discordUsername, isApproved);
103126
} catch (error: any) {
104127
logger.error(
105-
{ error: error.message, discordId, walletAddress }, // Log error message
128+
{ error: error.message, discordId, walletAddress, discordUsername }, // Log error message
106129
"Service error creating operator"
107130
);
108131
// Check for specific repository errors (like duplicate) and handle
@@ -159,6 +182,32 @@ class NodeOperatorService {
159182
throw error;
160183
}
161184
}
185+
186+
/**
187+
* Updates the approval status for a node operator.
188+
* @param discordId The Discord ID of the operator to update.
189+
* @param isApproved The approval status to set.
190+
* @returns True if the update was successful, false otherwise (e.g., operator not found).
191+
*/
192+
public async updateApprovalStatus(
193+
discordId: string,
194+
isApproved: boolean
195+
): Promise<boolean> {
196+
try {
197+
return await this.repository.updateApprovalStatus(
198+
discordId,
199+
isApproved
200+
);
201+
} catch (error) {
202+
logger.error(
203+
{ error, discordId, isApproved },
204+
"Service error updating operator approval status"
205+
);
206+
// The repository already returns false if not found due to condition check
207+
// If other errors occur, re-throw
208+
throw error;
209+
}
210+
}
162211
}
163212

164213
// Export a singleton instance of the service

0 commit comments

Comments
 (0)