diff --git a/.gitignore b/.gitignore index 1ee35b2..03513eb 100644 --- a/.gitignore +++ b/.gitignore @@ -168,4 +168,7 @@ $RECYCLE.BIN/ # Windows shortcuts *.lnk -# End of https://www.toptal.com/developers/gitignore/api/node,windows \ No newline at end of file +# End of https://www.toptal.com/developers/gitignore/api/node,windows + +# Configuration File +config.json \ No newline at end of file diff --git a/README.md b/README.md index 9fb0335..bbade61 100644 --- a/README.md +++ b/README.md @@ -4,23 +4,11 @@ This is a Discord Bot that dispenses Testnet ETH. ## Setup -Create a `.env` file and fill in the following info +Change the `example.config.json` into `config.json`, and fill in the required fields. -``` -# Discord Bot Token -DCB_TOKEN="aaaaaaaaaaaaaa.aaaaaaaaa" -# Discord Bot Client ID -DCB_CLIENT_ID="00000000000" -# Discord Server ID -DCB_GUILD_ID="00000000000" -# Discord Channel ID to post log -DCB_LOG_CHANNEL="000000000" -# Discord Admin Role ID -DCB_ADMIN_ROLE_ID="00000000" -# Discord Feedback Channel ID -DCB_FEEDBACK_CHANNEL="00000000000" -# Goerli Alchemy Key -ALCHEMY_GOERLI_URL="https://eth-goerli.g.alchemy.com/v2/xxxxxxxxxxxxxxxxxxxx" -# Own Wallet Address -WALLET_ADDRESS="xxxxxxxxxxxxxxxxxxxxx" -``` +## Adding Features + +1. Fork the project +2. Create a new branch +3. Commit the changes +4. Yeet the changes diff --git a/commands/balance.js b/commands/balance.js index d947867..3e70144 100644 --- a/commands/balance.js +++ b/commands/balance.js @@ -13,19 +13,29 @@ module.exports = { .setName("network") .setDescription("Select the network to view balance") .setRequired(true) - .addChoices({ - name: "Goerli", - value: "goerli", - }) + .addChoices( + { + name: "Goerli", + value: "goerli", + }, + { + name: "Rinkeby", + value: "rinkeby", + }, + { + name: "Mumbai", + value: "mumbai", + } + ) ) .addStringOption((option) => option .setName("token") - .setDescription("Token Type to search") + .setDescription("External ERC20 tokens if applicable") .setRequired(false) .addChoices({ - name: "ETH", - value: "eth", + name: "LINK", + value: "link", }) ), }; diff --git a/delete-commands.js b/delete-commands.js index e67d156..a543b2d 100644 --- a/delete-commands.js +++ b/delete-commands.js @@ -2,17 +2,17 @@ const { REST } = require("@discordjs/rest"); const { Routes } = require("discord.js"); -require("dotenv").config(); +const { bot } = require("./config.json"); // Get the REST -const rest = new REST({ version: "10" }).setToken(process.env.DCB_TOKEN); +const rest = new REST({ version: "10" }).setToken(bot.token); //* Use to remove all commands from the Guild // rest // .put( // Routes.applicationGuildCommands( -// process.env.DCB_CLIENT_ID, -// process.env.DCB_GUILD_ID +// bot.clientId, +// bot.guildId // ), // { body: [] } // ) @@ -21,6 +21,6 @@ const rest = new REST({ version: "10" }).setToken(process.env.DCB_TOKEN); //* Use to remove all commands publically rest - .put(Routes.applicationCommands(process.env.DCB_CLIENT_ID), { body: [] }) + .put(Routes.applicationCommands(bot.clientId), { body: [] }) .then(() => console.log("Successfully deleted all application commands.")) .catch(console.error); diff --git a/deploy-commands.js b/deploy-commands.js index 56de0f3..48bcf0c 100644 --- a/deploy-commands.js +++ b/deploy-commands.js @@ -4,7 +4,7 @@ const fs = require("node:fs"); const path = require("node:path"); const { REST } = require("@discordjs/rest"); const { Routes } = require("discord.js"); -require("dotenv").config(); +const { bot } = require("./config.json"); // Get the commands from the `/commands` folder and to the array const commands = []; @@ -20,23 +20,19 @@ for (const file of commandFiles) { } // Get the REST -const rest = new REST({ version: "10" }).setToken(process.env.DCB_TOKEN); +const rest = new REST({ version: "10" }).setToken(bot.token); //* Use for Development (Updates only the passed guild data) rest - .put( - Routes.applicationGuildCommands( - process.env.DCB_CLIENT_ID, - process.env.DCB_GUILD_ID - ), - { body: commands } - ) + .put(Routes.applicationGuildCommands(bot.clientId, bot.guildId), { + body: commands, + }) .then(() => console.log("Successfully registered application commands")) .catch(console.error); //* Use for Production as it updates the commands public // rest -// .put(Routes.applicationCommands(process.env.DCB_CLIENT_ID), { +// .put(Routes.applicationCommands(bot.clientId), { // body: commands, // }) // .then(() => console.log("Successfully registered application commands")) diff --git a/events/interactionCreate.js b/events/interactionCreate.js index b96f7cf..6669072 100644 --- a/events/interactionCreate.js +++ b/events/interactionCreate.js @@ -1,6 +1,7 @@ // For all the Interactions const { InteractionType } = require("discord.js"); +const { channels } = require("../config.json"); module.exports = { name: "interactionCreate", @@ -25,9 +26,7 @@ module.exports = { else if (interaction.type === InteractionType.ModalSubmit) { if (interaction.customId === "feedbackModal") { // Get the Feedback Channel - const fdChannel = await client.channels.cache.get( - process.env.DCB_FEEDBACK_CHANNEL - ); + const fdChannel = await client.channels.cache.get(channels.feedback); // Get the value of the sent messages and send on the feedback channel const subject = interaction.fields.getTextInputValue("subject"); const description = interaction.fields.getTextInputValue("description"); diff --git a/events/ready.js b/events/ready.js index f049cb5..6590211 100644 --- a/events/ready.js +++ b/events/ready.js @@ -1,6 +1,6 @@ // When the Bot Launches -require("dotenv").config({ path: "../.env" }); +const { channels } = require("../config.json"); module.exports = { name: "ready", @@ -8,9 +8,7 @@ module.exports = { async execute(client) { console.log(`Ready! Logged in as ${client.user.tag}`); - const logchannel = await client.channels.cache.get( - process.env.DCB_LOG_CHANNEL - ); + const logchannel = await client.channels.cache.get(channels.log); // logchannel.send( // `[LOGIN] | ${new Date( // Date.now() diff --git a/example.config.json b/example.config.json new file mode 100644 index 0000000..fb4cf23 --- /dev/null +++ b/example.config.json @@ -0,0 +1,40 @@ +{ + "bot": { + "clientId": "00000000000000", + "guildId": "00000000000", + "token": "xxxxxxxxxxxxx" + }, + "channels": { + "feedback": "00000000000", + "log": "00000000000" + }, + "roles": { + "admin": { + "id": "000000000000000" + } + }, + "networks": { + "goerli": { + "nativeCurrency": "eth", + "ALCHEMY_URL": "https://eth-goerli.g.alchemy.com/v2/xxxxxxxxxxx" + }, + "rinkeby": { + "nativeCurrency": "eth", + "ALCHEMY_URL": "https://eth-rinkeby.alchemyapi.io/v2/xxxxxxxxxx" + }, + "mumbai": { + "nativeCurrency": "matic", + "ALCHEMY_URL": "https://polygon-mumbai.g.alchemy.com/v2/xxxxxxxxxxxx" + } + }, + "tokens": { + "link": { + "goerli": "0x326C977E6efc84E512bB9C30f76E30c160eD06FB", + "rinkeby": "0x01BE23585060835E02B77ef475b0Cc51aA1e0709", + "mumbai": "0x326C977E6efc84E512bB9C30f76E30c160eD06FB" + } + }, + "stats": { + "walletAddress": "xxxxxx" + } +} diff --git a/index.js b/index.js index 23c75d8..06b4743 100644 --- a/index.js +++ b/index.js @@ -2,7 +2,7 @@ const fs = require("node:fs"); const path = require("node:path"); const { Collection } = require("discord.js"); -require("dotenv").config({ path: ".env" }); +const { bot } = require("./config.json"); const client = require("./client"); // Get Client // Run the Events depending on whether it's once or on. @@ -41,7 +41,7 @@ for (const file of commandFiles) { // Login to the Bot try { - client.login(process.env.DCB_TOKEN); + client.login(bot.token); } catch (error) { console.error(error); } diff --git a/libs/erc20.json b/libs/erc20.json new file mode 100644 index 0000000..405d6b3 --- /dev/null +++ b/libs/erc20.json @@ -0,0 +1,222 @@ +[ + { + "constant": true, + "inputs": [], + "name": "name", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_spender", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_from", + "type": "address" + }, + { + "name": "_to", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "decimals", + "outputs": [ + { + "name": "", + "type": "uint8" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "name": "balance", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "symbol", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_to", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_owner", + "type": "address" + }, + { + "name": "_spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "payable": true, + "stateMutability": "payable", + "type": "fallback" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "from", + "type": "address" + }, + { + "indexed": true, + "name": "to", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + } +] diff --git a/responses/balance_response.js b/responses/balance_response.js index ca0886b..b158a07 100644 --- a/responses/balance_response.js +++ b/responses/balance_response.js @@ -1,47 +1,57 @@ // Returns the balance of the contract const ethers = require("ethers"); -require("dotenv").config({ path: "../.env" }); +const getProvider = require("../utils/getProvider"); +const getExternalBalance = require("../utils/getExternalBalance"); +const { stats, networks } = require("../config.json"); module.exports = async (interaction) => { - await interaction.reply({ content: "Finding....", fetchReply: true }); - - // Get the Network from user input and the relevant provider - const network = interaction.options.getString("network"); - const token = interaction.options.getString("token"); - const provider = getProvider(network); + await interaction.reply({ content: "Calculating....", fetchReply: true }); let balance; - try { - // Get the balance of the network core currency - balance = await ethers.utils.formatEther( - await provider.getBalance(process.env.WALLET_ADDRESS) - ); - } catch (error) { - console.error(error); - await interaction.editReply({ - content: "Error Getting balance", - ephemeral: true, - }); + // Get the Network and token from user input + const networkName = interaction.options.getString("network"); + const tokenName = + interaction.options.getString("token") ?? + networks[networkName].nativeCurrency; + + // Get the Provider based on the network + const provider = getProvider(networkName); + + if (networks[networkName].nativeCurrency == tokenName) { + // Token not passed or native Currency (No ERC20 tokens) + try { + // Get the balance of the network core currency + balance = await ethers.utils.formatEther( + await provider.getBalance(stats.walletAddress) + ); + } catch (error) { + console.error(error); + await interaction.editReply({ + content: "Error Getting balance", + ephemeral: true, + }); + } + } else { + // Non native token (ERC 20 token) + try { + balance = await getExternalBalance(provider, tokenName, networkName); + } catch (error) { + console.error(error); + await interaction.editReply({ + content: "Error Getting balance", + ephemeral: true, + }); + } } // Rounding off the value - const balanceShort = balance + const balancefinal = balance .toString() .slice(0, balance.toString().indexOf(".") + 3); // Printing the value out await interaction.editReply({ - content: `Current Balance : ${balanceShort} ETH`, - ephemeral: true, + content: `[${networkName.toUpperCase()}] [${balancefinal}] [${tokenName.toUpperCase()}]`, }); }; - -function getProvider(network) { - switch (network) { - case "goerli": - return new ethers.providers.JsonRpcProvider( - process.env.INFURA_GOERLI_URL ?? process.env.ALCHEMY_GOERLI_URL - ); - } -} diff --git a/responses/ping_response.js b/responses/ping_response.js index 56384a8..f2e7b5f 100644 --- a/responses/ping_response.js +++ b/responses/ping_response.js @@ -1,24 +1,14 @@ // Responds user with the ping of the bot -require("dotenv").config({ path: "../.env" }); module.exports = async (interaction) => { - if ( - interaction.guild && - interaction.member.roles.cache.some( - (r) => r.id === process.env.DCB_ADMIN_ROLE_ID - ) - ) { - const sent = await interaction.reply({ - content: "Pinging...", - fetchReply: true, - }); + const sent = await interaction.reply({ + content: "Pinging...", + fetchReply: true, + }); - await interaction.editReply( - `Roundtrip latency: ${ - sent.createdTimestamp - interaction.createdTimestamp - }ms | Websocket heartbeat: ${interaction.client.ws.ping}ms.` - ); - } else { - await interaction.reply("You do not have permissions for this action"); - } + await interaction.editReply( + `Roundtrip latency: ${ + sent.createdTimestamp - interaction.createdTimestamp + }ms | Websocket heartbeat: ${interaction.client.ws.ping}ms.` + ); }; diff --git a/utils/getExternalBalance.js b/utils/getExternalBalance.js new file mode 100644 index 0000000..bbcc8a7 --- /dev/null +++ b/utils/getExternalBalance.js @@ -0,0 +1,11 @@ +const ethers = require("ethers"); +const erc20ABI = require("../libs/erc20.json"); +const { tokens, stats } = require("../config.json"); + +module.exports = async (provider, tokenName, networkName) => { + const address = tokens[tokenName][networkName]; + if (!address) throw Error("Token Address not found!"); + + const contract = new ethers.Contract(address, erc20ABI, provider); + return (await contract.balanceOf(stats.walletAddress)).toString(); +}; diff --git a/utils/getProvider.js b/utils/getProvider.js new file mode 100644 index 0000000..cd70852 --- /dev/null +++ b/utils/getProvider.js @@ -0,0 +1,11 @@ +const ethers = require("ethers"); +const { networks } = require("../config.json"); + +module.exports = (networkName) => { + let url = + networks[networkName].INFURA_URL ?? networks[networkName].ALCHEMY_URL; + + url ?? Error("Network not found"); + + return new ethers.providers.JsonRpcProvider(url); +};