├── .nvmrc ├── .dockerignore ├── .prettierrc ├── .mocharc.json ├── .gitattributes ├── .eslintignore ├── emails ├── images │ ├── alpacasinologo.png │ ├── wallfairlogo_1.png │ ├── instagram-logo-colored.png │ ├── linkedin-logo-colored.png │ ├── telegram-logo-colored.png │ └── twitter-logo-colored.png ├── buy-with-fiat.html ├── buy-with-crypto.html ├── withdraw-requested.html ├── deposit-created.html └── email-evaluate.html ├── infra ├── environments │ ├── alpa-prod │ │ ├── replicasPatch.yaml │ │ ├── environment │ │ ├── priorityPatch.yaml │ │ ├── podDisruptionBudget.yaml │ │ ├── hpaPatch.yaml │ │ ├── prodConfigPatch.yaml │ │ ├── resourcesPatch.yaml │ │ └── kustomization.yaml │ ├── development │ │ ├── environment │ │ ├── kustomization.yaml │ │ └── configurationPatch.yaml │ ├── staging │ │ └── kustomization.yaml │ ├── playmoney │ │ ├── environment │ │ └── kustomization.yaml │ └── production │ │ ├── kustomization.yaml │ │ ├── prodConfigPatch.yaml │ │ └── environment └── application │ ├── kustomization.yaml │ ├── environment │ └── backend-prod-deploy.yaml ├── __tests__ ├── basic.test.js └── stats │ └── fn.test.js ├── util ├── agenda.js ├── generateSlug.js ├── request-validator.js ├── retryHandler.js ├── logger.js ├── recaptcha.js ├── wallet.js ├── user.js ├── cryptopay.js ├── number-helper.js ├── cmc.js ├── error-handler.js ├── discord.oauth.js ├── challenge.js ├── twitch.oauth.js ├── facebook.oauth.js ├── google.oauth.js ├── outcomes.js ├── constants.js ├── auth.js └── promo-codes-migration.js ├── Dockerfile ├── docker ├── mongo-entrypoint │ └── init.js └── docker-compose.yml ├── .editorconfig ├── services ├── stream-service.js ├── moonpay-service.js ├── auth-service.js ├── lottery-service.js ├── aws-s3-service.js ├── chat-message-service.test.js ├── cryptopay-service.js ├── request-log-service.js ├── user-api.js ├── quote-storage-service.js ├── promo-codes-service.js ├── ws-info-channel-service.js ├── youtube-category-service.js ├── notification-events-service.js ├── amqp-service.js ├── mail-service.js ├── chat-message-service.js ├── subscribers-service.js ├── youtube-service.js ├── statistics-service.js ├── twitch-service.js └── leaderboard-service.js ├── routes ├── users │ ├── quote-routes.js │ ├── chat-routes.js │ ├── secure-rewards-routes.js │ ├── notification-events-routes.js │ ├── user-messages-routes.js │ ├── users-routes.js │ ├── admin-routes.js │ └── secure-users-routes.js ├── auth │ └── auth-routes.js └── webhooks │ └── twitch-webhook.js ├── .eslintrc.js ├── controllers ├── cmc-controller.js ├── chats-controller.js ├── twitch-controller.js ├── user-messages-controller.js ├── notification-events-controller.js ├── rewards-controller.js ├── admin-controller.js └── sessions-controller.js ├── .gitignore ├── .vscode └── settings.json ├── helper └── index.js ├── docs ├── evoplay │ └── generateEvoplayGames.js └── softswiss │ └── generateGames.js ├── .env-example ├── ssl ├── rm-prod-postgres.crt ├── staging.crt └── rm-prod-mongo.crt ├── jobs ├── twitch-subscribe-job.js └── youtube-live-check-job.js ├── package.json ├── index.js └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 14.18.2 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .env 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extension": ["js"], 3 | "spec": "__tests__/**/*.js" 4 | } 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | node_modules/ 3 | dist 4 | docker 5 | .husky 6 | .adminbro 7 | package.json 8 | -------------------------------------------------------------------------------- /emails/images/alpacasinologo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wholespace214/crash-game-backend/HEAD/emails/images/alpacasinologo.png -------------------------------------------------------------------------------- /emails/images/wallfairlogo_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wholespace214/crash-game-backend/HEAD/emails/images/wallfairlogo_1.png -------------------------------------------------------------------------------- /emails/images/instagram-logo-colored.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wholespace214/crash-game-backend/HEAD/emails/images/instagram-logo-colored.png -------------------------------------------------------------------------------- /emails/images/linkedin-logo-colored.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wholespace214/crash-game-backend/HEAD/emails/images/linkedin-logo-colored.png -------------------------------------------------------------------------------- /emails/images/telegram-logo-colored.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wholespace214/crash-game-backend/HEAD/emails/images/telegram-logo-colored.png -------------------------------------------------------------------------------- /emails/images/twitter-logo-colored.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wholespace214/crash-game-backend/HEAD/emails/images/twitter-logo-colored.png -------------------------------------------------------------------------------- /infra/environments/alpa-prod/replicasPatch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: backend 5 | spec: 6 | replicas: 2 7 | -------------------------------------------------------------------------------- /infra/environments/alpa-prod/environment: -------------------------------------------------------------------------------- 1 | CLIENT_URL=https://play.alpacasino.io 2 | VERIFY_URL=https://play.alpacasino.io/ 3 | REACT_APP_SHOW_UPCOMING_FEATURES=false 4 | -------------------------------------------------------------------------------- /infra/environments/development/environment: -------------------------------------------------------------------------------- 1 | BACKEND_URL=https://dev-api.alpacasino.io 2 | CLIENT_URL=https://dev.alpacasino.io 3 | VERIFY_URL=https://dev.alpacasino.io/ -------------------------------------------------------------------------------- /infra/application/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - backend-prod-deploy.yaml 3 | 4 | configMapGenerator: 5 | - name: backend-config 6 | envs: 7 | - environment 8 | -------------------------------------------------------------------------------- /__tests__/basic.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | 3 | describe('test', () => ( 4 | it('should not fail', () => { 5 | expect(true).to.eq(true); 6 | }) 7 | )); 8 | -------------------------------------------------------------------------------- /infra/environments/alpa-prod/priorityPatch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: backend 5 | spec: 6 | template: 7 | spec: 8 | priorityClassName: high-priority -------------------------------------------------------------------------------- /util/agenda.js: -------------------------------------------------------------------------------- 1 | const Agenda = require("agenda"); 2 | const agenda = new Agenda({ db: { address: process.env.DB_CONNECTION, collection: `INFO_CHANNEL_jobs` } }); 3 | 4 | module.exports = { 5 | agenda 6 | } 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14 2 | 3 | WORKDIR /usr/src/app 4 | 5 | ARG GOOGLE_RECAPTCHA_CLIENT_SECRET 6 | 7 | COPY . . 8 | RUN npm rebuild bcrypt --build-from-source 9 | EXPOSE 80 10 | CMD [ "node", "index.js" ] -------------------------------------------------------------------------------- /docker/mongo-entrypoint/init.js: -------------------------------------------------------------------------------- 1 | db.runCommand({ replSetInitiate : { 2 | _id : "rs0", 3 | members: [ 4 | { _id: 0, host: "localhost:27017" }, 5 | ] 6 | } }) 7 | rs.status() 8 | -------------------------------------------------------------------------------- /infra/environments/staging/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - ../../application 5 | 6 | namespace: staging 7 | commonLabels: 8 | environment: staging 9 | -------------------------------------------------------------------------------- /infra/environments/alpa-prod/podDisruptionBudget.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: policy/v1 2 | kind: PodDisruptionBudget 3 | metadata: 4 | name: backend-pdb 5 | spec: 6 | minAvailable: 1 7 | selector: 8 | matchLabels: 9 | app: backend 10 | -------------------------------------------------------------------------------- /util/generateSlug.js: -------------------------------------------------------------------------------- 1 | const slugify = require('slugify'); 2 | 3 | const generateSlug = (input) => { 4 | const slug = slugify(input, { 5 | lower: true, 6 | strict: true, 7 | }); 8 | 9 | return slug; 10 | }; 11 | 12 | module.exports = generateSlug; 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | end_of_line = lf 10 | # editorconfig-tools is unable to ignore longs strings or urls 11 | max_line_length = 100 12 | -------------------------------------------------------------------------------- /infra/environments/playmoney/environment: -------------------------------------------------------------------------------- 1 | VERIFY_URL=https://play.wallfair.io/ 2 | BACKEND_URL=https://play-api.wallfair.io 3 | CLIENT_URL=https://play.wallfair.io 4 | TWITCH_OAUTH_REDIRECT_URI=https://play.wallfair.io/oauth/twitch 5 | DISCORD_OAUTH_REDIRECT_URI=https://play.wallfair.io/oauth/discord 6 | PLAYMONEY=true -------------------------------------------------------------------------------- /services/stream-service.js: -------------------------------------------------------------------------------- 1 | // Import Stream model 2 | const { Stream } = require('@wallfair.io/wallfair-commons').models; 3 | 4 | exports.listStreams = async () => Stream.find(); 5 | 6 | exports.getStream = async (id) => Stream.findOne({ _id: id }); 7 | 8 | exports.saveStream = async (stream) => stream.save(); 9 | -------------------------------------------------------------------------------- /routes/users/quote-routes.js: -------------------------------------------------------------------------------- 1 | // Import the express Router to create routes 2 | const router = require('express').Router(); 3 | 4 | // Import controllers 5 | const cmcController = require('../../controllers/cmc-controller'); 6 | 7 | router.get('/', [], cmcController.getMarketPrice); 8 | 9 | module.exports = router; 10 | -------------------------------------------------------------------------------- /infra/environments/alpa-prod/hpaPatch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: autoscaling/v1 2 | kind: HorizontalPodAutoscaler 3 | metadata: 4 | name: backend 5 | spec: 6 | maxReplicas: 10 7 | minReplicas: 2 8 | scaleTargetRef: 9 | apiVersion: apps/v1 10 | kind: Deployment 11 | name: backend 12 | targetCPUUtilizationPercentage: 50 -------------------------------------------------------------------------------- /infra/environments/playmoney/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - ../../application 5 | 6 | namespace: playmoney 7 | commonLabels: 8 | environment: playmoney 9 | 10 | configMapGenerator: 11 | - name: backend-config 12 | behavior: merge 13 | envs: 14 | - environment -------------------------------------------------------------------------------- /routes/users/chat-routes.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const { check } = require('express-validator'); 3 | const chatsController = require('../../controllers/chats-controller'); 4 | 5 | router.get( 6 | '/chat-messages/:roomId', 7 | [check('roomId').notEmpty()], 8 | chatsController.getChatMessagesByRoom 9 | ); 10 | 11 | module.exports = router; 12 | -------------------------------------------------------------------------------- /infra/environments/alpa-prod/prodConfigPatch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: backend 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: backend 10 | envFrom: 11 | - configMapRef: 12 | name: app-secrets 13 | - configMapRef: 14 | name: backend-production-values 15 | 16 | -------------------------------------------------------------------------------- /infra/environments/alpa-prod/resourcesPatch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: backend 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: backend 10 | resources: 11 | limits: 12 | cpu: 2000m 13 | memory: 2048Mi 14 | requests: 15 | cpu: 1000m 16 | memory: 1024Mi -------------------------------------------------------------------------------- /infra/environments/production/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # apiVersion: kustomize.config.k8s.io/v1beta1 2 | # kind: Kustomization 3 | resources: 4 | - ../../application 5 | 6 | namespace: production 7 | commonLabels: 8 | environment: production 9 | 10 | configMapGenerator: 11 | - name: backend-config 12 | behavior: merge 13 | envs: 14 | - environment 15 | 16 | patches: 17 | - prodConfigPatch.yaml -------------------------------------------------------------------------------- /infra/environments/development/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - ../../application 5 | 6 | namespace: development 7 | commonLabels: 8 | environment: development 9 | 10 | 11 | patches: 12 | - configurationPatch.yaml 13 | 14 | configMapGenerator: 15 | - name: backend-config 16 | behavior: merge 17 | envs: 18 | - environment 19 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | node: true, 4 | mocha: true, 5 | es2021: true, 6 | }, 7 | extends: ['eslint:recommended'], 8 | parserOptions: { 9 | ecmaVersion: 12, 10 | extraFileExtensions: ['.json'], 11 | sourceType: 'module', 12 | ecmaFeatures: { 13 | modules: true, 14 | experimentalObjectRestSpread: true, 15 | }, 16 | }, 17 | rules: {}, 18 | }; 19 | -------------------------------------------------------------------------------- /routes/users/secure-rewards-routes.js: -------------------------------------------------------------------------------- 1 | // Import the express Router to create routes 2 | const router = require('express').Router(); 3 | 4 | // Import controllers 5 | const rewardsController = require('../../controllers/rewards-controller'); 6 | 7 | router.get('/questions', [], rewardsController.getQuestions); 8 | 9 | router.post('/answer', [], rewardsController.postRewardAnswer); 10 | 11 | module.exports = router; 12 | -------------------------------------------------------------------------------- /emails/buy-with-fiat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Deposit Fiat Request 6 | 7 | 8 |
Currency: {{currency}}
9 |
Transfer amount (You Pay field): {{amount}}
10 |
Estimate WFAIR (You receive estimate field): {{estimate}}
11 |
User's email: {{email}}
12 |
User's ID: {{userid}}
13 | 14 | 15 | -------------------------------------------------------------------------------- /emails/buy-with-crypto.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Buy with crypto form info 6 | 7 | 8 |
Currency: {{currency}}
9 |
Wallet address: {{wallet}}
10 |
Transfer amount (You Pay field): {{amount}}
11 |
Estimate WFAIR (You receive estimate field): {{estimate}}
12 |
User's email: {{email}}
13 | 14 | 15 | -------------------------------------------------------------------------------- /controllers/cmc-controller.js: -------------------------------------------------------------------------------- 1 | const cmcHelper = require('../util/cmc'); 2 | 3 | const getMarketPrice = async (req, res, next) => { 4 | try { 5 | const { convertFrom, convertTo, amount } = req.query; 6 | const quoteResponse = await cmcHelper.getMarketPrice({ convertFrom, convertTo, amount }); 7 | 8 | res.status(200).json(quoteResponse); 9 | } catch (err) { 10 | next(err); 11 | } 12 | }; 13 | 14 | exports.getMarketPrice = getMarketPrice; 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | .idea/ 3 | .eslintcache 4 | 5 | # dependencies 6 | node_modules 7 | 8 | # build 9 | dist 10 | build 11 | .env 12 | 13 | # testing 14 | coverage 15 | 16 | # server logs 17 | combined.log 18 | error.log 19 | 20 | # misc 21 | .DS_Store 22 | newrelic_agent.log 23 | 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # istanbul (nyc) temp_dir 29 | .nyc_output 30 | 31 | tmp/ 32 | -------------------------------------------------------------------------------- /util/request-validator.js: -------------------------------------------------------------------------------- 1 | const { validationResult } = require('express-validator'); 2 | const { ValidationError } = require('./error-handler'); 3 | 4 | /** 5 | * Checks if the request is valid, and eventually returns an http 422 error 6 | */ 7 | exports.validateRequest = (nextController) => (req, res, next) => { 8 | const result = validationResult(req); 9 | if (result.isEmpty()) { 10 | return nextController(req, res, next); 11 | } 12 | 13 | next(new ValidationError(result.array())); 14 | }; 15 | -------------------------------------------------------------------------------- /util/retryHandler.js: -------------------------------------------------------------------------------- 1 | const wait = interval => new Promise(resolve => setTimeout(resolve, interval)); 2 | 3 | const retry = async (fn, args, retriesLeft = 10, interval = 1500) => { 4 | try { 5 | return await fn(...args); 6 | } catch (error) { 7 | await wait(interval); 8 | if (retriesLeft === 0) { 9 | console.error('10 retries failed. Stop trying...', [...args]); 10 | return; 11 | } 12 | return retry(fn, args, --retriesLeft, interval); 13 | } 14 | }; 15 | 16 | module.exports = retry; 17 | -------------------------------------------------------------------------------- /util/logger.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | module.exports = { 3 | /** logs won't be displayed on NODE_ENV production */ 4 | info(message, ...args) { 5 | if (process.env.NODE_ENV === 'STAGING') console.log('INFO', message, args); 6 | }, 7 | /** Method to log errors */ 8 | error(message, ...args) { 9 | console.error('\x1b[31mERROR\x1b[0m', message, args); 10 | }, 11 | /** These logs will always be logged */ 12 | always(message, ...args) { 13 | console.log('ALAWYS', message, args); 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /infra/environments/alpa-prod/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # apiVersion: kustomize.config.k8s.io/v1beta1 2 | # kind: Kustomization 3 | resources: 4 | - ../../application 5 | - podDisruptionBudget.yaml 6 | 7 | namespace: alpa-prod 8 | commonLabels: 9 | environment: alpa-prod 10 | 11 | configMapGenerator: 12 | - name: backend-config 13 | behavior: merge 14 | envs: 15 | - environment 16 | 17 | patches: 18 | - prodConfigPatch.yaml 19 | - resourcesPatch.yaml 20 | - replicasPatch.yaml 21 | - hpaPatch.yaml 22 | - priorityPatch.yaml 23 | -------------------------------------------------------------------------------- /util/recaptcha.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | 3 | const RECAPTCHA_URL = 'https://www.google.com/recaptcha/api/siteverify'; 4 | 5 | exports.verifyRecaptcha = async (token) => { 6 | try { 7 | const res = await axios.post( 8 | `${RECAPTCHA_URL}?secret=${process.env.GOOGLE_RECAPTCHA_CLIENT_SECRET}&response=${token}` 9 | ); 10 | 11 | return res.data.success && res.data.score > 0.5 && res.data.action === 'join'; 12 | } catch (e) { 13 | console.error('RECAPTCHA FAILED: ', e.message); 14 | return false; 15 | } 16 | }; -------------------------------------------------------------------------------- /controllers/chats-controller.js: -------------------------------------------------------------------------------- 1 | const chatMessageService = require('../services/chat-message-service'); 2 | 3 | exports.getChatMessagesByRoom = async (req, res) => { 4 | const skip = req.query.skip ? +req.query.skip : 0; 5 | const limit = req.query.limit ? +req.query.limit : 20; 6 | 7 | const messages = await chatMessageService.getLatestChatMessagesByRoom( 8 | req.params.roomId, 9 | limit, 10 | skip 11 | ); 12 | 13 | res.status(200).json({ 14 | messages: messages?.data || [], 15 | total: messages?.total || 0, 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /util/wallet.js: -------------------------------------------------------------------------------- 1 | const { NetworkCode } = require("@wallfair.io/trading-engine"); 2 | 3 | exports.WALLETS = { 4 | 'ETH': { 5 | network: NetworkCode.ETH, 6 | wallet: process.env.DEPOSIT_WALLET_ETHEREUM, 7 | }, 8 | 'USDT': { 9 | network: NetworkCode.ETH, 10 | wallet: process.env.DEPOSIT_WALLET_ETHEREUM, 11 | }, 12 | 'BTC': { 13 | network: NetworkCode.BTC, 14 | wallet: process.env.DEPOSIT_WALLET_BITCOIN, 15 | }, 16 | 'LTC': { 17 | network: NetworkCode.LTC, 18 | wallet: process.env.DEPOSIT_WALLET_LITECOIN, 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /routes/users/notification-events-routes.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const notificationEventsController = require('../../controllers/notification-events-controller'); 3 | 4 | router.get( 5 | '/list', 6 | notificationEventsController.listNotificationEvents, 7 | ); 8 | 9 | router.get( 10 | '/list/bets/:betId', 11 | notificationEventsController.listNotificationEventsByBet, 12 | ); 13 | 14 | router.get( 15 | '/list/users/:userId', 16 | notificationEventsController.listNotificationEventsByUser, 17 | ); 18 | 19 | module.exports = router; 20 | -------------------------------------------------------------------------------- /emails/withdraw-requested.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Withdraw scheduled 6 | 7 | 8 |
originator: {{originator}}
9 |
external_system: {{external_system}}
10 |
status: {{status}}
11 |
network_code: {{network_code}}
12 |
receiver: {{receiver}}
13 |
amount (Wei): {{amount}}
14 |
symbol: {{symbol}}
15 |
userId: {{internal_user_id}}
16 | 17 | 18 | -------------------------------------------------------------------------------- /routes/users/user-messages-routes.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const { check } = require('express-validator'); 3 | const { validateRequest } = require('../../util/request-validator'); 4 | const controller = require('../../controllers/user-messages-controller'); 5 | 6 | router.get('/', controller.getMessagesByUser); 7 | 8 | router.put('/:id/read', [check('id').isMongoId()], validateRequest(controller.setMessageRead)); 9 | 10 | router.post( 11 | '/', 12 | [check('userId').exists(), check('userId'.isMongoId)], 13 | validateRequest(controller.sendMessage) 14 | ); 15 | 16 | module.exports = router; 17 | -------------------------------------------------------------------------------- /util/user.js: -------------------------------------------------------------------------------- 1 | exports.isUserBanned = (user) => { 2 | if (!user) { 3 | throw new Error('No user provided.'); 4 | } 5 | 6 | if (user.status !== 'banned') { 7 | return false; 8 | } 9 | 10 | if (user.reactivateOn !== null && user.reactivateOn > new Date()) { 11 | return true; 12 | } 13 | 14 | user.status = 'active'; 15 | user.reactivateOn = null; 16 | user.statusDescription = null; 17 | 18 | user.save(); 19 | 20 | return false; 21 | }; 22 | 23 | exports.getBanData = (user) => { 24 | return ['reactivateOn', 'statusDescription', 'status', 'username'].reduce( 25 | (acc, key) => ({ ...acc, [key]: user[key] }), 26 | {} 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /util/cryptopay.js: -------------------------------------------------------------------------------- 1 | const CryptoJS = require('crypto-js'); 2 | 3 | exports.signRequest = (requestData, method, path) => { 4 | const payload_MD5 = requestData ? CryptoJS.MD5(requestData).toString() : ''; 5 | const date = new Date(Date.now()).toUTCString(); 6 | const string_to_sign = method + "\n" + payload_MD5 + "\n" + "application/json" + "\n" + date + "\n" + path 7 | const hmac = CryptoJS.HmacSHA1(string_to_sign, process.env.CRYPTOPAY_SECRET); 8 | const signature = hmac.toString(CryptoJS.enc.Base64); 9 | 10 | return { 11 | 'Authorization': 'HMAC ' + process.env.CRYPTOPAY_API_KEY + ':' + signature, 12 | 'Content-Type': 'application/json', 13 | 'Date': date, 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /emails/deposit-created.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Deposit created 6 | 7 | 8 |
originator: {{originator}}
9 |
external_system: {{external_system}}
10 |
status: {{status}}
11 |
transaction_hash: {{transaction_hash}}
12 |
external_transaction_id: {{external_transaction_id}}
13 |
network_code: {{network_code}}
14 |
block_number: {{block_number}}
15 |
sender: {{sender}}
16 |
receiver: {{receiver}}
17 |
amount (Wei): {{amount}}
18 |
symbol: {{symbol}}
19 |
userId: {{internal_user_id}}
20 | 21 | 22 | -------------------------------------------------------------------------------- /infra/environments/development/configurationPatch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: backend 5 | labels: 6 | app: backend 7 | spec: 8 | template: 9 | spec: 10 | containers: 11 | - image: registry.digitalocean.com/wallfair/backend_k8s 12 | name: backend 13 | envFrom: 14 | - configMapRef: 15 | name: backend-config 16 | - configMapRef: 17 | name: app-secrets 18 | - configMapRef: 19 | name: app-global-secrets 20 | - configMapRef: 21 | name: postgres-conn 22 | - configMapRef: 23 | name: mongo-conn 24 | - configMapRef: 25 | name: redis-conn 26 | - configMapRef: 27 | name: rabbit-conn -------------------------------------------------------------------------------- /services/moonpay-service.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto') 2 | 3 | const { MOONPAY_BASE_URL, MOONPAY_API_KEY, MOONPAY_API_SECRET, MOONPAY_CURRENCY_CODE, ONRAMP_WEBHOOK_WALLET } = process.env; 4 | 5 | exports.generateUrl = (userId, amount, currency) => { 6 | const baseUrl = MOONPAY_BASE_URL + 7 | `?apiKey=${MOONPAY_API_KEY}` + 8 | `&externalCustomerId=${userId}` + 9 | `&baseCurrencyCode=${currency.toLowerCase()}` + 10 | `¤cyCode=${MOONPAY_CURRENCY_CODE}` + 11 | `&baseCurrencyAmount=${amount}` + 12 | `&walletAddress=${ONRAMP_WEBHOOK_WALLET}` + 13 | `&colorCode=%23ffd401`; 14 | 15 | const signature = crypto 16 | .createHmac('sha256', MOONPAY_API_SECRET) 17 | .update(new URL(baseUrl).search) 18 | .digest('base64'); 19 | 20 | return `${baseUrl}&signature=${encodeURIComponent(signature)}`; 21 | }; 22 | -------------------------------------------------------------------------------- /infra/environments/production/prodConfigPatch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: backend 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: backend 10 | envFrom: 11 | - configMapRef: 12 | name: backend-config 13 | - configMapRef: 14 | name: backend-secrets 15 | - configMapRef: 16 | name: app-global-secrets 17 | - configMapRef: 18 | name: real-money-config 19 | - configMapRef: 20 | name: fractal-secrets 21 | - configMapRef: 22 | name: postgres-conn 23 | - configMapRef: 24 | name: mongo-conn 25 | - configMapRef: 26 | name: redis-conn 27 | - configMapRef: 28 | name: rabbit-conn 29 | -------------------------------------------------------------------------------- /controllers/twitch-controller.js: -------------------------------------------------------------------------------- 1 | const { validationResult } = require('express-validator'); 2 | const twitchService = require('../services/twitch-service'); 3 | const { ErrorHandler } = require('../util/error-handler'); 4 | 5 | const getEventFromTwitchUrl = async (req, res, next) => { 6 | console.log('body', req.body); 7 | const errors = validationResult(req); 8 | if (!errors.isEmpty()) { 9 | return next(new ErrorHandler(422, 'Invalid input passed, please check it')); 10 | } 11 | 12 | try { 13 | const { streamUrl, category } = req.body; 14 | 15 | const event = await twitchService.getEventFromTwitchUrl(streamUrl, category); 16 | 17 | res.status(201).json(event); 18 | } catch (err) { 19 | console.error(err.message); 20 | next(new ErrorHandler(422, err.message)); 21 | } 22 | }; 23 | exports.getEventFromTwitchUrl = getEventFromTwitchUrl; 24 | -------------------------------------------------------------------------------- /controllers/user-messages-controller.js: -------------------------------------------------------------------------------- 1 | const chatMessageService = require('../services/chat-message-service'); 2 | 3 | exports.getMessagesByUser = async (req, res) => { 4 | const skip = req.query.skip ? +req.query.skip : 0; 5 | const limit = req.query.limit ? +req.query.limit : 20; 6 | 7 | const messages = await chatMessageService.getLatestChatMessagesByUserId( 8 | req.user._id, 9 | limit, 10 | skip 11 | ); 12 | 13 | res.status(200).json({ 14 | messages: messages?.data || [], 15 | total: messages?.total || 0, 16 | }); 17 | }; 18 | 19 | exports.setMessageRead = async (req, res) => { 20 | const { id } = req.params; 21 | const requestingUser = req.user; 22 | await chatMessageService.setMessageRead(id, requestingUser); 23 | return res.status(200).send(); 24 | }; 25 | 26 | exports.sendMessage = async (req, res) => { 27 | return res.status(200).send({}); 28 | }; 29 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "search.exclude": { 3 | "**/node_modules": true, 4 | "**/dist": true, 5 | "**/conf": true, 6 | "**/config": true, 7 | "**/coverge": true 8 | }, 9 | "editor.tabSize": 2, 10 | "editor.insertSpaces": true, 11 | "files.autoSave": "onFocusChange", 12 | "editor.defaultFormatter": "dbaeumer.vscode-eslint", 13 | "eslint.validate": ["javascript", "typescript", "json"], 14 | "git.autofetch": true, 15 | "eslint.options": { 16 | "configFile": ".eslintrc.js" 17 | }, 18 | "editor.codeActionsOnSave": { 19 | "source.fixAll.eslint": true, 20 | "source.fixAll.prettier": true 21 | }, 22 | "editor.formatOnSave": true, 23 | "editor.formatOnType": false, 24 | "editor.formatOnPaste": true, 25 | "editor.rulers": [ 26 | 80, 27 | 100 28 | ], 29 | "[javascript]": { 30 | "editor.defaultFormatter": "vscode.typescript-language-features" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /controllers/notification-events-controller.js: -------------------------------------------------------------------------------- 1 | const notificationEventsService = require('../services/notification-events-service'); 2 | 3 | exports.listNotificationEvents = async (req, res) => { 4 | const {limit, cat, gameId} = req.query; 5 | const eventList = await notificationEventsService.listNotificationEvents(limit, cat, gameId); 6 | res.status(200).json(eventList); 7 | }; 8 | 9 | exports.listNotificationEventsByBet = async (req, res) => { 10 | const {limit} = req.query; 11 | const {betId} = req.params; 12 | 13 | const eventList = await notificationEventsService.listNotificationEventsByBet(limit, betId); 14 | res.status(200).json(eventList); 15 | }; 16 | 17 | exports.listNotificationEventsByUser = async (req, res) => { 18 | const {limit} = req.query; 19 | const {userId} = req.params; 20 | 21 | const eventList = await notificationEventsService.listNotificationEventsByUser(limit, userId); 22 | res.status(200).json(eventList); 23 | }; 24 | -------------------------------------------------------------------------------- /util/number-helper.js: -------------------------------------------------------------------------------- 1 | const _ = require("lodash"); 2 | 3 | const BigNumber = require('bignumber.js'); 4 | const { ONE } = require('@wallfair.io/trading-engine'); 5 | 6 | const toScaledBigInt = (input) => { 7 | return BigInt(input) * ONE; 8 | }; 9 | 10 | const fromScaledBigInt = (input) => { 11 | return new BigNumber(input).dividedBy(ONE).toFixed(4); 12 | }; 13 | 14 | const calculateGain = (investmentAmount, outcomeAmount, precision = 2) => { 15 | const investment = _.toNumber(investmentAmount); 16 | const outcome = _.toNumber(outcomeAmount); 17 | const gain = ((outcome - investment) / investment) * 100; 18 | 19 | const negative = gain < 0; 20 | const value = isNaN(gain) ? '-' : negative ? `${gain.toFixed(precision)}%` : `+${gain.toFixed(precision)}%`; 21 | 22 | return { 23 | number: gain, 24 | value, 25 | negative, 26 | }; 27 | }; 28 | 29 | module.exports = { 30 | toScaledBigInt, 31 | fromScaledBigInt, 32 | calculateGain 33 | }; 34 | -------------------------------------------------------------------------------- /emails/email-evaluate.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 18 | 19 | 20 |
21 | Question: {{bet_question}} 22 |
23 |
24 | Rating: {{rating}} 25 |
26 |
27 | Comment: {{comment}} 28 |
29 | 30 | -------------------------------------------------------------------------------- /services/auth-service.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | const { getDiscordUserData } = require('../util/discord.oauth'); 3 | const { getFacebookUserData } = require('../util/facebook.oauth'); 4 | const { getGoogleUserData } = require('../util/google.oauth'); 5 | const { getTwitchUserData } = require('../util/twitch.oauth'); 6 | 7 | exports.generateJwt = async (user) => jwt.sign({ userId: user.id, phone: user.phone, isAdmin: Boolean(user.admin) }, process.env.JWT_KEY, { expiresIn: '48h' }); 8 | 9 | exports.getUserDataForProvider = async (provider, context) => { 10 | const dataGetter = { 11 | google: getGoogleUserData, 12 | facebook: getFacebookUserData, 13 | twitch: getTwitchUserData, 14 | discord: getDiscordUserData, 15 | }[provider]; 16 | 17 | if (!dataGetter) { 18 | throw new Error(`Provider '${JSON.stringify(provider)}' not supported.`); 19 | } 20 | 21 | return { 22 | ...await dataGetter(context), 23 | accountSource: provider, 24 | }; 25 | 26 | }; 27 | -------------------------------------------------------------------------------- /services/lottery-service.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const mongoose = require('mongoose'); 3 | const { Lottery, LotteryTicket } = require('@wallfair.io/wallfair-commons').models; 4 | 5 | exports.listLotteries = async () => Lottery.find({}); 6 | 7 | exports.listLotteriesForUser = async (userId) => { 8 | const completedTickets = await LotteryTicket.find({ userId: mongoose.Types.ObjectId(userId) }); 9 | const completedLotteries = _.uniq(completedTickets.map(({ lotteryId }) => lotteryId)); 10 | return Lottery.find({ 11 | _id: { 12 | // exclude completed lotteries 13 | $nin: completedLotteries, 14 | }, 15 | closed: false, 16 | }); 17 | }; 18 | 19 | exports.getLottery = async (id) => Lottery.findOne({ _id: id }); 20 | 21 | exports.saveLottery = async (lotteryId, lotteryQuestionIndex, userId) => { 22 | const lotteryTicket = new LotteryTicket({ 23 | lotteryId, 24 | lotteryQuestionIndex, 25 | userId, 26 | skip: false, 27 | }); 28 | return lotteryTicket.save(); 29 | }; 30 | -------------------------------------------------------------------------------- /infra/environments/production/environment: -------------------------------------------------------------------------------- 1 | CLIENT_URL=https://alpacasino.io 2 | VERIFY_URL=https://alpacasino.io/ 3 | REACT_APP_SHOW_UPCOMING_FEATURES=false 4 | FACEBOOK_OAUTH_REDIRECT_URI=https://alpacasino.io/oauth/facebook 5 | GOOGLE_OAUTH_REDIRECT_URI=https://alpacasino.io/oauth/google 6 | ENVIRONMENT=PRODUCTION 7 | BACKEND_URL=https://rm-api.alpacasino.io 8 | DISCORD_OAUTH_REDIRECT_URI=https://alpacasino.io/oauth/discord 9 | TWITCH_OAUTH_REDIRECT_URI=https://alpacasino.io/oauth/twitch 10 | DEPOSIT_NOTIFICATION_EMAIL=deposit-info@wallfair.io 11 | DEPOSIT_WALLET_BITCOIN=bc1qrdf9ndd8264dyl66n45cl22lavunymdgnjpj96 12 | DEPOSIT_WALLET_ETHEREUM=0xCdE7a3DCb537C81730DB750C0315D1730980759c 13 | DEPOSIT_WALLET_LITECOIN=ltc1qnzlfm88cgr58yxmjzm92se5ug6te6w5yhjfyjn 14 | REWARD_WALLET=0x15469F0c1854a072a20e593474F93eeE7d7aA3F3 15 | CRYPTOPAY_URL=https://business.cryptopay.me 16 | CRYPTOPAY_API_KEY=GUXsSke9TWvGmsve1BzuaQ 17 | MOONPAY_BASE_URL=https://buy.moonpay.com 18 | MOONPAY_API_KEY=pk_live_Km0JAmn8YVKCOybQ4WZfhGNsN7gxDZZr 19 | MOONPAY_CURRENCY_CODE=MATIC_POLYGON 20 | PLAYMONEY=false 21 | -------------------------------------------------------------------------------- /util/cmc.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios"); 2 | 3 | const getConversionData = async ({ convertFrom, convertTo }) => { 4 | let convertFromString = typeof convertFrom === 'string' ? convertFrom : convertFrom.join(','); 5 | 6 | const APIKEY = process.env.CMC_API_KEY || ""; 7 | 8 | const apiPath = 9 | `https://pro-api.coinmarketcap.com/v1/cryptocurrency/quotes/latest?convert=${convertFromString}&symbol=${convertTo}&CMC_PRO_API_KEY=${APIKEY}`; 10 | 11 | return await axios.get(apiPath) 12 | .then(response => { 13 | const { data } = response.data; 14 | return data; 15 | }) 16 | .catch((e) => { 17 | console.log(e.message); 18 | throw new Error(`Could not get CMC data.`); 19 | }); 20 | } 21 | 22 | exports.getMarketPrice = async ({ convertFrom, convertTo, amount }) => { 23 | const data = await getConversionData({ convertFrom, convertTo }); 24 | const symbol = convertTo; 25 | const price = data?.[symbol]?.quote?.[convertFrom]?.price; 26 | 27 | const convertedAmount = price ? (1 / price) * amount : 0; 28 | 29 | return { 30 | [symbol]: data[symbol], 31 | convertedAmount 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /helper/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {} req 3 | * returns if the user is an admin and if a userId url param was passed and equal 4 | * to the logged in user. 5 | * TODO: replace with utils/auth.isUserAdminOrSelf 6 | */ 7 | exports.isAdmin = (req) => !(req.user.admin === false && req.params.userId !== req.user.id); 8 | 9 | exports.generate = (n) => { 10 | const add = 1; 11 | let max = 12 - add; 12 | 13 | if (n > max) { 14 | return this.generate(max) + this.generate(n - max); 15 | } 16 | 17 | max = Math.pow(10, n + add); 18 | const min = max / 10; 19 | const number = Math.floor(Math.random() * (max - min + 1)) + min; 20 | 21 | return `${number}`.substring(add); 22 | }; 23 | 24 | 25 | 26 | exports.hasAcceptedLatestConsent = ({ tosConsentedAt }) => { 27 | const consentThreshold = process.env.CONSENT_THRESHOLD_DATE; 28 | if (!consentThreshold || isNaN(Date.parse(consentThreshold))) { 29 | console.warn('Missing CONSENT_THRESHOLD_DATE env var, no consents required'); 30 | return false; 31 | } 32 | 33 | if (!tosConsentedAt) { 34 | return true; 35 | } 36 | return new Date(tosConsentedAt) < new Date(consentThreshold); 37 | }; 38 | -------------------------------------------------------------------------------- /services/aws-s3-service.js: -------------------------------------------------------------------------------- 1 | const { S3Client, PutObjectCommand, GetObjectCommand } = require('@aws-sdk/client-s3'); 2 | const { getSignedUrl } = require("@aws-sdk/s3-request-presigner"); 3 | 4 | const CLIENT_ID = process.env.AWS_S3_CLIENT_ID; 5 | const CLIENT_SECRET = process.env.AWS_S3_CLIENT_SECRET; 6 | const BUCKET = process.env.AWS_S3_CLIENT_BUCKET; 7 | const REGION = process.env.AWS_S3_CLIENT_REGION; 8 | 9 | let s3Client; 10 | 11 | const init = () => { 12 | s3Client = new S3Client({ 13 | credentials: { 14 | accessKeyId: CLIENT_ID, 15 | secretAccessKey: CLIENT_SECRET 16 | }, 17 | region: REGION 18 | }); 19 | } 20 | 21 | const upload = async (user, image) => { 22 | const params = { 23 | Key: `${user}/${image.filename}`, 24 | Body: Buffer.from(image.src.replace(/^data:image\/\w+;base64,/, ""), 'base64'), 25 | Bucket: BUCKET, 26 | } 27 | 28 | try { 29 | await s3Client.send(new PutObjectCommand(params)); 30 | return await getSignedUrl(s3Client, new GetObjectCommand(params)); 31 | } catch (err) { 32 | console.log("AWS-S3 upload error", err.message); 33 | throw new Error(err.message); 34 | } 35 | } 36 | 37 | module.exports = { 38 | init, 39 | upload 40 | }; -------------------------------------------------------------------------------- /infra/application/environment: -------------------------------------------------------------------------------- 1 | TWILIO_SID=VA78f4f3e1b980107228cb6a1d407b15d0 2 | TWILIO_ACC_SID=AC32af9a1c4494dbf72b9cc2a436bc9344 3 | ENVIRONMENT=STAGING 4 | VERIFY_URL=https://main.alpacasino.io/ 5 | BACKEND_URL=https://staging-api.alpacasino.io 6 | TWITCH_CLIENT_ID=ducsd7kb44uqzp5u3gqgsmsbx2uggs 7 | ADMIN_USERNAME=wallfair 8 | CLIENT_URL=https://main.alpacasino.io 9 | REACT_APP_SHOW_UPCOMING_FEATURES=true 10 | TWITCH_OAUTH_REDIRECT_URI=https://main.alpacasino.io/oauth/twitch 11 | DISCORD_CLIENT_ID=920342099684126730 12 | DISCORD_OAUTH_REDIRECT_URI=https://main.alpacasino.io/oauth/discord 13 | CRYPTOPAY_URL=https://business-sandbox.cryptopay.me 14 | CRYPTOPAY_API_KEY=zlayiPlvC8luX5pgCZUKEw 15 | CRYPTOPAY_RECEIVER_CURRENCY=USDT 16 | CONSENT_THRESHOLD_DATE=2021-12-16T00:00:00 17 | DEPOSIT_WALLET_BITCOIN=mvBFHAfuccFDr7W86UNuJgPDrpkNLUishq 18 | DEPOSIT_WALLET_ETHEREUM=0x13F593f4e0fb2b71d71aaC17770455817Cf90589 19 | DEPOSIT_WALLET_LITECOIN=QRpuwHv8CQ5SciX15YD5NBett2qBfRrWQ8 20 | DEPOSIT_NOTIFICATION_EMAIL=mussi@wallfair.io 21 | REWARD_WALLET=0xFf3E6f877640a0793A0a99b2E23F5D4E04Ef59b0 22 | MOONPAY_BASE_URL=https://buy-staging.moonpay.com 23 | MOONPAY_API_KEY=pk_test_ZQ4tne5XCtmX73OXKlzeJ4dGw7zsDt 24 | MOONPAY_CURRENCY_CODE=MATIC 25 | PLAYMONEY=false 26 | BET_LIQUIDITY_WALLET=0xB56AE8254dF096173A27700bf1F1EC2b659F3eC8 27 | -------------------------------------------------------------------------------- /services/chat-message-service.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | const { profanityFilter, profanityReplacement } = require('./chat-message-service'); 3 | 4 | describe('profanity filter', () => { 5 | it('should not remove non-profanities', async () => { 6 | const message = 'a normal chat message'; 7 | const result = await profanityFilter({ message }); 8 | expect(result.message).to.equal(message); 9 | }); 10 | 11 | it('should remove english profanities', async () => { 12 | const message = 'a shit chat message'; 13 | const expected = `a ${profanityReplacement.repeat(4)} chat message`; 14 | const result = await profanityFilter({ message }); 15 | expect(result.message).to.equal(expected); 16 | }); 17 | 18 | it('should remove german profanities', async () => { 19 | const message = 'a Scheisse chat message'; 20 | const expected = `a ${profanityReplacement.repeat(8)} chat message`; 21 | const result = await profanityFilter({ message }); 22 | expect(result.message).to.equal(expected); 23 | }); 24 | 25 | it('should remove russian profanities', async () => { 26 | const message = 'a дерьмо chat message'; 27 | const expected = `************* chat message`; 28 | const result = await profanityFilter({ message }); 29 | expect(result.message).to.equal(expected); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /routes/users/users-routes.js: -------------------------------------------------------------------------------- 1 | // Import the express Router to create routes 2 | const router = require('express').Router(); 3 | const { check } = require('express-validator'); 4 | const userController = require('../../controllers/users-controller'); 5 | 6 | router.get('/getLeaderboard/:type/:skip/:limit', userController.getLeaderboard); 7 | 8 | router.get( 9 | '/confirm-email', 10 | [check('userId').isString(), check('code').isLength({ min: 6, max: 6 })], 11 | userController.confirmEmail 12 | ); 13 | 14 | router.get('/resend-confirm', 15 | [check('userId').isString()], 16 | userController.resendConfirmEmail); 17 | 18 | router.get('/:userId/info', userController.getBasicUserInfo); 19 | 20 | router.post('/check-username', userController.checkUsername); 21 | 22 | router.get('/:userId/stats', userController.getUserStats); 23 | 24 | router.get('/count', userController.getUserCount) 25 | 26 | router.post( 27 | '/verify-sms', 28 | [check('userId').isString(), check('phone').isMobilePhone(), check('smsToken').isNumeric().isLength({ min: 6, max: 6 })], 29 | userController.verifySms 30 | ); 31 | router.post( 32 | '/send-sms', 33 | [check('phone').isMobilePhone()], 34 | userController.sendSms 35 | ); 36 | 37 | router.post( 38 | '/send-email', 39 | [check('text').notEmpty(), check('subject').notEmpty(), check('recaptchaToken').notEmpty()], 40 | userController.sendAffiliateEmail 41 | ); 42 | 43 | module.exports = router; 44 | -------------------------------------------------------------------------------- /util/error-handler.js: -------------------------------------------------------------------------------- 1 | const logger = require('../util/logger'); 2 | const { getBanData } = require('./user'); 3 | 4 | class ErrorHandler extends Error { 5 | constructor(statusCode, message, errors) { 6 | super(); 7 | this.statusCode = statusCode; 8 | this.message = message; 9 | this.errors = errors; 10 | } 11 | } 12 | 13 | const handleError = (err, res) => { 14 | const { statusCode = 500, message, errors } = err; 15 | logger.error(err); 16 | 17 | return res.status(statusCode).json({ 18 | status: 'error', 19 | statusCode, 20 | message, 21 | errors, 22 | }); 23 | }; 24 | 25 | class ForbiddenError extends ErrorHandler { 26 | constructor() { 27 | super(403, 'The credentials provided are insufficient to access the requested resource'); 28 | } 29 | } 30 | 31 | class BannedError extends ErrorHandler { 32 | constructor(userData) { 33 | super(403, 'Your account is banned', { banData: getBanData(userData) }); 34 | } 35 | } 36 | 37 | class NotFoundError extends ErrorHandler { 38 | constructor() { 39 | super(404, "The requested resource wasn't found"); 40 | } 41 | } 42 | 43 | class ValidationError extends ErrorHandler { 44 | constructor(errors) { 45 | super(422, 'Invalid input passed, please check it.', errors); 46 | } 47 | } 48 | 49 | module.exports = { 50 | ErrorHandler, 51 | handleError, 52 | ForbiddenError, 53 | NotFoundError, 54 | ValidationError, 55 | BannedError, 56 | }; 57 | -------------------------------------------------------------------------------- /controllers/rewards-controller.js: -------------------------------------------------------------------------------- 1 | const { validationResult } = require('express-validator'); 2 | 3 | const userService = require('../services/user-service'); 4 | const lotteryService = require('../services/lottery-service'); 5 | 6 | const postLotteryAnswer = async (req, res, next) => { 7 | const errors = validationResult(req); 8 | 9 | if (!errors.isEmpty()) { 10 | return next(res.status(422).send('Invalid input passed, please check it')); 11 | } 12 | 13 | try { 14 | const { questionId, answerId } = req.body; 15 | 16 | const user = await userService.getUserById(req.user.id); 17 | const openLotteries = await lotteryService.listLotteriesForUser(req.user.id); 18 | 19 | if (!openLotteries.map(({ _id }) => _id.toString()).includes(questionId)) { 20 | throw new Error('Cannot submit more than one lottery answer per user.'); 21 | } 22 | 23 | const result = await lotteryService.saveLottery(questionId, answerId, user._id); 24 | 25 | res.status(201).json({ 26 | id: result._id, 27 | }); 28 | } catch (err) { 29 | console.error(err.message); 30 | const error = res.status(422).send(err.message); 31 | next(error); 32 | } 33 | }; 34 | 35 | const getQuestions = async (req, res) => { 36 | const questions = await lotteryService.listLotteriesForUser(req.user.id); 37 | res.status(200).json(questions); 38 | }; 39 | 40 | exports.getQuestions = getQuestions; 41 | exports.postRewardAnswer = postLotteryAnswer; 42 | -------------------------------------------------------------------------------- /docs/evoplay/generateEvoplayGames.js: -------------------------------------------------------------------------------- 1 | const evoplayGames = require('./evoplayCfg') ; 2 | const path = require('path') ; 3 | const fs = require('fs') 4 | 5 | const objectIdByName = (gamename) => { 6 | const encoded = new Buffer(String(gamename)).toString('hex').substring(0,23) 7 | const fill = 24 - encoded.length 8 | return encoded + ' '.repeat(fill).replace(/./g, (v, i) => 9 | ((parseInt(encoded[(i*2)%encoded.length], 16) + parseInt(i*2, 16))%16).toString(16) 10 | ) 11 | } 12 | 13 | const generateGamesInserts = () => { 14 | const filePath = path.join(__dirname, 'evoplayGames.sql'); 15 | const gameProvider = 'Evoplay'; 16 | 17 | if(fs.existsSync(filePath)) { 18 | fs.unlinkSync(filePath); 19 | } 20 | 21 | const fileStream = fs.createWriteStream(filePath, { 22 | flags: 'a' // 'a' means appending (old data will be preserved) 23 | }); 24 | fileStream.write(`BEGIN;`); 25 | fileStream.write('\n'); 26 | 27 | for (let key in evoplayGames) { 28 | const gameInfo = evoplayGames[key]; 29 | const catSubType = gameInfo.game_sub_type; 30 | const gameId = objectIdByName(key); 31 | const name = gameInfo.name; 32 | 33 | fileStream.write(`INSERT INTO games (id, name, label, provider, enabled, category) VALUES ($$${gameId}$$, $$${name}$$, $$${name}$$, $$${gameProvider}$$, true, $$${catSubType}$$);`) 34 | fileStream.write('\n') 35 | } 36 | 37 | fileStream.write(`COMMIT;;`); 38 | fileStream.write('\n'); 39 | } 40 | 41 | generateGamesInserts(); 42 | -------------------------------------------------------------------------------- /util/discord.oauth.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios"); 2 | 3 | const getDiscordTokenForAuthCode = async (code) => { 4 | const data = new URLSearchParams(); 5 | data.append('client_id', process.env.DISCORD_CLIENT_ID); 6 | data.append('code', code); 7 | data.append('redirect_uri', process.env.DISCORD_OAUTH_REDIRECT_URI); 8 | data.append('grant_type', 'authorization_code'); 9 | data.append('client_secret', process.env.DISCORD_CLIENT_SECRET,); 10 | 11 | return await axios.post('https://discord.com/api/v8/oauth2/token', data, { 12 | headers: { 13 | 'Content-Type': 'application/x-www-form-urlencoded' 14 | } 15 | }) 16 | .then(({ data }) => data) 17 | .catch((err) => { 18 | console.log(err.response.data); 19 | throw new Error(`Could not get user's data.`); 20 | }) 21 | } 22 | 23 | const getDiscordUserMeta = async (token) => { 24 | return await axios.get( 25 | 'https://discord.com/api/v8/users/@me', 26 | { headers: { 'Authorization': `Bearer ${token}` }, } 27 | ) 28 | .then(({ data }) => data) 29 | .catch((err) => { 30 | console.log(err.response.data); 31 | throw new Error(`Could not get user's data.`); 32 | }); 33 | } 34 | 35 | exports.getDiscordUserData = async ({ code }) => { 36 | const { access_token } = await getDiscordTokenForAuthCode(code); 37 | const data = await getDiscordUserMeta(access_token); 38 | 39 | const { username, email, verified } = data; 40 | 41 | return { 42 | email, 43 | username, 44 | name: '', 45 | emailConfirmed: verified, 46 | }; 47 | } -------------------------------------------------------------------------------- /util/challenge.js: -------------------------------------------------------------------------------- 1 | const { randomBytes, createHmac, timingSafeEqual } = require('crypto'); 2 | const { ethers } = require('ethers'); 3 | 4 | const HMAC_SECRET_KEY = process.env.HMAC_SECRET_KEY || randomBytes(32).toString('hex'); 5 | 6 | const generateMac = (msg) => createHmac('sha384', HMAC_SECRET_KEY) 7 | .update(msg) 8 | .digest('hex'); 9 | 10 | exports.generateChallenge = ( 11 | address, 12 | ttl = 60 13 | ) => { 14 | address = address.slice(2); 15 | const date = new Date(); 16 | const expiration = date.setSeconds(date.getSeconds() + ttl); 17 | const data = JSON.stringify({ 18 | address, 19 | expiration: +expiration, 20 | }); 21 | 22 | const msg = Buffer.from(data).toString('base64'); 23 | 24 | return msg + '.' + generateMac(msg); 25 | }; 26 | 27 | exports.verifyChallengeResponse = ( 28 | address, 29 | challenge, 30 | response, 31 | ) => { 32 | const data = challenge.split('.'); 33 | const decoded = JSON.parse(Buffer.from(data[0], 'base64').toString()); 34 | 35 | if ( 36 | address.slice(2) !== decoded.address || 37 | new Date(decoded.expiration) < new Date() || 38 | !timingSafeEqual(Buffer.from(generateMac(data[0])), Buffer.from(data[1])) 39 | ) { 40 | return false; 41 | } 42 | 43 | const signatureAddress = ethers.utils.verifyMessage(challenge, response); 44 | return address.toLowerCase() === signatureAddress.toLowerCase(); 45 | }; 46 | 47 | exports.isAddressValid = (address) => { 48 | try { 49 | return ethers.utils.getAddress(address); 50 | } catch (e) { 51 | return false; 52 | } 53 | }; -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.6' 2 | 3 | services: 4 | mongodb: 5 | image: mongo:4.4.3 6 | command: --replSet "rs0" 7 | container_name: mongodb-wall 8 | ports: 9 | - 27017:27017 10 | environment: 11 | - MONGO_INITDB_DATABASE=wallfair 12 | - MONGO_INITDB_ROOT_USERNAME=wallfair 13 | - MONGO_INITDB_ROOT_PASSWORD=wallfair 14 | volumes: 15 | # seeding scripts 16 | - ./mongo-entrypoint:/docker-entrypoint-initdb.d 17 | # named volumes 18 | - mongodb:/data/db 19 | - mongoconfig:/data/configdb 20 | postgres: 21 | image: postgres 22 | hostname: postgres 23 | ports: 24 | - 5432:5432 25 | environment: 26 | POSTGRES_USER: postgres 27 | POSTGRES_PASSWORD: postgres 28 | POSTGRES_DB: testdb 29 | volumes: 30 | - type: bind 31 | source: ../sql/initial-setup.sql 32 | target: /docker-entrypoint-initdb.d/initial-setup.sql 33 | - postgres-data:/var/lib/postgresql/data 34 | redis: 35 | image: redis:6.0-alpine 36 | hostname: redis 37 | ports: 38 | - 6379:6379 39 | rabbitmq: 40 | image: rabbitmq:3.8-management-alpine 41 | container_name: 'rabbitmq' 42 | ports: 43 | - 5673:5672 44 | - 15673:15672 45 | volumes: 46 | - ~/.docker-conf/rabbitmq/data/:/var/lib/rabbitmq/ 47 | - ~/.docker-conf/rabbitmq/log/:/var/log/rabbitmq 48 | networks: 49 | - rabbitmq_nodejs 50 | networks: 51 | rabbitmq_nodejs: 52 | driver: bridge 53 | 54 | volumes: 55 | mongodb: 56 | mongoconfig: 57 | postgres-data: 58 | -------------------------------------------------------------------------------- /.env-example: -------------------------------------------------------------------------------- 1 | JWT_KEY= 2 | 3 | TWILIO_SID= 4 | TWILIO_ACC_SID= 5 | TWILIO_AUTH_TOKEN= 6 | 7 | VERIFY_URL=http://localhost:3000/verify 8 | 9 | POSTGRES_USER=postgres 10 | POSTGRES_HOST=localhost 11 | POSTGRES_DB=testdb 12 | POSTGRES_PASSWORD=postgres 13 | POSTGRES_PORT=5432 14 | POSTGRES_DISABLE_SSL=true 15 | 16 | DB_CONNECTION='mongodb://wallfair:wallfair@127.0.0.1/wallfair?authSource=admin' 17 | 18 | RABBITMQ_CONNECTION='amqp://localhost:5673' 19 | 20 | CLIENT_URL='http://localhost:3000' 21 | BACKEND_URL='https://example.ngrok.io' 22 | 23 | TWITCH_CLIENT_ID= 24 | TWITCH_CLIENT_SECRET= 25 | TWITCH_CALLBACK_SECRET= 26 | 27 | GOOGLE_API_KEY= 28 | 29 | ADMIN_USERNAME=wallfair 30 | ADMIN_PASSWORD=wallfair 31 | 32 | # AWS S3 33 | AWS_S3_CLIENT_ID=client-id 34 | AWS_S3_CLIENT_SECRET=client-secret 35 | AWS_S3_CLIENT_BUCKET=client-bucket 36 | AWS_S3_CLIENT_REGION=client-region 37 | 38 | # G-Recaptcha v3 39 | GOOGLE_RECAPTCHA_CLIENT_SECRET=client-secret 40 | RECAPTCHA_SKIP_TOKEN=skip-token 41 | 42 | SENDGRID_API_KEY=api-key 43 | 44 | 45 | GOOGLE_CLIENT_ID= 46 | GOOGLE_CLIENT_SECRET= 47 | GOOGLE_OAUTH_REDIRECT_URI= 48 | 49 | FACEBOOK_CLIENT_ID= 50 | FACEBOOK_CLIENT_SECRET= 51 | FACEBOOK_OAUTH_REDIRECT_URI= 52 | 53 | TWITCH_CLIENT_ID= 54 | TWITCH_CLIENT_SECRET= 55 | TWITCH_OAUTH_REDIRECT_URI= 56 | 57 | DISCORD_CLIENT_ID= 58 | DISCORD_CLIENT_SECRET= 59 | DISCORD_OAUTH_REDIRECT_URI= 60 | 61 | CONSENT_THRESHOLD_DATE=2021-12-17T00:00:00 62 | 63 | DEPOSIT_NOTIFICATION_EMAIL=deposit-info@wallfair.io 64 | 65 | REWARD_WALLET= 66 | 67 | MOONPAY_BASE_URL= 68 | MOONPAY_API_KEY= 69 | MOONPAY_API_SECRET= 70 | MOONPAY_CURRENCY_CODE= 71 | DEPOSIT_WALLET= -------------------------------------------------------------------------------- /services/cryptopay-service.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const { signRequest } = require('../util/cryptopay'); 3 | 4 | const { CRYPTOPAY_URL, CRYPTOPAY_RECEIVER_CURRENCY } = process.env; 5 | 6 | exports.getChannel = async (userId, currency) => { 7 | try { 8 | const path = `/api/channels/custom_id/${currency}_${userId}`; 9 | const headers = signRequest('', 'GET', path); 10 | 11 | return await axios.get(`${CRYPTOPAY_URL}${path}`, 12 | { 13 | headers, 14 | } 15 | ); 16 | } catch (e) { 17 | if (['unauthenticated', 'unauthorized'].includes(e.response.data?.error?.code)) { 18 | console.error('Fetching channel failed', e.response.data); 19 | throw Error(`Failed to fetch channel with user id ${userId} and currency ${currency}`); 20 | } else { 21 | return undefined; 22 | } 23 | } 24 | }; 25 | 26 | exports.createChannel = async (userId, currency) => { 27 | const requestData = { 28 | pay_currency: currency, 29 | receiver_currency: CRYPTOPAY_RECEIVER_CURRENCY, 30 | name: `${currency}-${CRYPTOPAY_RECEIVER_CURRENCY}`, 31 | custom_id: `${currency}_${userId}`, 32 | }; 33 | 34 | try { 35 | const path = '/api/channels'; 36 | const headers = signRequest(JSON.stringify(requestData), 'POST', path); 37 | 38 | return await axios.post(`${CRYPTOPAY_URL}${path}`, 39 | requestData, 40 | { 41 | headers, 42 | } 43 | ); 44 | } catch (e) { 45 | console.error('Creating channel failed', e.response.data); 46 | throw new Error(`Failed to create a channel with user id ${userId} and currency ${currency}`); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /ssl/rm-prod-postgres.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEQTCCAqmgAwIBAgIUXHkrpEV4mzgVWemmZFPlPOty+dowDQYJKoZIhvcNAQEM 3 | BQAwOjE4MDYGA1UEAwwvNDg0YTJlMDEtOTBhZi00MmVjLWEwMWQtZjQ4YWM1Yzdm 4 | ZWMzIFByb2plY3QgQ0EwHhcNMjEwNTMxMTIwNDI0WhcNMzEwNTI5MTIwNDI0WjA6 5 | MTgwNgYDVQQDDC80ODRhMmUwMS05MGFmLTQyZWMtYTAxZC1mNDhhYzVjN2ZlYzMg 6 | UHJvamVjdCBDQTCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBAKQ9bEQ1 7 | N/LIdO4XENOnmV10c7m7yAOpPizqFJJv2ENnKfxZ5G0zZfo6BgbgwXzV6e9277gB 8 | 2zoC4lf52cqeaNMIo1MJfEGqua86PqAeCMOfJUF2u1Kco0FKVzpQL5NEorUyTRUb 9 | 0TGZbQVdfuQBdkfXz2W3+VUAcp/vN8SoI5ny7adFrNqtrCFoaKljdpDsyJ9qwk8o 10 | 3JhGAAd9ve3kGvYRc5SpJrRy36kRrkS6fGaynsSMG3nSYup6XaXv9GJhO4kj6Pod 11 | F1zyv4UeCEuz2QIRZe0ms6EGQNJtZlH45byygj9MDJRCszy4fShUWP2hIntQwLzq 12 | BqsmhkfXksYVBH8jJh9Sj53xooabZA8nLv0yZejivoEik+YuKo4NW+oaPwFHxzyt 13 | IvEGsEh56cH+lNTyUZ8HkcdCXFRGij7P58GBbTuGv3oi3iSmdSHpbwZO7GVQbKB9 14 | Yx7gmF5sMpeiqh/6bmVh/pHS9l9M4IVNKr9AX421uXrDeLdzDNj60VvkmQIDAQAB 15 | oz8wPTAdBgNVHQ4EFgQUzbMPeUshvx7MVoQsEIlW0jVq0mEwDwYDVR0TBAgwBgEB 16 | /wIBADALBgNVHQ8EBAMCAQYwDQYJKoZIhvcNAQEMBQADggGBACt4LL8NBgPjJXsH 17 | if6ApDjLVOO95BbJy8ljWXB4UmiTXxzo91ZEWn3QOmPN3haSFlxUVW1HRQEdQQRa 18 | VRQfy32cYz79Tv9DNSz2KEGdIlAr/1l4yLyMja1AnIExV2Zu02kjxSrtSlrioh/a 19 | e+/hJ9WyEzLoGgUmnlr/5Gw1W7YiixNy0cLhe0WkcxxN/PLwLjt0GacLvICnZ6so 20 | OL7f7WNhGgwVuso3na1VNX0k6q/m5Rf8sbjnTiWJrYKLwvYwkfg2eB72ss5kBf8W 21 | zK4J19Z3XQARFrUefpeBUTLPneSCe+GuSL0lqgefGS5U0k+1UJO8ySE/53OYOqMn 22 | 5RfJJHrYwVO9cDtHl5Hh1qxR/TJEVZtHm/lBRD1Yl3n9i8g3DZ3pLKs8lLD69oja 23 | I7XGt4C9IdZHs96GFU8iEbu4tirj3e1gQMaGyrUHj9BKx7TCiddflDNUYW5/CKnT 24 | wNFIYzSUiw4U6TxJtKkXwA6L14HqEkniyHdPIHyrkrVW06K6DQ== 25 | -----END CERTIFICATE----- 26 | -------------------------------------------------------------------------------- /util/twitch.oauth.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios"); 2 | 3 | const getTwitchTokenForAuthCode = async (code) => { 4 | return await axios.post('https://id.twitch.tv/oauth2/token', null, { 5 | params: { 6 | client_id: process.env.TWITCH_CLIENT_ID, 7 | client_secret: process.env.TWITCH_CLIENT_SECRET, 8 | code, 9 | grant_type: 'authorization_code', 10 | redirect_uri: process.env.TWITCH_OAUTH_REDIRECT_URI, 11 | }, 12 | headers: { 13 | 'Accept': 'application/json', 14 | }, 15 | }) 16 | .then(({ data }) => data) 17 | .catch((err) => { 18 | console.log(err.response.data); 19 | throw new Error(`Could not get user's data.`); 20 | }); 21 | } 22 | 23 | const getTwitchUserMeta = async (token) => { 24 | return await axios.get( 25 | 'https://api.twitch.tv/helix/users', 26 | { 27 | headers: { 28 | 'Authorization': `Bearer ${token}`, 29 | 'Client-ID': process.env.TWITCH_CLIENT_ID, 30 | }, 31 | } 32 | ) 33 | .then(({ data }) => data.data[0]) 34 | .catch((err) => { 35 | console.log(err.response.data); 36 | throw new Error(`Could not get user's data.`); 37 | }); 38 | 39 | } 40 | 41 | exports.getTwitchUserData = async ({ code }) => { 42 | const { access_token } = await getTwitchTokenForAuthCode(code); 43 | const userMeta = await getTwitchUserMeta(access_token); 44 | const { email, profile_image_url, display_name, login } = userMeta; 45 | 46 | return { 47 | email, 48 | username: display_name || login, 49 | name: '', 50 | profilePicture: profile_image_url, 51 | emailConfirmed: !!email, 52 | }; 53 | } -------------------------------------------------------------------------------- /util/facebook.oauth.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios"); 2 | 3 | const getFacebookTokenForAuthCode = async (code) => { 4 | return await axios.get('https://graph.facebook.com/v12.0/oauth/access_token', { 5 | params: { 6 | client_id: process.env.FACEBOOK_CLIENT_ID, 7 | redirect_uri: process.env.FACEBOOK_OAUTH_REDIRECT_URI, 8 | client_secret: process.env.FACEBOOK_CLIENT_SECRET, 9 | code, 10 | } 11 | }) 12 | .then(({ data }) => data) 13 | .catch(() => { 14 | throw new Error(`Could not get user's data.`); 15 | }) 16 | } 17 | 18 | const getFacebookUserMeta = async (token) => { 19 | return await axios.get( 20 | 'https://graph.facebook.com/v12.0/me?fields=id,name,email,picture,birthday', 21 | { headers: { 'Authorization': `Bearer ${token}` }, } 22 | ) 23 | .then(({ data }) => data) 24 | .catch((e) => { 25 | console.log(e.message); 26 | throw new Error(`Could not get user's data.`); 27 | }); 28 | 29 | } 30 | 31 | exports.getFacebookUserData = async ({ code }) => { 32 | const { access_token } = await getFacebookTokenForAuthCode(code); 33 | const { name, email, picture, birthday } = await getFacebookUserMeta(access_token); 34 | 35 | 36 | const profilePicture = picture?.data?.url; 37 | const [day, month, year] = birthday.split('/'); 38 | 39 | if (!birthday) { 40 | throw new Error(`User's birthday is missing.`); 41 | } 42 | 43 | const birthdate = new Date(year, month - 1, day); 44 | 45 | return { 46 | email, 47 | username: email.split('@')[0], 48 | name, 49 | profilePicture, 50 | birthdate, 51 | emailConfirmed: true, 52 | }; 53 | } -------------------------------------------------------------------------------- /util/google.oauth.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios"); 2 | 3 | const getGoogleTokenForAuthCode = async (code) => { 4 | return await axios.post('https://oauth2.googleapis.com/token', { 5 | code, 6 | client_id: process.env.GOOGLE_CLIENT_ID, 7 | redirect_uri: process.env.GOOGLE_OAUTH_REDIRECT_URI, 8 | grant_type: 'authorization_code', 9 | client_secret: process.env.GOOGLE_CLIENT_SECRET, 10 | }) 11 | .then(({ data }) => data) 12 | .catch(() => { 13 | throw new Error(`Could not get user's data.`); 14 | }) 15 | } 16 | 17 | const getGoogleUserMeta = async (token) => { 18 | return await axios.get( 19 | 'https://people.googleapis.com/v1/people/me?personFields=names,emailAddresses,birthdays', 20 | { headers: { 'Authorization': `Bearer ${token}` }, } 21 | ) 22 | .then(({ data }) => data) 23 | .catch((e) => { 24 | console.log(e.message); 25 | throw new Error(`Could not get user's data.`); 26 | }); 27 | } 28 | 29 | exports.getGoogleUserData = async ({ code }) => { 30 | const { access_token } = await getGoogleTokenForAuthCode(code); 31 | const { names, birthdays, emailAddresses } = await getGoogleUserMeta(access_token); 32 | 33 | const primary = ({ metadata }) => metadata.primary; 34 | 35 | const email = emailAddresses?.find(primary)?.value; 36 | const name = names?.find(primary)?.displayName; 37 | const birthday = birthdays?.find(primary)?.date; 38 | let birthdate = null; 39 | 40 | if (birthday) { 41 | const { year, month, day } = birthday; 42 | birthdate = new Date(year, month - 1, day); 43 | } 44 | 45 | return { 46 | email, 47 | username: email.split('@')[0], 48 | name, 49 | birthdate, 50 | emailConfirmed: true, 51 | }; 52 | } -------------------------------------------------------------------------------- /routes/users/admin-routes.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const { check } = require('express-validator'); 3 | const adminController = require('../../controllers/admin-controller'); 4 | const multer = require('multer'); 5 | const upload = multer({ dest: 'tmp/uploads/' }) 6 | 7 | router.post( 8 | '/transfers', 9 | [ 10 | check('amount').isNumeric(), 11 | check('transactionHash').notEmpty(), 12 | check('userAddress').notEmpty(), 13 | check('inputAmount').notEmpty(), 14 | check('inputCurrency').isIn(['ETH', 'USDT', 'BTC', 'LTC']), 15 | ], 16 | adminController.transferToUser, 17 | ); 18 | 19 | router.get('/users/:id', 20 | adminController.getUser 21 | ); 22 | 23 | router.get('/users', 24 | adminController.listUsers 25 | ); 26 | 27 | router.get('/user-referrals', 28 | adminController.listUsersWithReferrals 29 | ); 30 | 31 | router.post( 32 | '/promo-codes', 33 | [ 34 | check('name').notEmpty(), 35 | check('type').notEmpty(), 36 | check('value').isNumeric(), 37 | check('expiresAt').notEmpty(), 38 | ], 39 | adminController.createPromoCode 40 | ); 41 | 42 | router.get( 43 | '/promo-codes', 44 | adminController.getPromoCodes 45 | ); 46 | 47 | router.patch( 48 | '/promo-codes/:id', 49 | adminController.updatePromoCode 50 | ); 51 | 52 | router.post( 53 | '/promo-codes/add', 54 | [ 55 | check('promoCode').notEmpty() 56 | ], 57 | upload.single('file'), 58 | adminController.addBonusManually 59 | ); 60 | 61 | 62 | router.post( 63 | '/casino/mint-bonus', 64 | [ 65 | check('amount').isNumeric() 66 | ], 67 | adminController.mintCasinoBonusWallet 68 | ); 69 | 70 | router.post( 71 | '/bet/mint', 72 | [ 73 | check('amount').isNumeric() 74 | ], 75 | adminController.mintBetLiquidity 76 | ); 77 | 78 | module.exports = router; 79 | -------------------------------------------------------------------------------- /docs/softswiss/generateGames.js: -------------------------------------------------------------------------------- 1 | const SoftswissGames = [ 2 | ...require('./games/bgaming.json') 3 | ] ; 4 | const path = require('path') ; 5 | const fs = require('fs'); 6 | 7 | // const crypto = require('crypto') 8 | 9 | // const gameIdFromString = (gamename) => { 10 | // return crypto.createHash('sha1').update(String(gamename)).digest('hex'); 11 | // } 12 | 13 | const generateGamesInserts = () => { 14 | const filePath = path.join(__dirname, 'softswissBgamingGames.sql'); 15 | 16 | if(fs.existsSync(filePath)) { 17 | fs.unlinkSync(filePath); 18 | } 19 | 20 | const fileStream = fs.createWriteStream(filePath, { 21 | flags: 'a' // 'a' means appending (old data will be preserved) 22 | }); 23 | fileStream.write(`BEGIN;`); 24 | fileStream.write('\n'); 25 | 26 | for (let key in SoftswissGames) { 27 | const gameInfo = SoftswissGames[key]; 28 | const gameProvider = `${gameInfo.provider}/${gameInfo.producer}`; 29 | const catSubType = gameInfo.category; 30 | const label = gameInfo.title; 31 | const name = gameInfo.identifier2; 32 | const gameId = gameInfo.identifier; 33 | 34 | fileStream.write(`INSERT INTO games (id, name, label, provider, enabled, category) VALUES ($$${gameId}$$, $$${name}$$, $$${label}$$, $$${gameProvider}$$, true, $$${catSubType}$$);`) 35 | fileStream.write('\n') 36 | } 37 | 38 | fileStream.write(`COMMIT;;`); 39 | fileStream.write('\n'); 40 | } 41 | 42 | generateGamesInserts(); 43 | 44 | // const game1 = objectIdByName('softswiss:WildTexas'); 45 | // const game2 = objectIdByName('softswiss:WestTown'); 46 | // const game3 = objectIdByName('softswiss:WbcRingOfRiches'); 47 | 48 | // console.log({ 49 | // game1, game2, game3 50 | // }); 51 | // 52 | // // console.log(mongoose.Types.ObjectId('test')); 53 | // // console.log(mongoose.Types.ObjectId('test2')); 54 | // 55 | // console.log(game1 === game2); 56 | 57 | -------------------------------------------------------------------------------- /util/outcomes.js: -------------------------------------------------------------------------------- 1 | exports.getProbabilityMap = (outcomes) => 2 | outcomes.reduce( 3 | (acc, { probability }, index) => ({ ...acc, [index]: probability }), 4 | {} 5 | ); 6 | 7 | /** 8 | * @param {bigint} liquidity 9 | * @param {{ [key: number]: string }} probabilities 10 | * @returns { bigint[] } 11 | */ 12 | exports.getOutcomeDistributionHints = (probabilities) => { 13 | const hints = Object.keys(probabilities) 14 | .sort() 15 | .map( 16 | (outcomeKey) => BigInt(Math.round(+probabilities[outcomeKey] * 100)) 17 | ); 18 | 19 | const pooledHints = hints.reduce((sum, hint) => sum + hint, 0n); 20 | const targetPool = 100n; 21 | 22 | if (pooledHints !== targetPool) { 23 | 24 | let lowestValueIndex = 0; 25 | for (const hintIndex in hints) { 26 | if (hints[hintIndex] < hints[lowestValueIndex]) { 27 | lowestValueIndex = hintIndex; 28 | } 29 | } 30 | 31 | const unclaimedDistribution = targetPool - pooledHints; 32 | hints[lowestValueIndex] += unclaimedDistribution; 33 | } 34 | 35 | return hints; 36 | }; 37 | 38 | /** 39 | * @param {object[]} outcomes 40 | */ 41 | exports.areCreationOutcomesValid = (outcomes) => ( 42 | (outcomes.length >= 2 || outcomes.length <= 4) && // must between 2 and 4 43 | (outcomes.every(({ name, probability, index }) => !!name && !!probability && (!!index || index === 0))) && // each must have probability, name, and index 44 | ( 45 | outcomes.reduce((total, { probability }) => +(total + (+probability)).toFixed(2), 0) === 1 || // must add up to one 46 | outcomes.length === 3 && outcomes.every(({ probability }) => +probability === 0.33) // ... or be a three way split via 0.33 47 | ) && 48 | (new Set(outcomes.map(({ name }) => name)).size === outcomes.length) && // must have unique names 49 | (new Set(outcomes.map(({ index }) => index)).size === outcomes.length) // must have unique indices 50 | ); -------------------------------------------------------------------------------- /infra/application/backend-prod-deploy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: backend 5 | labels: 6 | app: backend 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: backend 12 | template: 13 | metadata: 14 | labels: 15 | app: backend 16 | spec: 17 | containers: 18 | - image: registry.digitalocean.com/wallfair/backend_k8s 19 | imagePullPolicy: Always 20 | resources: 21 | limits: 22 | cpu: 1300m 23 | requests: 24 | cpu: 1000m 25 | memory: 1024Mi 26 | livenessProbe: 27 | httpGet: 28 | path: / 29 | port: 8000 30 | initialDelaySeconds: 2 31 | periodSeconds: 10 32 | readinessProbe: 33 | httpGet: 34 | path: / 35 | port: 8000 36 | initialDelaySeconds: 2 37 | periodSeconds: 3 38 | name: backend 39 | envFrom: 40 | - configMapRef: 41 | name: backend-config 42 | - configMapRef: 43 | name: real-money-config 44 | - configMapRef: 45 | name: app-secrets 46 | - configMapRef: 47 | name: app-global-secrets 48 | - configMapRef: 49 | name: fractal-secrets 50 | - configMapRef: 51 | name: postgres-conn 52 | - configMapRef: 53 | name: mongo-conn 54 | - configMapRef: 55 | name: redis-conn 56 | - configMapRef: 57 | name: rabbit-conn 58 | ports: 59 | - containerPort: 8000 60 | 61 | --- 62 | 63 | apiVersion: autoscaling/v1 64 | kind: HorizontalPodAutoscaler 65 | metadata: 66 | name: backend 67 | spec: 68 | maxReplicas: 10 69 | minReplicas: 1 70 | scaleTargetRef: 71 | apiVersion: apps/v1 72 | kind: Deployment 73 | name: backend 74 | targetCPUUtilizationPercentage: 50 75 | -------------------------------------------------------------------------------- /ssl/staging.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFgjCCA2qgAwIBAgIRAKjTwvPY70rUrFB09I8OsWwwDQYJKoZIhvcNAQELBQAw 3 | WTEVMBMGA1UEChMMRGlnaXRhbE9jZWFuMUAwPgYDVQQDEzdtb25nb2RiIENsdXN0 4 | ZXIgQ0E6YThkM2MyZjMtZDhlZi00YWQ0LWFjNTAtNzRmNDhmMGViMTZjMB4XDTIx 5 | MDcwMjEzMTI0OFoXDTQxMDcwMjEzMTI0OFowWTEVMBMGA1UEChMMRGlnaXRhbE9j 6 | ZWFuMUAwPgYDVQQDEzdtb25nb2RiIENsdXN0ZXIgQ0E6YThkM2MyZjMtZDhlZi00 7 | YWQ0LWFjNTAtNzRmNDhmMGViMTZjMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC 8 | CgKCAgEAvyOkB4vLksxG7K3MWa/IPb2G3rPkhht2/pb7GiC6S4be1n+NbYtLUqZx 9 | tjRq6YeBMeQj4g2uhUGALUwouWK+TH7jz6ZqLEQdAMerOLxRIzrjIi+uDWS2NqiX 10 | 53vMj+fQ5Kxtfil6cu0kvKAjM+magwEwdmJX/iGI5BwTnS21LwRqtw34ImMxmlXT 11 | fF+rlmyyY8x+YLT+NOf68Pq4VTJCEgMZrX3+rORBLwu2rQCSBPZkecXDo8/cAJJV 12 | bVWOG5+BUnMmPHXmzDb0UoSylEXKGW7GCKEGIU1naGygijv7qjfVwyU28Gi77tOq 13 | 9G//F5sRYoXpYQ9lVovBCAQuS8C5AM38XlpjySsDTEat6Gvzj3scZl6c6qc/Mq8+ 14 | uEgXJFHr8aJEeIRuwQ8QeF2xTOm+17CwN0ctde/k7p2gLPbEETmW+gegnlOB0s8Q 15 | L/gM5wCxfk2VhGrwZApD3eSlcylqo3OnHx6Ct6QVKsE2KBx+5e0AQtf90tR4q1jd 16 | 3IfOJuDanSqhCQo2UCQ70ct1CE5JjN8yqCwLvIKBbFRmIY3bSa8Fv2z7RI49AquW 17 | umgOCNV37uYJpdZYAu32aiQ3OCeX+Jnn+7z3g8tjGj2Lxq9/P9p+quXvxy3XFhKv 18 | OUHDB40Kj43asYfNbWZOrIJNz1RPDElwBewa7hZ2wBu0Fis5qH0CAwEAAaNFMEMw 19 | DgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFI0s 20 | KgWfGWugQEiNMxuLuhbftT6rMA0GCSqGSIb3DQEBCwUAA4ICAQAKBEfBcaH20Nvy 21 | 9FpEkh4Hihhd8HWp+35Ox4EhPFAZal2pyiuBLRcT5+l5cOT0gJcXCCBfwDWngZIc 22 | HObwlAAtoQjkJcfa8JCAn5ZZYNGY4OIiQCLYr2TivxWcpb02QKw4TyEfTE6+8FgM 23 | bWhQYjf2FUbE00H+1Q30/A0C8JwgDneorjI7jabEnPruMr7Hdz21RysOOVi4IZAl 24 | Zmv8esLuH8oM6ytCmBInA6X3Wa5i4BZLDJ8fl0DuMpzPZHL7UEeoKH69bRs4BbGn 25 | tET0Ojma3hTdI4gDvS7vTDymCcb1BwnpBvAkbIR/wbm4GFOZvEWJ2HxSxCdg7l9y 26 | sJjsEW5TU0fX4QTiViGbJ0ve1mAcG9TmHJkT44Q31gocG8oQnkxh56MMKjmoq6l9 27 | 8REcMYAfTAxjAT4R4DHvaSLip8iyc2ZgFP7vaOpRxUibORgoJbHNmQCvJQIDKVV2 28 | 5sMOJ5O3OfbgQCXWUT4rOqm5P570d0lOZR8aXCKGeQN9Jx0wNWuXGkBTTu2ZBtNx 29 | LnWsj+EUR2wDAdCFhUU95URRYf8Bl3RHROIn5fFPQzksWpxOYjUlasbO/WBu34LK 30 | NBvnsR7rxhoBWEA+WjCga+tQ3m7r1WQpmP7qiqCftKs3SQP0uDwYubAr9KFmwlzj 31 | KvNTHFnbCSZ1GagUCGvz99Oa4F1INQ== 32 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /ssl/rm-prod-mongo.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFgTCCA2mgAwIBAgIQP77+1sWVQw+/4R/4wKqvxTANBgkqhkiG9w0BAQsFADBZ 3 | MRUwEwYDVQQKEwxEaWdpdGFsT2NlYW4xQDA+BgNVBAMTN21vbmdvZGIgQ2x1c3Rl 4 | ciBDQTozZmJlZmVkNi1jNTk1LTQzMGYtYmZlMS0xZmY4YzBhYWFmYzUwHhcNMjEx 5 | MjA4MTEzNzIxWhcNNDExMjA4MTEzNzIxWjBZMRUwEwYDVQQKEwxEaWdpdGFsT2Nl 6 | YW4xQDA+BgNVBAMTN21vbmdvZGIgQ2x1c3RlciBDQTozZmJlZmVkNi1jNTk1LTQz 7 | MGYtYmZlMS0xZmY4YzBhYWFmYzUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK 8 | AoICAQDXA+5FxeCcng9ffQIMfyJIi6poFVC407jHkqnPKlrr0zFU6xq1LjXGPvCP 9 | FQTKtFyXBavc1tBsRSm9MoWrbaoGTzexumDcnklIVRWy0+9ZKPJWEZliAX3cmOMt 10 | ssI66o3LiCoor7jUnx9jiSYwXeFM87w+mAc5bA8lWS+BwgVa/gxdw5Wqs+YmJJ/0 11 | JZHdNsIO2guMty+ifEVwjXTvtN0EJJTGOzuzYy49HrsgezTWdjhg/ucYJ3T24AgB 12 | 2GKsbwK2gjlu0xipfIGf/ZxE4onMz7/mGKbiccd226mV+I8dIVpq0ag0m+1xtjEu 13 | uiM9MFqx12X5Q7TrGfkwX7So/bAEhYaVr5zMReWgRtftzpH5oG0i+YTICaoSCPys 14 | kxVONxMNPb9TfYDghyAlDtliibo2CC0g0QIyA9GAYyryRLy6lkhf44z/XyM+RZTm 15 | 6vat8eMAZKhbS7fxh67nQqFJXmN8f7QFxXYZlQeB3BJFKV3nE7hXHdOCo0ZX3bXX 16 | PfvXtqe6FovK5zY2Yw1CP2sDfzsx3ExIR3MEuksOtH+4oAh0C2DyCxKNjnhIU8gb 17 | qyuCGcLe06cV0ZvXD3O7aQ2W7p8qfbWLQy/+lZ8pLSOUMokpBtBIlC+T+MIihhxc 18 | 2s9mydTmucRZhibfXV/esjLPjYhhRBk8Avq6r2+83CuhlqeTUwIDAQABo0UwQzAO 19 | BgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQULtB+ 20 | aED8fW5q+kPRKxkVYapYRXQwDQYJKoZIhvcNAQELBQADggIBADqKxGzECOiXKjMe 21 | zA3jC3IMyTVsd0LrT72H+qDLSqfPuVbHL47LNTBbVtnTP9pCKoNYCiUvRwbgZITN 22 | SeIZyXlYT/VTqvQaSuqpjhquUEDw0q37dubXt9byZPvrLMbZ2gYXRiJF4jgWL9Kh 23 | juFN4jOG33vjeQqdJ2JcBOlV7xPXdY5H/hODjeybLrQ0OW4Ak25MkPDZcfJuZhg7 24 | AGGW95bKt26m7IWDBWksg/LcTXDarH+aalnHF2x7fwVn9yKRP5xPltjyM3uvFn5y 25 | Ymd0ZC0CUgmkb4B7N2KCqO9o2sOjd1u79qq/ZJhFtTo2UEC+SybwLaKsX4ZK7xYs 26 | 6UQhsre5RcElsDGbzuVkF4FsQYCHkRE64Yq8pXuqsqXPi7L1Fab6NL6IMcofO9HB 27 | /3979SIa+riZSua1YesQ3mydAQLQT0hABF6MlDC70sPiWuXwh+FLm4b2l+cbh6Sl 28 | s34/BCQrutZEezXwAvMJOfIe4/+XwMXxA30nUwtq5HBaX1TNnqUx7RivlWEMEyE5 29 | mQEF8Pq0JDVG11ckLuaWWH1TUAGjUBafYO1j0GWdwpUXBbyaSU6Sd0vbTAJBo7ge 30 | IlQKoiALATXvHPpVHk4YpfvwEcUA4IvDiWq0uLSGUJf3vto/tErbB/FK0wacpcPd 31 | fXIg2+J3P5P2rPflJSedqqJv4fUg 32 | -----END CERTIFICATE----- 33 | -------------------------------------------------------------------------------- /jobs/twitch-subscribe-job.js: -------------------------------------------------------------------------------- 1 | // Import Event model 2 | const { Event } = require('@wallfair.io/wallfair-commons').models; 3 | 4 | const twitchService = require('../services/twitch-service'); 5 | 6 | const findAndSubscribe = async () => { 7 | const session = await Event.startSession(); 8 | try { 9 | await session.withTransaction(async () => { 10 | const unsubscribedEvents = await Event.find( 11 | // check which query performs better under heavy load 12 | // {type: "streamed", $or: [{"metadata": {$exists: false}}, {"metadata.twitch_subscribed": {$exists: false}}]} 13 | { 14 | type: 'streamed', 15 | $or: [ 16 | { 'metadata.twitch_subscribed_online': 'false' }, 17 | { 'metadata.twitch_subscribed_offline': 'false' }, 18 | ], 19 | } 20 | ) 21 | .limit(5) 22 | .exec(); 23 | 24 | for (const unsubscribedEvent of unsubscribedEvents) { 25 | console.log(new Date(), 'Subscribe on twitch for event', unsubscribedEvent.name); 26 | 27 | // subscribe for online events 28 | const onlineSubscriptionStatus = await twitchService.subscribeForOnlineNotifications( 29 | unsubscribedEvent.metadata.twitch_id 30 | ); 31 | unsubscribedEvent.metadata.twitch_subscribed_online = onlineSubscriptionStatus; 32 | 33 | // subscribe for offline events 34 | const offlineSubscriptionStatus = await twitchService.subscribeForOfflineNotifications( 35 | unsubscribedEvent.metadata.twitch_id 36 | ); 37 | unsubscribedEvent.metadata.twitch_subscribed_offline = offlineSubscriptionStatus; 38 | 39 | await unsubscribedEvent.save(); 40 | } 41 | }); 42 | } catch (err) { 43 | console.log(err); 44 | } finally { 45 | await session.endSession(); 46 | } 47 | }; 48 | 49 | const initTwitchSubscribeJob = () => { 50 | // only start the service if the env var is set 51 | if (process.env.TWITCH_CLIENT_ID) { 52 | setInterval(findAndSubscribe, 5_000); 53 | } 54 | }; 55 | 56 | exports.initTwitchSubscribeJob = initTwitchSubscribeJob; 57 | -------------------------------------------------------------------------------- /routes/auth/auth-routes.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const { check } = require('express-validator'); 3 | const sessionsController = require('../../controllers/sessions-controller'); 4 | 5 | router.post( 6 | '/login', 7 | [check('userIdentifier').notEmpty(), check('password').notEmpty().isLength({ min: 8, max: 255 })], 8 | sessionsController.login 9 | ); 10 | 11 | router.post( 12 | '/sign-up', 13 | [ 14 | check('email').notEmpty(), 15 | check('recaptchaToken').notEmpty(), 16 | check('passwordConfirm').notEmpty(), 17 | check('password') 18 | .notEmpty() 19 | .isLength({ min: 8, max: 255 }) 20 | .custom((value, { req }) => { 21 | if (value !== req.body.passwordConfirm) { 22 | throw new Error("Passwords don't match"); 23 | } else { 24 | return value; 25 | } 26 | }), 27 | ], 28 | sessionsController.createUser 29 | ); 30 | 31 | router.post( 32 | '/login/:provider', 33 | [ 34 | check('provider').notEmpty().isIn(['google', 'facebook', 'twitch', 'discord']), 35 | check('code').notEmpty(), 36 | ], 37 | sessionsController.loginThroughProvider 38 | ); 39 | 40 | router.post('/verify-email', [check('email').notEmpty().isEmail()], sessionsController.verifyEmail); 41 | 42 | /** Triggers the "I've forgot my passwort" process */ 43 | router.post( 44 | '/forgot-password', 45 | [check('email').notEmpty().isEmail()], 46 | sessionsController.forgotPassword 47 | ); 48 | 49 | /** Route to acutally reset your password */ 50 | router.post( 51 | '/reset-password', 52 | [ 53 | check('email').notEmpty().isEmail(), 54 | check('passwordResetToken').notEmpty().isLength({ min: 1, max: 12 }), 55 | check('password').notEmpty(), 56 | check('passwordConfirmation').notEmpty(), 57 | ], 58 | sessionsController.resetPassword, 59 | ); 60 | 61 | router.get( 62 | '/web3/:address', 63 | sessionsController.loginWeb3Challenge 64 | ); 65 | 66 | router.post( 67 | '/web3', 68 | [ 69 | check('address').notEmpty(), 70 | check('signResponse').notEmpty(), 71 | check('challenge').notEmpty(), 72 | ], 73 | sessionsController.loginWeb3, 74 | ); 75 | 76 | module.exports = router; 77 | -------------------------------------------------------------------------------- /util/constants.js: -------------------------------------------------------------------------------- 1 | const AWARD_TYPES = { 2 | EMAIL_CONFIRMED: 'EMAIL_CONFIRMED', 3 | AVATAR_UPLOADED: 'AVATAR_UPLOADED', 4 | SET_USERNAME: 'SET_USERNAME', 5 | CREATED_ACCOUNT_BY_INFLUENCER: 'CREATED_ACCOUNT_BY_INFLUENCER', 6 | CREATED_ACCOUNT_BY_THIS_REF: 'CREATED_ACCOUNT_BY_THIS_REF', 7 | }; 8 | 9 | const WFAIR_REWARDS = { 10 | referral: 1000, 11 | setAvatar: 500, 12 | setUsername: 500, 13 | confirmEmail: 100, 14 | registeredByInfluencer: 2500, 15 | totalBets: { 16 | 5: 100, 17 | 20: 200, 18 | 50: 300, 19 | 100: 500, 20 | 150: 1000, 21 | }, 22 | }; 23 | 24 | const INFLUENCERS = ['heet', 'nikoletta', 'earlygame']; 25 | 26 | const DEFAULT = { 27 | betLiquidity: 50_0000n, 28 | }; 29 | 30 | const BONUS_STATES = { 31 | Active: 0, 32 | Used: 1, 33 | Expired: 2 34 | } 35 | 36 | const BONUS_TYPES = { 37 | LAUNCH_1k_500: { 38 | type: 'LAUNCH_1k_500', 39 | amount: 500, 40 | endDate: '12/31/2021 23:59:59' 41 | }, 42 | LAUNCH_2k_400: { 43 | type: 'LAUNCH_2k_400', 44 | amount: 100 45 | }, 46 | EMAIL_CONFIRM_50: { 47 | type: 'EMAIL_CONFIRM_50', 48 | amount: 50, 49 | neededBonusType: 'LAUNCH_PROMO_2021' 50 | }, 51 | FIRST_DEPOSIT_DOUBLE_DEC21: { 52 | type: 'FIRST_DEPOSIT_DOUBLE_DEC21', 53 | neededBonusType: 'LAUNCH_PROMO_2021', 54 | max: 100_000 55 | }, 56 | LAUNCH_PROMO_2021: { 57 | type: 'LAUNCH_PROMO_2021' 58 | }, 59 | USER_OPTIONAL_BONUS: { 60 | optional: true, 61 | type: 'USER_OPTIONAL_BONUS' 62 | }, 63 | SURVEY_20220112: { 64 | type: 'SURVEY_20220112', 65 | amount: 500, 66 | } 67 | } 68 | 69 | const PROMO_CODE_STATUS = { 70 | new: 'NEW', 71 | claimed: 'CLAIMED', 72 | finalized: 'FINALIZED', 73 | }; 74 | 75 | const PROMO_CODE_DEFAULT_REF = 'default'; 76 | 77 | const PROMO_CODES = { 78 | FIRST_DEPOSIT_DOUBLE_DEC21: 'FIRST_DEPOSIT_DOUBLE_DEC21', 79 | }; 80 | 81 | const PROMO_CODES_TYPES = { 82 | BONUS: 'BONUS', 83 | FREESPIN: 'FREESPIN', 84 | }; 85 | 86 | module.exports = { 87 | AWARD_TYPES, 88 | WFAIR_REWARDS, 89 | INFLUENCERS, 90 | DEFAULT, 91 | BONUS_TYPES, 92 | BONUS_STATES, 93 | PROMO_CODE_STATUS, 94 | PROMO_CODE_DEFAULT_REF, 95 | PROMO_CODES, 96 | PROMO_CODES_TYPES, 97 | }; 98 | -------------------------------------------------------------------------------- /services/request-log-service.js: -------------------------------------------------------------------------------- 1 | const models = require('@wallfair.io/wallfair-commons').models; 2 | 3 | const API_TYPE = 'backend'; 4 | const DATA_SENSITIVE_ROUTES = [ 5 | '/auth/login' 6 | ]; 7 | 8 | const getRealIp = (req) => { 9 | const forwardedFor = req.headers['x-forwarded-for']; 10 | 11 | if(forwardedFor) { 12 | const isSplittable = forwardedFor.indexOf(',') > -1; 13 | if(isSplittable) { 14 | return forwardedFor.split(',')?.[0]?.replace(/\s+/g, ''); 15 | } 16 | return forwardedFor; 17 | } 18 | 19 | return req.connection.remoteAddress || req.socket.remoteAddress; 20 | } 21 | 22 | const getPath = (req) => { 23 | return req.baseUrl + req.path; 24 | } 25 | 26 | const getUsefullHeaders = (req) => { 27 | const output = {}; 28 | const headers = req.headers; 29 | const exlusionList = [ 30 | 'authorization' 31 | ]; 32 | 33 | for (const header in headers) { 34 | if (exlusionList.indexOf(header) === -1) { 35 | output[header] = headers[header]; 36 | } 37 | } 38 | 39 | return output; 40 | } 41 | 42 | 43 | /** 44 | * Get body, excluding some data-sensitive routes 45 | * @param req 46 | * @returns {{}} 47 | */ 48 | const getBody = (req) => { 49 | const path = getPath(req); 50 | const list = DATA_SENSITIVE_ROUTES; 51 | 52 | let output = {}; 53 | let skipBody = false; 54 | 55 | if (list.length) { 56 | for (const index in list) { 57 | if (path.indexOf(list[index]) > -1) { 58 | skipBody = true; 59 | } 60 | } 61 | } 62 | 63 | if(!skipBody) { 64 | output = req.body; 65 | } 66 | 67 | return output; 68 | } 69 | 70 | const requestLogHandler = async (req, res, next) => { 71 | res.on('finish', async () => { 72 | try { 73 | const entry = { 74 | api_type: API_TYPE, 75 | userId: req._userId, 76 | ip: getRealIp(req), 77 | method: req.method, 78 | path: getPath(req), 79 | query: req.query, 80 | headers: getUsefullHeaders(req), 81 | body: getBody(req), 82 | statusCode: res.statusCode 83 | } 84 | 85 | await models.ApiLogs.create(entry); 86 | 87 | } catch (error) { 88 | console.error(`${new Date()} [requestLogHandler] error`, error); 89 | } 90 | }); 91 | 92 | next(); 93 | }; 94 | 95 | 96 | module.exports = { 97 | requestLogHandler 98 | } 99 | -------------------------------------------------------------------------------- /services/user-api.js: -------------------------------------------------------------------------------- 1 | const { User } = require('@wallfair.io/wallfair-commons').models; 2 | 3 | /** 4 | * @param {Object} userData 5 | * @param {string} userData.phone 6 | * @param {string} userData.email 7 | * @param {username} userData.username 8 | * @param {Array} userData.openBets 9 | * @param {Array} userData.closedBets 10 | * @param {Boolean} userData.confirmed 11 | * @param {Boolean} userData.admin 12 | * @param {Date} userData.date 13 | * @param {Birth} userData.birthdate //to do 14 | * @param {Country} userData.country //to do 15 | * @param {String} userData.password 16 | * @param {String} userData.passwordResetToken 17 | */ 18 | const createUser = async (userData) => await new User(userData).save(); 19 | 20 | /** 21 | * @param {String} id 22 | * @param {Object} userData 23 | * @param {string} userData.phone 24 | * @param {string} userData.email 25 | * @param {string} userData.username 26 | * @param {Array} userData.openBets 27 | * @param {Array} userData.closedBets 28 | * @param {Boolean} userData.confirmed 29 | * @param {Boolean} userData.admin 30 | * @param {Date} userData.date 31 | * @param {String} userData.password 32 | * @param {String} userData.passwordResetToken 33 | */ 34 | const updateUser = async (userData) => await User.findOneAndUpdate({ 35 | _id: userData.id, 36 | }, userData, { new: true }).exec(); 37 | 38 | /** @param {String} userId */ 39 | const getOne = (userId) => User.findOne({ _id: userId }).exec(); 40 | 41 | /** @param {String} IdEmailPhoneOrUsername */ 42 | const getUserByIdEmailPhoneOrUsername = (IdEmailPhoneOrUsername) => { 43 | if (!IdEmailPhoneOrUsername) { 44 | throw new Error('no IdEmailPhoneOrUsername identifier provided'); 45 | } 46 | return User 47 | .findOne({ 48 | $or: [ 49 | { username: IdEmailPhoneOrUsername }, 50 | { phone: IdEmailPhoneOrUsername }, 51 | { email: IdEmailPhoneOrUsername }, 52 | ], 53 | }) 54 | .exec(); 55 | } 56 | 57 | const verifyEmail = async (email) => { 58 | return await User.findOneAndUpdate( 59 | { email }, 60 | { $set: { confirmed: true } }, 61 | { new: true } 62 | ).exec(); 63 | }; 64 | 65 | const getUserEntriesAmount = async () => User.countDocuments({}).exec(); 66 | 67 | module.exports = { 68 | createUser, 69 | updateUser, 70 | getOne, 71 | getUserByIdEmailPhoneOrUsername, 72 | verifyEmail, 73 | getUserEntriesAmount, 74 | }; 75 | -------------------------------------------------------------------------------- /services/quote-storage-service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * When bets being placed in the system, stores the price of every option after the price has been calculated. 3 | * This generates a time series to be consumed later to display price action history (how each option delevoped in price over time). 4 | * 5 | * TODO Move this logic to a microservice 6 | * 7 | * CREATE TABLE IF NOT EXISTS amm_price_action ( 8 | * betid varchar(255), 9 | * trx_timestamp timestamp, 10 | * outcomeIndex integer, 11 | * quote decimal, 12 | * PRIMARY KEY(betid, option, trx_timestamp) 13 | * ); 14 | */ 15 | const format = require('pg-format'); 16 | // const { Wallet } = require('@wallfair.io/trading-engine'); 17 | const { getPostgresConnection } = require('@wallfair.io/wallfair-commons').utils; 18 | // const WFAIR_TOKEN = 'WFAIR'; 19 | // const WFAIR = new Wallet(); 20 | // let one = parseInt(WFAIR.ONE); 21 | 22 | const INSERT_PRICE_ACTION = 'INSERT INTO amm_price_action (betid, trx_timestamp, outcomeindex, quote) values %L' 23 | 24 | const onBetPlaced = async (/*bet*/) => { 25 | // const { _id: betId, outcomes } = bet; 26 | // const betContract = new BetContract(betId, outcomes.length); 27 | 28 | // const timestamp = new Date().toISOString(); 29 | // const valuePromises = outcomes.map( 30 | // outcome => betContract.calcBuy(WFAIR.ONE, outcome.index).then(p => [ 31 | // betId.toString(), 32 | // timestamp, 33 | // outcome.index, 34 | // Math.min(1 / (parseInt(p) / one), 1), 35 | // ]) 36 | // ); 37 | 38 | // const values = await Promise.all(valuePromises); 39 | // await getPostgresConnection().query(format(INSERT_PRICE_ACTION, values)).catch(() => { 40 | //ignore this error for now 41 | // console.error('onBetPlaced => INSERT_PRICE_ACTION', err); 42 | // }); 43 | } 44 | 45 | const onNewBet = async (bet) => { 46 | const { _id: betId, outcomes } = bet; 47 | const initialQuote = 1 / outcomes.length; 48 | const timestamp = new Date(); 49 | timestamp.setMinutes(timestamp.getMinutes() - 5); 50 | const values = outcomes.map(outcome => [ 51 | betId.toString(), 52 | timestamp.toISOString(), 53 | outcome.index, 54 | initialQuote, 55 | ]); 56 | await getPostgresConnection().query(format(INSERT_PRICE_ACTION, values)).catch(() => { 57 | //ignore this error for now 58 | // console.error('onBetPlaced => INSERT_PRICE_ACTION', err); 59 | }); 60 | } 61 | 62 | module.exports = { 63 | onNewBet, 64 | onBetPlaced 65 | } 66 | -------------------------------------------------------------------------------- /services/promo-codes-service.js: -------------------------------------------------------------------------------- 1 | const { fromWei, Wallet, AccountNamespace } = require("@wallfair.io/trading-engine"); 2 | const { CasinoTradeContract } = require("@wallfair.io/wallfair-casino"); 3 | const { PROMO_CODE_DEFAULT_REF } = require("../util/constants"); 4 | 5 | const casinoContract = new CasinoTradeContract(); 6 | 7 | exports.getUserPromoCodes = async (userId, statuses) => { 8 | const promoCodes = await casinoContract.getUserPromoCodes(userId, statuses); 9 | return Promise.all(promoCodes.map(async (p) => { 10 | return { 11 | ...p, 12 | value: fromWei(p.value).toFixed(4), 13 | wagering_reached: p.status === 'CLAIMED' ? 14 | (await casinoContract.calculateWagering(userId, p)) : 15 | 0 16 | } 17 | })); 18 | }; 19 | 20 | exports.cancelUserPromoCode = async (userId, promoCodeName, ref) => { 21 | try { 22 | const wallet = new Wallet(); 23 | const balance = await wallet.getBalance(userId, AccountNamespace.USR, 'BFAIR'); 24 | await wallet.burn({ 25 | owner: userId, 26 | namespace: AccountNamespace.USR, 27 | symbol: 'BFAIR' 28 | }, balance); 29 | await casinoContract.finalizeUserPromoCode( 30 | userId, 31 | promoCodeName, 32 | ref, 33 | 'CANCELLED' 34 | ); 35 | } catch (e) { 36 | console.error(e); 37 | throw e; 38 | } 39 | } 40 | 41 | exports.addUserPromoCode = async (userId, promoCodeName) => { 42 | return await casinoContract.createUserPromoCode(userId, promoCodeName); 43 | }; 44 | 45 | exports.getDepositPromoCodes = async () => await casinoContract.getDepositPromoCodes('ACTIVE'); 46 | 47 | exports.isClaimedBonus = async (userId, promoCodeName) => { 48 | const result = await casinoContract.getPromoCodeUser( 49 | userId, 50 | promoCodeName, 51 | PROMO_CODE_DEFAULT_REF, 52 | ['CLAIMED', 'FINALIZED', 'EXPIRED', 'CANCELLED'] 53 | ); 54 | return result.length > 0; 55 | } 56 | 57 | exports.claimUserDeposit = async (userId, amount) => { 58 | return await casinoContract.claimDepositPromo(userId, amount); 59 | } 60 | 61 | exports.claimPromoCodeBonus = async (userId, promoCodeName) => { 62 | const res = await casinoContract.claimPromoCode(userId, promoCodeName); 63 | return { 64 | ...res, 65 | value: fromWei(res.value).toFixed(4), 66 | } 67 | } 68 | 69 | exports.withdraw = async (userId, promoCodeName, ref = PROMO_CODE_DEFAULT_REF) => { 70 | await casinoContract.withdrawBonusMoney(userId, promoCodeName, ref, process.env.REWARD_WALLET); 71 | }; -------------------------------------------------------------------------------- /services/ws-info-channel-service.js: -------------------------------------------------------------------------------- 1 | const cmcUtil = require('../util/cmc'); 2 | const amqp = require('../services/amqp-service'); 3 | const { agenda } = require('../util/agenda'); 4 | 5 | const INFO_CHANNEL_NAME = 'INFO_CHANNEL'; 6 | const INFO_KEY_PREFIX = `${INFO_CHANNEL_NAME}/`; 7 | 8 | let redisClient; 9 | 10 | const init = async (redis) => { 11 | try { 12 | redisClient = redis; 13 | console.log('ATTACH INFO CHANNEL JOBS'); 14 | await schedulePriceUpdate(); 15 | 16 | agenda.on("fail", (err, job) => { 17 | console.log(`Job ${job.attrs.name} failed with error: ${err.message}, Stack: ${err.stack}`); 18 | }); 19 | } catch (e) { 20 | throw new Error(e); 21 | } 22 | } 23 | 24 | const schedulePriceUpdate = async () => { 25 | agenda.define("schedulePriceUpdate", async () => { 26 | const PRICE_UPDATED_KEY = `${INFO_KEY_PREFIX}PRICE_UPDATED`; 27 | const res = await cmcUtil.getMarketPrice({ 28 | convertFrom: ['USD', 'EUR', 'BTC', 'LTC', 'ETH'], 29 | convertTo: 'WFAIR', 30 | amount: 1, 31 | }); 32 | 33 | // const test = { 34 | // "BTC": { 35 | // "price": 4.546456316270219e-7 36 | // }, 37 | // "ETH": { 38 | // "price": 0.000006137037292895983 39 | // }, 40 | // "EUR": { 41 | // "price": 0.016903690310012855 42 | // }, 43 | // "LTC": { 44 | // "price": 0.00014041518015643387 45 | // }, 46 | // "USD": { 47 | // "price": 0.01917550870368156 48 | // } 49 | // } 50 | 51 | const quote = res?.WFAIR?.quote || {}; 52 | const output = { 53 | 'EUR': quote?.EUR.price, 54 | 'USD': quote?.USD.price, 55 | 'BTC': quote?.BTC.price, 56 | 'ETH': quote?.ETH.price, 57 | 'LTC': quote?.LTC.price, 58 | _updatedAt: new Date().toUTCString() 59 | } 60 | 61 | // await redisClient.DEL(PRICE_UPDATED_KEY); 62 | await redisClient.hmset(PRICE_UPDATED_KEY, output, () => { }); 63 | 64 | amqp.send('api_info_events', 'event.price_updated', JSON.stringify({ 65 | to: 'API_INFO_CHANNEL', 66 | event: INFO_CHANNEL_NAME, 67 | producer: 'backend', 68 | data: { 69 | type: PRICE_UPDATED_KEY, 70 | data: output 71 | } 72 | })); 73 | }); 74 | 75 | let agendaInterval = "5 minutes"; 76 | 77 | if (process.env.NODE_ENV !== 'production') { 78 | agendaInterval = "4 hours"; 79 | } 80 | 81 | agenda.every(agendaInterval, "schedulePriceUpdate", null, { lockLifetime: 2 * 1000 * 60, skipImmediate: false }); 82 | } 83 | 84 | module.exports.init = init; 85 | -------------------------------------------------------------------------------- /util/auth.js: -------------------------------------------------------------------------------- 1 | const passport = require('passport'); 2 | const JWTstrategy = require('passport-jwt').Strategy; 3 | const ExtractJWT = require('passport-jwt').ExtractJwt; 4 | // Import User Service 5 | const userService = require('../services/user-service'); 6 | const { isUserBanned } = require('../util/user'); 7 | const { BannedError } = require('../util/error-handler'); 8 | 9 | exports.setPassportStrategies = () => { 10 | passport.use( 11 | 'jwt', 12 | new JWTstrategy( 13 | { 14 | secretOrKey: process.env.JWT_KEY, 15 | jwtFromRequest: ExtractJWT.fromAuthHeaderAsBearerToken(), 16 | }, 17 | async (token, done) => { 18 | try { 19 | const user = await userService.getUserById(token.userId); 20 | if (isUserBanned(user)) { 21 | throw new BannedError(user); 22 | } 23 | return done(null, user); 24 | } catch (error) { 25 | done(error); 26 | } 27 | } 28 | ) 29 | ); 30 | passport.use( 31 | 'jwt_admin', 32 | new JWTstrategy( 33 | { 34 | secretOrKey: process.env.JWT_KEY, 35 | jwtFromRequest: ExtractJWT.fromAuthHeaderAsBearerToken(), 36 | }, 37 | async (token, done) => { 38 | try { 39 | let user = await userService.getUserById(token.userId); 40 | if (!user.admin) { 41 | user = undefined; 42 | } 43 | return done(null, user); 44 | } catch (error) { 45 | done(error); 46 | } 47 | } 48 | ) 49 | ); 50 | }; 51 | 52 | /** 53 | * Adds req.isAdmin that indicates if the logged in user 54 | * add user id to req._userId for api_logs 55 | * is an admin 56 | */ 57 | exports.evaluateIsAdmin = (req, res, next) => { 58 | return passport.authenticate('jwt', { session: false }, function (err, user) { 59 | if (err) { 60 | console.log(err); 61 | } 62 | req.isAdmin = !err && user && user.admin; 63 | req._userId = user?._id?.toString(); 64 | next(); 65 | })(req, res, next); 66 | }; 67 | 68 | /** 69 | * Returns if the current logged in user is allowed to perform an action on a userId 70 | * provided in the request querystring or body 71 | * @param {} req an http request 72 | * @param {} userPropName optional name of property to look for 73 | */ 74 | exports.isUserAdminOrSelf = (req, userPropName = 'userId') => { 75 | if (req.isAdmin) return true; 76 | const actionableUserId = 77 | req.param[userPropName] || req.query[userPropName] || req.query['user-id']; 78 | return req.user?.id?.toString() === actionableUserId?.toString(); 79 | }; 80 | -------------------------------------------------------------------------------- /services/youtube-category-service.js: -------------------------------------------------------------------------------- 1 | const { google } = require('googleapis'); 2 | const logger = require('../util/logger').default; 3 | 4 | const ytApi = google.youtube({ 5 | version: 'v3', 6 | auth: process.env.GOOGLE_API_KEY, 7 | }); 8 | 9 | /** 10 | * Gets a category based on the YouTube category ID given. 11 | * @param {String} categoryId 12 | * @returns {Object} 13 | */ 14 | const getYoutubeCategoryById = async (/** @type string */ categoryId) => { 15 | try { 16 | if (!categoryId) throw new Error('No proper "categoryId" given'); 17 | 18 | const response = await ytApi.videoCategories.list({ 19 | part: 'snippet', 20 | id: categoryId, 21 | }); 22 | 23 | return response?.data?.items?.[0] || undefined; 24 | } catch (err) { 25 | logger.error(err); 26 | return undefined; 27 | } 28 | }; 29 | 30 | 31 | module.exports = { 32 | getYoutubeCategoryById 33 | }; 34 | 35 | /* 36 | { 37 | config: { 38 | url: 'https://youtube.googleapis.com/youtube/v3/videoCategories?part=snippet&id=17&key=aaaa-aaaaa', 39 | method: 'GET', 40 | userAgentDirectives: [ [Object] ], 41 | paramsSerializer: [Function (anonymous)], 42 | headers: { 43 | 'x-goog-api-client': 'gdcl/5.0.5 gl-node/14.17.3 auth/7.9.2', 44 | 'Accept-Encoding': 'gzip', 45 | 'User-Agent': 'google-api-nodejs-client/5.0.5 (gzip)', 46 | Accept: 'application/json' 47 | }, 48 | params: { 49 | part: 'snippet', 50 | id: '17', 51 | key: 'aaaaa-aaaaa' 52 | }, 53 | validateStatus: [Function (anonymous)], 54 | retry: true, 55 | responseType: 'json' 56 | }, 57 | data: { 58 | kind: 'youtube#videoCategoryListResponse', 59 | etag: 'UXjAvCu4TOQHemMFvhzgu-oQobY', 60 | items: [ [Object] ] 61 | }, 62 | headers: { 63 | 'alt-svc': 'h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-T051=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"', 64 | 'cache-control': 'private', 65 | connection: 'close', 66 | 'content-encoding': 'gzip', 67 | 'content-type': 'application/json; charset=UTF-8', 68 | date: 'Wed, 22 Sep 2021 14:41:23 GMT', 69 | server: 'scaffolding on HTTPServer2', 70 | 'transfer-encoding': 'chunked', 71 | vary: 'Origin, X-Origin, Referer', 72 | 'x-content-type-options': 'nosniff', 73 | 'x-frame-options': 'SAMEORIGIN', 74 | 'x-xss-protection': '0' 75 | }, 76 | status: 200, 77 | statusText: 'OK', 78 | request: { 79 | responseURL: 'https://youtube.googleapis.com/youtube/v3/videoCategories?part=snippet&id=17&key=aaaa-aaaa' 80 | } 81 | } 82 | */ 83 | -------------------------------------------------------------------------------- /services/notification-events-service.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const { UniversalEvent } = require('@wallfair.io/wallfair-commons').models; 3 | 4 | const betsCategory = [ 5 | 'Notification/EVENT_ONLINE', 6 | 'Notification/EVENT_OFFLINE', 7 | 'Notification/EVENT_NEW', 8 | 'Notification/EVENT_NEW_BET', 9 | 'Notification/EVENT_BET_UPCOMING', 10 | 'Notification/EVENT_BET_ACTIVE', 11 | 'Notification/EVENT_BET_PLACED', 12 | 'Notification/EVENT_BET_CASHED_OUT', 13 | 'Notification/EVENT_BET_WAITING_RESOLUTION', 14 | 'Notification/EVENT_BET_RESOLVED', 15 | 'Notification/EVENT_BET_CLOSED', 16 | 'Notification/EVENT_BET_DISPUTED', 17 | 'Notification/EVENT_BET_CANCELED', 18 | 'Notification/EVENT_BET_EVALUATED', 19 | 'Notification/EVENT_USER_REWARD' //its only for bet_resolve reward, so should be in bets category 20 | ] 21 | 22 | const usersCategory = [ 23 | `Notification/EVENT_USER_SIGNED_UP`, 24 | 'Notification/EVENT_USER_UPLOADED_PICTURE', 25 | 'Notification/EVENT_USER_CHANGED_USERNAME', 26 | 'Notification/EVENT_USER_CHANGED_NAME', 27 | 'Notification/EVENT_USER_CHANGED_ABOUT_ME' 28 | ] 29 | 30 | const gameCategory = [ 31 | 'Casino/CASINO_PLACE_BET', 32 | 'Casino/CASINO_CASHOUT', 33 | 'Casino/EVENT_CASINO_LOST' 34 | ] 35 | 36 | const categories = { 37 | 'all': [...betsCategory, ...usersCategory, ...gameCategory], 38 | 'bets': betsCategory, 39 | 'users': usersCategory, 40 | 'game': gameCategory 41 | } 42 | 43 | exports.listNotificationEvents = async (limit = 10, cat, gameId) => { 44 | let selectedCat = _.get(categories, cat, []); 45 | 46 | if (!cat) { 47 | selectedCat = categories.all; 48 | } 49 | 50 | if (cat === 'game' && gameId) { 51 | return UniversalEvent.find({ 'data.gameTypeId': gameId }).where('type').in(selectedCat).sort('-createdAt').limit(+limit); 52 | } 53 | 54 | return UniversalEvent.find({}).where('type').in(selectedCat).sort('-createdAt').limit(+limit); 55 | } 56 | 57 | exports.listNotificationEventsByBet = async (limit = 10, betId) => { 58 | let selectedCat = _.get(categories, "bets", []); 59 | 60 | return await UniversalEvent.find({ 61 | 'data.bet.id': betId 62 | }).where('type').in(selectedCat).sort('-createdAt').limit(+limit); 63 | } 64 | 65 | exports.listNotificationEventsByUser = async (limit = 10, userId) => { 66 | let selectedCat = _.get(categories, "users", []); 67 | 68 | return await UniversalEvent.find({ 69 | 'performedBy': 'user', 70 | 'userId': userId 71 | }).where('type').in(selectedCat).sort('-createdAt').limit(+limit); 72 | } 73 | 74 | exports.updateUserData = async (filter, data) => { 75 | return UniversalEvent.updateMany( 76 | filter, 77 | { 78 | $set: data, 79 | }, 80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wallfair-playmoney-backend", 3 | "description": "Blockchain meets Prediction Markets made Simple.", 4 | "version": "2.0.6", 5 | "engines": { 6 | "node": "14.15.0" 7 | }, 8 | "main": "index.js", 9 | "scripts": { 10 | "start": "node index.js", 11 | "debug": "node --inspect-brk index.js", 12 | "server": "nodemon index.js", 13 | "share": "ngrok http 8000 -host-header=\"localhost:8000\"", 14 | "test": "npm run test:unit", 15 | "lint": "eslint --max-warnings=-1 --cache --c .eslintrc.js --ext .js,.json '.'", 16 | "lint:fix": "eslint --cache --c .eslintrc.js --ext .js,.json --fix '.'", 17 | "test:unit": "cross-env TS_NODE_PROJECT='./tsconfig.mocha.json' mocha --sort" 18 | }, 19 | "author": "", 20 | "license": "ISC", 21 | "dependencies": { 22 | "@aws-sdk/client-s3": "^3.35.0", 23 | "@aws-sdk/s3-request-presigner": "^3.35.0", 24 | "@sendgrid/mail": "^7.4.7", 25 | "@wallfair.io/trading-engine": "^0.1.67", 26 | "@wallfair.io/wallfair-casino": "0.1.96", 27 | "@wallfair.io/wallfair-commons": "1.8.25", 28 | "agenda": "^4.2.1", 29 | "amqplib": "^0.8.0", 30 | "axios": "^0.21.4", 31 | "bcrypt": "^5.0.1", 32 | "bcryptjs": "^2.4.3", 33 | "cors": "^2.8.5", 34 | "crypto": "^1.0.1", 35 | "crypto-js": "^4.1.1", 36 | "dotenv": "^8.2.0", 37 | "ethers": "^5.5.1", 38 | "express": "^4.17.1", 39 | "express-formidable": "^1.2.0", 40 | "express-jwt": "^6.1.0", 41 | "express-jwt-authz": "^2.4.1", 42 | "express-session": "^1.17.2", 43 | "express-validator": "^6.12.1", 44 | "flat": "^5.0.2", 45 | "generate-password": "^1.6.1", 46 | "google-auth-library": "^7.9.2", 47 | "googleapis": "^86.1.0", 48 | "helmet": "^4.5.0", 49 | "http": "0.0.1-security", 50 | "js-big-decimal": "^1.3.4", 51 | "jsonwebtoken": "^8.5.1", 52 | "jwks-rsa": "^2.0.4", 53 | "lodash": "^4.17.21", 54 | "lodash.pick": "^4.4.0", 55 | "mailchimp-api-v3": "^1.15.0", 56 | "moment": "^2.29.1", 57 | "mongodb": "^4.1.2", 58 | "mongoose": "^5.13.9", 59 | "multer": "^1.4.4", 60 | "nodemailer": "^6.6.3", 61 | "nodemailer-smtp-transport": "^2.7.4", 62 | "passport": "^0.4.1", 63 | "passport-jwt": "^4.0.0", 64 | "passport-local": "^1.0.0", 65 | "pg-format": "^1.0.4", 66 | "redis": "3.1.2", 67 | "slugify": "^1.6.0", 68 | "tslib": "^2.3.1", 69 | "twilio": "^3.67.2" 70 | }, 71 | "devDependencies": { 72 | "babel-eslint": "^10.1.0", 73 | "chai": "^4.3.4", 74 | "cross-env": "^7.0.3", 75 | "eslint": "^7.32.0", 76 | "eslint-plugin-import": "^2.24.2", 77 | "mocha": "^9.1.1", 78 | "nodemon": "^2.0.12", 79 | "prettier": "2.3.2" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /util/promo-codes-migration.js: -------------------------------------------------------------------------------- 1 | const dotenv = require('dotenv'); 2 | dotenv.config(); 3 | 4 | const mongoose = require('mongoose'); 5 | const wallfair = require('@wallfair.io/wallfair-commons'); 6 | const { Query, initDb, toWei } = require('@wallfair.io/trading-engine'); 7 | const { PROMO_CODE_DEFAULT_REF } = require('./constants'); 8 | 9 | async function connectMongoDB() { 10 | const connection = await mongoose.connect(process.env.DB_CONNECTION, { 11 | useUnifiedTopology: true, 12 | useNewUrlParser: true, 13 | useFindAndModify: false, 14 | useCreateIndex: true, 15 | readPreference: 'primary', 16 | retryWrites: true, 17 | }); 18 | console.log('Connection to Mongo-DB successful'); 19 | 20 | wallfair.initModels(connection); 21 | console.log('Mongoose models initialized'); 22 | 23 | return connection; 24 | } 25 | 26 | const BONUSES = [ 27 | { 28 | name: 'FIRST_DEPOSIT_DOUBLE_DEC21', 29 | value: 100_000, 30 | expires_at: new Date(2023, 1, 1).toISOString(), 31 | }, 32 | { 33 | name: 'LAUNCH_1k_500', 34 | value: 500, 35 | expires_at: new Date().toISOString(), 36 | }, 37 | { 38 | name: 'SURVEY_20220112', 39 | value: 450, 40 | expires_at: new Date().toISOString(), 41 | }, 42 | { 43 | name: 'FIRST_DEPOSIT_450', 44 | value: 450, 45 | expires_at: new Date().toISOString(), 46 | }, 47 | { 48 | name: 'EMAIL_CONFIRM_50', 49 | value: 50, 50 | expires_at: new Date().toISOString(), 51 | } 52 | ]; 53 | 54 | const doMigration = async () => { 55 | await connectMongoDB(); 56 | await initDb(); 57 | 58 | const queryRunner = new Query(); 59 | 60 | console.log('Inserting promo codes...'); 61 | 62 | for (const bonus of BONUSES) { 63 | await queryRunner.query(` 64 | INSERT INTO promo_code(name, type, value, count, expires_at) 65 | VALUES ($1, 'BONUS', $2, 1, $3) 66 | ON CONFLICT(name) DO NOTHING; 67 | `, [bonus.name, toWei(bonus.value).toString(), bonus.expires_at]); 68 | } 69 | 70 | const users = await wallfair.models.User.find( 71 | { bonus: { $exists: true } } 72 | ).select({ bonus: 1 }); 73 | 74 | if (!users?.length) throw new Error('No users found!'); 75 | 76 | console.log(`Inserting promo codes for ${users.length} users...`); 77 | 78 | for (const user of users) { 79 | for (const b of user.bonus) { 80 | await queryRunner.query(` 81 | INSERT INTO promo_code_user(user_id, promo_code_id, ref_id, status, count, usage) 82 | SELECT $1, pc.id, $2, 'FINALIZED', 1, 1 83 | FROM promo_code pc 84 | WHERE name = $3 85 | ON CONFLICT(user_id, promo_code_id, ref_id) DO NOTHING; 86 | `, [user._id.toString(), PROMO_CODE_DEFAULT_REF, b.name]); 87 | } 88 | } 89 | 90 | console.log('Migration done!'); 91 | } 92 | 93 | (async () => { 94 | try { 95 | await doMigration(); 96 | } catch (e) { 97 | console.error('Migration script failed: ', e.message); 98 | } finally { 99 | process.exit(); 100 | } 101 | })(); 102 | -------------------------------------------------------------------------------- /jobs/youtube-live-check-job.js: -------------------------------------------------------------------------------- 1 | // Import Event model 2 | // const { URL } = require('url'); 3 | // const _ = require('lodash'); 4 | // const { Event } = require('@wallfair.io/wallfair-commons').models; 5 | // const logger = require('../util/logger'); 6 | // 7 | // const { getVideosById } = require('../services/youtube-service'); 8 | 9 | // const checkYoutubeVideos = async () => { 10 | // const session = await Event.startSession(); 11 | // try { 12 | // await session.withTransaction(async () => { 13 | // const eventsList = await Event.find( 14 | // // check which query performs better under heavy load 15 | // // {type: "streamed", $or: [{"metadata": {$exists: false}}, {"metadata.twitch_subscribed": {$exists: false}}]} 16 | // { 17 | // type: 'streamed', 18 | // streamUrl: { $regex: 'youtube.com', $options: 'i' }, 19 | // $or: [ 20 | // { 'metadata.youtube_last_synced': { $exists: false } }, 21 | // { 'metadata.youtube_last_synced': { $exists: true } } 22 | // ] 23 | // }, null, { 24 | // sort: { 'metadata.youtube_last_synced': -1 } 25 | // } 26 | // ) 27 | // .limit(10) 28 | // .exec(); 29 | // 30 | // const videosIdsToQuery = []; 31 | // 32 | // _.each(eventsList, (checkEvent) => { 33 | // const { streamUrl } = checkEvent; 34 | // const parsedUrl = new URL(streamUrl); 35 | // const urlParams = parsedUrl.searchParams; 36 | // const videoId = urlParams.get('v'); 37 | // 38 | // videosIdsToQuery.push({ 39 | // _id: _.get(checkEvent, '_id'), 40 | // videoId 41 | // }) 42 | // }); 43 | // 44 | // if (videosIdsToQuery.length == 0) { 45 | // return; 46 | // } 47 | // 48 | // const params = videosIdsToQuery.map(a => a.videoId); 49 | // 50 | // const checkVideosState = await getVideosById(params, true).catch((err) => { 51 | // logger.error(err); 52 | // }); 53 | // 54 | // for (const checkEvent of eventsList) { 55 | // const currentVideoId = _.get(_.find(videosIdsToQuery, { _id: checkEvent._id }), 'videoId'); 56 | // const findVideoResponse = _.find(checkVideosState, { id: currentVideoId }); 57 | // const channelId = _.get(findVideoResponse, 'snippet.channelId'); 58 | // const liveBroadcastingContent = _.get(findVideoResponse, 'snippet.liveBroadcastContent', 'none'); 59 | // 60 | // const isLive = liveBroadcastingContent === "live" ? true : false; 61 | // 62 | // if (isLive) { 63 | // checkEvent.state = "online"; 64 | // } else { 65 | // checkEvent.state = "offline"; 66 | // } 67 | // 68 | // _.set(checkEvent, 'metadata.youtube_last_synced', Date.now()) 69 | // _.set(checkEvent, 'metadata.youtube_channel_id', channelId) 70 | // 71 | // await checkEvent.save(); 72 | // } 73 | // }); 74 | // } catch (err) { 75 | // console.log(err); 76 | // } finally { 77 | // await session.endSession(); 78 | // } 79 | // }; 80 | 81 | const initYoutubeCheckJob = () => { 82 | // only start the service if the env var is set 83 | if (process.env.GOOGLE_API_KEY) { 84 | // setInterval(checkYoutubeVideos, 1000 * 60 * 5); // check every 5 min 85 | } 86 | }; 87 | 88 | exports.initYoutubeCheckJob = initYoutubeCheckJob; 89 | -------------------------------------------------------------------------------- /services/amqp-service.js: -------------------------------------------------------------------------------- 1 | const amqplib = require("amqplib"); 2 | 3 | const retry = require('../util/retryHandler'); 4 | const { PROCESSORS } = require("../services/subscribers-service"); 5 | 6 | const rabbitUrl = process.env.RABBITMQ_CONNECTION; 7 | 8 | let connection, channel; 9 | 10 | const SUBSCRIBERS = [ 11 | { 12 | exchange: 'universal_events', 13 | exchangeType: 'topic', 14 | queue: 'universal_events.backend', 15 | routingKeys: ['event.deposit_created', 'event.webhook_triggered', 'event.withdraw_requested'], 16 | durable: true, 17 | autoDelete: false, 18 | prefetch: 50 19 | }, 20 | { 21 | exchange: 'cron_jobs', 22 | exchangeType: 'topic', 23 | queue: 'cron_jobs.backend', 24 | routingKeys: ['backend.promo_code_expiration'], 25 | durable: true, 26 | autoDelete: false, 27 | prefetch: 50 28 | }, 29 | ]; 30 | 31 | const ROUTING_MAPPING = { 32 | ['event.deposit_created']: PROCESSORS.deposit, 33 | ['event.webhook_triggered']: PROCESSORS.deposit, 34 | ['event.withdraw_requested']: PROCESSORS.withdraw, 35 | ['backend.promo_code_expiration']: PROCESSORS.promoCodesExpiration, 36 | }; 37 | 38 | const init = async () => { 39 | connection = await amqplib.connect(rabbitUrl, { 40 | heartbeat: 60, 41 | noDelay: true, 42 | }); 43 | channel = await connection.createChannel(); 44 | }; 45 | 46 | const send = async (exchange, routingKey, data, options) => { 47 | try { 48 | await channel.assertExchange(exchange, "topic", { durable: true }); 49 | channel.publish(exchange, routingKey, Buffer.from(data), options); 50 | console.log("PUBLISH %s - %s", exchange, routingKey); 51 | } catch (e) { 52 | console.error("Error in publishing message", e); 53 | } 54 | }; 55 | 56 | const subscribe = async () => { 57 | try { 58 | SUBSCRIBERS.forEach(async (cfg) => { 59 | channel.prefetch(cfg.prefetch); 60 | await channel.assertExchange(cfg.exchange, cfg.exchangeType, { 61 | durable: cfg.durable, 62 | }); 63 | const q = await channel.assertQueue(cfg.queue, { 64 | durable: cfg.durable, 65 | autoDelete: cfg.autoDelete 66 | }); 67 | 68 | cfg.routingKeys.forEach(async (routingKey) => { 69 | await channel.bindQueue(q.queue, cfg.exchange, routingKey); 70 | }); 71 | 72 | channel.consume( 73 | q.queue, 74 | async (msg) => { 75 | const routingKey = msg.fields.routingKey; 76 | const content = JSON.parse(msg.content.toString()); 77 | const processor = ROUTING_MAPPING[routingKey]; 78 | 79 | try { 80 | if (processor && !processor.running) { 81 | await processor.call(routingKey, content); 82 | } 83 | } catch (e) { 84 | console.error(`${routingKey} failed`, e.message); 85 | retry(processor.call, [routingKey, content]); 86 | } 87 | }, 88 | { 89 | noAck: true 90 | } 91 | ); 92 | 93 | console.info(new Date(), `[*] rabbitMQ: "${cfg.exchange}" exchange on [${cfg.routingKeys.join(', ')}] routing keys subscribed. Waiting for messages...`); 94 | }); 95 | } catch (e) { 96 | console.error("subscribe error", e); 97 | } 98 | }; 99 | 100 | module.exports = { init, send, subscribe }; 101 | -------------------------------------------------------------------------------- /routes/webhooks/twitch-webhook.js: -------------------------------------------------------------------------------- 1 | // Import the express Router to create routes 2 | const router = require('express').Router(); 3 | const { notificationEvents } = require('@wallfair.io/wallfair-commons/constants/eventTypes'); 4 | const amqp = require('../../services/amqp-service'); 5 | const { removeSubscription } = require('../../services/twitch-service'); 6 | 7 | // Import Event model 8 | const { Event } = require('@wallfair.io/wallfair-commons').models; 9 | 10 | router.post('/', async (req, res) => { 11 | console.log(new Date(), 'TWITCH_MESSAGE', JSON.stringify(req.body)); 12 | 13 | // handle twitch challenges 14 | if (req.header('Twitch-Eventsub-Message-Type') === 'webhook_callback_verification') { 15 | const type = req.header('Twitch-Eventsub-Subscription-Type'); 16 | const { broadcaster_user_id } = req.body.subscription.condition; 17 | 18 | const session = await Event.startSession(); 19 | try { 20 | await session.withTransaction(async () => { 21 | const event = await Event.findOne({ 'metadata.twitch_id': broadcaster_user_id }).exec(); 22 | 23 | if (!event) { 24 | removeSubscription(req.body.subscription.id); 25 | throw Error(`Event with broadcaster_user_id:${broadcaster_user_id} does not exist`); 26 | } 27 | 28 | if (type == 'stream.online') { 29 | event.metadata.twitch_subscribed_online = 'true'; 30 | await event.save(); 31 | } else if (type == 'stream.offline') { 32 | event.metadata.twitch_subscribed_offline = 'true'; 33 | await event.save(); 34 | } 35 | }); 36 | } catch (err) { 37 | console.log('Twitch webhook challenge error', err); 38 | } finally { 39 | await session.endSession(); 40 | } 41 | 42 | res.send(req.body.challenge); 43 | } 44 | 45 | // handle twitch events 46 | if (req.header('Twitch-Eventsub-Message-Type') === 'notification') { 47 | console.log('TWITCH_NOTIFICATION', JSON.stringify(req.body)); 48 | 49 | const type = req.header('Twitch-Eventsub-Subscription-Type'); 50 | const { broadcaster_user_id } = req.body.subscription.condition; 51 | 52 | const session = await Event.startSession(); 53 | try { 54 | await session.withTransaction(async () => { 55 | const event = await Event.findOne({ 'metadata.twitch_id': broadcaster_user_id }).exec(); 56 | 57 | if (!event) { 58 | removeSubscription(req.body.subscription.id); 59 | throw Error(`Event with broadcaster_user_id:${broadcaster_user_id} does not exist`); 60 | } 61 | 62 | if (!['stream.online', 'stream.offline'].includes(type)) return; 63 | 64 | event.state = type === 'stream.online' ? 'online' : 'offline'; 65 | await event.save(); 66 | 67 | amqp.send('universal_events', 'event.stream_status', JSON.stringify({ 68 | event: type === 'stream.online' ? notificationEvents.EVENT_ONLINE : notificationEvents.EVENT_OFFLINE, 69 | producer: 'system', 70 | producerId: 'notification-service', 71 | data: { event }, 72 | date: Date.now(), 73 | broadcast: true 74 | })); 75 | }); 76 | } catch (err) { 77 | console.log('Twitch webhook event error', err); 78 | } finally { 79 | await session.endSession(); 80 | } 81 | 82 | res.sendStatus(200); 83 | } 84 | }); 85 | 86 | module.exports = router; 87 | -------------------------------------------------------------------------------- /routes/users/secure-users-routes.js: -------------------------------------------------------------------------------- 1 | // Import the express Router to create routes 2 | const router = require('express').Router(); 3 | 4 | // Imports from express validator to validate user input 5 | const { check, oneOf } = require('express-validator'); 6 | 7 | // Import User Controller 8 | const userController = require('../../controllers/users-controller'); 9 | 10 | router.post( 11 | '/saveAdditionalInformation', 12 | oneOf([ 13 | [ 14 | check('name').notEmpty(), 15 | check('username').notEmpty(), 16 | check('username').isLength({ min: 3, max: 25 }), 17 | check('name').isLength({ min: 3 }), 18 | ], 19 | check('email').isEmail(), 20 | ]), 21 | userController.saveAdditionalInformation 22 | ); 23 | 24 | router.post( 25 | '/acceptConditions', 26 | [check('conditions').isArray({ min: 3, max: 3 })], 27 | userController.saveAcceptConditions 28 | ); 29 | 30 | router.get('/refList', userController.getRefList); 31 | 32 | router.get('/history', userController.getHistory); 33 | 34 | router.patch( 35 | '/:userId', 36 | oneOf([[check('username').isLength({ min: 3, max: 25 })]]), 37 | userController.updateUser 38 | ); 39 | 40 | router.patch( 41 | '/:userId/preferences', 42 | [check('preferences').notEmpty()], 43 | userController.updateUserPreferences 44 | ); 45 | 46 | router.get('/:userId', userController.getUserInfo); 47 | 48 | router.get('/wallet/transactions', userController.getUserTransactions); 49 | 50 | router.post('/buy-with-crypto', userController.buyWithCrypto); 51 | router.post('/buy-with-fiat', userController.buyWithFiat); 52 | router.post('/consent', userController.updateUserConsent); 53 | 54 | router.post( 55 | '/cryptopay/channel', 56 | [ 57 | check('currency') 58 | .isIn(['BTC', 'ETH', 'LTC', 'USDT', 'USDC', 'DAI', 'XRP']) 59 | ], 60 | userController.cryptoPayChannel 61 | ); 62 | 63 | router.post( 64 | '/moonpay/url', 65 | [ 66 | check('amount').isNumeric(), 67 | check('currency').isIn(['EUR', 'USD']), 68 | ], 69 | userController.generateMoonpayUrl 70 | ) 71 | 72 | router.post( 73 | '/:userId/ban', 74 | [check('reactivateOn').notEmpty(), check('description').isString()], 75 | userController.banUser 76 | ); 77 | 78 | router.post( 79 | '/:userId/update-role', 80 | [check('role').notEmpty()], 81 | userController.updateRole 82 | ) 83 | 84 | router.get('/promo-codes/all', userController.getUserPromoCodes); 85 | 86 | router.get('/promo-codes/deposit', userController.getDepositPromoCodes); 87 | 88 | router.post( 89 | '/promo-codes', 90 | [check('promoCode').notEmpty()], 91 | userController.claimPromoCode 92 | ); 93 | 94 | router.post( 95 | '/promo-codes/deposit', 96 | [check('promoCode').notEmpty()], 97 | userController.claimDepositBonus 98 | ); 99 | 100 | router.post( 101 | '/promo-codes/withdraw', 102 | [check('promoCode').notEmpty()], 103 | userController.withdrawPromoCode 104 | ); 105 | 106 | router.patch('/promo-codes/:name', userController.cancelPromoCode); 107 | 108 | router.post('/tokens', userController.claimTokens); 109 | 110 | router.post('/upload-image', userController.uploadImage); 111 | 112 | router.post( 113 | '/deposits', 114 | [ 115 | check('networkCode').notEmpty(), 116 | check('hash').notEmpty(), 117 | ], 118 | userController.deposit 119 | ); 120 | 121 | module.exports = router; 122 | -------------------------------------------------------------------------------- /__tests__/stats/fn.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | const mongoose = require("mongoose"); 3 | const wallfair = require('@wallfair.io/wallfair-commons'); 4 | 5 | const dotenv = require('dotenv'); 6 | dotenv.config(); 7 | 8 | let mongoURL = process.env.DB_CONNECTION; 9 | 10 | describe.skip('testing some statistics methods', () => { 11 | before(async () => { 12 | const connection = await mongoose.connect(mongoURL, { 13 | useUnifiedTopology: true, 14 | useNewUrlParser: true, 15 | useFindAndModify: false, 16 | useCreateIndex: true, 17 | readPreference: 'primary', 18 | retryWrites: true, 19 | }); 20 | 21 | wallfair.initModels(connection); 22 | }) 23 | 24 | after(async () => { 25 | await mongoose.disconnect(); 26 | }); 27 | 28 | it('should get casino play game count per user', async () => { 29 | const {getCasinoGamePlayCount} = require('../../services/statistics-service') 30 | const data = await getCasinoGamePlayCount('616405198fca018cd9e233bf'); 31 | console.log("data", data); 32 | // expect(data).to.be.equal(9) 33 | }) 34 | 35 | it('should get casino cashed-out value per user', async () => { 36 | const {getCasinoGameCashoutCount} = require('../../services/statistics-service') 37 | const data = await getCasinoGameCashoutCount('616405198fca018cd9e233bf', '6166ec17b4c87de60914e143'); 38 | 39 | console.log("data", data); 40 | expect(data).to.be.equal(1) 41 | }) 42 | 43 | it('should get casino total amount won per user', async () => { 44 | const {getCasinoGamesAmountWon} = require('../../services/statistics-service') 45 | const data = await getCasinoGamesAmountWon('616405198fca018cd9e233bf', '6166ec17b4c87de60914e143'); 46 | 47 | console.log("data", data); 48 | // expect(data).to.deep.equal({ totalReward: 121, totalStaked: 100, totalWon: 21 }) 49 | }) 50 | 51 | it('should get casino total lost per user', async () => { 52 | const {getCasinoGamesAmountLost} = require('../../services/statistics-service') 53 | const data = await getCasinoGamesAmountLost('61682500817fa7f30fb70ca9', '614381d74f78686665a5bb76'); 54 | 55 | console.log("data", data); 56 | // expect(data).to.deep.equal({ totalReward: 121, totalStaked: 100, totalWon: 21 }) 57 | }) 58 | 59 | it('should get total bets per user', async () => { 60 | const {getUserBetsAmount} = require('../../services/statistics-service') 61 | const data = await getUserBetsAmount('616708bfc750391a69c974ba'); 62 | 63 | console.log("data", data); 64 | // expect(data).to.deep.equal({ 65 | // totalBettedAmount: 7090, 66 | // totalBets: 37, 67 | // totalOutcomeAmount: 13895.8514 68 | // }) 69 | }) 70 | 71 | 72 | it('should get total bets cashouts per user', async () => { 73 | const {getUserBetsCashouts} = require('../../services/statistics-service') 74 | const data = await getUserBetsCashouts('616708bfc750391a69c974ba'); 75 | 76 | console.log("data", data); 77 | // expect(data).to.deep.equal({ 78 | // totalBettedAmount: 7090, 79 | // totalBets: 37, 80 | // totalOutcomeAmount: 13895.8514 81 | // }) 82 | }) 83 | 84 | it('should get total bets rewards per user', async () => { 85 | const {getUserBetsRewards} = require('../../services/statistics-service') 86 | const data = await getUserBetsRewards('616708bfc750391a69c974ba'); 87 | 88 | console.log("data", data); 89 | // expect(data).to.deep.equal({ 90 | // totalBettedAmount: 7090, 91 | // totalBets: 37, 92 | // totalOutcomeAmount: 13895.8514 93 | // }) 94 | }) 95 | 96 | 97 | }) 98 | -------------------------------------------------------------------------------- /services/mail-service.js: -------------------------------------------------------------------------------- 1 | const sendGridMail = require('@sendgrid/mail'); 2 | sendGridMail.setApiKey(process.env.SENDGRID_API_KEY); 3 | 4 | const fs = require('fs'); 5 | const { generate } = require('../helper'); 6 | 7 | const email_confirm = fs.readFileSync('./emails/email-confirm.html', 'utf8'); 8 | const email_reset_password = fs.readFileSync('./emails/email-reset-password.html', 'utf8'); 9 | const email_buy_with_crypto = fs.readFileSync('./emails/buy-with-crypto.html', 'utf8'); 10 | const email_buy_with_fiat = fs.readFileSync('./emails/buy-with-fiat.html', 'utf8'); 11 | 12 | exports.sendConfirmMail = async (user) => { 13 | const emailCode = generate(6); 14 | const queryString = `?userId=${user._id}&code=${emailCode}`; 15 | const generatedTemplate = email_confirm 16 | .replace('{{username}}', user.username) 17 | .replace('{{query_string}}', queryString) 18 | .replace('{{verify_url}}', `${process.env.CLIENT_URL}/verify`); 19 | 20 | await sendMail(user.email, 'Thanks for signing up!', generatedTemplate); 21 | 22 | user.emailCode = emailCode; 23 | await user.save(); 24 | }; 25 | 26 | exports.sendPasswordResetMail = async (email, resetUrl) => { 27 | const generatedTemplate = email_reset_password 28 | .replace('{{resetPwUrl}}', resetUrl); 29 | 30 | await sendMail(email, 'Password reset', generatedTemplate); 31 | } 32 | 33 | exports.sendBuyWithCryptoEmail = async (data) => { 34 | const generatedTemplate = email_buy_with_crypto 35 | .replace('{{currency}}', data.currency) 36 | .replace('{{wallet}}', data.wallet) 37 | .replace('{{amount}}', data.amount) 38 | .replace('{{estimate}}', data.estimate) 39 | .replace('{{email}}', data.email) 40 | 41 | await sendMail('deposits@alpacasino.io', `${process.env.ENVIRONMENT} - Buy With Crypto Form`, generatedTemplate); 42 | } 43 | exports.sendBuyWithFiatEmail = async (data) => { 44 | const generatedTemplate = email_buy_with_fiat 45 | .replace('{{currency}}', data.currency) 46 | .replace('{{amount}}', data.amount) 47 | .replace('{{estimate}}', data.estimate) 48 | .replace('{{email}}', data.email) 49 | .replace('{{userid}}', data.userId) 50 | 51 | await sendMail('deposits@alpacasino.io', `${process.env.ENVIRONMENT} - Buy with Fiat Request`, generatedTemplate); 52 | } 53 | 54 | /*** 55 | * 56 | * @param email 57 | * @param subject 58 | * @param template 59 | * @param attachments we can target this in email using 'src="cid:imagecid"' 60 | * example attachments [{ 61 | filename: "image.png", 62 | content: base64, 63 | content_id: "imagecid", 64 | }] 65 | * @returns {Promise} 66 | */ 67 | const sendMail = async (email, subject, template, attachments = []) => { 68 | try { 69 | const info = { 70 | to: email, 71 | from: 'no-reply@wallfair.io', 72 | subject: subject, 73 | html: template, 74 | attachments 75 | }; 76 | 77 | await sendGridMail.send(info); 78 | console.info('email sent successfully to: %s', email); 79 | } catch (err) { 80 | console.log(err); 81 | console.log('email sent failed to: %s', email); 82 | } 83 | }; 84 | 85 | const sendTextMail = async (email, subject, text) => { 86 | try { 87 | const info = { 88 | to: email, 89 | from: 'no-reply@wallfair.io', 90 | subject, 91 | text, 92 | }; 93 | 94 | await sendGridMail.send(info); 95 | console.info('email sent successfully to: %s', email); 96 | } catch (err) { 97 | console.log(err); 98 | console.log('email sent failed to: %s', email); 99 | } 100 | } 101 | 102 | exports.sendMail = sendMail; 103 | exports.sendTextMail = sendTextMail; 104 | -------------------------------------------------------------------------------- /services/chat-message-service.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const axios = require('axios'); 3 | const { ChatMessage } = require('@wallfair.io/wallfair-commons').models; 4 | const notificationsTypes = require('@wallfair.io/wallfair-commons').constants.events.notification; 5 | const { ForbiddenError, NotFoundError } = require('../util/error-handler'); 6 | 7 | const profanityReplacement = '*'; 8 | exports.profanityReplacement = profanityReplacement; 9 | 10 | exports.getChatMessagesByEvent = async (eventId) => ChatMessage.find({ roomId: eventId }); 11 | 12 | exports.getLatestChatMessagesByRoom = async (roomId, limit = 100, skip = 0) => 13 | ChatMessage.aggregate([ 14 | { 15 | $match: { roomId: roomId }, 16 | }, 17 | { $sort: { date: -1 } }, 18 | { 19 | $lookup: { 20 | localField: 'userId', 21 | from: 'users', 22 | foreignField: '_id', 23 | as: 'user', 24 | }, 25 | }, 26 | { 27 | $facet: { 28 | total: [ 29 | { 30 | $group: { 31 | _id: null, 32 | count: { $sum: 1 }, 33 | }, 34 | }, 35 | ], 36 | data: [ 37 | { $skip: skip }, 38 | { $limit: limit }, 39 | { 40 | $project: { 41 | userId: 1, 42 | roomId: 1, 43 | type: 1, 44 | message: 1, 45 | date: 1, 46 | user: { 47 | $let: { 48 | vars: { 49 | userMatch: { 50 | $arrayElemAt: ['$user', 0], 51 | }, 52 | }, 53 | in: { 54 | username: '$$userMatch.username', 55 | name: '$$userMatch.name', 56 | profilePicture: '$$userMatch.profilePicture', 57 | }, 58 | }, 59 | }, 60 | }, 61 | }, 62 | ], 63 | }, 64 | }, 65 | { $unwind: '$total' }, 66 | { 67 | $project: { 68 | total: '$total.count', 69 | data: '$data', 70 | }, 71 | }, 72 | ]) 73 | .exec() 74 | .then((items) => items[0]); 75 | 76 | 77 | async function replaceProfanity(text) { 78 | return axios.get('http://api1.webpurify.com/services/rest/', { 79 | params: { 80 | method: 'webpurify.live.replace', 81 | api_key: process.env.PROFANITY_FILTER_API_KEY, 82 | text, 83 | replacesymbol: profanityReplacement, 84 | format: 'json', 85 | lang: 'en,de,ru' // check english, german and russian 86 | }, 87 | }).then(x => x.data?.rsp.text); 88 | } 89 | 90 | async function profanityFilter(data) { 91 | if (!process.env.PROFANITY_FILTER_API_KEY) { 92 | return data; 93 | } 94 | 95 | const parsed = await replaceProfanity(data.message); 96 | if (parsed !== data.message) { 97 | console.debug(`Profanity filter. Replaced '${data.message}' with ${parsed}`); 98 | } 99 | return { 100 | ...data, 101 | message: parsed, 102 | }; 103 | } 104 | exports.profanityFilter = profanityFilter; 105 | 106 | exports.createChatMessage = async (data) => { 107 | const parsed = await profanityFilter(data); 108 | console.log('chatemessage create', parsed); 109 | return ChatMessage.create(parsed); 110 | }; 111 | 112 | exports.saveChatMessage = async (chatMessage) => chatMessage.save(); 113 | 114 | exports.getLatestChatMessagesByUserId = async (userId, limit = 100, skip = 0) => 115 | ChatMessage.aggregate([ 116 | { 117 | $match: { 118 | userId: mongoose.Types.ObjectId(userId), 119 | read: { $exists: false }, 120 | type: { $in: Object.values(notificationsTypes) }, 121 | }, 122 | }, 123 | { $sort: { date: -1 } }, 124 | { 125 | $facet: { 126 | total: [ 127 | { 128 | $group: { 129 | _id: null, 130 | count: { $sum: 1 }, 131 | }, 132 | }, 133 | ], 134 | data: [ 135 | { $skip: skip }, 136 | { $limit: limit }, 137 | { 138 | $project: { 139 | userId: 1, 140 | roomId: 1, 141 | type: 1, 142 | message: 1, 143 | date: 1, 144 | payload: 1, 145 | }, 146 | }, 147 | ], 148 | }, 149 | }, 150 | { $unwind: '$total' }, 151 | { 152 | $project: { 153 | total: '$total.count', 154 | data: '$data', 155 | }, 156 | }, 157 | ]) 158 | .exec() 159 | .then((items) => items[0]); 160 | 161 | exports.setMessageRead = async (messageId, requestingUser) => { 162 | const message = await ChatMessage.findById(messageId); 163 | if (!message) { 164 | throw new NotFoundError(); 165 | } 166 | if (!requestingUser?.admin && message.userId.toString() !== requestingUser._id.toString()) { 167 | throw new ForbiddenError(); 168 | } 169 | message.read = new Date(); 170 | await message.save(message); 171 | }; 172 | -------------------------------------------------------------------------------- /services/subscribers-service.js: -------------------------------------------------------------------------------- 1 | const { fromWei, WFAIR_SYMBOL, TransactionManager, AccountNamespace, Transactions, ExternalTransactionOriginator } = require("@wallfair.io/trading-engine"); 2 | const { notificationEvents } = require('@wallfair.io/wallfair-commons/constants/eventTypes'); 3 | const { sendMail } = require("../services/mail-service"); 4 | const fs = require("fs"); 5 | const { claimUserDeposit } = require("./promo-codes-service"); 6 | const emailDepositCreated = fs.readFileSync(__dirname + '/../emails/deposit-created.html', 'utf8'); 7 | const emailWithdrawRequested = fs.readFileSync(__dirname + '/../emails/withdraw-requested.html', 'utf8'); 8 | 9 | /* 10 | data example for notificationEvents.EVENT_DEPOSIT_CREATED 11 | 12 | const exampleDepositData = { 13 | event: 'event.deposit_created', 14 | data: { 15 | event: 'Transaction/DEPOSIT_CREATED', 16 | producer: 'system', 17 | producerId: 'deposit-worker', 18 | data: { 19 | originator: 'deposit', 20 | external_system: 'deposit', 21 | status: 'completed', 22 | transaction_hash: '0x99d99657118be95b40fce740e11f846d910ab0309f93d1828177bc7bf9bc437a', 23 | external_transaction_id: '0x99d99657118be95b40fce740e11f846d910ab0309f93d1828177bc7bf9bc437a', 24 | network_code: 'ETH', 25 | block_number: 28881415, 26 | sender: '0xAF22FF226c8D55aF403C76898aB50477bC2Bc764', 27 | receiver: '0x27f9D825274bA0c54D33373bE15eB512B833d7F3', 28 | amount: '2000000000000000000', 29 | symbol: 'WFAIR' 30 | }, 31 | date: 1640102863562, 32 | broadcast: false 33 | } 34 | }; 35 | 36 | */ 37 | 38 | const processDepositEvent = async (_, data) => { 39 | const eventName = data?.event; 40 | 41 | if ([notificationEvents.EVENT_DEPOSIT_CREATED, notificationEvents.EVENT_WEBHOOK_TRIGGERED].includes(eventName)) { 42 | const dd = data?.data; 43 | dd.symbol = WFAIR_SYMBOL; 44 | 45 | if (eventName === notificationEvents.EVENT_WEBHOOK_TRIGGERED && dd.status !== 'completed') { 46 | return; 47 | } 48 | 49 | const deposits = await new Transactions().getExternalTransactionLogs({ 50 | where: { 51 | internal_user_id: dd.internal_user_id, 52 | originator: ExternalTransactionOriginator.DEPOSIT 53 | } 54 | }); 55 | 56 | if (deposits.length === 1) { 57 | await claimUserDeposit(dd.internal_user_id, dd.amount) 58 | .catch((e) => console.log('DEPOSIT CLAIM: ', e.message)); 59 | } 60 | 61 | if (!process.env.DEPOSIT_NOTIFICATION_EMAIL) { 62 | console.log('DEPOSIT_NOTIFICATION_EMAIL is empty, skipping email notification for deposits...'); 63 | return; 64 | } 65 | 66 | const formattedAmount = fromWei(dd.amount).decimalPlaces(0); 67 | let emailHtml = emailDepositCreated; 68 | 69 | for (const entry in dd) { 70 | emailHtml = emailHtml.replace(`{{${entry}}}`, dd[entry]); 71 | } 72 | await sendMail( 73 | process.env.DEPOSIT_NOTIFICATION_EMAIL, 74 | `${notificationEvents.EVENT_DEPOSIT_CREATED} - ${process.env.ENVIRONMENT} - ${formattedAmount} ${dd.symbol}`, 75 | emailHtml 76 | ); 77 | } 78 | } 79 | 80 | const processWithdrawEvent = async (_, data) => { 81 | const eventName = data?.event; 82 | 83 | if (!process.env.DEPOSIT_NOTIFICATION_EMAIL) { 84 | console.log('DEPOSIT_NOTIFICATION_EMAIL is empty, skipping email notification for withdraws...'); 85 | return; 86 | } 87 | 88 | if (eventName === notificationEvents.EVENT_WITHDRAW_APPROVED) { 89 | const dd = data?.data; 90 | const formattedAmount = fromWei(dd.amount).decimalPlaces(0); 91 | let emailHtml = emailWithdrawRequested; 92 | 93 | for (const entry in dd) { 94 | emailHtml = emailHtml.replace(`{{${entry}}}`, dd[entry]); 95 | } 96 | await sendMail(process.env.DEPOSIT_NOTIFICATION_EMAIL, `${notificationEvents.EVENT_WITHDRAW_SCHEDULED} - ${process.env.ENVIRONMENT} - ${formattedAmount} ${dd.symbol}`, emailHtml); 97 | } 98 | } 99 | 100 | const checkPromoCodesExpiration = async () => { 101 | PROCESSORS.promoCodesExpiration.running = true; 102 | const transaction = new TransactionManager(); 103 | 104 | try { 105 | await transaction.startTransaction(); 106 | 107 | const result = await transaction.queryRunner.query(` 108 | UPDATE promo_code_user pcu 109 | SET status = 'EXPIRED' 110 | FROM promo_code pc 111 | WHERE pcu.promo_code_id = pc.id AND 112 | pcu.status = 'CLAIMED' AND 113 | (pcu.expires_at <= now() OR pc.expires_at <= now()) 114 | RETURNING *` 115 | ); 116 | const users = result[0].map(r => r.user_id); 117 | users.length > 0 && 118 | await transaction.wallet.burnAll(users, AccountNamespace.USR, 'BFAIR'); 119 | 120 | await transaction.commitTransaction(); 121 | 122 | console.log(new Date(), `${result[1]} user promo codes expired`); 123 | } catch (e) { 124 | console.error(e); 125 | await transaction.rollbackTransaction(); 126 | } 127 | 128 | PROCESSORS.promoCodesExpiration.running = false; 129 | }; 130 | 131 | const PROCESSORS = { 132 | deposit: { 133 | call: processDepositEvent, 134 | running: false, 135 | }, 136 | withdraw: { 137 | call: processWithdrawEvent, 138 | running: false, 139 | }, 140 | promoCodesExpiration: { 141 | call: checkPromoCodesExpiration, 142 | running: false, 143 | }, 144 | }; 145 | 146 | module.exports = { PROCESSORS } 147 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Import and configure dotenv to enable use of environmental variable 2 | const dotenv = require('dotenv'); 3 | 4 | dotenv.config(); 5 | 6 | // Import express 7 | const express = require('express'); 8 | const http = require('http'); 9 | 10 | // Import mongoose to connect to Database 11 | const mongoose = require('mongoose'); 12 | 13 | // Import Models from Wallfair Commons 14 | const wallfair = require('@wallfair.io/wallfair-commons'); 15 | const { handleError } = require('./util/error-handler'); 16 | 17 | const { initDb } = require('@wallfair.io/trading-engine'); 18 | // const { initDatabase } = require('@wallfair.io/wallfair-casino'); 19 | 20 | const { requestLogHandler } = require('./services/request-log-service'); 21 | 22 | let mongoURL = process.env.DB_CONNECTION; 23 | 24 | /** 25 | * CORS options 26 | * @type import('cors').CorsOptions 27 | */ 28 | const corsOptions = { 29 | origin: ["wallfair.io", 30 | /\.wallfair\.io$/, 31 | "alpacasino.io", 32 | "https://alpacasino.io", 33 | /\.alpacasino\.io$/, 34 | /\.ngrok\.io$/, 35 | /\.netlify\.app$/, 36 | /localhost:?.*$/m, 37 | ], 38 | credentials: true, 39 | allowedMethods: ['GET', 'PUT', 'POST', 'PATCH', 'DELETE'], 40 | allowedHeaders: [ 41 | 'Origin', 42 | 'X-Requested-With', 43 | 'Content-Type', 44 | 'Accept', 45 | 'X-Access-Token', 46 | 'Authorization', 47 | ], 48 | exposedHeaders: ['Content-Length'], 49 | preflightContinue: false, 50 | }; 51 | 52 | // Connection to Database 53 | async function connectMongoDB() { 54 | await mongoose.connect(mongoURL, { 55 | useUnifiedTopology: true, 56 | useNewUrlParser: true, 57 | useFindAndModify: false, 58 | useCreateIndex: true, 59 | readPreference: 'primary', 60 | retryWrites: true, 61 | }); 62 | console.log('Connection to Mongo-DB successful'); 63 | 64 | wallfair.initModels(mongoose); 65 | console.log('Mongoose models initialized'); 66 | 67 | return mongoose; 68 | } 69 | async function main() { 70 | const mongoDBConnection = await connectMongoDB(); 71 | 72 | // Initialize the postgres database (trading-engine) 73 | await initDb(); 74 | // await initDatabase(); 75 | 76 | const amqp = require('./services/amqp-service'); 77 | await amqp.init(); 78 | await amqp.subscribe(); 79 | 80 | //init redis connection 81 | const { createClient } = require('redis'); 82 | const redisClient = createClient({ 83 | url: process.env.REDIS_CONNECTION, 84 | no_ready_check: false 85 | }); 86 | redisClient.on('connect', () => console.log('::> Redis Client Connected')); 87 | redisClient.on('error', (err) => console.error('<:: Redis Client Error', err)); 88 | //init agenda 89 | const { agenda } = require('./util/agenda'); 90 | await agenda.start(); 91 | //init api-info-channel 92 | const wsInfoChannelService = require('./services/ws-info-channel-service'); 93 | await wsInfoChannelService.init(redisClient); 94 | 95 | // Import cors 96 | const cors = require('cors'); 97 | 98 | // Initialise server using express 99 | const server = express(); 100 | const httpServer = http.createServer(server); 101 | server.use(cors(corsOptions)); 102 | 103 | const awsS3Service = require('./services/aws-s3-service'); 104 | awsS3Service.init(); 105 | 106 | //(auto migration) convert roomId to string, when ObjectID 107 | const isObjectIdStillExist = await mongoDBConnection.models.ChatMessage.find( 108 | { roomId : { $type: "objectId" } } 109 | ); 110 | 111 | if(isObjectIdStillExist && isObjectIdStillExist.length) { 112 | await mongoDBConnection.models.ChatMessage.updateMany( 113 | { roomId : { $type: "objectId" } }, 114 | [{ $set: { roomId: { $toString: "$$CURRENT.roomId" } } }] 115 | ) 116 | } 117 | 118 | // Jwt verification 119 | const passport = require('passport'); 120 | const auth = require('./util/auth'); 121 | auth.setPassportStrategies(); 122 | server.use(passport.initialize()); 123 | server.use(passport.session()); 124 | server.use(auth.evaluateIsAdmin); 125 | server.use(express.json({ limit: '5mb' })); 126 | server.use(express.urlencoded({ limit: '5mb', extended: true })); 127 | 128 | // request log handler 129 | server.use(requestLogHandler); 130 | 131 | // Home Route 132 | server.get('/', (req, res) => { 133 | res.status(200).send({ 134 | message: 'Blockchain meets Prediction Markets made Simple. - Wallfair.', 135 | }); 136 | }); 137 | 138 | // Import Routes 139 | const userRoute = require('./routes/users/users-routes'); 140 | const secureRewardsRoutes = require('./routes/users/secure-rewards-routes'); 141 | const secureUserRoute = require('./routes/users/secure-users-routes'); 142 | const twitchWebhook = require('./routes/webhooks/twitch-webhook'); 143 | const chatRoutes = require('./routes/users/chat-routes'); 144 | const notificationEventsRoutes = require('./routes/users/notification-events-routes'); 145 | const authRoutes = require('./routes/auth/auth-routes'); 146 | const userMessagesRoutes = require('./routes/users/user-messages-routes'); 147 | const quoteRoutes = require('./routes/users/quote-routes'); 148 | const adminRoutes = require('./routes/users/admin-routes'); 149 | 150 | // Using Routes 151 | server.use('/api/user', userRoute); 152 | server.use('/api/user', passport.authenticate('jwt', { session: false }), secureUserRoute); 153 | server.use('/api/rewards', passport.authenticate('jwt', { session: false }), secureRewardsRoutes); 154 | server.use('/webhooks/twitch/', twitchWebhook); 155 | server.use('/api/chat', chatRoutes); 156 | server.use('/api/notification-events', notificationEventsRoutes); 157 | server.use('/api/auth', authRoutes); 158 | server.use( 159 | '/api/user-messages', 160 | passport.authenticate('jwt', { session: false }), 161 | userMessagesRoutes 162 | ); 163 | server.use('/api/quote', passport.authenticate('jwt', { session: false }), quoteRoutes); 164 | server.use('/api/admin', passport.authenticate('jwt_admin', { session: false }), adminRoutes); 165 | 166 | // Error handler middleware 167 | // eslint-disable-next-line no-unused-vars 168 | server.use((err, req, res, next) => { 169 | handleError(err, res); 170 | }); 171 | 172 | // Let server run and listen 173 | const appServer = httpServer.listen(process.env.PORT || 8000, () => { 174 | const { port } = appServer.address(); 175 | 176 | console.log(`API runs on port: ${port}`); 177 | }); 178 | } 179 | 180 | main(); 181 | -------------------------------------------------------------------------------- /services/youtube-service.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | const { google } = require('googleapis'); 3 | const logger = require('../util/logger'); 4 | 5 | const generateSlug = require('../util/generateSlug'); 6 | 7 | // Import Event model 8 | const { Event } = require('@wallfair.io/wallfair-commons').models; 9 | const ytCategoryService = require('./youtube-category-service') 10 | 11 | const ytApi = google.youtube({ 12 | version: 'v3', 13 | auth: process.env.GOOGLE_API_KEY, 14 | }); 15 | 16 | /** 17 | * Gets a list of videos based on the Ids given. 18 | * @param Array. videoId 19 | * @returns Object 20 | */ 21 | const getVideosById = async (/** @type string[] */ videoIds, all = false) => { 22 | try { 23 | if (!videoIds || !videoIds.length) throw new Error('No or empty array of "videoIds" given'); 24 | 25 | let response = await ytApi.videos.list({ 26 | part: ['snippet,contentDetails,player,recordingDetails,statistics,status,topicDetails'], 27 | id: videoIds, 28 | }); 29 | if(all) { 30 | return response?.data?.items || []; 31 | } 32 | return response?.data?.items?.[0] || undefined; 33 | } catch (err) { 34 | logger.error(err); 35 | return undefined; 36 | } 37 | }; 38 | 39 | /** 40 | * 41 | * @param {String} streamUrl 42 | * @param {String} category 43 | * @returns 44 | */ 45 | const getEventFromYoutubeUrl = async (streamUrl) => { 46 | const videoId = streamUrl.substring(streamUrl.lastIndexOf("v=")+2); 47 | 48 | const streamItem = await getVideosById(videoId); 49 | const ytCategory = (streamItem && streamItem.snippet && !!streamItem.snippet.categoryId) 50 | ? await ytCategoryService.getYoutubeCategoryById(streamItem.snippet.categoryId) 51 | : undefined; 52 | const slug = generateSlug(streamItem.snippet.channelTitle); 53 | 54 | let event = await Event.findOne({ streamUrl }).exec(); 55 | 56 | if (!event) { 57 | event = new Event({ 58 | name: streamItem.snippet.channelTitle, 59 | slug, 60 | streamUrl, 61 | category : ytCategory?.snippet?.title || '', 62 | type: "streamed", 63 | previewImageUrl: 64 | streamItem.snippet.thumbnails?.maxres.url || 65 | streamItem.snippet.thumbnails?.default.url || 66 | '', 67 | tags: streamItem.snippet.tags.map((tag) => ({ name: tag })), 68 | // TODO - We're not getting the real date of when the streamed event starts from the API. 69 | date: new Date(), 70 | }); 71 | await event.save(); 72 | console.debug(new Date(), 'Successfully created a new youtube Event'); 73 | } else { 74 | event.name = streamItem.snippet.channelTitle; 75 | event.previewImageUrl = 76 | streamItem.snippet.thumbnails?.maxres.url || 77 | streamItem.snippet.thumbnails?.default.url || 78 | ''; 79 | event.tags = streamItem.snippet.tags.map((tag) => ({ name: tag })); 80 | await event.save(); 81 | console.debug(new Date(), 'Successfully updated a youtube Event'); 82 | } 83 | 84 | return event; 85 | } 86 | 87 | module.exports = { 88 | getEventFromYoutubeUrl, 89 | getVideosById 90 | }; 91 | 92 | /** 93 | { 94 | "kind": "youtube#videoListResponse", 95 | "etag": "zLQ0kqoxGEuGWhRPMDfz-nsCwDw", 96 | "items": [ 97 | { 98 | "kind": "youtube#video", 99 | "etag": "hdH_zplS7K_BhE5UAQgMqJAAgmo", 100 | "id": "4sJQ8uMmti4", 101 | "snippet": { 102 | "publishedAt": "2021-09-16T09:06:05Z", 103 | "channelId": "UCZkcxFIsqW5htimoUQKA0iA", 104 | "title": "🎙 Pressetalk mit Leon Goretzka, Oliver Kahn und Hasan Salihamidzic zur Vertragsverlängerung", 105 | "description": "Leon Goretzka hat ein neues Arbeitspapier beim FC Bayern unterschrieben und bis 2026 verlängert. Schau dir jetzt den Pressetalk dazu live an und höre dir an, was Goretzka, Kahn und Salihamidzic dazu sagen.\n\n► #MiaSanMia - Abonnieren & die Glocke aktivieren 🔔: https://fc.bayern/YouTubeAbo\n\nFacebook: https://www.facebook.com/FCBayern\nTwitter: https://twitter.com/fcbayern\nInstagram: https://www.instagram.com/fcbayern\nTikTok: https://www.tiktok.com/@fcbayern\nSnapchat: https://fc.bayern/FCBayernSnaps\nWebsite: https://fcbayern.com\nFC Bayern.tv: https://fcbayern.com/fcbayerntv\nFC Bayern.tv live: https://fcbayern.com/fcbayerntv/de/fcbayerntvlive", 106 | "thumbnails": { 107 | "default": { 108 | "url": "https://i.ytimg.com/vi/4sJQ8uMmti4/default_live.jpg", 109 | "width": 120, 110 | "height": 90 111 | }, 112 | "medium": { 113 | "url": "https://i.ytimg.com/vi/4sJQ8uMmti4/mqdefault_live.jpg", 114 | "width": 320, 115 | "height": 180 116 | }, 117 | "high": { 118 | "url": "https://i.ytimg.com/vi/4sJQ8uMmti4/hqdefault_live.jpg", 119 | "width": 480, 120 | "height": 360 121 | }, 122 | "standard": { 123 | "url": "https://i.ytimg.com/vi/4sJQ8uMmti4/sddefault_live.jpg", 124 | "width": 640, 125 | "height": 480 126 | }, 127 | "maxres": { 128 | "url": "https://i.ytimg.com/vi/4sJQ8uMmti4/maxresdefault_live.jpg", 129 | "width": 1280, 130 | "height": 720 131 | } 132 | }, 133 | "channelTitle": "FC Bayern München", 134 | "tags": [ 135 | "FC Bayern München", 136 | "Bayern Munich", 137 | "FCB", 138 | "FC Bayern", 139 | "Fußball", 140 | "Football", 141 | "Soccer", 142 | "Pressetalk", 143 | "Pressekonferenz", 144 | "Hasan Salihamidzic", 145 | "Salihamidzic", 146 | "Oliver Kahn", 147 | "Kahn", 148 | "Leon Goretzka", 149 | "Goretzka", 150 | "Goretzka 2026", 151 | "Vertrag", 152 | "Vertragsverlängerung", 153 | "2026", 154 | "LG2026" 155 | ], 156 | "categoryId": "17", 157 | "liveBroadcastContent": "upcoming", 158 | "defaultLanguage": "en", 159 | "localized": { 160 | "title": "🎙 Pressetalk mit Leon Goretzka, Oliver Kahn und Hasan Salihamidzic zur Vertragsverlängerung", 161 | "description": "Leon Goretzka hat ein neues Arbeitspapier beim FC Bayern unterschrieben und bis 2026 verlängert. Schau dir jetzt den Pressetalk dazu live an und höre dir an, was Goretzka, Kahn und Salihamidzic dazu sagen.\n\n► #MiaSanMia - Abonnieren & die Glocke aktivieren 🔔: https://fc.bayern/YouTubeAbo\n\nFacebook: https://www.facebook.com/FCBayern\nTwitter: https://twitter.com/fcbayern\nInstagram: https://www.instagram.com/fcbayern\nTikTok: https://www.tiktok.com/@fcbayern\nSnapchat: https://fc.bayern/FCBayernSnaps\nWebsite: https://fcbayern.com\nFC Bayern.tv: https://fcbayern.com/fcbayerntv\nFC Bayern.tv live: https://fcbayern.com/fcbayerntv/de/fcbayerntvlive" 162 | }, 163 | "defaultAudioLanguage": "de" 164 | }, 165 | "contentDetails": { 166 | "duration": "P0D", 167 | "dimension": "2d", 168 | "definition": "sd", 169 | "caption": "false", 170 | "licensedContent": true, 171 | "contentRating": {}, 172 | "projection": "rectangular" 173 | }, 174 | "status": { 175 | "uploadStatus": "uploaded", 176 | "privacyStatus": "public", 177 | "license": "youtube", 178 | "embeddable": true, 179 | "publicStatsViewable": true, 180 | "madeForKids": false 181 | }, 182 | "statistics": { 183 | "viewCount": "0", 184 | "likeCount": "49", 185 | "dislikeCount": "1", 186 | "favoriteCount": "0", 187 | "commentCount": "0" 188 | }, 189 | "player": { 190 | "embedHtml": "\u003ciframe width=\"480\" height=\"360\" src=\"//www.youtube.com/embed/4sJQ8uMmti4\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen\u003e\u003c/iframe\u003e" 191 | }, 192 | "recordingDetails": {} 193 | } 194 | ], 195 | "pageInfo": { 196 | "totalResults": 1, 197 | "resultsPerPage": 1 198 | } 199 | } 200 | */ 201 | -------------------------------------------------------------------------------- /services/statistics-service.js: -------------------------------------------------------------------------------- 1 | const {UniversalEvent} = require('@wallfair.io/wallfair-commons').models; 2 | const _ = require('lodash'); 3 | 4 | /*** 5 | * Get how many times user played the game by userId 6 | * @param userId 7 | * @param gameId 8 | * @returns {Promise} 9 | */ 10 | const getCasinoGamePlayCount = async (userId, gameId) => { 11 | const filter = { 12 | type: 'Casino/CASINO_PLACE_BET', 13 | userId 14 | }; 15 | 16 | if (gameId) { 17 | filter['data.gameTypeId'] = gameId; 18 | } 19 | 20 | const query = await UniversalEvent.countDocuments(filter).catch((err) => { 21 | console.error('[getCasinoGamePlayCount]', err); 22 | }); 23 | 24 | return query; 25 | }; 26 | 27 | /*** 28 | * Get how many times user cashout-ed in game 29 | * @param userId 30 | * @param gameId 31 | * @returns {Promise} 32 | */ 33 | const getCasinoGameCashoutCount = async (userId, gameId) => { 34 | const filter = { 35 | type: 'Casino/CASINO_CASHOUT', 36 | userId 37 | }; 38 | 39 | if (gameId) { 40 | filter['data.gameTypeId'] = gameId; 41 | } 42 | 43 | const query = await UniversalEvent.countDocuments(filter).catch((err) => { 44 | console.error(err); 45 | }); 46 | 47 | return query; 48 | }; 49 | 50 | /*** 51 | * Get total amount won by user 52 | * @param userId 53 | * @param gameId 54 | * @returns {Promise} 55 | * object.totalWon 56 | * object.totalReward 57 | * object.totalStaked 58 | */ 59 | const getCasinoGamesAmountWon = async (userId, gameId) => { 60 | const defaultOutput = { 61 | totalWon: 0, 62 | totalReward: 0, 63 | totalStaked: 0 64 | }; 65 | 66 | const filter = { 67 | type: 'Casino/CASINO_CASHOUT', 68 | userId 69 | }; 70 | 71 | if (gameId) { 72 | filter['data.gameTypeId'] = gameId; 73 | } 74 | 75 | const query = await UniversalEvent.aggregate([ 76 | { 77 | $match: filter 78 | }, 79 | { 80 | $group: { 81 | _id: null, 82 | totalReward: {$sum: "$data.reward"}, 83 | totalStaked: {$sum: "$data.stakedAmount"} 84 | }, 85 | }, { 86 | $project: { 87 | _id: 0, 88 | totalWon: {"$subtract": ["$totalReward", "$totalStaked"]}, 89 | totalReward: 1, 90 | totalStaked: 1 91 | } 92 | }]).catch((err) => { 93 | console.error(err); 94 | }); 95 | 96 | return _.get(query, 0) || defaultOutput; 97 | }; 98 | 99 | /*** 100 | * Get total amount lost by user 101 | * @param userId 102 | * @param gameId - gameTypeId 103 | * @returns {Promise} - return negative value, when user lost in general 104 | */ 105 | const getCasinoGamesAmountLost = async (userId, gameId) => { 106 | const matchFilter = { 107 | type: 'Casino/CASINO_PLACE_BET', 108 | userId 109 | }; 110 | 111 | if(gameId) { 112 | matchFilter['data.gameTypeId'] = gameId; 113 | } 114 | 115 | const queryTotalBetted = await UniversalEvent.aggregate([ 116 | { 117 | $match: matchFilter 118 | }, 119 | { 120 | $group: { 121 | _id: null, 122 | totalBettedAmount: {$sum: "$data.amount"} 123 | }, 124 | }, { 125 | $project: { 126 | _id: 0, 127 | totalBettedAmount: 1 128 | } 129 | }]).catch((err) => { 130 | console.error(err); 131 | }); 132 | 133 | const queryTotalRewarded = await getCasinoGamesAmountWon(userId, gameId).catch((err) => { 134 | console.error(err); 135 | }); 136 | const totalBetted = parseFloat(_.get(queryTotalBetted, '0.totalBettedAmount', 0)); 137 | 138 | if (queryTotalRewarded && queryTotalBetted) { 139 | const totalRewarded = parseFloat(_.get(queryTotalRewarded, 'totalReward')); 140 | return totalRewarded - totalBetted; 141 | } else { 142 | return -totalBetted; 143 | } 144 | }; 145 | 146 | /*** 147 | * Get total amount of bets per user 148 | * @param userId 149 | * @returns {Promise} - return 150 | * object.totalBettedAmount 151 | * object.totalBets 152 | * object.totalOutcomeAmount 153 | */ 154 | const getUserBetsAmount = async (userId) => { 155 | const defaultOutput = { 156 | totalBettedAmount: 0, 157 | totalBets: 0, 158 | totalOutcomeAmount: 0 159 | }; 160 | const queryTotalBetted = await UniversalEvent.aggregate([ 161 | { 162 | $match: { 163 | type: 'Notification/EVENT_BET_PLACED', 164 | userId 165 | } 166 | }, 167 | { 168 | $group: { 169 | _id: null, 170 | totalBettedAmount: {$sum: "$data.trade.investmentAmount"}, 171 | totalBets: {$sum: 1}, 172 | totalOutcomeAmount: {$sum: "$data.trade.outcomeTokens"} 173 | }, 174 | }, { 175 | $project: { 176 | _id: 0, 177 | totalBettedAmount: 1, 178 | totalBets: 1, 179 | totalOutcomeAmount: 1 180 | } 181 | }]).catch((err) => { 182 | console.error(err); 183 | }); 184 | 185 | return _.get(queryTotalBetted, '0') || defaultOutput; 186 | }; 187 | 188 | 189 | /*** 190 | * Get user cashout amounts 191 | * @param userId 192 | * @returns {Promise} - return 193 | * object.totalBettedAmount 194 | * object.totalBets 195 | * object.totalOutcomeAmount 196 | */ 197 | const getUserBetsCashouts = async (userId) => { 198 | const defaultOutput = { 199 | totalAmount: 0, 200 | totalCashouts: 0 201 | }; 202 | const queryTotalBetted = await UniversalEvent.aggregate([ 203 | { 204 | $match: { 205 | type: 'Notification/EVENT_BET_CASHED_OUT', 206 | userId 207 | } 208 | }, 209 | { 210 | $group: { 211 | _id: null, 212 | totalAmount: { 213 | $sum: { 214 | "$toDouble": "$data.amount" //@todo save this directly as float, to avoid from string conversion 215 | } 216 | }, 217 | totalCashouts: {$sum: 1} 218 | }, 219 | }, { 220 | $project: { 221 | _id: 0, 222 | totalAmount: 1, 223 | totalCashouts: 1 224 | } 225 | }]).catch((err) => { 226 | console.error(err); 227 | }); 228 | 229 | return _.get(queryTotalBetted, '0') || defaultOutput; 230 | }; 231 | 232 | 233 | /*** 234 | * Get user bet rewards 235 | * @param userId 236 | * @returns {Promise} - return 237 | * object.totalBettedAmount 238 | * object.totalBets 239 | * object.totalOutcomeAmount 240 | */ 241 | const getUserBetsRewards = async (userId) => { 242 | const defaultOutput = { 243 | totalWonAmount: 0, 244 | totalRewards: 0 245 | }; 246 | const query = await UniversalEvent.aggregate([ 247 | { 248 | $match: { 249 | type: 'Notification/EVENT_USER_REWARD', 250 | userId 251 | } 252 | }, 253 | { 254 | $group: { 255 | _id: null, 256 | totalWonAmount: { 257 | $sum: { 258 | "$toDouble": "$data.winToken" 259 | } 260 | }, 261 | totalRewards: {$sum: 1} 262 | }, 263 | }, { 264 | $project: { 265 | _id: 0, 266 | totalWonAmount: 1, 267 | totalRewards: 1 268 | } 269 | }]).catch((err) => { 270 | console.error(err); 271 | }); 272 | 273 | return _.get(query, '0') || defaultOutput; 274 | }; 275 | 276 | const getUserStats = async (userId) => { 277 | const casinoGamePlayCount = await getCasinoGamePlayCount(userId).catch((err)=> { 278 | console.error(err); 279 | }); 280 | 281 | const casinoGameCashoutCount = await getCasinoGameCashoutCount(userId).catch((err)=> { 282 | console.error(err); 283 | }); 284 | 285 | const casinoGamesAmountWon = await getCasinoGamesAmountWon(userId).catch((err)=> { 286 | console.error(err); 287 | }); 288 | 289 | const casinoGamesAmountLost = await getCasinoGamesAmountLost(userId).catch((err)=> { 290 | console.error(err); 291 | }); 292 | 293 | const userBetsAmount = await getUserBetsAmount(userId).catch((err)=> { 294 | console.error(err); 295 | }); 296 | 297 | const userBetsCashouts = await getUserBetsCashouts(userId).catch((err)=> { 298 | console.error(err); 299 | }); 300 | 301 | const userBetsRewards = await getUserBetsRewards(userId).catch((err)=> { 302 | console.error(err); 303 | }); 304 | 305 | return { 306 | casinoGamePlayCount, 307 | casinoGameCashoutCount, 308 | casinoGamesAmountWon, 309 | casinoGamesAmountLost, 310 | userBetsAmount, 311 | userBetsCashouts, 312 | userBetsRewards 313 | } 314 | }; 315 | 316 | module.exports = { 317 | getCasinoGamePlayCount, 318 | getCasinoGameCashoutCount, 319 | getCasinoGamesAmountWon, 320 | getCasinoGamesAmountLost, 321 | getUserBetsAmount, 322 | getUserBetsCashouts, 323 | getUserBetsRewards, 324 | getUserStats 325 | }; 326 | -------------------------------------------------------------------------------- /services/twitch-service.js: -------------------------------------------------------------------------------- 1 | const dotenv = require('dotenv'); 2 | 3 | dotenv.config(); 4 | 5 | const axios = require('axios'); 6 | 7 | const clientId = process.env.TWITCH_CLIENT_ID; 8 | const clientSecret = process.env.TWITCH_CLIENT_SECRET; 9 | 10 | // Import Event model 11 | const { Event } = require('@wallfair.io/wallfair-commons').models; 12 | 13 | const generateSlug = require('../util/generateSlug'); 14 | 15 | const credentials = { 16 | access_token: null, 17 | expired_in: null, 18 | expires_at: null, 19 | }; 20 | 21 | const updateToken = async () => { 22 | const authURL = `https://id.twitch.tv/oauth2/token?client_id=${clientId}&client_secret=${clientSecret}&grant_type=client_credentials`; 23 | console.log('Logging in to twitch using url ', authURL); 24 | 25 | const tokenResponse = await axios.post(authURL); 26 | 27 | credentials.access_token = tokenResponse.data.access_token; 28 | credentials.expired_in = tokenResponse.data.expired_in; 29 | credentials.expires_at = Date.now() + credentials.expired_in - 200; 30 | }; 31 | 32 | const isTokenExpired = () => credentials.expires_at == null || Date.now() > credentials.expires_at; 33 | 34 | const getAccessToken = async () => { 35 | if (isTokenExpired()) { 36 | await updateToken(); 37 | } 38 | 39 | return credentials.access_token; 40 | }; 41 | 42 | const twitchRequest = async (url) => { 43 | const token = await getAccessToken(); 44 | 45 | try { 46 | const response = await axios.get(url, { 47 | headers: { 48 | 'Client-Id': clientId, 49 | Authorization: `Bearer ${token}`, 50 | }, 51 | }); 52 | return response.data; 53 | } catch (err) { 54 | console.log(new Date(), 'Failed to make a twitch request', err); 55 | } 56 | }; 57 | 58 | const getTwitchUser = async (twitchUsername) => { 59 | const userData = await twitchRequest(`https://api.twitch.tv/helix/users?login=${twitchUsername}`); 60 | 61 | return userData.data[0]; 62 | }; 63 | 64 | const getTwitchTags = async (broadcaster_id) => { 65 | const tagsData = await twitchRequest( 66 | `https://api.twitch.tv/helix/streams/tags?broadcaster_id=${broadcaster_id}` 67 | ); 68 | 69 | return tagsData.data.map((i) => ({ name: i.localization_names['en-us'] })); 70 | }; 71 | 72 | const getTwitchChannel = async (broadcaster_id) => { 73 | const channelData = await twitchRequest( 74 | `https://api.twitch.tv/helix/channels?broadcaster_id=${broadcaster_id}` 75 | ); 76 | return channelData.data[0]; 77 | }; 78 | 79 | const getEventFromTwitchUrl = async (streamUrl, category) => { 80 | const username = streamUrl.substring(streamUrl.lastIndexOf('/') + 1); 81 | 82 | const userData = await getTwitchUser(username); 83 | const channelData = await getTwitchChannel(userData.id); 84 | const tags = await getTwitchTags(userData.id); 85 | 86 | const metadata = { 87 | twitch_id: userData.id, 88 | twitch_login: userData.login, 89 | twitch_name: userData.display_name, 90 | twitch_game_id: channelData.game_id, 91 | twitch_game_name: channelData.game_name, 92 | twitch_channel_title: channelData.title, 93 | }; 94 | 95 | // first check if event exists 96 | let event = await Event.findOne({ streamUrl }).exec(); 97 | if (!event) { 98 | const slug = generateSlug(userData.display_name); 99 | 100 | event = new Event({ 101 | name: userData.display_name, 102 | slug, 103 | previewImageUrl: userData.offline_image_url, 104 | streamUrl, 105 | tags, 106 | date: Date.now(), 107 | type: 'streamed', 108 | category: category || channelData.game_name, 109 | metadata: { 110 | ...metadata, 111 | twitch_last_synced: null, 112 | twitch_subscribed_online: 'false', 113 | twitch_subscribed_offline: 'false', 114 | }, 115 | }); 116 | await event.save(); 117 | } else { 118 | event.metadata = event.metadata || { 119 | twitch_last_synced: null, 120 | twitch_subscribed_online: 'false', 121 | twitch_subscribed_offline: 'false', 122 | }; 123 | for (const prop in metadata) { 124 | event.metadata[prop] = metadata[prop]; 125 | } 126 | event.tags = tags; 127 | event.category = channelData.game_name; 128 | await event.save(); 129 | } 130 | 131 | return event; 132 | }; 133 | 134 | const subscribeForOnlineNotifications = async (broadcaster_user_id) => { 135 | if (!process.env.BACKEND_URL || !process.env.TWITCH_CALLBACK_SECRET) { 136 | console.log( 137 | 'WARNING: Attempted to subscribe to twich events without backend properly configured.' 138 | ); 139 | return; 140 | } 141 | 142 | const token = await getAccessToken(); 143 | 144 | const data = { 145 | type: 'stream.online', 146 | version: '1', 147 | condition: { 148 | broadcaster_user_id, 149 | }, 150 | transport: { 151 | method: 'webhook', 152 | callback: `${process.env.BACKEND_URL}/webhooks/twitch/`, 153 | secret: process.env.TWITCH_CALLBACK_SECRET, 154 | }, 155 | }; 156 | 157 | try { 158 | await axios.post('https://api.twitch.tv/helix/eventsub/subscriptions', data, { 159 | headers: { 160 | 'Client-Id': clientId, 161 | Authorization: `Bearer ${token}`, 162 | }, 163 | }); 164 | return 'pending'; 165 | } catch (err) { 166 | if (err.response.statusText === 'Conflict') { 167 | // already subscribed. Store info and continue; 168 | return 'true'; 169 | } 170 | console.log('Could not subscribe to twitch online events', err.response); 171 | } 172 | 173 | return 'false'; 174 | }; 175 | 176 | const subscribeForOfflineNotifications = async (broadcaster_user_id) => { 177 | if (!process.env.BACKEND_URL || !process.env.TWITCH_CALLBACK_SECRET) { 178 | console.log( 179 | 'WARNING: Attempted to subscribe to twitch events without backend properly configured.' 180 | ); 181 | return; 182 | } 183 | 184 | const token = await getAccessToken(); 185 | 186 | const data = { 187 | type: 'stream.offline', 188 | version: '1', 189 | condition: { 190 | broadcaster_user_id, 191 | }, 192 | transport: { 193 | method: 'webhook', 194 | callback: `${process.env.BACKEND_URL}/webhooks/twitch/`, 195 | secret: process.env.TWITCH_CALLBACK_SECRET, 196 | }, 197 | }; 198 | 199 | try { 200 | await axios.post('https://api.twitch.tv/helix/eventsub/subscriptions', data, { 201 | headers: { 202 | 'Client-Id': clientId, 203 | Authorization: `Bearer ${token}`, 204 | }, 205 | }); 206 | return 'pending'; 207 | } catch (err) { 208 | if (err.response.statusText === 'Conflict') { 209 | // already subscribed. Store info and continue; 210 | return 'true'; 211 | } 212 | console.log('Could not subscribe to twitch offline events', err.response); 213 | } 214 | 215 | return 'false'; 216 | }; 217 | 218 | const removeSubscription = async (subscription_id) => { 219 | if (!process.env.BACKEND_URL || !process.env.TWITCH_CALLBACK_SECRET) { 220 | console.log('WARNING: Attempted to remove twitch event without backend properly configured.'); 221 | return; 222 | } 223 | 224 | const token = await getAccessToken(); 225 | 226 | try { 227 | const response = await axios.delete( 228 | `https://api.twitch.tv/helix/eventsub/subscriptions?id=${subscription_id}`, 229 | { 230 | headers: { 231 | 'Client-Id': clientId, 232 | Authorization: `Bearer ${token}`, 233 | }, 234 | } 235 | ); 236 | return response.data; 237 | } catch (err) { 238 | console.log(new Date(), 'Failed to remove a twitch subscription', err.data); 239 | } 240 | }; 241 | 242 | const listSubscriptions = async () => { 243 | if (!process.env.BACKEND_URL || !process.env.TWITCH_CALLBACK_SECRET) { 244 | console.log( 245 | 'WARNING: Attempted to list twitch subscriptions without backend properly configured.' 246 | ); 247 | return; 248 | } 249 | 250 | const subscriptionsList = await twitchRequest( 251 | 'https://api.twitch.tv/helix/eventsub/subscriptions' 252 | ); 253 | 254 | return subscriptionsList.data; 255 | }; 256 | 257 | const removeAllSubscriptions = async () => { 258 | const subscriptionsList = await listSubscriptions(); 259 | 260 | for (const subscription of subscriptionsList) { 261 | await removeSubscription(subscription.id); 262 | } 263 | 264 | const result = await listSubscriptions(); 265 | 266 | return result.length === 0; 267 | }; 268 | 269 | module.exports = { 270 | getEventFromTwitchUrl, 271 | subscribeForOnlineNotifications, 272 | subscribeForOfflineNotifications, 273 | removeSubscription, 274 | listSubscriptions, 275 | removeAllSubscriptions, 276 | }; 277 | -------------------------------------------------------------------------------- /services/leaderboard-service.js: -------------------------------------------------------------------------------- 1 | const { User, UniversalEvent } = require('@wallfair.io/wallfair-commons').models; 2 | const mongoose = require('mongoose'); 3 | const moment = require('moment'); 4 | 5 | const getList = async (type, limit, skip) => { 6 | let result; 7 | 8 | const dates = { 9 | start: moment().startOf('day').toDate(), 10 | end: moment().endOf('day').toDate() 11 | }; 12 | 13 | switch (type) { 14 | case 'high_events': 15 | result = await getHighEvents(limit, skip, dates); 16 | break; 17 | case 'high_games': 18 | result = await getHighGames(limit, skip, dates); 19 | break; 20 | case 'high_volume': 21 | result = await getHighVolume(limit, skip, dates); 22 | break; 23 | case 'jackpot_winners': 24 | result = await getJackpotWinners(); 25 | break; 26 | default: 27 | result = await getOverall(limit, skip); 28 | break; 29 | } 30 | 31 | return { 32 | ...(result.users.length ? result : { users: [], total: 0 }), 33 | limit, 34 | skip 35 | } 36 | } 37 | 38 | const getJackpotWinners = async () => { 39 | const yesterdayDates = { 40 | start: moment().startOf('day').subtract(1, 'days').toDate(), 41 | end: moment().endOf('day').subtract(1, 'days').toDate() 42 | }; 43 | 44 | const highEventsWinner = await getHighEvents(1, 0, yesterdayDates); 45 | const highGamesWinner = await getHighGames(1, 0, yesterdayDates); 46 | const highVolumeWinner = await getHighVolume(1, 0, yesterdayDates); 47 | 48 | return { 49 | total: 3, 50 | users: [ 51 | ...highEventsWinner.users, 52 | ...highGamesWinner.users, 53 | ...highVolumeWinner.users 54 | ] 55 | } 56 | } 57 | 58 | const getHighEvents = async (limit, skip, dates) => { 59 | const res = await UniversalEvent.aggregate([ 60 | { 61 | $match: { 62 | type: { 63 | $in: [ 64 | 'Notification/EVENT_USER_REWARD', 'Notification/EVENT_BET_CASHED_OUT' 65 | ] 66 | }, 67 | createdAt: { 68 | $gte: dates.start, 69 | $lte: dates.end 70 | } 71 | } 72 | }, { 73 | $addFields: { 74 | profilePicture: '$data.user.profilePicture', 75 | username: '$data.user.username' 76 | } 77 | }, { 78 | $group: { 79 | _id: '$data.user._id', 80 | winReward: { 81 | $sum: { 82 | $cond: [ 83 | { 84 | $gt: ['$data.gainAmount', 0] 85 | }, 86 | '$data.gainAmount', 0 87 | ] 88 | } 89 | }, 90 | winCashout: { 91 | $sum: { 92 | $cond: [ 93 | { 94 | $gt: ['$data.gain.gainAmount', 0] 95 | }, 96 | '$data.gain.gainAmount', 0 97 | ] 98 | }, 99 | }, 100 | tmp: { 101 | $push: { 102 | profilePicture: '$data.user.profilePicture', 103 | username: '$data.user.username' 104 | } 105 | } 106 | } 107 | }, { 108 | $project: { 109 | amountWon: { 110 | $add: [ 111 | '$winReward', '$winCashout' 112 | ] 113 | }, 114 | profilePicture: { 115 | $first: '$tmp.profilePicture' 116 | }, 117 | username: { 118 | $first: '$tmp.username' 119 | } 120 | } 121 | }, 122 | { 123 | $redact: { 124 | $cond: { 125 | if: { 126 | $lte: [ 127 | '$amountWon', 0 128 | ] 129 | }, 130 | then: '$$PRUNE', 131 | else: '$$DESCEND' 132 | } 133 | } 134 | }, 135 | { 136 | $sort: { 137 | amountWon: -1 138 | } 139 | }, { 140 | $facet: { 141 | total: [ 142 | { 143 | $group: { 144 | _id: null, 145 | count: { 146 | $sum: 1 147 | } 148 | } 149 | } 150 | ], 151 | users: [ 152 | { 153 | $skip: skip 154 | }, { 155 | $limit: limit 156 | }, { 157 | $project: { 158 | _id: 1, 159 | amountWon: 1, 160 | profilePicture: 1, 161 | username: 1 162 | } 163 | } 164 | ] 165 | } 166 | }, { 167 | $unwind: '$total' 168 | }, { 169 | $project: { 170 | total: '$total.count', 171 | users: '$users' 172 | } 173 | } 174 | ]).catch((err) => { 175 | console.error(err); 176 | }); 177 | 178 | return res[0] || { users: [], total: 0 }; 179 | } 180 | 181 | const getHighGames = async (limit, skip, dates) => { 182 | const res = await UniversalEvent.aggregate([ 183 | { 184 | $match: { 185 | type: 'Casino/CASINO_CASHOUT', 186 | createdAt: { 187 | $gte: dates.start, 188 | $lte: dates.end 189 | } 190 | } 191 | }, { 192 | $addFields: { 193 | profilePicture: '$data.profilePicture', 194 | username: '$data.username' 195 | } 196 | }, { 197 | $group: { 198 | _id: '$data.userId', 199 | winReward: { 200 | $sum: '$data.profitWfair' 201 | }, 202 | tmp: { 203 | $push: { 204 | profilePicture: '$data.profilePicture', 205 | username: '$data.username' 206 | } 207 | } 208 | } 209 | }, { 210 | $project: { 211 | amountWon: '$winReward', 212 | profilePicture: { 213 | $first: '$tmp.profilePicture' 214 | }, 215 | username: { 216 | $first: '$tmp.username' 217 | } 218 | } 219 | }, { 220 | $sort: { 221 | amountWon: -1 222 | } 223 | }, { 224 | $facet: { 225 | total: [ 226 | { 227 | $group: { 228 | _id: null, 229 | count: { 230 | $sum: 1 231 | } 232 | } 233 | } 234 | ], 235 | users: [ 236 | { 237 | $skip: skip 238 | }, { 239 | $limit: limit 240 | }, { 241 | $project: { 242 | _id: 1, 243 | amountWon: 1, 244 | profilePicture: 1, 245 | username: 1 246 | } 247 | } 248 | ] 249 | } 250 | }, { 251 | $unwind: '$total' 252 | }, { 253 | $project: { 254 | total: '$total.count', 255 | users: '$users' 256 | } 257 | } 258 | ]).catch((err) => { 259 | console.error(err); 260 | }); 261 | 262 | return res[0] || { users: [], total: 0 }; 263 | } 264 | 265 | const getHighVolume = async (limit, skip, dates) => { 266 | const res = await UniversalEvent.aggregate([ 267 | { 268 | $match: { 269 | type: 'Notification/EVENT_BET_PLACED', 270 | createdAt: { 271 | $gte: dates.start, 272 | $lte: dates.end 273 | }, 274 | 'data.creator.admin': false 275 | } 276 | }, { 277 | $group: { 278 | _id: '$data.bet.creator', 279 | amountWon: { 280 | $sum: '$data.trade.investment_amount' 281 | } 282 | } 283 | }, { 284 | $sort: { 285 | amountWon: -1 286 | } 287 | }, { 288 | $facet: { 289 | total: [ 290 | { 291 | $group: { 292 | _id: null, 293 | count: { 294 | $sum: 1 295 | } 296 | } 297 | } 298 | ], 299 | users: [ 300 | { 301 | $skip: skip 302 | }, { 303 | $limit: limit 304 | }, { 305 | $project: { 306 | _id: 1, 307 | amountWon: 1, 308 | } 309 | } 310 | ] 311 | } 312 | }, { 313 | $unwind: '$total' 314 | }, { 315 | $project: { 316 | total: '$total.count', 317 | users: '$users' 318 | } 319 | } 320 | ]).catch((err) => { 321 | console.error(err); 322 | }); 323 | 324 | let infos = []; 325 | 326 | if (res[0] && res[0].users.length) { 327 | infos = await User.find({ 328 | _id: { 329 | $in: res[0].users.map(user => { 330 | return mongoose.Types.ObjectId(user._id); 331 | }) 332 | } 333 | }) 334 | .select({ username: 1, profilePicture: 1 }) 335 | .exec(); 336 | } 337 | 338 | return !res[0] ? { users: [], total: 0 } : { 339 | users: res[0].users.map(v => { 340 | const info = infos.find(s => s._id.toString() === v._id); 341 | return { ...v, ...(info && info._doc) } 342 | }), 343 | total: res[0].total 344 | }; 345 | } 346 | 347 | const getOverall = async (limit, skip) => { 348 | const users = await User.find({ username: { $exists: true } }) 349 | .sort({ amountWon: -1, date: -1 }) 350 | .select({ username: 1, amountWon: 1, profilePicture: 1 }) 351 | .limit(limit) 352 | .skip(skip) 353 | .exec(); 354 | 355 | const total = await User.countDocuments().exec(); 356 | 357 | return { 358 | total, 359 | users, 360 | }; 361 | } 362 | 363 | module.exports = { 364 | getList 365 | }; -------------------------------------------------------------------------------- /controllers/admin-controller.js: -------------------------------------------------------------------------------- 1 | const { validationResult } = require("express-validator"); 2 | const { ErrorHandler } = require("../util/error-handler"); 3 | const { 4 | AccountNamespace, 5 | WFAIR_SYMBOL, 6 | TransactionManager, 7 | ExternalTransactionOriginator, 8 | ExternalTransactionStatus, 9 | Transactions, 10 | toWei, 11 | Query, 12 | fromWei, 13 | Wallet, 14 | Account 15 | } = require("@wallfair.io/trading-engine"); 16 | const { getOne } = require("../services/user-api"); 17 | const { WALLETS } = require("../util/wallet"); 18 | const userService = require('../services/user-service'); 19 | const promoCodeService = require('../services/promo-codes-service'); 20 | const fs = require('fs'); 21 | const readline = require('readline'); 22 | const { promisify } = require('util'); 23 | const { PROMO_CODE_DEFAULT_REF, PROMO_CODES_TYPES } = require("../util/constants"); 24 | const unlinkAsync = promisify(fs.unlink) 25 | 26 | exports.transferToUser = async (req, res, next) => { 27 | if (!req.user) { 28 | return next(new ErrorHandler(403, 'Not authorized')); 29 | } 30 | 31 | const errors = validationResult(req); 32 | if (!errors.isEmpty()) { 33 | return next(new ErrorHandler(422, 'Invalid input passed, please check it')); 34 | } 35 | 36 | const { 37 | amount, 38 | transactionHash, 39 | userAddress, 40 | inputAmount, 41 | inputCurrency, 42 | } = req.body; 43 | 44 | const transactionManager = new TransactionManager(); 45 | 46 | try { 47 | const userAccount = await new Account().getUserLink(userAddress); 48 | 49 | if (!userAccount) { 50 | return next(new ErrorHandler(404, 'User does not exist')); 51 | } 52 | 53 | const user = await getOne(userAccount.user_id); 54 | 55 | const extTransaction = await new Transactions() 56 | .getExternalTransactionByHash(transactionHash); 57 | 58 | if (extTransaction) { 59 | return next(new ErrorHandler(409, 'Transaction already processed')); 60 | } 61 | 62 | const amountToTransfer = toWei(amount).toString(); 63 | 64 | await transactionManager.startTransaction(); 65 | 66 | await transactionManager.wallet.transfer( 67 | { 68 | owner: process.env.ONRAMP_WEBHOOK_WALLET, 69 | namespace: AccountNamespace.ETH, 70 | symbol: WFAIR_SYMBOL 71 | }, 72 | { 73 | owner: user._id.toString(), 74 | namespace: AccountNamespace.USR, 75 | symbol: WFAIR_SYMBOL 76 | }, 77 | amountToTransfer 78 | ); 79 | 80 | const externalData = { 81 | originator: ExternalTransactionOriginator.CRYPTO, 82 | external_system: 'manual', 83 | status: ExternalTransactionStatus.COMPLETED, 84 | external_transaction_id: transactionHash, 85 | transaction_hash: transactionHash, 86 | network_code: WALLETS[inputCurrency].network, 87 | internal_user_id: user._id.toString(), 88 | }; 89 | 90 | await transactionManager.transactions.insertExternalTransaction(externalData); 91 | 92 | await transactionManager.transactions.insertExternalTransactionLog({ 93 | ...externalData, 94 | symbol: WFAIR_SYMBOL, 95 | sender: userAddress, 96 | receiver: WALLETS[inputCurrency].wallet, 97 | amount: amountToTransfer, 98 | input_currency: inputCurrency, 99 | input_amount: inputAmount, 100 | }); 101 | 102 | await transactionManager.commitTransaction(); 103 | 104 | console.log(`Transferred ${amount} WFAIR to user ${user._id} successfully`); 105 | 106 | return res.status(204).send(); 107 | } catch (e) { 108 | console.error('Transfer error: ', e.message); 109 | await transactionManager.rollbackTransaction(); 110 | return next(new ErrorHandler('Something went wrong')); 111 | } 112 | }; 113 | 114 | exports.getUser = async (req, res, next) => { 115 | const { id } = req.params; 116 | try { 117 | const data = await userService.getUserDataForAdmin(id) 118 | return res.send(data) 119 | } catch (e) { 120 | console.error(e) 121 | return next(new ErrorHandler(500)); 122 | } 123 | } 124 | 125 | exports.listUsers = async (req, res, next) => { 126 | if (!req.user) { 127 | return next(new ErrorHandler(403, 'Not authorized')); 128 | } 129 | 130 | const errors = validationResult(req); 131 | if (!errors.isEmpty()) { 132 | return next(new ErrorHandler(400, errors)); 133 | } 134 | 135 | const { 136 | search, 137 | sortField = 'date', 138 | sortOrder = 'desc', 139 | limit = 10, 140 | page = 1, 141 | account = null, 142 | } = req.query; 143 | 144 | try { 145 | const data = await userService.searchUsers(+limit, +limit * (+page - 1), search, sortField, sortOrder, account); 146 | return res.send(data) 147 | } catch (e) { 148 | console.error(e) 149 | return next(new ErrorHandler(500)); 150 | } 151 | } 152 | 153 | exports.listUsersWithReferrals = async (req, res, next) => { 154 | if (!req.user) { 155 | return next(new ErrorHandler(403, 'Not authorized')); 156 | } 157 | 158 | try { 159 | const data = await userService.usersWithReferrals(); 160 | return res.send(data); 161 | } catch (e) { 162 | console.error(e) 163 | return next(new ErrorHandler(500)); 164 | } 165 | } 166 | 167 | exports.createPromoCode = async (req, res, next) => { 168 | const errors = validationResult(req); 169 | if (!errors.isEmpty()) { 170 | return next(new ErrorHandler(400, errors)); 171 | } 172 | 173 | const { 174 | name, 175 | ref, 176 | provider, 177 | type, 178 | value, 179 | count, 180 | description, 181 | coverUrl, 182 | wagering, 183 | duration, 184 | expiresAt, 185 | available, 186 | deposit, 187 | } = req.body; 188 | 189 | if (type === PROMO_CODES_TYPES.FREESPIN && ref === PROMO_CODE_DEFAULT_REF) { 190 | return next(new ErrorHandler(400, 'Missing reference for FREESPIN type')); 191 | } 192 | 193 | const transactionManager = new TransactionManager(); 194 | 195 | try { 196 | await transactionManager.startTransaction(); 197 | 198 | const result = await transactionManager.queryRunner.query( 199 | `INSERT INTO promo_code(name, ref_id, type, value, count, description, expires_at, cover_url, wagering, duration, provider, available) 200 | VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) 201 | RETURNING *`, 202 | [name, ref || PROMO_CODE_DEFAULT_REF, type, toWei(value).toString(), count || 1, 203 | description, expiresAt, coverUrl, wagering, duration, provider, available] 204 | ); 205 | deposit && await transactionManager.queryRunner.query( 206 | `INSERT INTO promo_code_deposit(promo_code_id, type, status, min_deposit, top_up) 207 | VALUES ($1, $2, $3, $4, $5)`, 208 | [result[0]?.id, deposit.type, deposit.status, toWei(deposit.min).toString(), deposit.topUp] 209 | ); 210 | 211 | await transactionManager.commitTransaction(); 212 | 213 | return res.status(201).send(result[0]); 214 | } catch (e) { 215 | await transactionManager.rollbackTransaction(); 216 | console.error('CREATE PROMO CODE: ', e.message); 217 | return next(new ErrorHandler(500, 'Failed to create a promo code')); 218 | } 219 | }; 220 | 221 | exports.getPromoCodes = async (req, res, next) => { 222 | try { 223 | const { order = 'created_at', name } = req.query; 224 | const result = await new Query().query( 225 | `SELECT promo_code.*, (SELECT COUNT(*) FROM promo_code_user WHERE promo_code_user.promo_code_id = promo_code.id) AS claims 226 | FROM promo_code ${name ? 'WHERE name = ' + name : ''} ORDER BY ${order} DESC` 227 | ); 228 | return res.status(200) 229 | .send(result.map((r) => { 230 | return { 231 | ...r, 232 | value: fromWei(r.value).toFixed(2), 233 | } 234 | })); 235 | } catch (e) { 236 | console.error('GET PROMO CODES: ', e.message); 237 | return next(new ErrorHandler(500, 'Failed to fetch promo codes')); 238 | } 239 | }; 240 | 241 | exports.updatePromoCode = async (req, res, next) => { 242 | try { 243 | const { id } = req.params; 244 | const result = await new Query().query( 245 | 'UPDATE promo_code SET expires_at = $1 WHERE id = $2 RETURNING *', 246 | [req.body.expiresAt, id] 247 | ); 248 | return res.status(200).send(result); 249 | } catch (e) { 250 | console.error('UPDATE PROMO CODE: ', e.message); 251 | return next(new ErrorHandler(500, 'Failed to update promo code')); 252 | } 253 | }; 254 | 255 | exports.addBonusManually = async (req, res, next) => { 256 | try { 257 | const { promoCode, refId } = req.body; 258 | const file = req?.file; 259 | 260 | if (!file) { 261 | return next(new ErrorHandler(400, 'File not defined in form-data')); 262 | } 263 | 264 | const output = { 265 | totalBonusAdded: 0, 266 | totalEntries: 0, 267 | emailsNotFound: [], 268 | bonusClaimed: [], 269 | } 270 | 271 | const fileStream = fs.createReadStream(file.path); 272 | 273 | const rl = readline.createInterface({ 274 | input: fileStream, 275 | crlfDelay: Infinity // '\r\n' as linebreak 276 | }); 277 | 278 | for await (const email of rl) { 279 | if (email) { 280 | const userFromEmail = await userService.getUserByEmail(email); 281 | const userId = userFromEmail?._id; 282 | 283 | if (userId) { 284 | try { 285 | const bonusUsed = await promoCodeService.isClaimedBonus(userId.toString(), promoCode); 286 | 287 | if (!bonusUsed) { 288 | await promoCodeService.claimPromoCodeBonus( 289 | userId.toString(), 290 | promoCode, 291 | refId 292 | ); 293 | output.totalBonusAdded += 1; 294 | } else { 295 | output.bonusClaimed.push(userFromEmail.email); 296 | } 297 | } catch (e) { 298 | console.error(`Failed to add bonus to user ${userId}. ${e.message}`); 299 | } 300 | } else { 301 | output.emailsNotFound.push(email); 302 | } 303 | 304 | output.totalEntries += 1; 305 | } 306 | } 307 | 308 | //cleanup after processing file 309 | await unlinkAsync(file.path); 310 | 311 | return res.send(output); 312 | } catch (err) { 313 | console.error(err); 314 | next(new ErrorHandler(422, err.message)); 315 | } 316 | } 317 | 318 | exports.mintCasinoBonusWallet = async (req, res, next) => { 319 | try { 320 | new Wallet().mint({ 321 | owner: 'CASINO', 322 | namespace: AccountNamespace.CAS, 323 | symbol: 'BFAIR' 324 | }, toWei(req.body.amount).toString()); 325 | 326 | return res.status(204).send(); 327 | } catch (e) { 328 | console.error(e.message); 329 | return next(new ErrorHandler(500, e.message)); 330 | } 331 | }; 332 | 333 | exports.mintBetLiquidity = async (req, res, next) => { 334 | try { 335 | new Wallet().mint({ 336 | owner: process.env.BET_LIQUIDITY_WALLET, 337 | namespace: AccountNamespace.ETH, 338 | symbol: WFAIR_SYMBOL 339 | }, toWei(req.body.amount).toString()); 340 | 341 | return res.status(204).send(); 342 | } catch (e) { 343 | console.error(e.message); 344 | return next(new ErrorHandler(500, e.message)); 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /controllers/sessions-controller.js: -------------------------------------------------------------------------------- 1 | const { ObjectId } = require('mongodb'); 2 | const logger = require('../util/logger'); 3 | const userApi = require('../services/user-api'); 4 | const userService = require('../services/user-service'); 5 | const { ErrorHandler, BannedError } = require('../util/error-handler'); 6 | const authService = require('../services/auth-service'); 7 | const { validationResult } = require('express-validator'); 8 | // const userService = require('../services/user-service'); 9 | const mailService = require('../services/mail-service'); 10 | const { generate, hasAcceptedLatestConsent } = require('../helper'); 11 | const bcrypt = require('bcryptjs'); 12 | const axios = require('axios'); 13 | const { notificationEvents } = require('@wallfair.io/wallfair-commons/constants/eventTypes'); 14 | const { Account, Wallet, AccountNamespace, WFAIR_SYMBOL, toWei } = require('@wallfair.io/trading-engine'); 15 | const amqp = require('../services/amqp-service'); 16 | const { isUserBanned } = require('../util/user'); 17 | const { generateChallenge, isAddressValid, verifyChallengeResponse } = require('../util/challenge'); 18 | 19 | const isPlayMoney = process.env.PLAYMONEY === 'true'; 20 | 21 | module.exports = { 22 | async createUser(req, res, next) { 23 | const errors = validationResult(req); 24 | if (!errors.isEmpty()) { 25 | return next(new ErrorHandler(422, errors)); 26 | } 27 | 28 | try { 29 | const { password, email, username, ref, cid, sid, recaptchaToken } = req.body; 30 | const { skip } = req.query; 31 | if (!process.env.RECAPTCHA_SKIP_TOKEN || process.env.RECAPTCHA_SKIP_TOKEN !== skip) { 32 | const recaptchaRes = await axios.post( 33 | `https://www.google.com/recaptcha/api/siteverify?secret=${process.env.GOOGLE_RECAPTCHA_CLIENT_SECRET}&response=${recaptchaToken}` 34 | ); 35 | 36 | console.log('[RECAPTCHA DATA - VERIFY]:', recaptchaRes.data) 37 | console.log('[RECAPTHCA - TOKEN]:', recaptchaToken); 38 | 39 | if ( 40 | !recaptchaRes.data.success || 41 | recaptchaRes.data.score < 0.5 || 42 | recaptchaRes.data.action !== 'join' 43 | ) { 44 | return next( 45 | new ErrorHandler(422, 'Recaptcha verification failed, please try again later.') 46 | ); 47 | } 48 | } 49 | 50 | const existing = await userApi.getUserByIdEmailPhoneOrUsername(email); 51 | 52 | if (existing) { 53 | return next( 54 | new ErrorHandler(400, 'User with provided email/phone/username already exists') 55 | ); 56 | } 57 | 58 | // init data 59 | const wFairUserId = new ObjectId().toHexString(); 60 | const counter = ((await userApi.getUserEntriesAmount()) || 0) + 1; 61 | const passwordHash = await bcrypt.hash(password, 8); 62 | const emailCode = generate(6); 63 | const createdUser = await userApi.createUser({ 64 | _id: wFairUserId, 65 | email, 66 | emailCode, 67 | username: username || `wallfair-${counter}`, 68 | password: passwordHash, 69 | preferences: { 70 | currency: WFAIR_SYMBOL, 71 | gamesCurrency: isPlayMoney ? WFAIR_SYMBOL : 'USD' 72 | }, 73 | ref, cid, sid, 74 | tosConsentedAt: new Date(), 75 | }); 76 | 77 | const account = new Account(); 78 | await account.createAccount({ 79 | owner: wFairUserId, 80 | namespace: AccountNamespace.USR, 81 | symbol: WFAIR_SYMBOL, 82 | }, isPlayMoney ? toWei(100).toString() : '0'); 83 | 84 | if (isPlayMoney && (await userApi.getOne(ref))) { 85 | await new Wallet().mint({ 86 | owner: ref, 87 | namespace: AccountNamespace.USR, 88 | symbol: WFAIR_SYMBOL, 89 | }, toWei(50).toString()); 90 | } 91 | 92 | let initialReward = 0; 93 | 94 | amqp.send( 95 | 'universal_events', 96 | 'event.user_signed_up', 97 | JSON.stringify({ 98 | event: notificationEvents.EVENT_USER_SIGNED_UP, 99 | producer: 'user', 100 | producerId: createdUser._id, 101 | data: { 102 | email: createdUser.email, 103 | userId: createdUser._id, 104 | username: createdUser.username, 105 | ref, cid, sid, 106 | initialReward, 107 | updatedAt: Date.now(), 108 | }, 109 | date: Date.now(), 110 | broadcast: true, 111 | }) 112 | ); 113 | 114 | mailService 115 | .sendConfirmMail(createdUser) 116 | .then(() => { 117 | console.log(`[SIGNUP] Confirmation email sent to ${createdUser.email}`); 118 | }) 119 | .catch((e) => { 120 | console.error(`[SIGNUP] Error sending email to ${createdUser.email}`, e); 121 | }); 122 | 123 | return res.status(201).json({ 124 | userId: createdUser.id, 125 | email: createdUser.email, 126 | initialReward, 127 | }); 128 | } catch (err) { 129 | logger.error(err); 130 | return next(new ErrorHandler(500, 'Something went wrong.')); 131 | } 132 | }, 133 | 134 | async loginThroughProvider(req, res, next) { 135 | const errors = validationResult(req); 136 | if (!errors.isEmpty()) { 137 | return next(new ErrorHandler(422, errors)); 138 | } 139 | 140 | try { 141 | const { provider } = req.params; 142 | const { ref = null, sid = null, cid = null } = req.body; 143 | 144 | const userData = await authService.getUserDataForProvider(provider, req.body); 145 | 146 | if (!userData.email) { 147 | throw new Error('NO_SOCIAL_ACCOUNT_EMAIL'); 148 | } 149 | 150 | const existingUser = await userApi.getUserByIdEmailPhoneOrUsername(userData.email); 151 | 152 | if (existingUser) { 153 | // if exists, log user in 154 | if (isUserBanned(existingUser)) { 155 | return next(new BannedError(existingUser)); 156 | } 157 | amqp.send( 158 | 'universal_events', 159 | 'event.user_signed_in', 160 | JSON.stringify({ 161 | event: notificationEvents.EVENT_USER_SIGNED_IN, 162 | producer: 'user', 163 | producerId: existingUser._id, 164 | data: { 165 | userIdentifier: existingUser.email, 166 | userId: existingUser._id, 167 | username: existingUser.username, 168 | updatedAt: Date.now(), 169 | }, 170 | broadcast: true, 171 | }) 172 | ); 173 | res.status(200).json({ 174 | userId: existingUser.id, 175 | session: await authService.generateJwt(existingUser), 176 | newUser: false, 177 | shouldAcceptToS: hasAcceptedLatestConsent(existingUser), 178 | }); 179 | } else { 180 | const newUserId = new ObjectId().toHexString(); 181 | // create user and log them it 182 | const createdUser = await userApi.createUser({ 183 | _id: newUserId, 184 | ...userData, 185 | birthdate: null, 186 | ...(!userData.emailConfirmed && { emailCode: generate(6) }), 187 | preferences: { 188 | currency: WFAIR_SYMBOL, 189 | gamesCurrency: isPlayMoney ? WFAIR_SYMBOL : 'USD' 190 | }, 191 | ref, cid, sid 192 | }); 193 | 194 | const account = new Account(); 195 | await account.createAccount({ 196 | owner: newUserId, 197 | namespace: AccountNamespace.USR, 198 | symbol: WFAIR_SYMBOL, 199 | }, isPlayMoney ? toWei(100).toString() : '0'); 200 | 201 | if (isPlayMoney && (await userApi.getOne(ref))) { 202 | await new Wallet().mint({ 203 | owner: ref, 204 | namespace: AccountNamespace.USR, 205 | symbol: WFAIR_SYMBOL, 206 | }, toWei(50).toString()); 207 | } 208 | 209 | const initialReward = 0; 210 | amqp.send( 211 | 'universal_events', 212 | 'event.user_signed_up', 213 | JSON.stringify({ 214 | event: notificationEvents.EVENT_USER_SIGNED_UP, 215 | producer: 'user', 216 | producerId: createdUser._id, 217 | data: { 218 | email: createdUser.email, 219 | userId: createdUser._id, 220 | username: createdUser.username, 221 | initialReward, 222 | updatedAt: Date.now(), 223 | provider, 224 | }, 225 | date: Date.now(), 226 | broadcast: true, 227 | }) 228 | ); 229 | 230 | return res.status(200).json({ 231 | userId: createdUser.id, 232 | session: await authService.generateJwt(createdUser), 233 | newUser: true, 234 | initialReward, 235 | user: createdUser, 236 | }); 237 | } 238 | } catch (e) { 239 | console.log(e); 240 | const errorCode = e.message === e.message.toUpperCase() ? e.message : 'UNKNOWN'; 241 | return res.status(400).json({ errorCode }); 242 | } 243 | }, 244 | 245 | async login(req, res, next) { 246 | const errors = validationResult(req); 247 | if (!errors.isEmpty()) { 248 | return next(new ErrorHandler(422, errors)); 249 | } 250 | 251 | const isAdminOnly = req.query.admin === 'true'; 252 | 253 | try { 254 | const { userIdentifier, password } = req.body; 255 | const user = await userApi.getUserByIdEmailPhoneOrUsername(userIdentifier); 256 | 257 | if (!user || (isAdminOnly && !user.admin)) { 258 | return next(new ErrorHandler(401, 'Invalid login')); 259 | } 260 | 261 | if (user.status === 'locked') { 262 | return next(new ErrorHandler(403, 'Your account is locked')); 263 | } 264 | 265 | if (isUserBanned(user)) { 266 | return next(new BannedError(user)); 267 | } 268 | 269 | const valid = user?.password && (await bcrypt.compare(password, user.password)); 270 | 271 | if (!valid) { 272 | return next(new ErrorHandler(401, 'Invalid login')); 273 | } 274 | 275 | amqp.send( 276 | 'universal_events', 277 | 'event.user_signed_in', 278 | JSON.stringify({ 279 | event: notificationEvents.EVENT_USER_SIGNED_IN, 280 | producer: 'user', 281 | producerId: user._id, 282 | data: { 283 | userIdentifier, 284 | userId: user._id, 285 | username: user.username, 286 | updatedAt: Date.now(), 287 | }, 288 | broadcast: true, 289 | }) 290 | ); 291 | 292 | res.status(200).json({ 293 | userId: user.id, 294 | session: await authService.generateJwt(user), 295 | shouldAcceptToS: hasAcceptedLatestConsent(user), 296 | }); 297 | } catch (err) { 298 | logger.error(err); 299 | return next(new ErrorHandler(401, "Couldn't verify user")); 300 | } 301 | }, 302 | 303 | async loginWeb3Challenge(req, res, next) { 304 | const errors = validationResult(req); 305 | if (!errors.isEmpty()) { 306 | return next(new ErrorHandler(422, errors)); 307 | } 308 | 309 | if (!isAddressValid(req.params.address)) { 310 | return next( 311 | new ErrorHandler( 312 | 400, 313 | 'Checksum of address is invalid, please check it', 314 | [] 315 | ) 316 | ); 317 | } 318 | 319 | try { 320 | const challenge = generateChallenge(req.params.address); 321 | const userAccount = await new Account().getUserLink(req.params.address); 322 | return res.status(200).json({ 323 | challenge, 324 | existing: !!userAccount, 325 | }); 326 | } catch (e) { 327 | logger.error(e); 328 | return next(new ErrorHandler(400, 'Failed to generate the challenge')); 329 | } 330 | }, 331 | 332 | async loginWeb3(req, res, next) { 333 | console.log('Starting web3 log-in'); 334 | const errors = validationResult(req); 335 | if (!errors.isEmpty()) { 336 | return next(new ErrorHandler(422, errors)); 337 | } 338 | 339 | const { address, signResponse, challenge, username, ref, sid, cid, recaptchaToken } = req.body; 340 | 341 | const verified = verifyChallengeResponse(address, challenge, signResponse); 342 | if (!verified) { 343 | return next(new ErrorHandler(401, 'Failed to verify signer')); 344 | } 345 | 346 | try { 347 | const user = await userService.processWeb3Login( 348 | address, 349 | username, 350 | ref, sid, cid, 351 | recaptchaToken, 352 | ); 353 | 354 | if (req.query?.admin === 'true' && !user.admin) { 355 | return next(new ErrorHandler(401, 'Failed to login')); 356 | } 357 | 358 | if (isUserBanned(user)) { 359 | return next(new BannedError(user)); 360 | } 361 | 362 | const token = await authService.generateJwt(user); 363 | 364 | return res.status(200).json({ 365 | session: token, 366 | userId: user.id, 367 | admin: user.admin, 368 | shouldAcceptToS: hasAcceptedLatestConsent(user), 369 | }); 370 | } catch (e) { 371 | logger.error(e); 372 | return next(new ErrorHandler(401, 'Failed to login')); 373 | } 374 | }, 375 | 376 | async verifyEmail(req, res, next) { 377 | try { 378 | const user = await userApi.verifyEmail(req.body.email); 379 | if (!user) return next(new ErrorHandler(404, "Couldn't find user")); 380 | return res.status(200).send(); 381 | } catch (err) { 382 | logger.error(err); 383 | return res.status(500).send(); 384 | } 385 | }, 386 | 387 | /** Handler to acutally reset your password */ 388 | async resetPassword(req, res, next) { 389 | try { 390 | // get user 391 | const user = await userApi.getUserByIdEmailPhoneOrUsername(req.body.email); 392 | if (!user) return next(new ErrorHandler(404, "Couldn't find user")); 393 | 394 | // check if token matches 395 | if (user.passwordResetToken !== req.body.passwordResetToken) { 396 | logger.error(`Expected ${user.passwordResetToken} password token but got ${req.body.passwordResetToken}`); 397 | return next(new ErrorHandler(401, 'Token not valid')); 398 | } 399 | 400 | // check if email matches 401 | if (user.email !== req.body.email) { 402 | return next(new ErrorHandler(401, 'Emails do not match')); 403 | } 404 | 405 | // check if given passwords match 406 | if (req.body.password !== req.body.passwordConfirmation) { 407 | return next(new ErrorHandler(401, 'Passwords do not match')); 408 | } 409 | 410 | user.password = await bcrypt.hash(req.body.password, 8); 411 | user.passwordResetToken = undefined; 412 | await user.save(); 413 | 414 | amqp.send( 415 | 'universal_events', 416 | 'event.user_changed_password', 417 | JSON.stringify({ 418 | event: notificationEvents.EVENT_USER_CHANGED_PASSWORD, 419 | producer: 'user', 420 | producerId: user._id, 421 | data: { 422 | email: user.email, 423 | passwordResetToken: req.body.passwordResetToken, 424 | }, 425 | }) 426 | ); 427 | 428 | return res.status(200).send(); 429 | } catch (err) { 430 | logger.error(err); 431 | return res.status(500).send(); 432 | } 433 | }, 434 | 435 | /** Hanlder to init the "I've forgot my passwort" process */ 436 | async forgotPassword(req, res, next) { 437 | try { 438 | const user = await userApi.getUserByIdEmailPhoneOrUsername(req.body.email); 439 | if (!user) { 440 | console.log('ERROR', 'Forgot password: User not found ', req.body); 441 | return next(new ErrorHandler(404, "Couldn't find user")); 442 | } 443 | 444 | const passwordResetToken = generate(10); 445 | const resetPwUrl = `${process.env.CLIENT_URL}/reset-password?email=${user.email}&passwordResetToken=${passwordResetToken}`; 446 | 447 | user.passwordResetToken = passwordResetToken; 448 | await user.save(); 449 | await mailService.sendPasswordResetMail(user.email, resetPwUrl); 450 | 451 | amqp.send( 452 | 'universal_events', 453 | 'event.user_forgot_password', 454 | JSON.stringify({ 455 | event: notificationEvents.EVENT_USER_FORGOT_PASSWORD, 456 | producer: 'user', 457 | producerId: user._id, 458 | data: { 459 | email: user.email, 460 | passwordResetToken: user.passwordResetToken, 461 | }, 462 | }) 463 | ); 464 | 465 | return res.status(200).send(); 466 | } catch (err) { 467 | logger.error(err); 468 | return res.status(500).send(); 469 | } 470 | }, 471 | }; 472 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Crash Game 2 | 3 | This is the backend code for Crash Game. 4 | 5 | # Usage 6 | 7 | Step 1: Download repo and install modules 8 | 9 | ```bash 10 | git clone https://github.com/wholespace214/crash-game-backend 11 | cd crash-game-backend 12 | npm install 13 | ``` 14 | 15 | Step 2: Start the docker containers needed 16 | 17 | ``` 18 | docker-compose -f docker/docker-compose.yml up -d 19 | ``` 20 | 21 | Step 3: Configure the mongo container for replication 22 | 23 | ``` 24 | docker exec -it mongodb-wall bash 25 | 26 | mongo -u wallfair -p wallfair admin 27 | use wallfair 28 | 29 | rs.initiate( { 30 | _id : "rs0", 31 | members: [ 32 | { _id: 0, host: "localhost:27017" }, 33 | ] 34 | }); 35 | ``` 36 | 37 | Step 4: Run the postgresql config 38 | 39 | ``` 40 | docker exec -it docker_postgres_1 bash 41 | psql -U postgres testdb 42 | 43 | CREATE TABLE IF NOT EXISTS token_transactions (ID SERIAL PRIMARY KEY, sender varchar(255) not null, receiver varchar(255) not null, amount bigint not null, symbol varchar(255) not null, trx_timestamp timestamp not null); 44 | CREATE TABLE IF NOT EXISTS token_balances (owner varchar(255) not null, balance bigint not null, symbol varchar(255) not null, last_update timestamp not null, PRIMARY KEY(owner, symbol)); 45 | CREATE TABLE IF NOT EXISTS bet_reports (bet_id varchar(255) not null PRIMARY KEY, reporter varchar(255) not null, outcome smallint not null, report_timestamp timestamp not null); 46 | CREATE TABLE IF NOT EXISTS amm_interactions (ID SERIAL PRIMARY KEY, buyer varchar(255) NOT NULL, bet varchar(255) NOT NULL, outcome smallint NOT NULL, direction varchar(10) NOT NULL, investmentAmount bigint NOT NULL, feeAmount bigint NOT NULL, outcomeTokensBought bigint NOT NULL, trx_timestamp timestamp NOT NULL); 47 | CREATE TABLE IF NOT EXISTS casino_trades (ID SERIAL PRIMARY KEY, userId varchar(255) NOT NULL, crashFactor decimal NOT NULL, stakedAmount bigint NOT NULL, state smallint NOT NULL, gameId varchar(255), created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP); 48 | ``` 49 | 50 | Step 5: Create a `.env` file (see `.env.example`) and start the server with: 51 | 52 | ``` 53 | # copy .env file from email 54 | npm run start 55 | ``` 56 | 57 | # Api-Endpoints 58 | 59 | ### Auth 60 | 61 | Header: 62 | "Authorization: Bearer jwtToken" 63 | 64 | ## User Endpoints 65 | ### POST http://localhost:8000/api/user/login 66 | 67 | Request: 68 | ```json 69 | { 70 | "phone": "+49123123123" 71 | } 72 | ``` 73 | 74 | Successful Result: 75 | ```json 76 | { 77 | "phone": "+49123123123", 78 | "smsStatus": "pending" 79 | } 80 | ``` 81 | 82 | ### POST http://localhost:8000/api/user/verifyLogin 83 | 84 | Request: 85 | ```json 86 | { 87 | "phone": "+49123123123", 88 | "smsToken": "013416" 89 | } 90 | ``` 91 | 92 | Successful Result: 93 | ```json 94 | { 95 | "userId": "608ae87f8e78eb0224ad3e28", 96 | "phone": "+49123123123", 97 | "name*": "Max", 98 | "email*": "max@max.de", 99 | "session": "jwtToken" 100 | } 101 | ``` 102 | 103 | *Optional only if provided 104 | 105 | 106 | ### GET http://localhost:8000/api/user/:userId 107 | 108 | Successful Result: 109 | ```json 110 | { 111 | "userId": "608ae87f8e78eb0224ad3e28", 112 | "name*": "Max", 113 | "profilePictureUrl*": "https://previewImageUrl.asd", 114 | "balance": 900 115 | } 116 | ``` 117 | 118 | ### GET http://localhost:8000/api/user/history 119 | 120 | Successful Result: 121 | ```json 122 | [ 123 | { 124 | "id": 1, 125 | "buyer": "60cf4588bf102d5fc4c1b475", 126 | "bet": "60cf46a61ef95168648364ab", 127 | "outcome": "yes", 128 | "direction": "BUY", 129 | "investmentamount": "100000", 130 | "feeamount": "1000", 131 | "outcometokensbought": "197902", 132 | "trx_timestamp": "2021-06-20T13:47:38.937Z" 133 | }, 134 | { 135 | "id": 2, 136 | "buyer": "60cf4588bf102d5fc4c1b475", 137 | "bet": "60cf46a61ef95168648364ab", 138 | "outcome": "yes", 139 | "direction": "SELL", 140 | "investmentamount": "49541", 141 | "feeamount": "1000", 142 | "outcometokensbought": "100000", 143 | "trx_timestamp": "2021-06-20T19:20:08.487Z" 144 | } 145 | ] 146 | ``` 147 | 148 | 149 | ### GET http://localhost:8000/api/user/refList 150 | 151 | Successful Result: 152 | ```json 153 | { 154 | "userId": "60b50d820619b44617959d43", 155 | "refList": [ 156 | { 157 | "id": "60b50d820619b44617959d43", 158 | "name": "Nicholas", 159 | "email": "nicholas@wallfair.io" 160 | } 161 | ] 162 | } 163 | ``` 164 | 165 | *Optional only if provided 166 | 167 | 168 | ## Event Endpoints 169 | ### GET http://localhost:8000/api/event/get/ID 170 | 171 | Successful Result: 172 | ```json 173 | { 174 | "_id": "6091c24cae92745024088c74", 175 | "title": "test", 176 | "liveMode": true, 177 | "liveStreamUrl": "https://www.google.de/", 178 | "endDate": "1999-12-31T23:00:00.000Z", 179 | "date": "2021-05-04T21:53:16.734Z", 180 | "__v": 0 181 | } 182 | ``` 183 | 184 | ### GET http://localhost:8000/api/event/list 185 | 186 | Successful Result: 187 | ```json 188 | [ 189 | { 190 | "bets": [ 191 | { 192 | "_id": "60a7ff5364dee4f956660797", 193 | "marketQuestion": "Wer gewinnt Redbull", 194 | "hot": true, 195 | "outcomes": [ 196 | { 197 | "index": 0, 198 | "name": "Jonas" 199 | }, 200 | { 201 | "index": 0, 202 | "name": "Jörn" 203 | } 204 | ], 205 | "event": "60a7f9bdc0a1a7f8913b4a23", 206 | "creator": "60a35b31bbb1f700155f2066", 207 | "date": "2021-05-21T18:43:31.908Z", 208 | "__v": 0 209 | }, 210 | { 211 | "_id": "60a7ffb464dee4f956660799", 212 | "marketQuestion": "Wer gewinnt Redbull2", 213 | "hot": true, 214 | "outcomes": [ 215 | { 216 | "index": 0, 217 | "name": "Jonas" 218 | }, 219 | { 220 | "index": 0, 221 | "name": "Jörn" 222 | } 223 | ], 224 | "event": "60a7f9bdc0a1a7f8913b4a23", 225 | "creator": "60a35b31bbb1f700155f2066", 226 | "date": "2021-05-21T18:45:08.324Z", 227 | "__v": 0 228 | } 229 | ], 230 | "_id": "60a7f9bdc0a1a7f8913b4a23", 231 | "name": "Redbull", 232 | "tags": [ 233 | { 234 | "_id": "60a7f9bdc0a1a7f8913b4a24", 235 | "name": "jo" 236 | }, 237 | { 238 | "_id": "60a7f9bdc0a1a7f8913b4a25", 239 | "name": "joooo" 240 | } 241 | ], 242 | "previewImageUrl": "https://previewImageUrl.asd", 243 | "streamUrl": "https://google.com", 244 | "date": "2021-05-21T18:19:41.671Z", 245 | "__v": 2 246 | } 247 | ] 248 | ``` 249 | 250 | ### GET http://localhost:8000/api/event/list/:type/:category/:count/:page/:sortby/:searchQuery 251 | 252 | * :type can be 'all', 'streamed', 'non-streamed', 'game' 253 | * :category can be 'all', 'streamed-esports', 'streamed-shooter', 'streamed-mmorpg', 'streamed-other', 'sports', 'politics', 'crypto', 'celebrities', 'other' 254 | * :searchQuery is optional 255 | * :page is 1-based 256 | * :sortby is an Event property to be used in mongoose syntax (ex: name (asc), -name (desc)) 257 | 258 | Successful Result: 259 | ```json 260 | [ 261 | { 262 | "_id": "6107e58bf0a40958ecaab7f3", 263 | "bets": [ 264 | "6107e5c9f0a40958ecaab932", 265 | "6107e704f0a40958ecaac05a" 266 | ], 267 | "name": "FIFA Match CyrusTwo", 268 | "streamUrl": "...", 269 | "previewImageUrl": "...", 270 | "tags": [ 271 | { 272 | "_id": "6107e58bf0a40958ecaab7f4", 273 | "name": "fifa" 274 | }, 275 | { 276 | "_id": "6107e58bf0a40958ecaab7f5", 277 | "name": "soccer" 278 | } 279 | ], 280 | "date": "2021-08-02T22:00:00.000Z", 281 | "__v": 2, 282 | "category": "Esports", 283 | "type": "streamed" 284 | } 285 | ] 286 | ``` 287 | 288 | ### POST http://localhost:8000/api/event/create 289 | 290 | Request: 291 | ```json 292 | { 293 | "name": "Redbull", 294 | "tags": [ 295 | { "name": "jo" }, 296 | { "name": "joooo" } 297 | ], 298 | "streamUrl": "https://google.com", 299 | "previewImageUrl": "https://previewImageUrl.asd" 300 | } 301 | ``` 302 | 303 | Successful Result: 304 | ```json 305 | { 306 | "_id": "60a7f9bdc0a1a7f8913b4a23", 307 | "name": "Redbull", 308 | "tags": [ 309 | { 310 | "_id": "60a7f9bdc0a1a7f8913b4a24", 311 | "name": "jo" 312 | }, 313 | { 314 | "_id": "60a7f9bdc0a1a7f8913b4a25", 315 | "name": "joooo" 316 | } 317 | ], 318 | "previewImageUrl": "https://previewImageUrl.asd", 319 | "streamUrl": "https://google.com", 320 | "bets": [], 321 | "date": "2021-05-21T18:19:41.671Z", 322 | "__v": 0 323 | } 324 | ``` 325 | 326 | ## Bet Endpoints 327 | ### POST http://localhost:8000/api/event/bet/create 328 | 329 | Request: 330 | ```json 331 | { 332 | "eventId": "60a7f9bdc0a1a7f8913b4a23", 333 | "marketQuestion": "Wer gewinnt Redbull", 334 | "hot": true, 335 | "outcomes": [ 336 | { 337 | "index": 0, 338 | "name": "Jonas" 339 | }, 340 | { 341 | "index": 0, 342 | "name": "Jörn" 343 | } 344 | ], 345 | "endDate": "1621622318001" 346 | } 347 | ``` 348 | 349 | Successful Result: 350 | ```json 351 | { 352 | "bets": [ 353 | { 354 | "_id": "60a7ff5364dee4f956660797", 355 | "marketQuestion": "Wer gewinnt Redbull", 356 | "hot": true, 357 | "outcomes": [ 358 | { 359 | "index": 0, 360 | "name": "Jonas" 361 | }, 362 | { 363 | "index": 0, 364 | "name": "Jörn" 365 | } 366 | ], 367 | "event": "60a7f9bdc0a1a7f8913b4a23", 368 | "creator": "60a35b31bbb1f700155f2066", 369 | "date": "2021-05-21T18:43:31.908Z", 370 | "__v": 0 371 | }, 372 | { 373 | "_id": "60a7ffb464dee4f956660799", 374 | "marketQuestion": "Wer gewinnt Redbull2", 375 | "hot": true, 376 | "outcomes": [ 377 | { 378 | "index": 0, 379 | "name": "Jonas" 380 | }, 381 | { 382 | "index": 0, 383 | "name": "Jörn" 384 | } 385 | ], 386 | "event": "60a7f9bdc0a1a7f8913b4a23", 387 | "creator": "60a35b31bbb1f700155f2066", 388 | "date": "2021-05-21T18:45:08.324Z", 389 | "__v": 0 390 | } 391 | ], 392 | "_id": "60a7f9bdc0a1a7f8913b4a23", 393 | "name": "Redbull", 394 | "tags": [ 395 | { 396 | "_id": "60a7f9bdc0a1a7f8913b4a24", 397 | "name": "jo" 398 | }, 399 | { 400 | "_id": "60a7f9bdc0a1a7f8913b4a25", 401 | "name": "joooo" 402 | } 403 | ], 404 | "previewImageUrl": "https://previewImageUrl.asd", 405 | "streamUrl": "https://google.com", 406 | "date": "2021-05-21T18:19:41.671Z", 407 | "__v": 2 408 | } 409 | ``` 410 | 411 | ### POST http://localhost:8000/api/event/bet/:id/place 412 | Request: 413 | ```json 414 | { 415 | "amount": 10, 416 | "outcome": 1, 417 | "minOutcomeTokens*": 400 418 | } 419 | ``` 420 | 421 | *Optional 422 | 423 | Successful Result: 424 | ```json 425 | { 426 | "_id": "60a7ff5364dee4f956660797", 427 | "marketQuestion": "Wer gewinnt Redbull", 428 | "hot": true, 429 | "outcomes": [ 430 | { 431 | "index": 0, 432 | "name": "Jonas" 433 | }, 434 | { 435 | "index": 0, 436 | "name": "Jörn" 437 | } 438 | ], 439 | "event": "60a7f9bdc0a1a7f8913b4a23", 440 | "creator": "60a35b31bbb1f700155f2066", 441 | "date": "2021-05-21T18:43:31.908Z", 442 | "__v": 0 443 | } 444 | ``` 445 | 446 | ### POST http://localhost:8000/api/event/bet/:id/pullout 447 | Request: 448 | ```json 449 | { 450 | "amount": 10, 451 | "outcome": 1, 452 | "minReturnAmount*": 400 453 | } 454 | ``` 455 | 456 | *Optional 457 | 458 | Successful Result: 459 | ```json 460 | { 461 | "_id": "60a7ff5364dee4f956660797", 462 | "marketQuestion": "Wer gewinnt Redbull", 463 | "hot": true, 464 | "outcomes": [ 465 | { 466 | "index": 0, 467 | "name": "Jonas" 468 | }, 469 | { 470 | "index": 0, 471 | "name": "Jörn" 472 | } 473 | ], 474 | "event": "60a7f9bdc0a1a7f8913b4a23", 475 | "creator": "60a35b31bbb1f700155f2066", 476 | "date": "2021-05-21T18:43:31.908Z", 477 | "__v": 0 478 | } 479 | ``` 480 | 481 | ### POST http://localhost:8000/api/event/bet/:id/outcomes/buy 482 | Request: 483 | ```json 484 | { 485 | "amount": 10 486 | } 487 | ``` 488 | Der "amount" ist in WFAIR andgegeben 489 | 490 | Successful Result: 491 | ```json 492 | [ 493 | { 494 | "index": 0, 495 | "outcome": 9.10 496 | }, 497 | { 498 | "index": 1, 499 | "outcome": 9.21 500 | } 501 | ] 502 | ``` 503 | 504 | ### POST http://localhost:8000/api/event/bet/:id/outcomes/sell 505 | Request: 506 | ```json 507 | { 508 | "amount": 10 509 | } 510 | ``` 511 | Der "amount" ist in Outcome-Token (Potential Winnings) andgegeben 512 | 513 | Successful Result: 514 | ```json 515 | [ 516 | { 517 | "index": 0, 518 | "outcome": 9.10 519 | }, 520 | { 521 | "index": 1, 522 | "outcome": 9.21 523 | } 524 | ] 525 | ``` 526 | 527 | ### GET http://localhost:8000/api/event/bet/:id/payout 528 | 529 | Successful Result: 530 | ```json 531 | { 532 | "_id": "60a7ff5364dee4f956660797", 533 | "marketQuestion": "Wer gewinnt Redbull", 534 | "hot": true, 535 | "outcomes": [ 536 | { 537 | "index": 0, 538 | "name": "Jonas" 539 | }, 540 | { 541 | "index": 1, 542 | "name": "Jörn" 543 | } 544 | ], 545 | "event": "60a7f9bdc0a1a7f8913b4a23", 546 | "creator": "60a35b31bbb1f700155f2066", 547 | "date": "2021-05-21T18:43:31.908Z", 548 | "__v": 0 549 | } 550 | ``` 551 | 552 | 553 | ### GET http://localhost:8000/api/user/confirm-email/?userId=${userId}&code=${code} 554 | 555 | Successful Result: 556 | ```json 557 | {"status":"OK"} 558 | ``` 559 | Error Results: 560 | ```json 561 | { 562 | "errors": [ 563 | { 564 | "msg": "Invalid value", 565 | "param": "userId", 566 | "location": "body" 567 | }, 568 | { 569 | "msg": "Invalid value", 570 | "param": "code", 571 | "location": "body" 572 | } 573 | ] 574 | } 575 | ``` 576 | ```json 577 | { 578 | "error": "EMAIL_ALREADY_CONFIRMED", 579 | "message": "The email has already been confirmed!" 580 | } 581 | ``` 582 | ```json 583 | { 584 | "error": "INVALID_EMAIL_CODE", 585 | "message": "The email code is invalid!" 586 | } 587 | ``` 588 | ### GET http://localhost:8000/api/user/resend-confirm/ 589 | Successful Result: 590 | ```json 591 | {"status":"OK"} 592 | ``` 593 | 594 | ### POST http://localhost:8000/api/event/extract/twitch 595 | ```json 596 | { 597 | "streamUrl": "https://www.twitch.tv/chess" 598 | } 599 | ``` 600 | 601 | Successful Result: 602 | ```json 603 | { 604 | "name": "Chess", 605 | "previewImageUrl": "https://static-cdn.jtvnw.net/jtv_user_pictures/6eb1c704-e4b4-4269-9e26-ec762668fb79-channel_offline_image-1920x1080.png", 606 | "streamUrl": "https://www.twitch.tv/chess", 607 | "tags": [ 608 | { 609 | "name": "Esports" 610 | }, 611 | { 612 | "name": "English" 613 | } 614 | ], 615 | "date": 1629448490358, 616 | "type": "streamed", 617 | "category": "Chess" 618 | } 619 | ``` 620 | 621 | ### GET http://localhost:8000/api/rewards/questions 622 | Successful Result: 623 | ```json 624 | [ 625 | { 626 | "closed": false, 627 | "_id": "613efc97cbad81c04dbf7198", 628 | "title": "Do you like wallfair?", 629 | "questions": [ 630 | { 631 | "index": 0, 632 | "name": "Yes", 633 | "imageUrl": "https://photostorage.mock/457y8hurbge8h79j2w8" 634 | }, 635 | { 636 | "index": 1, 637 | "name": "No", 638 | "imageUrl": "https://photostorage.mock/457f87h7n4789fh3nw8" 639 | } 640 | ], 641 | "createdAt": "1631517847345", 642 | "closedAt": "1631517847345" 643 | } 644 | ] 645 | ``` 646 | 647 | ### POST http://localhost:8000/api/rewards/answer 648 | ```json 649 | { 650 | "answerId": 1, 651 | "questionId": "613efc97cbad81c04dbf7198", 652 | } 653 | ``` 654 | 655 | Successful Result: Lottery Ticket ID 656 | ```json 657 | { 658 | "id":"613f0bc91612edc558d0e5c9" 659 | } 660 | ``` 661 | 662 | ### GET http://localhost:8000/api/bet-template 663 | 664 | Successful Result: Lottery Ticket ID 665 | ```json 666 | [{ 667 | "_id": "613f0bc91612edc558d0e5c9", 668 | "marketQuestion": "Who will win?", 669 | "name": "winner template", 670 | "category": "counter-strike", 671 | "outcomes": [ 672 | { 673 | "index": 0, 674 | "name": "Team A", 675 | }, 676 | { 677 | "index": 1, 678 | "name": "Team B", 679 | } 680 | ] 681 | }] 682 | ``` 683 | 684 | 685 | # Auth Endpoints 686 | ### POST http://localhost:8000/api/auth/login 687 | ```json 688 | { 689 | "username": "foo", 690 | "password": "bar", 691 | } 692 | ``` 693 | Successful Result: 694 | ```json 695 | [ 696 | { 697 | "userId": "613efc97cbad81c04dbf7198", 698 | "session": "" 699 | } 700 | ] 701 | ``` 702 | ### POST http://localhost:8000/api/auth/sign-up 703 | ```json 704 | { 705 | "username": "foo", 706 | "password": "bar", 707 | "passwordConfirm": "bar", 708 | } 709 | ``` 710 | Successful Result: 711 | ```json 712 | [ 713 | { 714 | "userId": "613efc97cbad81c04dbf7198", 715 | "email": "user@example.com" 716 | } 717 | ] 718 | ``` 719 | ### POST http://localhost:8000/api/auth/verify-email 720 | ```json 721 | { 722 | "email": "user@example.com" 723 | } 724 | ``` 725 | ### POST http://localhost:8000/api/auth/reset-password 726 | ```json 727 | { 728 | "email": "user@example.com" 729 | } 730 | ``` 731 | ### GET http://localhost:8000/api/user/:userId/stats 732 | Example response 733 | ```json 734 | { 735 | "userId": "6167e53e758b98bbbccf11d3", 736 | "username": "tester50", 737 | "stats": { 738 | "casinoGamePlayCount": 0, 739 | "casinoGameCashoutCount": 0, 740 | "casinoGamesAmountWon": { 741 | "totalWon": 0, 742 | "totalReward": 0, 743 | "totalStaked": 0 744 | }, 745 | "casinoGamesAmountLost": 0, 746 | "userBetsAmount": { 747 | "totalBettedAmount": 20035, 748 | "totalBets": 50, 749 | "totalOutcomeAmount": 39575.6393 750 | }, 751 | "userBetsCashouts": { 752 | "totalAmount": 15300.8771, 753 | "totalCashouts": 8 754 | }, 755 | "userBetsRewards": { 756 | "totalWonAmount": 0, 757 | "totalRewards": 0 758 | } 759 | } 760 | } 761 | ``` 762 | 763 | ## Example link with email confirmation: 764 | ```txt 765 | http://localhost:8000/api/user/confirm-email/?userId=615c3d8d8b6d8bc4066046b1&code=555555 766 | ``` 767 | --------------------------------------------------------------------------------