├── .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