├── config ├── dev.json └── default.json ├── .gitignore ├── docker ├── postgres │ ├── Dockerfile │ └── init-db.sh ├── nginx │ ├── Dockerfile │ └── templates │ │ └── test.conf.conf └── api │ └── Dockerfile ├── modules ├── discord-bot │ ├── bot.js │ ├── config │ │ └── index.js │ ├── price │ │ ├── priceAPI.js │ │ └── PriceService.js │ ├── solana │ │ ├── publicKey.js │ │ ├── transaction.js │ │ ├── account.js │ │ └── index.js │ ├── discord │ │ ├── commands │ │ │ ├── commands │ │ │ │ ├── logout.js │ │ │ │ ├── deleteDiscordKey.js │ │ │ │ ├── getDiscordKey.js │ │ │ │ ├── help.js │ │ │ │ ├── cluster.js │ │ │ │ ├── createNew.js │ │ │ │ ├── saveDiscordKey.js │ │ │ │ ├── me.js │ │ │ │ ├── login.js │ │ │ │ ├── balance.js │ │ │ │ ├── verify.js │ │ │ │ └── send.js │ │ │ └── index.js │ │ ├── first-message.js │ │ ├── index.js │ │ └── role-claim.js │ └── wallet │ │ ├── SessionStorageService.js │ │ └── index.js ├── aggregator │ └── aggregator.js └── api │ └── controllers │ └── main.controller.js ├── nacl.json ├── ecosystem.config.js ├── .dockerignore ├── .env.example ├── lib ├── util.js ├── bot.js ├── logger.js └── db.js ├── models ├── Server.js ├── UserSession.js ├── UserWallet.js ├── UserServer.js └── User.js ├── .eslintrc ├── package.json ├── docker-compose.yml ├── README.md ├── app.js └── controllers └── main.controller.js /config/dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": "4000" 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | logs 3 | **/*.log 4 | **.env 5 | -------------------------------------------------------------------------------- /docker/postgres/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM postgres:latest 2 | ADD init-db.sh /docker-entrypoint-initdb.d -------------------------------------------------------------------------------- /modules/discord-bot/bot.js: -------------------------------------------------------------------------------- 1 | import DiscordHandler from './discord'; 2 | import './config'; 3 | 4 | const main = async () => { 5 | await DiscordHandler.initHandler(); 6 | }; 7 | 8 | main(); 9 | -------------------------------------------------------------------------------- /docker/nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:latest 2 | 3 | RUN rm -rf /etc/nginx/sites-enabled/default 4 | RUN rm -rf /etc/nginx/conf.d/default.conf 5 | RUN cp /home/client/build /app/client 6 | # ADD nginx.conf /etc/nginx/nginx.conf 7 | -------------------------------------------------------------------------------- /nacl.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "group": "guest", 4 | "id": "1", 5 | "permissions": [ 6 | { 7 | "resource": "*", 8 | "methods": "*", 9 | "action": "allow", 10 | "subRoutes": [] 11 | } 12 | ] 13 | } 14 | ] -------------------------------------------------------------------------------- /modules/discord-bot/config/index.js: -------------------------------------------------------------------------------- 1 | import 'dotenv/config.js'; 2 | 3 | export const CLUSTERS = { 4 | MAINNET: 'mainnet-beta', 5 | TESTNET: 'testnet', 6 | DEVNET: 'devnet', 7 | }; 8 | 9 | export const COMMAND_PREFIX = '!'; 10 | -------------------------------------------------------------------------------- /docker/api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16-alpine 2 | WORKDIR /app 3 | 4 | COPY package.json . 5 | 6 | RUN npm install 7 | 8 | ENTRYPOINT ["/bin/sh"] 9 | CMD ["-c", "if [ \"$NODE_ENV\" = \"dev\" ]; then npm run api:dev; else npm run api:prod; fi"] 10 | -------------------------------------------------------------------------------- /ecosystem.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps: [{ 3 | name: 'API', 4 | script: 'npm', 5 | args : 'start api:prod', 6 | instances: 1, 7 | autorestart: true, 8 | watch: true, 9 | max_memory_restart: '1G', 10 | }], 11 | }; -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # virtual environments 2 | venv 3 | 4 | # Secret environment settings 5 | .env 6 | env 7 | 8 | # Node and Webpack files 9 | node_modules 10 | npm-debug.log 11 | webpack-stats* 12 | 13 | # Static assests 14 | staticfiles 15 | static_cdn 16 | media_cdn 17 | 18 | # Ignore sqlite 19 | db.sqlite3 20 | 21 | # Apple files 22 | .DS_Store 23 | -------------------------------------------------------------------------------- /modules/discord-bot/price/priceAPI.js: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | 3 | const getSolPriceInUSD = async () => { 4 | const json = await fetch('https://api.coingecko.com/api/v3/coins/solana') 5 | .then((res) => res.json()); 6 | return json.market_data.current_price.usd; 7 | }; 8 | 9 | export default { 10 | getSolPriceInUSD, 11 | }; 12 | -------------------------------------------------------------------------------- /config/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": "4000", 3 | "discord": { 4 | "scopes": [ 5 | "identify" 6 | ] 7 | }, 8 | "session": { 9 | "secret": "youshall", 10 | "cookie": { 11 | "maxAge": 86400000 12 | }, 13 | "resave": true, 14 | "saveUninitialized": false 15 | }, 16 | "logPath": "./logs/" 17 | } -------------------------------------------------------------------------------- /modules/discord-bot/solana/publicKey.js: -------------------------------------------------------------------------------- 1 | import * as web3 from '@solana/web3.js'; 2 | 3 | const isValidPublicKey = (publicKeyString) => { 4 | if (typeof publicKeyString !== 'string') { 5 | return false; 6 | } 7 | 8 | try { 9 | new web3.PublicKey(publicKeyString); 10 | return true; 11 | } catch (e) { 12 | return false; 13 | } 14 | }; 15 | 16 | export default { 17 | isValidPublicKey, 18 | }; 19 | -------------------------------------------------------------------------------- /docker/postgres/init-db.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | echo 'trying to start postgres script...' 5 | echo 'username : ' 6 | echo $POSTGRES_USER 7 | 8 | psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname="$POSTGRES_DB"<<-EOSQL 9 | CREATE USER $DB_USER WITH PASSWORD '$DB_PASSWORD'; 10 | GRANT ALL PRIVILEGES ON DATABASE $DB_NAME TO $DB_USER; 11 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; 12 | EOSQL 13 | -------------------------------------------------------------------------------- /modules/discord-bot/discord/commands/commands/logout.js: -------------------------------------------------------------------------------- 1 | import Wallet from '../../../wallet'; 2 | import { COMMAND_PREFIX } from '../../../config'; 3 | 4 | export default { 5 | name: 'logout', 6 | description: 'Logs you out of the wallet.', 7 | usage: [`${COMMAND_PREFIX}logout`], 8 | async execute(message) { 9 | await Wallet.logout(message.author.id); 10 | message.channel.send('🥳 Successfully logged out 🥳'); 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /modules/discord-bot/price/PriceService.js: -------------------------------------------------------------------------------- 1 | import PriceAPI from './priceAPI'; 2 | 3 | const convertLamportsToSol = (lamports) => (lamports * 0.000000001).toFixed(4); 4 | 5 | const getDollarValueForSol = async (sol, price) => { 6 | const currentPrice = price || await PriceAPI.getSolPriceInUSD(); 7 | return (sol * currentPrice).toFixed(2); 8 | }; 9 | 10 | export default { 11 | convertLamportsToSol, 12 | getSolPriceInUSD: PriceAPI.getSolPriceInUSD, 13 | getDollarValueForSol, 14 | }; 15 | -------------------------------------------------------------------------------- /modules/discord-bot/discord/commands/commands/deleteDiscordKey.js: -------------------------------------------------------------------------------- 1 | /* 2 | import UserService from '../../../publicKeyStorage/UserService'; 3 | import { COMMAND_PREFIX } from '../../../config'; 4 | 5 | export default { 6 | name: 'delete-discordkey', 7 | description: 'Deletes your discord public key.', 8 | usage: [`${COMMAND_PREFIX}delete-discordkey`], 9 | async execute(message) { 10 | await UserService.deleteUser(message.author.id); 11 | message.channel.send('🥳 Successfully deleted discord public key 🥳'); 12 | message.channel.send('ℹ️ You can no longer be tipped through discord ℹ️'); 13 | }, 14 | }; 15 | */ -------------------------------------------------------------------------------- /modules/discord-bot/solana/transaction.js: -------------------------------------------------------------------------------- 1 | import * as web3 from '@solana/web3.js'; 2 | 3 | export default { 4 | async transfer(fromAccount, toPubkey, connection, sol) { 5 | const transaction = web3.SystemProgram.transfer({ 6 | fromPubkey: fromAccount.publicKey, 7 | toPubkey, 8 | lamports: sol * 1000000000, 9 | }); 10 | 11 | let signature = ''; 12 | try { 13 | signature = await web3.sendAndConfirmTransaction( 14 | connection, 15 | transaction, 16 | [fromAccount], 17 | { confirmations: 1 }, 18 | ); 19 | } catch (err) { 20 | console.log(err); 21 | throw new Error('⚠️ Transaction failed ⚠️'); 22 | } 23 | return signature; 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /modules/discord-bot/discord/commands/commands/getDiscordKey.js: -------------------------------------------------------------------------------- 1 | /* 2 | import UserService from '../../../publicKeyStorage/UserService'; 3 | import { COMMAND_PREFIX } from '../../../config'; 4 | 5 | export default { 6 | name: 'get-discordkey', 7 | description: 'Displays your discord public key.', 8 | usage: [`${COMMAND_PREFIX}get-discordkey`], 9 | async execute(message) { 10 | let user = ''; 11 | try { 12 | user = await UserService.getUser(message.author.id); 13 | } catch (e) { 14 | message.channel.send('It seems there is a problem with the bot. Please talk to the server admins.'); 15 | return; 16 | } 17 | if (!user) { 18 | message.channel.send('You don\'t have a discord public key configured yet. Configure one to receive tips through discord!'); 19 | return; 20 | } 21 | message.channel.send(`Your discord public key: ${user.publicKey}`); 22 | }, 23 | }; 24 | */ -------------------------------------------------------------------------------- /modules/discord-bot/discord/commands/commands/help.js: -------------------------------------------------------------------------------- 1 | import Discord from 'discord.js'; 2 | import CommandUtil from '../index'; 3 | import { COMMAND_PREFIX } from '../../../config'; 4 | 5 | export default { 6 | name: 'help', 7 | description: 'Displays bot instructions.', 8 | usage: [`${COMMAND_PREFIX}help`], 9 | async execute(message) { 10 | const allCommands = CommandUtil.getAllCommands(); 11 | const commandInstructions = allCommands.map((c) => ({ name: COMMAND_PREFIX + c.name, value: `${c.description}\n\nExample usage: ${c.usage.join(', ')}` })); 12 | 13 | const embed = new Discord.MessageEmbed(); 14 | embed 15 | .setColor('#2E145D') 16 | .setTitle('GRAPE') 17 | .setDescription('SPL Token Gated access!') 18 | // .addFields(...commandInstructions) 19 | .setFooter(''); 20 | 21 | message.channel.send(embed); 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /modules/discord-bot/solana/account.js: -------------------------------------------------------------------------------- 1 | import * as bip39 from 'bip39'; 2 | import nacl from 'tweetnacl'; 3 | import * as web3 from '@solana/web3.js'; 4 | 5 | const createAccountFromMnemonic = async (mnemonic) => { 6 | if (!bip39.validateMnemonic(mnemonic)) { 7 | throw new Error('⚠️ Invalid seed phrase ⚠️'); 8 | } 9 | const seed = await bip39.mnemonicToSeed(mnemonic); 10 | const keyPair = nacl.sign.keyPair.fromSeed(seed.slice(0, 32)); 11 | const acc = new web3.Account(keyPair.secretKey); 12 | return { 13 | privateKey: acc.secretKey, 14 | publicKey: acc.publicKey.toString(), 15 | }; 16 | }; 17 | 18 | const createAccount = async () => { 19 | const mnemonic = bip39.generateMnemonic(); 20 | const { publicKey, privateKey } = await createAccountFromMnemonic(mnemonic); 21 | 22 | return { 23 | privateKey, 24 | publicKey, 25 | mnemonic, 26 | }; 27 | }; 28 | 29 | export default { 30 | createAccountFromMnemonic, 31 | createAccount, 32 | }; 33 | -------------------------------------------------------------------------------- /modules/discord-bot/discord/first-message.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-syntax */ 2 | const addReactions = (message, reactions) => { 3 | message.react(reactions[0]); 4 | reactions.shift(); 5 | if (reactions.length > 0) { 6 | setTimeout(() => addReactions(message, reactions), 750); 7 | } 8 | }; 9 | 10 | export default async (client, id, text, reactions = []) => { 11 | const channel = await client.channels.fetch(id); 12 | 13 | channel.messages.fetch().then((messages) => { 14 | if (messages.size === 0) { 15 | // Send a new message 16 | channel.send(text).then((message) => { 17 | addReactions(message, reactions); 18 | }); 19 | } 20 | else { 21 | // Edit the existing message 22 | for (const message of messages) { 23 | message[1].edit(text); 24 | addReactions(message[1], reactions); 25 | } 26 | } 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /modules/discord-bot/discord/commands/commands/cluster.js: -------------------------------------------------------------------------------- 1 | import { CLUSTERS, COMMAND_PREFIX } from '../../../config'; 2 | import Wallet from '../../../wallet'; 3 | 4 | export default { 5 | name: 'cluster', 6 | description: `Lets you switch between clusters. Valid clusters are: ${Object.values(CLUSTERS).join(', ') 7 | }. You must be logged in to use this command.`, 8 | usage: [`${COMMAND_PREFIX}cluster to get the current cluster`, `${COMMAND_PREFIX}cluster to switch cluster`], 9 | async execute(message, args) { 10 | if (args.length === 1) { 11 | message.channel.send(`Currently selected cluster: ${await Wallet.getCluster(message.author.id)}`); 12 | return; 13 | } 14 | try { 15 | await Wallet.setCluster(message.author.id, args[1].toLowerCase()); 16 | message.channel.send(`Successfully switched to cluster: ${args[1].toLowerCase()}`); 17 | } catch (e) { 18 | message.channel.send(e.message); 19 | } 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NODE_ENV=dev 2 | 3 | API_PORT=5000 4 | 5 | #DATABASE 6 | DB_USER=graperoot 7 | DB_HOST=postgres 8 | DB_PASSWORD=elephant 9 | DB_NAME=grape 10 | DB_PORT=5432 11 | 12 | #POSTGRES DOCKER 13 | POSTGRES_PASSWORD= 14 | POSTGRES_DB= #should be same as DB_NAME 15 | 16 | #PG ADMIN 17 | PGADMIN_DEFAULT_EMAIL=toto@test.com 18 | PGADMIN_DEFAULT_PASSWORD=123456 19 | PG_ADMIN_PORT=5050 20 | 21 | #NGINX 22 | NGINX_ENVSUBST_TEMPLATE_SUFFIX=.conf 23 | 24 | CLIENT_URL= 25 | 26 | DISCORD_OAUTH_REDIRECT_URL= 27 | DISCORD_OAUTH_CLIENT_ID= 28 | DISCORD_OAUTH_SECRET= 29 | DISCORD_BOT_TOKEN= 30 | DISCORD_BOT_ID= 31 | 32 | ETH_RPC_URL= 33 | 34 | DISCORD_CHANNEL_ID = 35 | 36 | DISCORD_ROLE_1 =Verified Wallet 37 | DISCORD_ROLE_2 =MEDIA Holder 38 | DISCORD_ROLE_3 =Mercurial Holder 39 | DISCORD_ROLE_4 =SAMO Holder 40 | 41 | MINT_TOKEN_1 =ETAtLmCmsoiEEKfNrHKJ2kYy3MoABhU6NQvpSfij5tDs 42 | MINT_TOKEN_2 =MERt85fc5boKw3BW1eYdxonEuJNvXbiMbs6hvheau5K 43 | MINT_TOKEN_3 =7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU 44 | 45 | MINT_MIN_BALANCE_1 =0.099 46 | MINT_MIN_BALANCE_2 =9.99 47 | MINT_MIN_BALANCE_3 =9999.99 48 | -------------------------------------------------------------------------------- /modules/discord-bot/discord/commands/commands/createNew.js: -------------------------------------------------------------------------------- 1 | import Wallet from '../../../wallet'; 2 | import { COMMAND_PREFIX } from '../../../config'; 3 | 4 | export default { 5 | name: 'create-new', 6 | description: 'Creates a new wallet and gives you seed phrase to write down. Logs you in.', 7 | usage: [`${COMMAND_PREFIX}create-new`], 8 | async execute(message) { 9 | const userId = message.author.id; 10 | const account = await Wallet.createAccount(); 11 | const { publicKey, privateKey, mnemonic } = account; 12 | await Wallet.login(userId, privateKey, publicKey); 13 | message.channel.send('🎁 Here\'s your new account! 🎁'); 14 | message.channel.send(`Public key: ${account.publicKey}`); 15 | const seedPhraseMessage = await message.channel.send(`Seed phrase: ${mnemonic}`); 16 | message.channel.send('☢️ The previous message will self-destruct in 5 minutes ☢️'); 17 | seedPhraseMessage.delete({ timeout: 1000 * 60 * 5 }); 18 | message.channel.send('🥳 You\'re logged in, use \'!logout\' to logout! 🥳'); 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /modules/discord-bot/solana/index.js: -------------------------------------------------------------------------------- 1 | import * as web3 from '@solana/web3.js'; 2 | import TransactionUtil from './transaction'; 3 | import AccountUtil from './account'; 4 | import PublicKeyUtil from './publicKey'; 5 | 6 | const getBalance = (publicKey, cluster) => { 7 | const connection = new web3.Connection(web3.clusterApiUrl(cluster), 'max'); 8 | return connection.getBalance(new web3.PublicKey(publicKey)); 9 | }; 10 | 11 | const transfer = (cluster, fromPrivateKey, toPublicKeyString, sol) => { 12 | if (!PublicKeyUtil.isValidPublicKey(toPublicKeyString)) { 13 | throw new Error('⚠️ Invalid recipient key ⚠️'); 14 | } 15 | const connection = new web3.Connection(web3.clusterApiUrl(cluster), 'max'); 16 | const publicKey = new web3.PublicKey(toPublicKeyString); 17 | return TransactionUtil.transfer(new web3.Account(fromPrivateKey), publicKey, connection, sol); 18 | }; 19 | 20 | export default { 21 | getBalance, 22 | transfer, 23 | isValidPublicKey: PublicKeyUtil.isValidPublicKey, 24 | createAccountFromMnemonic: AccountUtil.createAccountFromMnemonic, 25 | createAccount: AccountUtil.createAccount, 26 | }; 27 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | import logger from './logger.js'; 2 | 3 | class Util { 4 | static sendResponse(httpCode, status) { 5 | response.status(httpCode).send(status); 6 | 7 | } 8 | 9 | static sendErrorRespons(response) { 10 | response.status(500).send(response); 11 | 12 | } 13 | 14 | static handleError(err, req, res, next) { 15 | if (err) { 16 | logger.error(err); 17 | 18 | if (process.env.NODE_ENV === 'production') { 19 | return utils.sendErrorResponse(res); 20 | } 21 | else { 22 | let errorObject = { 23 | name: err.name, 24 | message: err.message, 25 | stack: err.stack, 26 | code: err.code, 27 | status: 500 28 | }; 29 | 30 | return res 31 | .status(errorObject.status) 32 | .json(errorObject); 33 | } 34 | } 35 | else { 36 | return utils.sendErrorResponse(res); 37 | } 38 | } 39 | } 40 | 41 | export default Util; -------------------------------------------------------------------------------- /modules/discord-bot/wallet/SessionStorageService.js: -------------------------------------------------------------------------------- 1 | import Keyv from 'keyv'; 2 | 3 | const privateKeys = new Keyv(); 4 | const publicKeys = new Keyv(); 5 | const clusters = new Keyv(); 6 | 7 | const setKeyPair = async (id, privateKey, publicKey) => { 8 | await Promise 9 | .all([ 10 | privateKeys.set(id, privateKey), 11 | publicKeys.set(id, publicKey), 12 | ]); 13 | return { privateKey, publicKey }; 14 | }; 15 | 16 | const getKeyPair = async (id) => { 17 | const [privateKey, publicKey] = await Promise 18 | .all([await privateKeys.get(id), await publicKeys.get(id)]); 19 | 20 | return { 21 | privateKey, 22 | publicKey, 23 | }; 24 | }; 25 | 26 | const setCluster = (id, clusterName) => clusters.set(id, clusterName); 27 | 28 | const getCluster = (id) => clusters.get(id); 29 | 30 | const deleteAll = (id) => Promise 31 | .all([privateKeys.delete(id), publicKeys.delete(id), clusters.delete(id)]); 32 | 33 | const getPrivateKey = (id) => privateKeys.get(id); 34 | 35 | export default { 36 | getPrivateKey, 37 | setKeyPair, 38 | getKeyPair, 39 | getCluster, 40 | setCluster, 41 | deleteAll, 42 | }; 43 | -------------------------------------------------------------------------------- /models/Server.js: -------------------------------------------------------------------------------- 1 | import db from '../lib/db.js'; 2 | 3 | class Server { 4 | constructor(data) { 5 | this.serverId = data.server_id; 6 | this.name = data.name; 7 | this.discordId = data.discord_id; 8 | this.logo = data.logo; 9 | } 10 | 11 | static async createServer(name, discordId, logo) { 12 | const text = 'INSERT INTO servers(name, discord_id, logo) VALUES($1,$,$3) RETURNING *'; 13 | const values = [name, discordId || null, logo]; 14 | 15 | let response = await db.query(text, values); 16 | response = response[0]; 17 | return new Server(response); 18 | } 19 | 20 | static async getById(serverId) { 21 | const text = 'SELECT * FROM servers WHERE server_id = $1'; 22 | const values = [serverId]; 23 | let response = await db.query(text, values); 24 | response = response[0]; 25 | return new Server(response); 26 | } 27 | 28 | static async getServers() { 29 | const text = 'SELECT * FROM servers'; 30 | let response = await db.query(text); 31 | let servers = response.map(server => new Server(server)); 32 | return servers; 33 | } 34 | } 35 | 36 | export default Server; 37 | -------------------------------------------------------------------------------- /modules/discord-bot/discord/commands/commands/saveDiscordKey.js: -------------------------------------------------------------------------------- 1 | /* 2 | import UserService from '../../../publicKeyStorage/UserService'; 3 | import { COMMAND_PREFIX } from '../../../config'; 4 | import Wallet from '../../../wallet'; 5 | 6 | export default { 7 | name: 'save-discordkey', 8 | description: 9 | 'Use this command to connect your discordId to a public key.' 10 | + ' Whenever someone sends you money using: \'!send @\', this is the public key their SOL will be sent to.', 11 | usage: [`${COMMAND_PREFIX}save-discordkey `], 12 | async execute(message, args) { 13 | if (args.length === 1) { 14 | message.channel.send('⚠️ Public key missing! ⚠️'); 15 | return; 16 | } 17 | const publicKeyString = args[1]; 18 | 19 | if (!Wallet.isValidPublicKey(publicKeyString)) { 20 | message.channel.send('⚠️ Invalid public key! ⚠️'); 21 | return; 22 | } 23 | 24 | try { 25 | await UserService.saveUser({ discordId: message.author.id, publicKey: publicKeyString }); 26 | } catch (e) { 27 | message.channel.send('⚠️ Failed to save public key ⚠️'); 28 | return; 29 | } 30 | message.channel.send(`🥳 You can now receive tips through discord at this address: ${publicKeyString} 🥳`); 31 | }, 32 | }; 33 | */ -------------------------------------------------------------------------------- /lib/bot.js: -------------------------------------------------------------------------------- 1 | import 'dotenv/config.js'; 2 | import Token from '../schemas/old/token.js'; 3 | import Discord from 'discord.js'; 4 | 5 | const client = new Discord.Client(); 6 | 7 | console.log('bot logging in...'); 8 | client.login(process.env.DISCORD_BOT_TOKEN); 9 | console.log('bot logged in... listening to events.'); 10 | 11 | client.on('guildMemberAdd', async member => { 12 | if (member.user.bot) return; 13 | 14 | console.log(`User ${member.id} joins guild "${member.guild.name}"`); 15 | 16 | /* 17 | get user from DB 18 | if user doesn't exists, send him DM or @him on channel to send link to register wallet 19 | if user exists: 20 | check server tokens/roles 21 | get user balance for these tokens 22 | assign role to user if it meets any requirement 23 | */ 24 | const userBalance = await Token.getBalance('0x23c84cbc8c3d786048f2ed85e3c5cee2877f2118'); 25 | console.log(userBalance); 26 | }); 27 | 28 | client.on('guildMemberAvailable', member => { 29 | console.log(`member becomes available in a large guild: ${member.tag}`); 30 | }); 31 | 32 | client.on('guildMemberUpdate', (oldMember, newMember) => { 33 | console.error(newMember.id + ' a guild member changes - i.e. new role, removed role, nickname.'); 34 | }); 35 | 36 | client.on('presenceUpdate', (oldMember, newMember) => { 37 | console.log(newMember.id + ' a guild member\'s presence changes'); 38 | }); 39 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 2020, 4 | "sourceType": "module", 5 | "allowImportExportEverywhere": false, 6 | "ecmaFeatures": { 7 | "globalReturn": false 8 | }, 9 | "requireConfigFile": false 10 | }, 11 | "env": { 12 | "es6": true, 13 | "node": true, 14 | "mocha": true 15 | }, 16 | "extends": [ 17 | "eslint:recommended" 18 | ], 19 | "parser": "@babel/eslint-parser", 20 | "rules": { 21 | "indent": ["error", 4, { 22 | "SwitchCase": 1 23 | }], 24 | "quotes": ["error", "single"], 25 | "semi": ["error", "always"], 26 | "no-console": "warn", 27 | "no-trailing-spaces": "error", 28 | "no-return-await": "error", 29 | "no-multiple-empty-lines": "error", 30 | "no-unused-vars": "warn", 31 | "eqeqeq": ["error", "always"], 32 | "space-before-function-paren": ["error", { 33 | "anonymous": "always", 34 | "named": "ignore", 35 | "asyncArrow": "always" 36 | }], 37 | "space-before-blocks": ["error", "always"], 38 | "keyword-spacing": ["error", { 39 | "before": true 40 | }], 41 | "no-unused-expressions": "error", 42 | "array-callback-return": "error", 43 | "no-useless-constructor": "error", 44 | "radix": "error" 45 | } 46 | } -------------------------------------------------------------------------------- /modules/discord-bot/wallet/index.js: -------------------------------------------------------------------------------- 1 | import { CLUSTERS } from '../config'; 2 | import SessionStorageService from './SessionStorageService'; 3 | import Solana from '../solana'; 4 | 5 | const assertValidClusterName = (clusterName) => { 6 | if (!Object.values(CLUSTERS).includes(clusterName.toLowerCase())) { 7 | throw new Error(`⚠️ Invalid cluster name: ${clusterName} ⚠️`); 8 | } 9 | }; 10 | 11 | const setCluster = (id, clusterName) => { 12 | assertValidClusterName(clusterName); 13 | return SessionStorageService.setCluster(id, clusterName); 14 | }; 15 | 16 | const login = async (id, privateKey, publicKey) => { 17 | await Promise.all([ 18 | SessionStorageService.setKeyPair(id, privateKey, publicKey), 19 | SessionStorageService.setCluster(id, CLUSTERS.MAINNET), 20 | ]); 21 | return { keypair: { privateKey, publicKey }, cluster: CLUSTERS.MAINNET }; 22 | }; 23 | 24 | const isLoggedIn = async (id) => (!!(await SessionStorageService.getPrivateKey(id))); 25 | 26 | export default { 27 | assertValidClusterName, 28 | getKeyPair: SessionStorageService.getKeyPair, 29 | getCluster: SessionStorageService.getCluster, 30 | setCluster, 31 | login, 32 | logout: SessionStorageService.deleteAll, 33 | isLoggedIn, 34 | createAccount: Solana.createAccount, 35 | createAccountFromMnemonic: Solana.createAccountFromMnemonic, 36 | getBalance: Solana.getBalance, 37 | isValidPublicKey: Solana.isValidPublicKey, 38 | transfer: Solana.transfer, 39 | }; 40 | -------------------------------------------------------------------------------- /modules/discord-bot/discord/commands/commands/me.js: -------------------------------------------------------------------------------- 1 | import PriceService from '../../../price/PriceService'; 2 | import Wallet from '../../../wallet'; 3 | import { COMMAND_PREFIX } from '../../../config'; 4 | 5 | const getDollarValue = async (sol) => { 6 | try { 7 | return await PriceService.getDollarValueForSol(sol); 8 | } catch { 9 | return false; 10 | } 11 | }; 12 | 13 | export default { 14 | name: 'me', 15 | description: 'Returns public key and its balance for the currently selected cluster. You must be logged in to use this command.', 16 | usage: [`${COMMAND_PREFIX}me`], 17 | async execute(message) { 18 | const { id: userId } = message.author; 19 | const [{ publicKey }, cluster] = await Promise 20 | .all([Wallet.getKeyPair(userId), Wallet.getCluster(userId)]); 21 | let balanceInLamports; 22 | try { 23 | balanceInLamports = await Wallet.getBalance(publicKey, cluster); 24 | } catch (e) { 25 | message.channel.send('It seems there is a problem with the solana cluster. Please talk to the server admins.'); 26 | return; 27 | } 28 | 29 | const sol = PriceService.convertLamportsToSol(balanceInLamports); 30 | const dollarValue = await getDollarValue(sol); 31 | 32 | message.channel.send(`Your public key: ${publicKey}\nYour account balance: ${sol} SOL ${dollarValue ? `(~$${dollarValue}) ` : ''}on cluster: ${cluster}`); 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /docker/nginx/templates/test.conf.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80 default_server; 3 | listen [::]:80 default_server; 4 | 5 | # SSL configuration 6 | # 7 | # listen 443 ssl default_server; 8 | # listen [::]:443 ssl default_server; 9 | # 10 | # Note: You should disable gzip for SSL traffic. 11 | # See: https://bugs.debian.org/773332 12 | # 13 | # Read up on ssl_ciphers to ensure a secure configuration. 14 | # See: https://bugs.debian.org/765782 15 | # 16 | # Self signed certs generated by the ssl-cert package 17 | # Don't use them in a production server! 18 | # 19 | # include snippets/snakeoil.conf; 20 | 21 | root /var/www/nginx-default; 22 | 23 | # Add index.php to the list if you are using PHP 24 | index index.html index.htm index.nginx-debian.html; 25 | 26 | server_name _; 27 | 28 | 29 | location / { 30 | root /app/client; 31 | index index.html index.htm; 32 | } 33 | 34 | location /api/ { 35 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 36 | proxy_set_header X-Forwarded-Host $host; 37 | proxy_set_header Host $http_host; 38 | proxy_set_header X-Real-IP $remote_addr; 39 | proxy_pass http://api:${API_PORT}/api/; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /modules/discord-bot/discord/commands/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import Discord from 'discord.js'; 3 | 4 | const COMMANDS = { 5 | // CREATE_NEW: 'create-new', 6 | // LOGIN: 'login', 7 | // ME: 'me', 8 | // CLUSTER: 'cluster', 9 | // SEND: 'send', 10 | //LOGOUT: 'logout', 11 | // SAVE_DISCORDKEY: 'save-discordkey', 12 | // DELETE_DISCORDKEY: 'delete-discordkey', 13 | // GET_DISCORDKEY: 'get-discordkey', 14 | HELP: 'help', 15 | // BALANCE: 'balance', 16 | VERIFY: 'verify', 17 | }; 18 | 19 | const OK_WITHOUT_LOGIN_COMMANDS = [ 20 | // COMMANDS.CREATE_NEW, 21 | COMMANDS.LOGIN, 22 | //COMMANDS.SAVE_DISCORDKEY, 23 | // COMMANDS.DELETE_DISCORDKEY, 24 | // COMMANDS.GET_DISCORDKEY, 25 | COMMANDS.HELP, 26 | COMMANDS.VERIFY, 27 | ]; 28 | 29 | let allCommands; 30 | 31 | const initCommands = async (client) => { 32 | client.commands = new Discord.Collection(); 33 | 34 | const commandFiles = fs.readdirSync(new URL('./commands', import.meta.url)).filter((file) => file.endsWith('.js')); 35 | 36 | const setCommand = async file => { 37 | const path = `./commands/${file}`; 38 | const command = (await import(path)).default; 39 | if (command) client.commands.set(command.name, command); 40 | }; 41 | 42 | const promises = commandFiles.map(setCommand); 43 | 44 | await Promise.all(promises); 45 | allCommands = client.commands; 46 | }; 47 | 48 | export default { 49 | initCommands, 50 | creationCommands: [COMMANDS.CREATE_NEW, COMMANDS.LOGIN], 51 | OK_WITHOUT_LOGIN_COMMANDS, 52 | getAllCommands: () => allCommands, 53 | COMMANDS, 54 | }; 55 | -------------------------------------------------------------------------------- /modules/discord-bot/discord/commands/commands/login.js: -------------------------------------------------------------------------------- 1 | import Wallet from '../../../wallet'; 2 | import PriceService from '../../../price/PriceService'; 3 | import { COMMAND_PREFIX } from '../../../config'; 4 | 5 | const getDollarValue = async (sol) => { 6 | try { 7 | return await PriceService.getDollarValueForSol(sol); 8 | } catch { 9 | return false; 10 | } 11 | }; 12 | 13 | export default { 14 | name: 'login', 15 | description: 'Logs you in. Use "!logout" to logout.', 16 | usage: [`${COMMAND_PREFIX}login `], 17 | async execute(message, args) { 18 | const userId = message.author.id; 19 | let account; 20 | try { 21 | account = await Wallet.createAccountFromMnemonic(args.slice(1).join(' ')); 22 | } catch (e) { 23 | message.channel.send(e.message); 24 | return; 25 | } 26 | const { publicKey, privateKey } = account; 27 | const { cluster } = await Wallet.login(userId, privateKey, publicKey); 28 | 29 | const sol = PriceService 30 | .convertLamportsToSol( 31 | await Wallet.getBalance(publicKey, cluster), 32 | ); 33 | 34 | const dollarValue = await getDollarValue(sol); 35 | 36 | message.channel.send('🥳 You\'re logged in, use \'!logout\' to logout! 🥳'); 37 | message.channel.send(`ℹ️ You're currently on cluster: ${cluster}. Use '!cluster' to switch between clusters! ℹ️`); 38 | message.channel.send(`Your public key: ${publicKey}\nYour account balance: ${sol} SOL ${dollarValue ? `(~$${dollarValue})` : ''}`); 39 | message.channel.send('🚨 Please consider deleting your previous message now to keep your seed phrase secret 🚨'); 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /modules/discord-bot/discord/index.js: -------------------------------------------------------------------------------- 1 | import Discord from 'discord.js'; 2 | import CommandUtil from './commands'; 3 | import { COMMAND_PREFIX } from '../config'; 4 | import Wallet from '../wallet'; 5 | 6 | import roleClaim from './role-claim'; 7 | 8 | const initHandler = async () => { 9 | const client = new Discord.Client(); 10 | await CommandUtil.initCommands(client); 11 | 12 | client.once('ready', () => { 13 | console.log('Ready!'); 14 | roleClaim(client); 15 | }); 16 | 17 | client.on('message', async (message) => { 18 | if (!message.content.startsWith(COMMAND_PREFIX) || message.author.bot) return; 19 | 20 | const args = message.content.slice(COMMAND_PREFIX.length).trim().split(/ +/); 21 | const command = args[0]; 22 | 23 | if (!client.commands.keyArray().includes(command)) { 24 | return; 25 | } 26 | 27 | if (!(await Wallet.isLoggedIn(message.author.id)) 28 | && !CommandUtil.OK_WITHOUT_LOGIN_COMMANDS.includes(command) 29 | ) { 30 | message.channel.send( 31 | ` You are already connected, nothing to do here.` 32 | ); 33 | return; 34 | } 35 | 36 | if (message.channel.type === 'dm') { 37 | await client.commands.get(command).execute(message, args); 38 | } else if (CommandUtil.COMMANDS.SEND === command) { 39 | await client.commands.get(command).execute(message, args); 40 | } 41 | }); 42 | 43 | try { 44 | await client.login(process.env.DISCORD_BOT_TOKEN); 45 | } 46 | catch (e) { 47 | console.error('Bot has failed to connect to discord.'); 48 | process.exit(1); 49 | } 50 | }; 51 | 52 | export default { 53 | initHandler, 54 | }; 55 | -------------------------------------------------------------------------------- /modules/discord-bot/discord/commands/commands/balance.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-syntax */ 2 | // import PriceService from '../../../price/PriceService'; 3 | // import Wallet from '../../../wallet'; 4 | import { COMMAND_PREFIX } from '../../../config'; 5 | 6 | import fetch from 'node-fetch'; 7 | 8 | const body = { 9 | method: 'getTokenAccountsByOwner', 10 | jsonrpc: '2.0', 11 | params: [ 12 | // Get the public key of the account you want the balance for. 13 | 'FidaKmZFztWo5bx7EBwZ1Z7Wra7PeELXKxdoYEGAjodq', 14 | { programId: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA' }, 15 | { encoding: 'jsonParsed', commitment: 'processed' }, 16 | ], 17 | id: '35f0036a-3801-4485-b573-2bf29a7c77d2', 18 | }; 19 | 20 | export default { 21 | name: 'balance-all', 22 | description: 'Checks the token balance for a given wallet', 23 | usage: [`${COMMAND_PREFIX}balance-all`], 24 | async execute(message) { 25 | const response = await fetch('https://solana-api.projectserum.com/', { 26 | method: 'POST', 27 | body: JSON.stringify(body), 28 | headers: { 'Content-Type': 'application/json' }, 29 | }); 30 | let value = ''; 31 | const json = await response.json(); 32 | const resultValues = json.result.value; 33 | const theOwner = body.params[0]; 34 | 35 | message.channel.send(`[+] Token(s) for Address: ${theOwner} [+]\n`); 36 | for (value of resultValues) { 37 | const parsedInfo = value.account.data.parsed.info; 38 | const { mint, tokenAmount } = parsedInfo; 39 | const uiAmount = tokenAmount.uiAmountString; 40 | message.channel.send(`Mint: ${mint} | Balance: ${uiAmount}`); 41 | } 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /models/UserSession.js: -------------------------------------------------------------------------------- 1 | import Server from './Server.js'; 2 | import User from './User.js'; 3 | import UserServer from './UserServer.js'; 4 | import UserWallet from './UserWallet.js'; 5 | 6 | class UserSession { 7 | 8 | static async registerFromDiscord(userId, publicKey) { 9 | let user = await User.getById(userId); 10 | let userWallet = false; 11 | 12 | if (user) { 13 | userWallet = await UserWallet.createUserWallet(userId, publicKey); 14 | user.hasWallet = true; 15 | await user.save(); 16 | } 17 | 18 | if (userWallet) { 19 | let user = await UserSession.getByAddress(publicKey); 20 | return user; 21 | } else { 22 | throw new Error('Invalid user'); 23 | } 24 | } 25 | 26 | static async registerFromClient(address) { 27 | let user = await User.createUser(); 28 | let userWallet = await UserWallet.createUserWallet(user.userId, address); 29 | let userServers = []; 30 | 31 | return UserSession.createUserSession(user, [userWallet], userServers); 32 | } 33 | 34 | static async getByAddress(address) { 35 | let userWallet = await UserWallet.getByAddress(address); 36 | 37 | 38 | if (userWallet) { 39 | let userId = userWallet.userId; 40 | let user = await User.getById(userId); 41 | let userWallets = await UserWallet.getByUser(userId); 42 | let userServers = await UserServer.getByUser(userId); 43 | 44 | return UserSession.createUserSession(user, userWallets, userServers); 45 | } else { 46 | return UserSession.registerFromClient(address); 47 | } 48 | } 49 | 50 | static async createUserSession(user, userWallets, userServers) { 51 | let servers = await Server.getServers(); 52 | 53 | return { 54 | ...user, 55 | userWallets: userWallets, 56 | userServers: userServers, 57 | servers 58 | }; 59 | } 60 | } 61 | 62 | export default UserSession; 63 | -------------------------------------------------------------------------------- /models/UserWallet.js: -------------------------------------------------------------------------------- 1 | import db from '../lib/db.js'; 2 | 3 | class UserWallet { 4 | constructor(data) { 5 | this.userWalletId = data.user_wallet_id; 6 | this.userId = data.user_id; 7 | this.address = data.address; 8 | this.providerId = data.provider_id; 9 | this.networkId = data.network_id; 10 | this.isPrimary = data.is_primary; 11 | } 12 | 13 | static async getByAddress(address) { 14 | const text = 'SELECT * FROM user_wallets WHERE address = $1'; 15 | const values = [address]; 16 | let response = await db.query(text, values); 17 | response = response[0]; 18 | 19 | if (response) { 20 | return new UserWallet(response); 21 | } 22 | 23 | return null; 24 | } 25 | 26 | static async getByUser(userId) { 27 | const text = 'SELECT * FROM user_wallets WHERE user_id = $1'; 28 | const values = [userId]; 29 | let response = await db.query(text, values); 30 | 31 | if (response) { 32 | response = response.map(wallet => new UserWallet(wallet)); 33 | return response; 34 | } 35 | 36 | return []; 37 | } 38 | 39 | static async createUserWallet(userId, address) { 40 | let wallet = await UserWallet.getByAddress(address); 41 | 42 | if (!wallet) { 43 | const text = 'INSERT INTO user_wallets(user_id, address) VALUES($1, $2) RETURNING *'; 44 | const values = [userId, address]; 45 | 46 | let response = await db.query(text, values); 47 | response = response[0]; 48 | return new UserWallet(response); 49 | } else if (wallet.userId === userId) { 50 | return wallet; 51 | } 52 | } 53 | 54 | async save() { 55 | const text = 'UPDATE user_wallets SET user_id = $1, address = $2 WHERE user_wallet_id = $3'; 56 | const values = [this.userId, this.address, this.userWalletId]; 57 | let response = await db.query(text, values); 58 | return response; 59 | } 60 | } 61 | 62 | export default UserWallet; -------------------------------------------------------------------------------- /models/UserServer.js: -------------------------------------------------------------------------------- 1 | import db from '../lib/db.js'; 2 | import Server from './Server.js'; 3 | 4 | class UserServer { 5 | constructor(data) { 6 | this.userServerId = data.user_server_id; 7 | this.userId = data.user_id; 8 | this.serverId = data.server_id; 9 | this.name = data.name; 10 | this.discordId = data.discord_id; 11 | this.logo = data.logo; 12 | } 13 | 14 | static async createUserServer(userId, serverId) { 15 | let server = await Server.getById(serverId); 16 | let userServers = await UserServer.getByUser(userId); 17 | let userServer; 18 | 19 | userServers.forEach(item=>{ 20 | if (item.serverId === serverId) { 21 | userServer = item; 22 | } 23 | }); 24 | 25 | if (userServer) { 26 | return userServer; 27 | } 28 | 29 | const text = 'INSERT INTO user_servers(user_id, server_id) VALUES($1,$2) RETURNING *'; 30 | const values = [userId, serverId]; 31 | 32 | let response = await db.query(text, values); 33 | response = response[0]; 34 | response = Object.assign({}, response, server); 35 | return new UserServer(response); 36 | } 37 | 38 | static async deleteUserServer(userId, serverId) { 39 | const text = 'DELETE FROM user_servers WHERE user_id = $1 AND server_id = $2'; 40 | const values = [userId, serverId]; 41 | let response = await db.query(text, values); 42 | return response; 43 | } 44 | 45 | static async deleteByUser(userId) { 46 | const text = 'DELETE FROM user_servers WHERE user_id = $1'; 47 | const values = [userId]; 48 | let response = await db.query(text, values); 49 | return response; 50 | } 51 | 52 | static async getByUser(userId) { 53 | const text = 'SELECT * FROM user_servers INNER JOIN servers on user_servers.server_id = servers.server_id WHERE user_id = $1'; 54 | const values = [userId]; 55 | let response = await db.query(text, values); 56 | 57 | if (response) { 58 | response = response.map(server => new UserServer(server)); 59 | return response; 60 | } 61 | 62 | return []; 63 | } 64 | } 65 | 66 | export default UserServer; 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grape_app", 3 | "version": "6.29.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "docker-compose up --build --force-recreate api", 7 | "start:daemon": "docker-compose up -d --build --force-recreate api", 8 | "api:dev": "nodemon app.js", 9 | "api:prod": "node --experimental-specifier-resolution=node app.js", 10 | "start:bot": "docker-compose up --build --force-recreate bot", 11 | "bot": "node --experimental-specifier-resolution=node modules/discord-bot/bot.js", 12 | "pgadmin": "docker-compose up --build -d pgadmin", 13 | "script": "node ./script/script-app.js" 14 | }, 15 | "_moduleAliases": { 16 | "$lib": "./lib", 17 | "$schema": "./schemas", 18 | "@model": "models" 19 | }, 20 | "type": "module", 21 | "dependencies": { 22 | "@solana/web3.js": "^1.15.0", 23 | "axios": "^0.21.1", 24 | "bip39": "^3.0.4", 25 | "body-parser": "^1.19.0", 26 | "bs58": "^4.0.1", 27 | "compression": "^1.7.4", 28 | "config": "^3.0.1", 29 | "cors": "^2.8.3", 30 | "crypto-js": "^4.0.0", 31 | "discord.js": "^12.5.3", 32 | "dotenv": "^10.0.0", 33 | "express": "^4.16.3", 34 | "express-async-errors": "^3.1.1", 35 | "express-session": "^1.17.2", 36 | "express-winston": "^4.1.0", 37 | "form-data": "^4.0.0", 38 | "helmet": "^4.6.0", 39 | "jsonwebtoken": "^8.5.1", 40 | "keyv": "^4.0.3", 41 | "lodash": "^4.17.19", 42 | "mkpath": "^1.0.0", 43 | "module-alias": "^2.2.0", 44 | "moment": "^2.24.0", 45 | "moment-timezone": "^0.5.32", 46 | "mongoose": "^5.7.11", 47 | "morgan": "^1.9.0", 48 | "noble-ed25519": "^1.2.1", 49 | "node-fetch": "^2.6.1", 50 | "node-schedule": "^2.0.0", 51 | "pg": "^8.6.0", 52 | "walkdir": "0.4.1", 53 | "web3": "^1.3.6", 54 | "winston": "^3.2.1", 55 | "winston-daily-rotate-file": "^4.4.0", 56 | "winston-transport": "^4.4.0" 57 | }, 58 | "devDependencies": { 59 | "@babel/core": "^7.14.3", 60 | "@babel/eslint-parser": "^7.14.4", 61 | "eslint": "^7.27.0", 62 | "nodemon": "^2.0.7" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | 3 | volumes: 4 | postgres_data_dev: {} 5 | postgres_backup_dev: {} 6 | pgadmin_data: {} 7 | node_modules: {} 8 | 9 | services: 10 | postgres: 11 | container_name: grape_postgres_server 12 | build: ./docker/postgres 13 | volumes: 14 | - postgres_data_dev:/var/lib/postgresql/data 15 | - postgres_backup_dev:/backups 16 | env_file: .env 17 | networks: 18 | - db 19 | - back 20 | 21 | api: 22 | restart: always 23 | container_name: grape_api 24 | build: 25 | context: . 26 | dockerfile: ./docker/api/Dockerfile 27 | depends_on: 28 | - postgres 29 | ports: 30 | - "${API_PORT}:${API_PORT}" 31 | env_file: .env 32 | networks: 33 | - back 34 | tty: true 35 | stdin_open: true 36 | volumes: 37 | - .:/app 38 | #- node_modules:/app/node_modules 39 | logging: 40 | options: 41 | max-file: "10" 42 | max-size: "10m" 43 | 44 | bot: 45 | restart: always 46 | container_name: grape_bot 47 | build: 48 | context: . 49 | dockerfile: ./docker/api/Dockerfile 50 | command: npm run bot 51 | depends_on: 52 | - postgres 53 | env_file: .env 54 | networks: 55 | - back 56 | volumes: 57 | - .:/app 58 | - node_modules:/app/node_modules 59 | logging: 60 | options: 61 | max-file: "10" 62 | max-size: "10m" 63 | 64 | nginx: 65 | restart: always 66 | container_name: nginx_server 67 | build: ./docker/nginx 68 | env_file: .env 69 | volumes: 70 | - ./docker/nginx/templates:/etc/nginx/templates 71 | depends_on: 72 | - api 73 | ports: 74 | - "0.0.0.0:80:80" 75 | networks: 76 | - back 77 | 78 | pgadmin: 79 | container_name: pgadmin4_container 80 | image: dpage/pgadmin4 81 | restart: always 82 | depends_on: 83 | - postgres 84 | env_file: .env 85 | ports: 86 | - "${PG_ADMIN_PORT}:80" 87 | volumes: 88 | - pgadmin_data:/var/lib/pgadmin 89 | networks: 90 | - db 91 | 92 | networks: 93 | back: 94 | db: -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | import winston from 'winston'; 2 | import DailyRotateFile from 'winston-daily-rotate-file'; 3 | import config from 'config'; 4 | import mkpath from 'mkpath'; 5 | 6 | mkpath(config.logPath, function (err) { 7 | if (err) { 8 | throw err; 9 | } 10 | }); 11 | 12 | mkpath.sync(config.logPath, 0o700); 13 | 14 | const { 15 | combine, 16 | timestamp, 17 | printf, 18 | colorize 19 | } = winston.format; 20 | 21 | const customFormat = printf(info => { 22 | let logString = `[${process.pid}] ${info.timestamp} ${info.level}:`; 23 | 24 | if (info.errors) { 25 | for (let errObject of info.errors) { 26 | logString += ` ${errObject.err ? errObject.err.stack : errObject.name}`; 27 | } 28 | } else { 29 | logString += ` ${info.message} ${info.stack ? '- ' + info.stack : ''}`; 30 | } 31 | 32 | return logString; 33 | }); 34 | 35 | const options = { 36 | level: process.env.LOG_LEVEL, 37 | handleExceptions: true, 38 | humanReadableUnhandledException: true, 39 | json: false, 40 | format: combine( 41 | colorize(), 42 | timestamp(), 43 | winston.format.splat(), 44 | winston.format.errors({ stack: true }), 45 | winston.format.json(), 46 | customFormat 47 | ) 48 | }; 49 | 50 | const logger = winston.createLogger({ 51 | transports: [ 52 | new DailyRotateFile({ 53 | name: 'file', 54 | filename: config.logPath + '/api.%DATE%.log', 55 | colorize: true, 56 | datePattern: 'YYYY-MM-DD', 57 | zippedArchive: true, 58 | maxFiles: 10, 59 | ...options 60 | }) 61 | ], 62 | format: combine( 63 | winston.format.splat(), 64 | winston.format.errors({ stack: true }), 65 | winston.format.json() 66 | ), 67 | handleExceptions: true, 68 | humanReadableUnhandledException: true, 69 | exitOnError: false 70 | }); 71 | 72 | if (process.env.NODE_ENV !== 'production') { 73 | logger.add(new winston.transports.Console(options)); 74 | } 75 | 76 | logger.setMaxListeners(0); 77 | 78 | // we need to redefine the .error function, since it is buggy 79 | // see https://github.com/winstonjs/winston/issues/1338 80 | logger.error = item => { 81 | const message = item instanceof Error && item.stack 82 | ? item.stack.replace('\n', '').replace(' ', ' - trace: ') 83 | : item; 84 | 85 | logger.log('error', message); 86 | }; 87 | 88 | export default logger; 89 | -------------------------------------------------------------------------------- /modules/aggregator/aggregator.js: -------------------------------------------------------------------------------- 1 | const logger = require('$lib/logger'); 2 | const schedule = require('node-schedule'); 3 | const Task = require('$schemas/Task.schema'); 4 | 5 | class Aggregator { 6 | static EVERY_X_MINUTES(minutes) { 7 | return '*/' + minutes + ' * * * *'; 8 | } 9 | 10 | static get EVERY_DAY() { 11 | return { second: 59, minute: 59, hour: 23 }; 12 | } 13 | 14 | static get EVERY_MORNING() { 15 | return { second: 0, minute: 0, hour: 0 }; 16 | } 17 | 18 | static get EVERY_HOUR() { 19 | return { minute: 0 }; 20 | } 21 | 22 | static get EVERY_HALF_HOUR() { 23 | return { minute: 30 }; 24 | } 25 | 26 | static get EVERY_MINUTE() { 27 | return { second: 0 }; 28 | } 29 | 30 | static get EVERY_FIVE_MINUTES() { 31 | return { minute: 5 }; 32 | } 33 | 34 | static get EVERY_MONTH() { 35 | return { date: 1, hour: 0, minute: 0 }; 36 | } 37 | 38 | static get EVERY_SECOND() { 39 | return { second: 1 }; 40 | } 41 | 42 | static get START_OF_WEEK() { 43 | return { second: 0, minute: 0, hour: 0, dayOfWeek: 1 }; 44 | } 45 | 46 | static get tasks() { 47 | return [ 48 | { 49 | name: 'test', 50 | description: 'test', 51 | interval: Aggregator.EVERY_DAY, 52 | run: async () => { 53 | console.log('test'); 54 | } 55 | } 56 | ]; 57 | } 58 | 59 | constructor() { 60 | this.id = process.pid; 61 | this.jobs = []; 62 | } 63 | 64 | schedule(task) { 65 | if (!(task && task.runTask)) return; 66 | 67 | let job = schedule.scheduleJob(task.interval, task.runTask.bind(task)); 68 | return job; 69 | } 70 | 71 | async startTasks() { 72 | try { 73 | this.jobs = []; 74 | 75 | for (let i = 0; i < Aggregator.tasks.length; i++) { 76 | let task = Aggregator.tasks[i]; 77 | task = await Task.createTask(task); 78 | let job = this.schedule(task); 79 | this.jobs.push(job); 80 | } 81 | 82 | } catch (err) { 83 | logger.error(`[Aggregator][Error]: ${err}`); 84 | } 85 | } 86 | 87 | onClose() { 88 | try { 89 | this.jobs.forEach(job => { 90 | job.cancel(); 91 | }); 92 | } catch (err) { 93 | logger.error(`[Aggregator][Error]: ${err}`); 94 | } 95 | 96 | } 97 | } 98 | 99 | module.exports = Aggregator; 100 | -------------------------------------------------------------------------------- /modules/discord-bot/discord/commands/commands/verify.js: -------------------------------------------------------------------------------- 1 | import sha256 from 'crypto-js/sha256'; 2 | // import Discord from 'discord.js'; 3 | import moment from 'moment'; 4 | import { COMMAND_PREFIX } from '../../../config'; 5 | import User from '../../../../../models/User.js'; 6 | 7 | // import UserService from "../../../publicKeyStorage/UserService"; 8 | 9 | 10 | export default { 11 | name: 'verify', 12 | description: 'Verifying user by linking external wallet with discord wallet', 13 | usage: [`${COMMAND_PREFIX}verify`], 14 | async execute(message, args) { 15 | // const client = new Discord.Client(); 16 | const getTokenLink = (token) => `https://verify.grapes.network/?token=${token}`; 17 | const hashVerifyToken = sha256(`GRAPE${moment().format('x')}`); 18 | // const isVerified = Boolean(false); 19 | 20 | 21 | // let dbDiscordId=0; 22 | // const userId = 'c4425d86-6790-4a17-a854-1afcd2f4214'; 23 | console.log(message.author.id); 24 | const discordId = message.author.id; 25 | const dbDiscordId = await User.checkDiscordId(discordId); 26 | 27 | console.log(dbDiscordId); 28 | //console.log(user); 29 | //console.log(user); 30 | 31 | // const sql = "SELECT a.discord_id as discord_id FROM users a, user_wallets b WHERE a.user_id=b.user_id and a.discord_id like '$1' group by discord_id"; 32 | 33 | try { 34 | // await UserService.saveVerifiedUser({ discordId: message.author.id, discordServerId: '837189238289203201', generatedUserToken: hashVerifyToken.toString() }); 35 | } catch (e) { 36 | // Object.keys(client.guilds).forEach((row) => { 37 | // console.log(row); 38 | // }); 39 | // const ids = client.guilds.cache.map((dids) => dids.id); 40 | // console.log('IDs list', ids); 41 | // console.log('THIS IS THE SERVER ID: ', client.guilds); 42 | message.channel.send(`User Error Message: ${e}`); 43 | } 44 | 45 | // if (args.length === 1) { 46 | // message.channel.send('Public key is missing'); 47 | // } else { 48 | 49 | // console.log('test'); 50 | const mydiscordid = message.author.id.toString().trim(); 51 | 52 | // console.log(discordId); 53 | 54 | // console.log(mydiscordid); 55 | console.log(dbDiscordId); 56 | 57 | // console.log(mydiscordid.localeCompare(dbDiscordId)); 58 | 59 | if (mydiscordid.localeCompare(dbDiscordId) === 1) { 60 | message.channel.send(getTokenLink(hashVerifyToken)); 61 | } 62 | 63 | if (mydiscordid.localeCompare(dbDiscordId) === 0) { 64 | message.channel.send('You have already linked your Discord id with your wallet.'); 65 | } 66 | 67 | // } 68 | }, 69 | }; 70 | -------------------------------------------------------------------------------- /models/User.js: -------------------------------------------------------------------------------- 1 | import db from '../lib/db.js'; 2 | 3 | class User { 4 | constructor(data) { 5 | this.userId = data.user_id; 6 | this.discordId = data.discord_id; 7 | this.twitterId = data.twitter_id; 8 | this.botToken = data.bottoken; 9 | this.is_og = data.is_og; 10 | this.hasWallet = data.has_wallet; 11 | 12 | } 13 | 14 | static async createUser(discordId) { 15 | let user = discordId && await User.getByDiscordId(discordId); 16 | console.log(user); 17 | 18 | if (!user) { 19 | const text = 'INSERT INTO users(discord_id, has_wallet) VALUES ($1, false) RETURNING *'; 20 | const values = [discordId || null]; 21 | 22 | let response = await db.query(text, values); 23 | response = response[0]; 24 | return new User(response); 25 | } 26 | 27 | return user; 28 | } 29 | 30 | static async getById(userId) { 31 | const text = 'SELECT * FROM users WHERE user_id = $1'; 32 | const values = [userId]; 33 | let response = await db.query(text, values); 34 | response = response[0]; 35 | return response && new User(response) || null; 36 | } 37 | 38 | static async deleteUser(userId) { 39 | const text = 'DELETE FROM users WHERE user_id = $1'; 40 | const values = [userId]; 41 | let response = await db.query(text, values); 42 | return response; 43 | } 44 | 45 | static async checkDiscordId(discordId) { 46 | try { 47 | const text = 'SELECT a.discord_id as discord_id FROM users a, user_wallets b WHERE a.user_id=b.user_id and a.discord_id = $1 group by discord_id'; 48 | const values = [discordId]; 49 | let response = await db.query(text, values); 50 | response = response[0]; 51 | if (response) 52 | return response.discord_id; 53 | 54 | return 0; 55 | } catch (err) { 56 | console.error(err); 57 | } 58 | } 59 | 60 | static async getByDiscordId(discordId) { 61 | try { 62 | const text = 'SELECT * FROM users WHERE discord_id = $1'; 63 | const values = [discordId]; 64 | let response = await db.query(text, values); 65 | response = response[0]; 66 | return response && new User(response) || null; 67 | 68 | } catch (err) { 69 | console.error(err); 70 | } 71 | } 72 | 73 | static async getWalletByDiscordId(discordId) { 74 | try { 75 | const text = 'SELECT a.address as address FROM user_wallets a, users b WHERE a.user_id=b.user_id and b.discord_id = $1'; 76 | const values = [discordId]; 77 | let response = await db.query(text, values); 78 | response = response[0]; 79 | if (response) 80 | return response.address; 81 | 82 | return 0; 83 | } catch (err) { 84 | console.error(err); 85 | } 86 | } 87 | 88 | async save_og() { 89 | const text = 'UPDATE users SET is_og = $2 WHERE user_id = $1'; 90 | const values = [this.userId, this.is_og]; 91 | //console.log(this); 92 | let response = await db.query(text, values); 93 | } 94 | 95 | 96 | async save() { 97 | const text = 'UPDATE users SET discord_id = $2, has_wallet=$3 WHERE user_id = $1'; 98 | const values = [this.userId, this.discordId, this.hasWallet]; 99 | let response = await db.query(text, values); 100 | } 101 | } 102 | 103 | export default User; 104 | -------------------------------------------------------------------------------- /lib/db.js: -------------------------------------------------------------------------------- 1 | import pg from 'pg'; 2 | 3 | const { Pool } = pg; 4 | 5 | class DB { 6 | constructor() { 7 | this.pool = new Pool({ 8 | user: process.env.DB_USER, 9 | host: process.env.DB_HOST, 10 | database: process.env.DB_NAME, 11 | password: process.env.DB_PASSWORD, 12 | port: process.env.DB_PORT, 13 | max: 20, 14 | idleTimeoutMillis: 30000, 15 | connectionTimeoutMillis: 2000 16 | }); 17 | 18 | this.syncTables(); 19 | } 20 | 21 | async query(...args) { 22 | let client, response; 23 | 24 | try { 25 | client = await this.pool.connect(); 26 | response = await client.query(...args); 27 | } 28 | catch (err) { 29 | console.log(err); 30 | throw err; 31 | } 32 | finally { 33 | client?.release(); 34 | } 35 | 36 | return response && response.rows || []; 37 | } 38 | 39 | async getTables() { 40 | const text = ` 41 | SELECT table_name 42 | FROM information_schema.tables 43 | WHERE table_schema = 'public' 44 | `; 45 | let response = await this.query(text); 46 | let tables = response && response.map(row => row.table_name); 47 | tables = tables.reduce((result, row) => { 48 | result[row] = true; 49 | return result; 50 | }, {}); 51 | 52 | return tables; 53 | } 54 | 55 | async syncTables() { 56 | let tables = await this.getTables(); 57 | 58 | if (!tables.users) { 59 | await this.createUserTable(); 60 | } 61 | 62 | if (!tables.user_wallets) { 63 | await this.createUserWalletTable(); 64 | } 65 | 66 | if (!tables.servers) { 67 | await this.createServerTable(); 68 | } 69 | 70 | if (!tables.user_servers) { 71 | await this.createUserServerTable(); 72 | } 73 | } 74 | 75 | //Tables 76 | async createServerTable() { 77 | const text = ` 78 | CREATE TABLE servers ( 79 | server_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 80 | name VARCHAR (512) NOT NULL, 81 | discord_id VARCHAR(64), 82 | logo VARCHAR(512) 83 | )`; 84 | let response = await this.query(text); 85 | console.log('Created servers table'); 86 | 87 | return response; 88 | } 89 | 90 | async createUserServerTable() { 91 | 92 | const text = ` 93 | CREATE TABLE user_servers ( 94 | user_server_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 95 | user_id uuid NOT NULL, 96 | server_id uuid NOT NULL 97 | )`; 98 | let response = await this.query(text); 99 | console.log('Created user_servers table'); 100 | 101 | return response; 102 | } 103 | 104 | async createUserTable() { 105 | 106 | const text = ` 107 | CREATE TABLE users ( 108 | user_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), 109 | discord_id VARCHAR (32), 110 | twitter_id VARCHAR (32), 111 | bot_token VARCHAR (64), 112 | is_og BOOLEAN, 113 | has_wallet BOOLEAN 114 | )`; 115 | let response = await this.query(text); 116 | console.log('Created users table'); 117 | 118 | return response; 119 | } 120 | 121 | async createUserWalletTable() { 122 | 123 | const text = ` 124 | CREATE TABLE user_wallets ( 125 | user_wallet_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 126 | user_id UUID, 127 | address VARCHAR (64) NOT NULL, 128 | network_id UUID, 129 | provider_id UUID, 130 | is_primary BOOLEAN 131 | )`; 132 | let response = await this.query(text); 133 | console.log('Created user_wallets table'); 134 | 135 | return response; 136 | } 137 | } 138 | 139 | let db = new DB(); 140 | export default db; -------------------------------------------------------------------------------- /modules/discord-bot/discord/commands/commands/send.js: -------------------------------------------------------------------------------- 1 | // import PriceService from '../../../price/PriceService'; 2 | // import Wallet from '../../../wallet'; 3 | // import UserService from '../../../publicKeyStorage/UserService'; 4 | // import { COMMAND_PREFIX } from '../../../config'; 5 | 6 | // const getCurrentSolPriceInUSD = async () => { 7 | // try { 8 | // return await PriceService.getSolPriceInUSD(); 9 | // } catch { 10 | // return false; 11 | // } 12 | // }; 13 | 14 | // const getUserFromMention = (mention) => { 15 | // const matches = mention.match(/^<@!?(\d+)>$/); 16 | // if (!matches) { 17 | // return null; 18 | // } 19 | // return matches[1]; 20 | // }; 21 | 22 | // export default { 23 | // name: 'send', 24 | // description: 'Lets you send SOL to someone on the currently selected cluster. To specify the recipient,' 25 | // + 'you can use a public key or tag someone with @ someone. You must be logged in to use this command.' 26 | // + ' You can add the cluster name after the recipient to send the tx on a specific cluster.', 27 | // usage: [ 28 | // `${COMMAND_PREFIX}send `, 29 | // `${COMMAND_PREFIX}send @`, 30 | // `${COMMAND_PREFIX}send @ `, 31 | // ], 32 | // async execute(message, args) { 33 | // if (args.length !== 3 && args.length !== 4) { 34 | // message.channel.send('⚠️ Wrong number of arguments ⚠️'); 35 | // return; 36 | // } 37 | 38 | // let clusterArg; 39 | // if (args.length >= 4) { 40 | // try { 41 | // Wallet.assertValidClusterName(args[3]); 42 | // } catch (e) { 43 | // message.channel.send(e.message); 44 | // return; 45 | // } 46 | 47 | // // eslint-disable-next-line prefer-destructuring 48 | // clusterArg = args[3]; 49 | // } 50 | 51 | // const solToSend = parseFloat(args[1]); 52 | // let toPublicKeyString = args[2]; 53 | 54 | // let recipientId; 55 | // if (!Wallet.isValidPublicKey(toPublicKeyString)) { 56 | // recipientId = getUserFromMention(toPublicKeyString); 57 | // if (!recipientId) { 58 | // message.channel.send('⚠️ Given recipient is neither a public key nor a user ⚠️'); 59 | // return; 60 | // } 61 | // const recipient = await UserService.getUser(recipientId); 62 | // if (!recipient) { 63 | // message.channel.send('⚠️ Given recipient has not registered a discord public key ⚠️'); 64 | // return; 65 | // } 66 | 67 | // toPublicKeyString = recipient.publicKey; 68 | // } 69 | 70 | // // eslint-disable-next-line no-restricted-globals 71 | // if (isNaN(solToSend) || solToSend <= 0) { 72 | // message.channel.send('⚠️ Invalid sol amount ⚠️'); 73 | // return; 74 | // } 75 | 76 | // message.channel.send('Sending...'); 77 | 78 | // const userId = message.author.id; 79 | // const cluster = clusterArg || await Wallet.getCluster(userId); 80 | // const keypair = await Wallet.getKeyPair(userId); 81 | // const { privateKey } = keypair; 82 | 83 | // let signature = ''; 84 | // try { 85 | // signature = await Wallet 86 | // .transfer(cluster, Object.values(privateKey), toPublicKeyString, solToSend); 87 | // } catch (e) { 88 | // message.channel.send(e.message); 89 | // return; 90 | // } 91 | 92 | // const currentPrice = await getCurrentSolPriceInUSD(); 93 | 94 | // const dollarValue = currentPrice 95 | // ? await PriceService.getDollarValueForSol(solToSend, currentPrice) 96 | // : null; 97 | 98 | // const recipient = recipientId ? `<@${recipientId}>` : toPublicKeyString; 99 | 100 | // const txLink = ``; 101 | // const data = []; 102 | // data.push(`💸 Successfully sent ${solToSend} SOL ${dollarValue ? `(~$${dollarValue}) ` : ''}to ${recipient} on cluster: ${cluster} 💸`); 103 | // data.push('Click the link to see when your tx has been finalized (reached MAX confirmations)!'); 104 | // data.push(`${txLink}`); 105 | // message.channel.send(data); 106 | // }, 107 | // }; 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | logo GRAPE 4 |
5 |

6 | 7 | # GRAPE ACCESS 8 | 9 | ## SPL Token gated communities on Discord 10 | Connecting members social accounts to unique cryptographic keys is the core of our Dynamic Balance-Based Membership solution. 11 | 12 | ### Prerequisites 13 | 1. Must have an existing Discord server with at least a blank channel (Read-only channel). 14 | 2. Invite the bot to your server using the following link: ```https://discord.com/oauth2/authorize?client_id=&scope=bot``` (replace YOUR_APPLICATION_CLIENT_ID with your respective application client ID from Discord). 15 | 3. Upload 4 customs server emojis, and create appropriate roles for assigning to users (reacting with emojis). 16 | 17 | Currently we are using the following emoji - role pairs: 18 | 19 | ```javascript 20 | // Uploaded server Emojis 21 | const emojis = { 22 | walletv2: 'Verified Wallet', 23 | medianetwork: 'MEDIA Holder', 24 | mercurial: 'Mercurial Holder', 25 | samo: 'SAMO Holder', 26 | }; 27 | ``` 28 | Input the ID of you're read-only welcome channel in /modules/discord-bot/discord/role-claim.js file. 29 | ```javascript 30 | export default (client) => { 31 | // Read from Channel 32 | const channelId = 'Paste read-only channel ID'; 33 | 34 | const getEmoji = (emojiName) => client.emojis.cache.find((emoji) => emoji.name === emojiName); 35 | ``` 36 | 37 | 4. Install [PostgresSQL](https://www.postgresql.org/). 38 | 5. Rename the .env.example file to .env and fill in the following variables: 39 | ``` 40 | NODE_ENV = dev 41 | 42 | API_PORT = 5000 43 | 44 | #DATABASE 45 | DB_USER = graperoot 46 | DB_HOST = postgres 47 | DB_PASSWORD = elephant 48 | DB_NAME = grape 49 | DB_PORT = 5432 50 | 51 | #POSTGRES DOCKER 52 | POSTGRES_PASSWORD = 53 | POSTGRES_DB = #should be same as DB_NAME 54 | 55 | #PG ADMIN (OPTIONAL) 56 | PGADMIN_DEFAULT_EMAIL = toto@test.com 57 | PGADMIN_DEFAULT_PASSWORD = 123456 58 | PG_ADMIN_PORT = 5050 59 | 60 | #NGINX 61 | NGINX_ENVSUBST_TEMPLATE_SUFFIX = .conf 62 | 63 | CLIENT_URL = http://localhost/ # where client is being served from (e.g localhost) 64 | 65 | DISCORD_OAUTH_REDIRECT_URL = http://localhost/api/discord/callback # Discord callback endpoint 66 | DISCORD_OAUTH_CLIENT_ID = # Get from Discord 67 | DISCORD_OAUTH_SECRET = # Get from Discord 68 | DISCORD_BOT_TOKEN = # Get from Discord 69 | DISCORD_BOT_ID = # Get from Discord 70 | 71 | ETH_RPC_URL = # https://mainnet.infura.io/v3/ 72 | 73 | DISCORD_CHANNEL_ID = 74 | 75 | DISCORD_ROLE_1 =Verified Wallet 76 | DISCORD_ROLE_2 =MEDIA Holder 77 | DISCORD_ROLE_3 =Mercurial Holder 78 | DISCORD_ROLE_4 =SAMO Holder 79 | 80 | MINT_TOKEN_1 =ETAtLmCmsoiEEKfNrHKJ2kYy3MoABhU6NQvpSfij5tDs 81 | MINT_TOKEN_2 =MERt85fc5boKw3BW1eYdxonEuJNvXbiMbs6hvheau5K 82 | MINT_TOKEN_3 =7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU 83 | 84 | MINT_MIN_BALANCE_1 =0.099 85 | MINT_MIN_BALANCE_2 =9.99 86 | MINT_MIN_BALANCE_3 =9999.99 87 | 88 | ``` 89 | 6. Clone ane deploy client from this repository: https://github.com/The-Great-Ape/client 90 | 91 | ### Deployment 92 | + Clone this repository. 93 | + Install dependencies using: ``` npm install ``` 94 | + Start up server using: ``` npm start ``` 95 | + Start up Discord bot using ``` npm run bot ``` 96 | 97 | ### Features 98 | - Validate Solana wallet ownership. 99 | - Validate Discordid ownership. 100 | - Creates a link between the validated discord id and validated Wallet. 101 | - Allows token balance rules to obtain roles on Discord and gate channel access based on ownership. 102 | 103 | 104 | ## Acknowledgements 105 | * This project uses [Solana](https://solana.com/). A fast, secure, and censorship resistant blockchain providing the open infrastructure required for global adoption. 106 | * This project uses [Discord](https://discord.com/brand-new). A VoIP, instant messaging and digital distribution platform designed for creating communities. 107 | * The project uses parts of [Solbot](https://github.com/paul-schaaf/solbot) by Paul Schaaf, a tipping bot for Solana. 108 | * This project uses [Project Serum's markets](https://projectserum.com/). An ecosystem that brings unprecedented speed and low transaction costs to decentralized finance. 109 | * This project uses [noble-ed25519](https://github.com/paulmillr/noble-ed25519). Fastest JS implementation of ed25519, an elliptic curve that could be used for asymmetric encryption and EDDSA signature scheme. 110 | * This project uses [Serum Price API](https://github.com/sonar-watch/serum-price-api) by Sonar 111 | 112 | If you are having difficulty with this application please contact BillysDiscord#5191 or DeanMachine#9058 or GintoniQ#3987 on Discord for assistance and access 113 | to a live working Demo. 114 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | /*------------- 2 | Dependencies 3 | ---------------*/ 4 | 5 | //modules 6 | import 'dotenv/config.js'; 7 | 8 | import cors from 'cors'; 9 | import express from 'express'; 10 | import 'express-async-errors'; 11 | import morgan from 'morgan'; 12 | import helmet from 'helmet'; 13 | import config from 'config'; 14 | import session from 'express-session'; 15 | 16 | //lib 17 | import db from './lib/db.js'; 18 | import util from './lib/util.js'; 19 | import logger from './lib/logger.js'; 20 | 21 | //controllers 22 | import MainController from './controllers/main.controller.js'; 23 | 24 | class App { 25 | constructor(port) { 26 | //props 27 | this.port = port || null; 28 | 29 | //refs 30 | this.server = null; 31 | this.db = null; 32 | this.websocket = null; 33 | 34 | //states 35 | this.isClosing = false; 36 | } 37 | 38 | /*------------- 39 | Commands 40 | ---------------*/ 41 | start() { 42 | this.initServer(); 43 | } 44 | 45 | async initServer() { 46 | this.app = express(); 47 | this.port = this.port ? this.port : parseInt(process.env.API_PORT || 5000, 10); 48 | 49 | try { 50 | await this.initDB(); 51 | 52 | this.server = this.app.listen(this.port); 53 | this.server.on('listening', this.onListening.bind(this)); 54 | this.server.on('error', this.onError.bind(this)); 55 | 56 | process.on('SIGINT', this.stopServer.bind(this)); 57 | } catch (error) { 58 | logger.error(error); 59 | process.exit(1); 60 | } 61 | } 62 | 63 | /*------------- 64 | Modules 65 | ---------------*/ 66 | //Express 67 | initExpress() { 68 | //Helmet - simple security headers 69 | this.app.use(helmet()); 70 | 71 | //Default No Cache 72 | this.app.use((req, res, next) => { 73 | res.set('Cache-Control', 'no-store, no-cache, must-revalidate, private'); 74 | next(); 75 | }); 76 | 77 | //Pass API version 78 | this.app.use((req, res, next) => { 79 | res.set('Api-Version', process.env.npm_package_version); 80 | next(); 81 | }); 82 | 83 | this.app.use(express.urlencoded({ extended: true })); 84 | this.app.use(express.json()); 85 | 86 | //Session 87 | this.app.use(session(config.session)); 88 | 89 | //Errors 90 | // this.app.use((err, req, res, next) => { 91 | // if (err.message === 'Access denied') { 92 | // res.status(403); 93 | // res.json({ error: err.message }); 94 | // } 95 | 96 | // next(err); 97 | // }); 98 | 99 | logger.info(`Worker ${process.pid}: [Express]: Initialized`); 100 | } 101 | 102 | //CORs 103 | initCORS() { 104 | this.app.use(cors()); 105 | } 106 | 107 | //Mongoose 108 | async initDB() { 109 | await db.syncTables(); 110 | logger.info(`Worker ${process.pid}: [Postgres]: Connected.`); 111 | } 112 | 113 | //Controllers 114 | initControllers() { 115 | MainController.addRoutes(this.app); 116 | } 117 | 118 | //Logs 119 | initLogs() { 120 | const logType = process.env.NODE_ENV === 'production' ? 'combined' : 'dev'; 121 | 122 | this.app.use( 123 | morgan(logType, { 124 | skip: function (req, res) { 125 | return res.statusCode < 400; 126 | }, 127 | stream: process.stderr 128 | }) 129 | ); 130 | } 131 | 132 | //State 133 | async onListening() { 134 | this.initLogs(); 135 | 136 | logger.info(`Worker ${process.pid}: Listening on port ${this.port}, in ${process.env.NODE_ENV}`); 137 | 138 | this.initCORS(); 139 | this.initExpress(); 140 | this.initControllers(); 141 | 142 | this.app.use((err, req, res, next) => { 143 | util.handleError(err, req, res, next); 144 | }); 145 | } 146 | 147 | async stopServer() { 148 | logger.info(`Worker ${process.pid}: Shutting down...`); 149 | 150 | //Close DB 151 | if (this.db) { 152 | await this.db.close(err => { 153 | if (err) { 154 | this.onError(err); 155 | } else { 156 | logger.info(`Worker ${process.pid}: [Postgres]: Connection closed.`); 157 | } 158 | }); 159 | } 160 | 161 | //Close Websocket 162 | if (this.websocket) { 163 | this.websocket.close(err => { 164 | logger.warn(`Worker ${process.pid}: [WebSocket]: Closing connection...`); 165 | 166 | if (err) { 167 | this.onError(err); 168 | } else { 169 | logger.warn(`Worker ${process.pid}: [WebSocket]: Connection closed.`); 170 | } 171 | }); 172 | } 173 | 174 | this.server.close(); 175 | 176 | if (this.isClosing) { 177 | process.exit(1); 178 | } else { 179 | logger.warn('Shutdown in progress... (Close again to force shutdown)'); 180 | this.isClosing = true; 181 | } 182 | } 183 | 184 | onError(err) { 185 | logger.error(err); 186 | } 187 | 188 | async onClose() { 189 | logger.warn('Server is closed.'); 190 | process.exit(1); 191 | } 192 | } 193 | 194 | let app = new App(); 195 | app.start(); 196 | -------------------------------------------------------------------------------- /modules/api/controllers/main.controller.js: -------------------------------------------------------------------------------- 1 | import config from 'config'; 2 | import FormData from 'form-data'; 3 | import fetch from 'node-fetch'; 4 | import ed from 'noble-ed25519'; 5 | import UserSession from '../../../models/UserSession.js'; 6 | import UserServer from '../../../models/UserServer.js'; 7 | import Server from '../../../models/Server.js'; 8 | import User from '../../../models/User.js'; 9 | import UserWallet from '../../../models/UserWallet.js'; 10 | import logger from '../../../lib/logger.js'; 11 | 12 | class MainController { 13 | //Signature 14 | //--------------------------- 15 | static async validateSignature(req, resp, next) { 16 | let { token, signature, address } = req.body; 17 | address = Uint8Array.from(address.data); 18 | signature = Uint8Array.from(signature.data); 19 | token = new TextEncoder().encode('$GRAPE'); 20 | const isSigned = await ed.verify(signature, token, address); 21 | 22 | if (isSigned) { 23 | next(); 24 | } else { 25 | resp.status(500).send('Invalid signature'); 26 | } 27 | } 28 | 29 | //User 30 | //--------------------------- 31 | static async login(req, resp) { 32 | let { publicKey } = req.body; 33 | 34 | let user = await UserSession.getByAddress(publicKey); 35 | if (user) { 36 | resp.status(200).send(user); 37 | } else { 38 | resp.status(500).send('Invalid address'); 39 | } 40 | } 41 | 42 | static async register(req, resp) { 43 | let { userId, publicKey } = req.body; 44 | 45 | let user = await User.getById(userId); 46 | let userWallet = false; 47 | 48 | if (user) { 49 | userWallet = await UserWallet.getByAddress(publicKey); 50 | 51 | if (!userWallet) { 52 | userWallet = await UserWallet.createUserWallet(userId, publicKey); 53 | user.hasWallet = true; 54 | await user.save(); 55 | } else if (userWallet.userId !== userId) { 56 | userWallet.userId = userId; 57 | await userWallet.save(); 58 | } 59 | } 60 | 61 | if (userWallet) { 62 | let user = await UserSession.getByAddress(publicKey); 63 | resp.status(200).send(user); 64 | } else { 65 | resp.status(500).send('Invalid address'); 66 | } 67 | } 68 | 69 | static async updateUser(req, resp) { 70 | const discordId = req.body.discordId; 71 | const userId = req.params.userId; 72 | 73 | let user = await User.getById(userId); 74 | user.discordId = discordId; 75 | await user.save(); 76 | 77 | if (user) { 78 | resp.status(200).send(user); 79 | } else { 80 | resp.status(500).send('Invalid address'); 81 | } 82 | } 83 | 84 | //Server 85 | //--------------------------- 86 | static async getServers(req, resp) { 87 | let servers = await Server.getServers(); 88 | 89 | if (servers) { 90 | resp.status(200).send(servers); 91 | } else { 92 | resp.status(500).send('Missing servers'); 93 | } 94 | } 95 | 96 | static async registerServer(req, resp) { 97 | const { userId } = req.body; 98 | const { serverId } = req.params; 99 | 100 | let userServer = await UserServer.createUserServer(userId, serverId); 101 | 102 | if (userServer) { 103 | resp.status(200).send(userServer); 104 | } else { 105 | resp.status(500).send('Invalid server'); 106 | } 107 | } 108 | 109 | static async unregisterServer(req, resp) { 110 | const { userId } = req.body; 111 | const { serverId } = req.params; 112 | 113 | let userServer = await UserServer.deleteUserServer(userId, serverId); 114 | 115 | if (userServer) { 116 | resp.status(200).send(userServer); 117 | } else { 118 | resp.status(500).send('Invalid server'); 119 | } 120 | } 121 | 122 | //Discord 123 | //--------------------------- 124 | static async discordLogin(req, res) { 125 | 126 | //create pendingUser record 127 | let state = encodeURIComponent(JSON.stringify({ 128 | register: req.query.register, 129 | serverId: req.query.serverId 130 | })); 131 | 132 | res.redirect('https://discord.com/api/oauth2/authorize' + 133 | `?client_id=${process.env.DISCORD_OAUTH_CLIENT_ID}` + 134 | `&redirect_uri=${encodeURIComponent(process.env.DISCORD_OAUTH_REDIRECT_URL)}` + 135 | `&state=${state}` + 136 | `&response_type=code&scope=${encodeURIComponent(config.discord.scopes.join(' '))}`); 137 | } 138 | 139 | static async discordCallback(req, resp) { 140 | let { state } = req.query; 141 | const accessCode = req.query.code; 142 | 143 | state = state && JSON.parse(decodeURIComponent(state)); 144 | let register = state.register; 145 | let serverId = state.serverId; 146 | let address = state.address; 147 | 148 | if (!accessCode) 149 | return resp.send('No access code specified'); 150 | 151 | const data = new FormData(); 152 | data.append('client_id', process.env.DISCORD_OAUTH_CLIENT_ID); 153 | data.append('client_secret', process.env.DISCORD_OAUTH_SECRET); 154 | data.append('grant_type', 'authorization_code'); 155 | data.append('redirect_uri', process.env.DISCORD_OAUTH_REDIRECT_URL); 156 | data.append('scope', 'identify'); 157 | data.append('code', accessCode); 158 | 159 | const json = await (await fetch('https://discord.com/api/oauth2/token', { method: 'POST', body: data })).json(); 160 | let discordInfo = await fetch('https://discord.com/api/users/@me', { headers: { Authorization: `Bearer ${json.access_token}` } }); // Fetching user data 161 | discordInfo = await discordInfo.json(); 162 | const discordId = discordInfo && discordInfo.id; 163 | let userId, server; 164 | 165 | if (register && discordId) { 166 | let user = discordId && await User.createUser(discordId); 167 | userId = user && user.userId; 168 | 169 | server = await Server.getById(serverId); 170 | 171 | if (server && userId) { 172 | await UserServer.createUserServer(userId, serverId); 173 | } 174 | } 175 | 176 | resp.redirect(process.env.CLIENT_URL + 177 | `?token=${accessCode}` + 178 | `&avatar=${discordInfo.avatar}` + 179 | `&username=${discordInfo.username}` + 180 | `&serverName=${server && server.name}` + 181 | `&serverLogo=${server && encodeURIComponent(server.logo)}` + 182 | `&discord_id=${discordId}` + 183 | `&user_id=${userId}` + 184 | '&provider=discord' + 185 | (register ? '#/register' : '#/confirmation')); 186 | } 187 | 188 | static addRoutes(app) { 189 | app.put('/api/user/:userId', MainController.validateSignature, MainController.updateUser); 190 | app.post('/api/register', MainController.register); 191 | app.post('/api/login', MainController.validateSignature, MainController.login); 192 | app.get('/api/server', MainController.getServers); 193 | app.post('/api/server/:serverId/register', MainController.validateSignature, MainController.registerServer); 194 | app.post('/api/server/:serverId/unregister', MainController.validateSignature, MainController.unregisterServer); 195 | app.get('/api/discord', MainController.discordLogin); 196 | app.get('/api/discord/callback', MainController.discordCallback); 197 | } 198 | } 199 | 200 | export default MainController; -------------------------------------------------------------------------------- /controllers/main.controller.js: -------------------------------------------------------------------------------- 1 | import config from 'config'; 2 | import FormData from 'form-data'; 3 | import fetch from 'node-fetch'; 4 | import ed from 'noble-ed25519'; 5 | import UserSession from '../models/UserSession.js'; 6 | import UserServer from '../models/UserServer.js'; 7 | import Server from '../models/Server.js'; 8 | import User from '../models/User.js'; 9 | import UserWallet from '../models/UserWallet.js'; 10 | import bs58 from "bs58"; 11 | 12 | class MainController { 13 | //Signature 14 | //--------------------------- 15 | static async validateSignature(req, resp, next) { 16 | let { token, signature, address } = req.body; 17 | address = Uint8Array.from(address.data); 18 | signature = Uint8Array.from(signature.data); 19 | token = new TextEncoder().encode('$GRAPE'); 20 | const isSigned = await ed.verify(signature, token, address); 21 | 22 | if (isSigned) { 23 | next(); 24 | } else { 25 | resp.status(500).send('Invalid signature'); 26 | } 27 | } 28 | 29 | //User 30 | //--------------------------- 31 | static async login(req, resp) { 32 | let { publicKey, address } = req.body; 33 | address = bs58.encode(Uint8Array.from(address.data)); 34 | 35 | let user = await UserSession.getByAddress(address); 36 | if (user) { 37 | resp.status(200).send(user); 38 | } else { 39 | resp.status(500).send('Invalid address'); 40 | } 41 | } 42 | 43 | static async register(req, resp) { 44 | let { userId, publicKey,address } = req.body; 45 | address = bs58.encode(Uint8Array.from(address.data)); 46 | 47 | let user = await User.getById(userId); 48 | let userWallet = false; 49 | 50 | 51 | 52 | if (user) { 53 | 54 | 55 | userWallet = await UserWallet.getByAddress(address); 56 | 57 | if (!userWallet) { 58 | 59 | 60 | userWallet = await UserWallet.createUserWallet(userId, address); 61 | user.hasWallet = true; 62 | await user.save(); 63 | } else if (userWallet.userId !== userId) { 64 | let oldUser = await User.getById(userWallet.userId); 65 | oldUser.discordId = user.discordId; 66 | await oldUser.save(); 67 | 68 | //delete temp user 69 | await User.deleteUser(userId); 70 | await UserServer.deleteByUser(userId); 71 | } 72 | } 73 | 74 | if (userWallet) { 75 | let user = await UserSession.getByAddress(address); 76 | resp.status(200).send(user); 77 | } else { 78 | resp.status(500).send('Invalid address'); 79 | } 80 | } 81 | 82 | static async updateUser(req, resp) { 83 | const discordId = req.body.discordId; 84 | const userId = req.params.userId; 85 | 86 | let user = await User.getById(userId); 87 | user.discordId = discordId; 88 | await user.save(); 89 | 90 | if (user) { 91 | resp.status(200).send(user); 92 | } else { 93 | resp.status(500).send('Invalid address'); 94 | } 95 | } 96 | 97 | //Server 98 | //--------------------------- 99 | static async getServers(req, resp) { 100 | let servers = await Server.getServers(); 101 | 102 | if (servers) { 103 | resp.status(200).send(servers); 104 | } else { 105 | resp.status(500).send('Missing servers'); 106 | } 107 | } 108 | 109 | static async registerServer(req, resp) { 110 | const { userId } = req.body; 111 | const { serverId } = req.params; 112 | 113 | let userServer = await UserServer.createUserServer(userId, serverId); 114 | 115 | if (userServer) { 116 | resp.status(200).send(userServer); 117 | } else { 118 | resp.status(500).send('Invalid server'); 119 | } 120 | } 121 | 122 | static async unregisterServer(req, resp) { 123 | const { userId } = req.body; 124 | const { serverId } = req.params; 125 | 126 | let userServer = await UserServer.deleteUserServer(userId, serverId); 127 | 128 | if (userServer) { 129 | resp.status(200).send(userServer); 130 | } else { 131 | resp.status(500).send('Invalid server'); 132 | } 133 | } 134 | 135 | //Discord 136 | //--------------------------- 137 | static async discordLogin(req, res) { 138 | //create pendingUser record 139 | let state = encodeURIComponent(JSON.stringify({ 140 | register: req.query.register, 141 | serverId: req.query.serverId 142 | })); 143 | 144 | res.redirect('https://discord.com/api/oauth2/authorize' + 145 | `?client_id=${process.env.DISCORD_OAUTH_CLIENT_ID}` + 146 | `&redirect_uri=${encodeURIComponent(process.env.DISCORD_OAUTH_REDIRECT_URL)}` + 147 | `&state=${state}` + 148 | `&response_type=code&scope=${encodeURIComponent(config.discord.scopes.join(' '))}`); 149 | } 150 | 151 | static async discordCallback(req, resp) { 152 | let { state } = req.query; 153 | const accessCode = req.query.code; 154 | 155 | state = state && JSON.parse(decodeURIComponent(state)); 156 | let register = state.register; 157 | let serverId = state.serverId; 158 | 159 | if (!accessCode) 160 | return resp.send('No access code specified'); 161 | 162 | const data = new FormData(); 163 | data.append('client_id', process.env.DISCORD_OAUTH_CLIENT_ID); 164 | data.append('client_secret', process.env.DISCORD_OAUTH_SECRET); 165 | data.append('grant_type', 'authorization_code'); 166 | data.append('redirect_uri', process.env.DISCORD_OAUTH_REDIRECT_URL); 167 | data.append('scope', 'identify'); 168 | data.append('code', accessCode); 169 | 170 | const json = await (await fetch('https://discord.com/api/oauth2/token', { method: 'POST', body: data })).json(); 171 | let discordInfo = await fetch('https://discord.com/api/users/@me', { headers: { Authorization: `Bearer ${json.access_token}` } }); // Fetching user data 172 | discordInfo = await discordInfo.json(); 173 | const discordId = discordInfo && discordInfo.id; 174 | let userId, server, isRegistered = false; 175 | 176 | if (register && discordId) { 177 | let user = await User.getByDiscordId(discordId); 178 | if (!user) { 179 | user = await User.createUser(discordId); 180 | } else { 181 | let wallets = await UserWallet.getByUser(user.userId); 182 | if (wallets && wallets.length) { 183 | isRegistered = true; 184 | } 185 | } 186 | 187 | userId = user && user.userId; 188 | server = await Server.getById(serverId); 189 | 190 | if (server && userId) { 191 | await UserServer.createUserServer(userId, serverId); 192 | } 193 | } 194 | 195 | resp.redirect(process.env.CLIENT_URL + 196 | `?token=${accessCode}` + 197 | `&avatar=${discordInfo.avatar}` + 198 | `&username=${discordInfo.username}` + 199 | `&serverName=${server && server.name}` + 200 | `&serverLogo=${server && encodeURIComponent(server.logo)}` + 201 | `&discord_id=${discordId}` + 202 | `&user_id=${userId}` + 203 | '&provider=discord' + 204 | `&is_registered=${isRegistered}` + 205 | (register ? '#/register' : '#/confirmation')); 206 | } 207 | 208 | static addRoutes(app) { 209 | app.put('/api/user/:userId', MainController.validateSignature, MainController.updateUser); 210 | app.post('/api/register', MainController.validateSignature,MainController.register); 211 | app.post('/api/login', MainController.validateSignature, MainController.login); 212 | app.get('/api/server', MainController.getServers); 213 | app.post('/api/server/:serverId/register', MainController.validateSignature, MainController.registerServer); 214 | app.post('/api/server/:serverId/unregister', MainController.validateSignature, MainController.unregisterServer); 215 | app.get('/api/discord', MainController.discordLogin); 216 | app.get('/api/discord/callback', MainController.discordCallback); 217 | } 218 | } 219 | 220 | export default MainController; 221 | -------------------------------------------------------------------------------- /modules/discord-bot/discord/role-claim.js: -------------------------------------------------------------------------------- 1 | import User from '../../../models/User'; 2 | import firstMessage from './first-message'; 3 | import fetch from 'node-fetch'; 4 | 5 | export default (client) => { 6 | // Read from Channel 7 | const channelId = process.env.DISCORD_CHANNEL_ID 8 | ; 9 | 10 | const getEmoji = (emojiName) => client.emojis.cache.find((emoji) => emoji.name === emojiName); 11 | 12 | // Uploaded server Emojis 13 | const emojis = { 14 | walletv2: `${process.env.DISCORD_ROLE_1}`, 15 | medianetwork:`${process.env.DISCORD_ROLE_2}` , 16 | mercurial: `${process.env.DISCORD_ROLE_3}`, 17 | samo: `${process.env.DISCORD_ROLE_4}`, 18 | }; 19 | 20 | const reactions = []; 21 | 22 | // Welcome text which is rebuilt everytime 23 | 24 | let emojiText = "Gated SPL token community access!\n https://verify.grapes.network/start\n 0.1 MEDIA for Media access \n 10 MER for Mercurial access \n 10,000 SAMO for Samo access \n \n"; 25 | for (const key in emojis) { 26 | const emoji = getEmoji(key); 27 | reactions.push(emoji); 28 | 29 | const role = emojis[key]; 30 | emojiText += `${emoji} = ${role}\n`; 31 | } 32 | 33 | firstMessage(client, channelId, emojiText, reactions); 34 | 35 | const handleReaction = async (reaction, user, add) => { 36 | if (user.id === process.env.DISCORD_BOT_ID) { 37 | return; 38 | } 39 | 40 | const emoji = reaction._emoji.name; 41 | 42 | const { guild } = reaction.message; 43 | 44 | const roleName = emojis[emoji]; 45 | if (!roleName) { 46 | return; 47 | } 48 | 49 | const role = guild.roles.cache.find((role) => role.name === roleName); 50 | const member = guild.members.cache.find((member) => member.id === user.id); 51 | 52 | 53 | // logic on add reaction 54 | 55 | if (add) { 56 | 57 | 58 | 59 | // Check which role the user is clicking for 60 | 61 | 62 | if(role.name === `${process.env.DISCORD_ROLE_1}` ) 63 | { 64 | // Just check that a wallet exists 65 | // console.log(role.name); 66 | 67 | const discordId = member.id; 68 | const dbDiscordId = await User.checkDiscordId(discordId); 69 | console.log(discordId,dbDiscordId); 70 | if (discordId===dbDiscordId) 71 | { 72 | member.roles.add(role); 73 | // Save OG users 74 | if (member.roles.cache.find(r => r.name === "OG") ) 75 | { 76 | console.log('member is OG'); 77 | 78 | let user = await User.getByDiscordId(discordId); 79 | user.is_og = 1; 80 | await user.save_og(); 81 | // 82 | } 83 | 84 | } 85 | 86 | } 87 | 88 | 89 | if(role.name === `${process.env.DISCORD_ROLE_2}` ) 90 | { 91 | // Check wallet exists + specific token 92 | 93 | const discordId = member.id; 94 | const dbDiscordId = await User.checkDiscordId(discordId); 95 | // if the user is has a verified wallet, now lets check specific token balance 96 | if (discordId===dbDiscordId) 97 | { 98 | 99 | 100 | // Save OG users 101 | if (member.roles.cache.find(r => r.name === "OG") ) 102 | { 103 | console.log('member is OG'); 104 | 105 | let user = await User.getByDiscordId(discordId); 106 | user.is_og = 1; 107 | await user.save_og(); 108 | // 109 | } 110 | 111 | 112 | // Get Wallet Address 113 | 114 | const DiscordWallet = await User.getWalletByDiscordId(discordId); 115 | console.log(DiscordWallet); 116 | const body = { 117 | method: 'getTokenAccountsByOwner', 118 | jsonrpc: '2.0', 119 | params: [ 120 | // Get the public key of the account you want the balance for. 121 | DiscordWallet, 122 | { programId: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA' }, 123 | { encoding: 'jsonParsed', commitment: 'processed' }, 124 | ], 125 | id: '35f0036a-3801-4485-b573-2bf29a7c77d2', 126 | }; 127 | const response = await fetch('https://solana-api.projectserum.com/', { 128 | method: 'POST', 129 | body: JSON.stringify(body), 130 | headers: { 'Content-Type': 'application/json' }, 131 | }); 132 | let value = ''; 133 | const json = await response.json(); 134 | const resultValues = json.result.value; 135 | const theOwner = body.params[0]; 136 | 137 | for (value of resultValues) 138 | { 139 | const parsedInfo = value.account.data.parsed.info; 140 | const { mint, tokenAmount } = parsedInfo; 141 | const uiAmount = tokenAmount.uiAmountString; 142 | //console.log(mint); 143 | //message.channel.send(`Mint: ${mint} | Balance: ${uiAmount}`); 144 | // Check for Media Token over 0.1 145 | // MEDIA ETAtLmCmsoiEEKfNrHKJ2kYy3MoABhU6NQvpSfij5tDs 146 | //EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v USDC 147 | if (mint===`${process.env.MINT_TOKEN_1}` ) 148 | 149 | { 150 | 151 | if (uiAmount>`${process.env.MINT_MIN_BALANCE_1}`) 152 | { 153 | member.roles.add(role); 154 | 155 | } 156 | 157 | } 158 | } 159 | 160 | 161 | 162 | 163 | 164 | } 165 | 166 | 167 | 168 | } 169 | // End MEDIA 170 | 171 | if(role.name === `${process.env.DISCORD_ROLE_3}` ) 172 | { 173 | // Check wallet exists + specific token 174 | 175 | 176 | const discordId = member.id; 177 | const dbDiscordId = await User.checkDiscordId(discordId); 178 | // if the user is has a verified wallet, now lets check specific token balance 179 | if (discordId===dbDiscordId) 180 | { 181 | 182 | // Save OG users 183 | if (member.roles.cache.find(r => r.name === "OG") ) 184 | { 185 | console.log('member is OG'); 186 | 187 | let user = await User.getByDiscordId(discordId); 188 | user.is_og = 1; 189 | await user.save_og(); 190 | // 191 | } 192 | 193 | // Get Wallet Address 194 | 195 | const DiscordWallet = await User.getWalletByDiscordId(discordId); 196 | console.log(DiscordWallet); 197 | const body = { 198 | method: 'getTokenAccountsByOwner', 199 | jsonrpc: '2.0', 200 | params: [ 201 | // Get the public key of the account you want the balance for. 202 | DiscordWallet, 203 | { programId: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA' }, 204 | { encoding: 'jsonParsed', commitment: 'processed' }, 205 | ], 206 | id: '35f0036a-3801-4485-b573-2bf29a7c77d2', 207 | }; 208 | const response = await fetch('https://solana-api.projectserum.com/', { 209 | method: 'POST', 210 | body: JSON.stringify(body), 211 | headers: { 'Content-Type': 'application/json' }, 212 | }); 213 | let value = ''; 214 | const json = await response.json(); 215 | const resultValues = json.result.value; 216 | const theOwner = body.params[0]; 217 | 218 | for (value of resultValues) 219 | { 220 | const parsedInfo = value.account.data.parsed.info; 221 | const { mint, tokenAmount } = parsedInfo; 222 | const uiAmount = tokenAmount.uiAmountString; 223 | //console.log(mint); 224 | //message.channel.send(`Mint: ${mint} | Balance: ${uiAmount}`); 225 | // Check for Media Token over 0.1 226 | // MEDIA ETAtLmCmsoiEEKfNrHKJ2kYy3MoABhU6NQvpSfij5tDs 227 | //EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v USDC 228 | 229 | if (mint=== `${process.env.MINT_TOKEN_2}` ) 230 | 231 | { 232 | 233 | 234 | if (uiAmount> 9.99) 235 | { 236 | console.log(process.env.MINT_MIN_BALANCE_2); 237 | 238 | member.roles.add(role); 239 | 240 | } 241 | 242 | } 243 | } 244 | 245 | 246 | 247 | 248 | 249 | } 250 | 251 | 252 | 253 | } 254 | if(role.name === `${process.env.DISCORD_ROLE_4}` ) 255 | { 256 | // Check wallet exists + specific token 257 | 258 | const discordId = member.id; 259 | const dbDiscordId = await User.checkDiscordId(discordId); 260 | // if the user is has a verified wallet, now lets check specific token balance 261 | if (discordId===dbDiscordId) 262 | { 263 | 264 | // Save OG users 265 | if (member.roles.cache.find(r => r.name === "OG") ) 266 | { 267 | console.log('member is OG'); 268 | 269 | let user = await User.getByDiscordId(discordId); 270 | user.is_og = 1; 271 | await user.save_og(); 272 | // 273 | } 274 | 275 | 276 | // Get Wallet Address 277 | 278 | const DiscordWallet = await User.getWalletByDiscordId(discordId); 279 | console.log(DiscordWallet); 280 | const body = { 281 | method: 'getTokenAccountsByOwner', 282 | jsonrpc: '2.0', 283 | params: [ 284 | // Get the public key of the account you want the balance for. 285 | DiscordWallet, 286 | { programId: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA' }, 287 | { encoding: 'jsonParsed', commitment: 'processed' }, 288 | ], 289 | id: '35f0036a-3801-4485-b573-2bf29a7c77d2', 290 | }; 291 | const response = await fetch('https://solana-api.projectserum.com/', { 292 | method: 'POST', 293 | body: JSON.stringify(body), 294 | headers: { 'Content-Type': 'application/json' }, 295 | }); 296 | let value = ''; 297 | const json = await response.json(); 298 | const resultValues = json.result.value; 299 | const theOwner = body.params[0]; 300 | 301 | for (value of resultValues) 302 | { 303 | const parsedInfo = value.account.data.parsed.info; 304 | const { mint, tokenAmount } = parsedInfo; 305 | const uiAmount = tokenAmount.uiAmountString; 306 | //console.log(mint); 307 | //message.channel.send(`Mint: ${mint} | Balance: ${uiAmount}`); 308 | // Check for Media Token over 0.1 309 | // MEDIA ETAtLmCmsoiEEKfNrHKJ2kYy3MoABhU6NQvpSfij5tDs 310 | //EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v USDC 311 | if (mint=== `${process.env.MINT_TOKEN_3}` ) 312 | 313 | { 314 | 315 | if (uiAmount>9.999 ) 316 | { 317 | member.roles.add(role); 318 | 319 | } 320 | 321 | } 322 | } 323 | 324 | 325 | 326 | 327 | 328 | } 329 | 330 | 331 | 332 | } 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | } 343 | // else { 344 | // member.roles.remove(role); 345 | // } 346 | }; 347 | 348 | client.on('messageReactionAdd', async (reaction, user) => { 349 | if (reaction.message.channel.id === channelId) { 350 | await handleReaction(reaction, user, true); 351 | } 352 | }); 353 | 354 | client.on('guildMemberAdd', async member => { 355 | if (member.user.bot) return; 356 | 357 | //member.roles.add(member.guild.roles.cache.find((role) => role.name === 'MEDIA Holder')); 358 | }); 359 | 360 | // client.on('messageReactionRemove', (reaction, user) => { 361 | // if (reaction.message.channel.id === channelId) { 362 | // handleReaction(reaction, user, false); 363 | // } 364 | // }); 365 | }; 366 | --------------------------------------------------------------------------------