Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,15 @@ A Discord bot powered by discord.js and TypeScript.
```

For development with auto-restart:

```bash
npm dev
```

## Local development

Run cloudflared in a terminal and add he tunnel url to Url mapping in discord developer console

```bash
docker run cloudflare/cloudflared:latest tunnel --url http://localhost:3000
```
191 changes: 191 additions & 0 deletions src/commands/burn-or-revoke-role.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import {
BundlerService,
getAccountAddress,
getAccountBalance,
getCardAddress,
} from "@citizenwallet/sdk";
import {
APIRole,
ChatInputCommandInteraction,
Client,
GuildMember,
Role,
} from "discord.js";
import { keccak256, toUtf8Bytes, Wallet } from "ethers";
import { getCommunity } from "../cw";
import { ContentResponse, generateContent } from "../utils/content";
import { createProgressSteps } from "../utils/progress";

export const handleBurnOrRevokeRoleCommand = async (
client: Client,
interaction: ChatInputCommandInteraction
) => {
await interaction.reply({
content: createProgressSteps(0),
ephemeral: true,
});

const alias = interaction.options.getString("token");
if (!alias) {
await interaction.editReply("You need to specify a token!");
return;
}

const amount = interaction.options.getNumber("amount");
if (!amount) {
await interaction.editReply("You need to specify an amount!");
return;
}

const role = interaction.options.getRole("role");
if (!role) {
await interaction.editReply("You need to specify a role!");
return;
}

const message = interaction.options.getString("message");

const community = getCommunity(alias);
const token = community.primaryToken;
const guild = await client.guilds.fetch(interaction.guildId);

const privateKey = process.env.BOT_PRIVATE_KEY;
if (!privateKey) {
await interaction.editReply({
content: "Private key is not set",
});
return;
}

const signer = new Wallet(privateKey);

const signerAccountAddress = await getAccountAddress(
community,
signer.address
);
if (!signerAccountAddress) {
await interaction.editReply({
content: "Could not find an account for you!",
});
return;
}

// signer setup done

const content: ContentResponse = {
header: createProgressSteps(1),
content: [],
};
await interaction.editReply({
content: generateContent(content),
});

const users = guild.members.cache.filter((member) => {
// check whether member is not a bot and has the given role
return !member.user.bot && member.roles.cache.has(role.id);
});

for (const [userId, user] of users) {
const hashedUserId = keccak256(toUtf8Bytes(userId));

const cardAddress = await getCardAddress(community, hashedUserId);
if (!cardAddress) {
content.content.push(
`Could not find an account to send to for user ${user.user.displayName}!`
);
await interaction.editReply({
content: generateContent(content),
});
continue;
}

content.header = createProgressSteps(2);
await interaction.editReply({
content: generateContent(content),
});

// check user status
const burnStatus = { status: "new", remainingBurns: amount }; //await getBurnStatus(user, role);

if (burnStatus.status === "burnt") {
content.content.push(`${user} has already burned`);
await interaction.editReply({
content: generateContent(content),
});
} else {
const balance = await getAccountBalance(community, cardAddress);

content.content.push(
`Handling user: ${user}, balance: ${balance} ${token.symbol}`
);
await interaction.editReply({
content: generateContent(content),
});

if (burnStatus.remainingBurns > balance) {
await guild.members.removeRole({
user: user,
role: role.id,
});
content.content.push(
`${user} has not enough ${token.symbol}, removed role ${role.name}.`
);
await interaction.editReply({
content: generateContent(content),
});
} else {
const bundler = new BundlerService(community);

try {
const hash = await bundler.burnFromERC20Token(
signer,
token.address,
signerAccountAddress,
cardAddress,
burnStatus.remainingBurns.toString(),
message
);
content.content.push(
`Burnt ${burnStatus.remainingBurns.toString()} ${
token.symbol
} for ${user}: ${hash}`
);
await interaction.editReply({
content: generateContent(content),
});
} catch (e) {
content.content.push(
`Failed to burnt ${burnStatus.remainingBurns.toString()} ${
token.symbol
} for ${user} (${e.message})`
);
await interaction.editReply({
content: generateContent(content),
});
}
}
}

content.header = createProgressSteps(3);

await interaction.editReply({
content: generateContent(content),
});
}

content.header = "✅ Done";

await interaction.editReply({
content: generateContent(content),
});
};

const getBurnStatus = async (
user: GuildMember,
role: NonNullable<Role | APIRole>
) => {
return {
status: "partial",
remainingBurns: 2,
};
};
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { handleShareAddressCommand } from "./commands/shareAddress";
import { handleBurnManyCommand } from "./commands/burn-many";
import { handleDoCommand } from "./commands/do";
import { startLiveUpdates } from "./live";
import { handleBurnOrRevokeRoleCommand } from "./commands/burn-or-revoke-role";

// Create a new client instance
const client = new Client({
Expand Down Expand Up @@ -109,6 +110,9 @@ client.on(Events.InteractionCreate, async (interaction) => {
case "burn-many":
await handleBurnManyCommand(client, interaction);
break;
case "burn-or-revoke-role":
await handleBurnOrRevokeRoleCommand(client, interaction);
break;
case "add-owner":
await handleAddOwnerCommand(client, interaction);
break;
Expand Down
2 changes: 1 addition & 1 deletion src/openai/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
} from "../commands/do/tasks";

const client = new OpenAI({
apiKey: process.env["OPENAI_API_KEY"],
apiKey: process.env["OPENAI_API_KEY"] || "invalid key",
});

const constructSystemPrompt = (
Expand Down
26 changes: 26 additions & 0 deletions src/register-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,32 @@ const getCommands = () =>
},
],
},
{
name: "burn-or-revoke-role",
description: "Burn tokens or revoke role if not enough!",
default_member_permissions: "32",
options: [
{
name: "token",
description: "The token of debts",
type: 3, // STRING type
required: true,
autocomplete: true,
},
{
name: "amount",
description: "The amount to burn",
type: 10, // NUMBER type
required: true,
},
{
name: "role",
description: "The role to revoke if user has not burnt enough tokens",
type: 8, // ROLE type
required: true,
},
],
},
{
name: "add-owner",
description: "Add an owner to your Safe! 🔑",
Expand Down