Skip to content

Instantly share code, notes, and snippets.

@TheJazzDev
Created May 7, 2023 13:41
Show Gist options
  • Save TheJazzDev/ade773846287a8616a1450d7c7afcef8 to your computer and use it in GitHub Desktop.
Save TheJazzDev/ade773846287a8616a1450d7c7afcef8 to your computer and use it in GitHub Desktop.
thejazzdev
import { discordRequest } from './utils';
export const hasGuildCommands = async (appId, guildId, commands) => {
if (guildId === '' || appId === '') return;
commands.forEach((c) => hasGuildCommand(appId, guildId, c));
};
const hasGuildCommand = async (appId, guildId, command) => {
const endpoint = `applications/${appId}/guilds/${guildId}/commands`;
try {
const res = await discordRequest(endpoint, { method: 'GET' });
const data: object[] = await res.data;
if (data) {
const installedNames = data.map((c) => c['name']);
if (!installedNames.includes(command['name'])) {
console.log(`[FAUCET]Installing "${command['name']}"`);
installGuildCommand(appId, guildId, command);
}
}
} catch (err) {
console.error(err);
}
};
export const installGuildCommand = async (appId, guildId, command) => {
const endpoint = `applications/${appId}/guilds/${guildId}/commands`;
try {
await discordRequest(endpoint, { method: 'POST', body: command });
} catch (err) {
console.error(err);
}
};
export const CREATE_FAUCET_COMMAND = {
name: 'faucet',
description: 'Command to give testnet token',
options: [
{
name: 'address',
description: 'Your address',
type: 3,
required: true,
},
],
type: 1,
};
import 'dotenv/config';
import { verifyKey } from 'discord-interactions';
import axios from 'axios';
import * as zksync from 'zksync-web3';
import * as ethers from 'ethers';
import * as fs from 'fs/promises';
const zkSyncProvider = new zksync.Provider('https://testnet.era.zksync.dev');
const ethProvider = ethers.getDefaultProvider('goerli');
const zkSyncWallet = new zksync.Wallet(process.env.WALLET_PRIVATE_KEY, zkSyncProvider, ethProvider);
const ethWallet = new ethers.Wallet(process.env.WALLET_PRIVATE_KEY, ethProvider);
interface User {
userId: string;
lastFaucet: string;
}
interface Address {
address: string;
lastFaucet: string;
}
export const verifyDiscordRequest = (clientKey) => {
return (req, res, buf, encoding) => {
const signature = req.get('X-Signature-Ed25519');
const timestamp = req.get('X-Signature-Timestamp');
const isValidRequest = verifyKey(buf, signature, timestamp, clientKey);
if (!isValidRequest) {
res.status(401).send('Bad request signature');
throw new Error('Bad request signature');
}
};
};
export const discordRequest = async (endpoint, options) => {
const url = 'https://discord.com/api/v10/' + endpoint;
if (options.body) options.body = JSON.stringify(options.body);
let res;
if (options.method && options.method === 'GET') {
res = await axios.get(url, {
headers: {
Authorization: `Bot ${process.env.DISCORD_TOKEN}`,
'Content-Type': 'application/json; charset=UTF-8',
},
});
}
if (options.method && options.method === 'POST') {
try {
res = await axios.post(url, {
headers: {
Authorization: `Bot ${process.env.DISCORD_TOKEN}`,
'Content-Type': 'application/json; charset=UTF-8',
},
...options,
});
} catch (error) {
console.log(error);
}
}
if (res.status !== 200) {
console.log(res.status);
throw new Error(JSON.stringify(res));
}
return res;
};
export const sendToken = async (userId: string, address: string) => {
const amount = ethers.utils.parseEther(process.env.TOKEN_AMOUNT);
try {
await zkSyncWallet.transfer({
to: address,
token: zksync.utils.ETH_ADDRESS,
amount,
});
} catch (err) {
console.error(err);
}
// await ethWallet.sendTransaction({
// to: address,
// value: amount,
// });
};
const userInCooldown = async (userId: string) => {
const data: { users: User[]; addresses: Address[] } = await getObjectFromFile();
const user = data.users.find((user) => user.userId === userId);
if (!user) return undefined;
if (Date.parse(user.lastFaucet) + 1000 * 60 * 60 * parseInt(process.env.COOLDOWN_IN_HOURS, 10) < new Date().getTime())
return undefined;
return Date.parse(user.lastFaucet) + 1000 * 60 * 60 * parseInt(process.env.COOLDOWN_IN_HOURS, 10);
};
const addressInCooldown = async (userAddress: string) => {
const data: { users: User[]; addresses: Address[] } = await getObjectFromFile();
const address = data.addresses.find((address) => address.address === userAddress);
if (!address) return undefined;
if (
Date.parse(address.lastFaucet) + 1000 * 60 * 60 * parseInt(process.env.COOLDOWN_IN_HOURS, 10) <
new Date().getTime()
)
return undefined;
return Date.parse(address.lastFaucet) + 1000 * 60 * 60 * parseInt(process.env.COOLDOWN_IN_HOURS, 10);
};
export const inCooldown = async (userId: string, userAddress: string, whitelistedUsers: string[]) => {
const userCooldown = await userInCooldown(userId);
const addressCooldown = await addressInCooldown(userAddress);
if (userCooldown) return userCooldown;
// Bypass the wallet cooldown for whitelisted users
if (!whitelistedUsers.includes(userId) && addressCooldown) {
return addressCooldown;
}
return undefined;
};
export const setCooldown = async (userId: string, userAddress: string) => {
const data: { users: User[]; addresses: Address[] } = await getObjectFromFile();
data.users = data.users.filter((user) => user.userId !== userId);
data.addresses = data.addresses.filter((address) => address.address !== userAddress);
data.users.push({ userId, lastFaucet: new Date().toISOString() });
data.addresses.push({ address: userAddress, lastFaucet: new Date().toISOString() });
await writeObjectToFile(data);
};
export const writeObjectToFile = async (data: any) => {
await fs.writeFile('data.json', JSON.stringify(data, null, '\t'));
};
export const getObjectFromFile = async () => {
const data = await fs.readFile('data.json', { encoding: 'utf8' });
return JSON.parse(data);
};
import express from 'express';
import { InteractionResponseFlags, InteractionResponseType, InteractionType } from 'discord-interactions';
import { inCooldown, sendToken, setCooldown } from '../utils';
const router = express.Router();
const WHITELISTED_USERS = JSON.parse(process.env.WHITELISTED_USERS);
router.post('/interactions', async (req, res) => {
const { type, id, data } = req.body;
if (type === InteractionType.PING) {
return res.send({ type: InteractionResponseType.PONG });
}
if (type === InteractionType.APPLICATION_COMMAND) {
const { name } = data;
if (name === 'faucet' && id) {
const channelId = req.body.channel_id;
const userId = req.body.member.user.id;
const userAddress = req.body.data.options[0].value;
if (channelId !== process.env.CHANNEL_ID) {
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: `This command is only available in <#${process.env.CHANNEL_ID}>`,
flags: InteractionResponseFlags.EPHEMERAL,
},
});
}
if (!userAddress || !userAddress.startsWith('0x') || userAddress.length !== 42) {
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: `:warning: Invalid Wallet Address`,
flags: InteractionResponseFlags.EPHEMERAL,
},
});
}
const cooldown = await inCooldown(userId, userAddress, WHITELISTED_USERS);
if (cooldown) {
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: '',
embeds: [
{
fields: [],
color: 16711680,
description: `You are currently in cooldown mode.\nYou will be able to use the faucet again on <t:${(
cooldown / 1000
).toFixed(0)}:F>`,
},
],
components: [],
},
});
}
if (!WHITELISTED_USERS.includes(userId)) {
await setCooldown(userId, userAddress);
}
sendToken(userId, userAddress);
console.log(`Sent ${process.env.TOKEN_AMOUNT} ETH to ${userAddress} for user ${userId}`);
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: '',
embeds: [
{
fields: [],
color: 917248,
description: `Successfully sent 0.05 ETH to <@${userId}> with wallet address ${userAddress}`,
},
],
components: [],
},
});
}
}
});
export default router;
import 'dotenv/config';
import express from 'express';
import { CREATE_FAUCET_COMMAND, hasGuildCommands } from './commands.js';
import { verifyDiscordRequest } from './utils';
import faucet from './commands/faucet';
const app = express();
const PORT = process.env.PORT || 5000;
app.use(express.json({ verify: verifyDiscordRequest(process.env.DISCORD_PUBLIC_KEY) }));
app.listen(PORT, () => {
console.log('[VastFluid Faucet Bot]Listening on port', PORT);
hasGuildCommands(process.env.APP_ID, process.env.GUILD_ID, [CREATE_FAUCET_COMMAND]);
});
app.use('/', faucet);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment