├── .dockerignore ├── Procfile ├── bot └── src │ ├── chains │ ├── index.ts │ ├── agoric.ts │ ├── desmos.ts │ ├── osmosis.ts │ ├── akash.ts │ ├── band.ts │ ├── comdex.ts │ ├── cosmos.ts │ ├── emoney.ts │ ├── evmos.ts │ ├── juno.ts │ ├── regen.ts │ ├── rizon.ts │ ├── secret.ts │ ├── terra.ts │ ├── bitsong.ts │ ├── cheqd.ts │ ├── crescent.ts │ ├── sifchain.ts │ ├── stargaze.ts │ ├── likecoin.ts │ ├── assetMantle.ts │ ├── provenance.ts │ ├── chihuahua.ts │ ├── persistance.ts │ ├── config.ts │ └── junoCW20.ts │ ├── menu │ ├── utils │ │ ├── index.ts │ │ └── keyboards.ts │ ├── callbacks │ │ ├── csv_example.png │ │ ├── index.ts │ │ ├── dailyReminder.callbacks.ts │ │ ├── wallet.callbacks.ts │ │ ├── networkStatistic.callback.ts │ │ ├── alarm.callbacks.ts │ │ └── assets.callbacks.ts │ ├── notification │ │ ├── index.ts │ │ ├── alarm.menu.ts │ │ ├── dailyReminder.menu.ts │ │ ├── notification.menu.ts │ │ ├── timezone.menu.ts │ │ ├── networksReminder.menu.ts │ │ ├── timeReminder.menu.ts │ │ ├── proposal.menu.ts │ │ └── networksAlarm.menu.ts │ ├── index.ts │ ├── networksStatistic.menu.ts │ ├── wallet.menu.ts │ ├── assets.menu.ts │ ├── walletRemove.menu.ts │ ├── networksResources.menu.ts │ └── networksProposals.menu.ts │ ├── api │ ├── index.ts │ ├── handlers │ │ ├── index.ts │ │ ├── getProposals.ts │ │ ├── getTokenPrice.ts │ │ ├── getStatistic.ts │ │ └── getBalance.ts │ └── requests │ │ ├── index.ts │ │ ├── fetchProposal.ts │ │ ├── fetchTokenPrice.ts │ │ ├── fetchBalance.ts │ │ └── fetchStatistic.ts │ ├── transformers │ ├── index.ts │ └── apiCallsLogger.transformer.ts │ ├── utils │ ├── isNumber.ts │ ├── capitalizeFirstLetter.ts │ ├── calcTVLPercent.ts │ ├── template.ts │ ├── formatTokenPrice.ts │ ├── restRequest.ts │ ├── getCW20tokens.ts │ ├── getPnlDate.ts │ ├── getFilterDenom.ts │ ├── nFormatter.ts │ ├── getBlocksPerYearReal.ts │ ├── validation.ts │ ├── getEmoji.ts │ ├── calculateApr.ts │ ├── index.ts │ ├── formatToken.ts │ ├── menuCreator.ts │ ├── crypto.ts │ └── loadAddresses.ts │ ├── constants │ ├── keyboard.ts │ ├── step.ts │ ├── regex.ts │ ├── proposalStatus.ts │ ├── api.ts │ ├── route.ts │ ├── en.ts │ └── country.ts │ ├── types │ ├── index.ts │ ├── context.ts │ └── general.ts │ ├── redis.ts │ ├── middlewares │ ├── setupRouter.middleware.ts │ ├── updatesLogger.middleware.ts │ ├── setupLogger.middleware.ts │ ├── setupLocalContext.middleware.ts │ ├── setUser.middleware.ts │ ├── collectMetrics.middleware.ts │ ├── index.ts │ ├── setupSession.middleware.ts │ └── setupWalletsMigrate.middleware.ts │ ├── dao │ ├── resources.dao.ts │ ├── networks.dao.ts │ ├── alarms.dao.ts │ ├── notifications.dao.ts │ ├── users.dao.ts │ ├── alarmPrices.dao.ts │ ├── networksInNotification.dao.ts │ ├── wallets.dao.ts │ └── index.ts │ ├── context.ts │ ├── helpers │ ├── gracefulShutdownHandler.ts │ ├── errorHandler.ts │ └── logging.ts │ ├── services │ ├── resources.service.ts │ ├── index.ts │ ├── alarmPrices.service.ts │ ├── networks.service.ts │ ├── users.service.ts │ ├── alarms.service.ts │ ├── wallets.service.ts │ ├── networksInNotification.service.ts │ └── notifications.service.ts │ ├── features │ ├── help.feature.ts │ ├── start.feature.ts │ ├── support.feature.ts │ ├── reset.feature.ts │ ├── about.feature.ts │ ├── proposal.feature.ts │ ├── resources.feature.ts │ ├── statistic.feature.ts │ ├── index.ts │ ├── assets.feature.ts │ ├── botAdmin.feature.ts │ ├── notification.feature.ts │ └── wallet.feature.ts │ ├── metrics.ts │ ├── config.ts │ ├── logger.ts │ └── bot.ts ├── .husky └── pre-commit ├── .example.postgres.env ├── server ├── assets │ ├── csv_example.png │ └── logo_small.png ├── types.ts ├── telegram.ts ├── run.ts ├── prisma.ts ├── server.ts └── cron.ts ├── prisma ├── migrations │ ├── migration_lock.toml │ └── 20211221124412_initial │ │ └── migration.sql └── schema.prisma ├── .babelrc ├── .example.bot.env ├── .github └── renovate.json ├── tsconfig.json ├── .eslintrc.json ├── .gitignore ├── package.json └── README.md /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web:node dist/server/run.js 2 | -------------------------------------------------------------------------------- /bot/src/chains/index.ts: -------------------------------------------------------------------------------- 1 | export { config } from "./config"; 2 | -------------------------------------------------------------------------------- /bot/src/menu/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { agreementKeyboard } from "./keyboards"; 2 | -------------------------------------------------------------------------------- /bot/src/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./handlers"; 2 | export * from "./requests"; 3 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.example.postgres.env: -------------------------------------------------------------------------------- 1 | POSTGRES_USER=bot 2 | POSTGRES_DB=bot 3 | POSTGRES_PASSWORD=bot 4 | PGDATA=/data/postgres -------------------------------------------------------------------------------- /bot/src/transformers/index.ts: -------------------------------------------------------------------------------- 1 | export { transformer as apiCallsLogger } from "./apiCallsLogger.transformer"; 2 | -------------------------------------------------------------------------------- /server/assets/csv_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonRoyenko/cosmos-space-bot/HEAD/server/assets/csv_example.png -------------------------------------------------------------------------------- /server/assets/logo_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonRoyenko/cosmos-space-bot/HEAD/server/assets/logo_small.png -------------------------------------------------------------------------------- /bot/src/utils/isNumber.ts: -------------------------------------------------------------------------------- 1 | export function isNumber(n: string) { 2 | return !isNaN(parseFloat(n)) && isFinite(Number(n)); 3 | } 4 | -------------------------------------------------------------------------------- /bot/src/menu/callbacks/csv_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonRoyenko/cosmos-space-bot/HEAD/bot/src/menu/callbacks/csv_example.png -------------------------------------------------------------------------------- /bot/src/constants/keyboard.ts: -------------------------------------------------------------------------------- 1 | export const KEYBOARD = { 2 | TEXT_YES: "Yes", 3 | TEXT_NO: "No", 4 | CALLBACK_YES: "yes", 5 | CALLBACK_NO: "no", 6 | }; 7 | -------------------------------------------------------------------------------- /bot/src/utils/capitalizeFirstLetter.ts: -------------------------------------------------------------------------------- 1 | export function capitalizeFirstLetter(string: string) { 2 | return string.charAt(0).toUpperCase() + string.slice(1); 3 | } 4 | -------------------------------------------------------------------------------- /bot/src/api/handlers/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./getProposals"; 2 | export * from "./getBalance"; 3 | export * from "./getTokenPrice"; 4 | export * from "./getStatistic"; 5 | -------------------------------------------------------------------------------- /bot/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export { Context, LocalContextFlavor } from "./context"; 2 | 3 | export type DeepPartial = { 4 | [P in keyof T]?: DeepPartial; 5 | }; 6 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /bot/src/api/requests/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./fetchProposal"; 2 | export * from "./fetchStatistic"; 3 | export * from "./fetchBalance"; 4 | export * from "./fetchTokenPrice"; 5 | -------------------------------------------------------------------------------- /bot/src/redis.ts: -------------------------------------------------------------------------------- 1 | import Redis from "ioredis"; 2 | 3 | import { config } from "@bot/config"; 4 | 5 | export const connection = new Redis(config.REDIS_URL, { 6 | maxRetriesPerRequest: null, 7 | }); 8 | -------------------------------------------------------------------------------- /bot/src/middlewares/setupRouter.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "@grammyjs/router"; 2 | import { Context } from "@bot/types"; 3 | 4 | export const middleware = new Router((ctx) => ctx.session.step); 5 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": [ 4 | [ 5 | "@babel/plugin-transform-react-jsx", 6 | { 7 | "runtime": "automatic" 8 | } 9 | ] 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /bot/src/menu/callbacks/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./alarm.callbacks"; 2 | export * from "./dailyReminder.callbacks"; 3 | export * from "./assets.callbacks"; 4 | export * from "./networkStatistic.callback"; 5 | export * from "./wallet.callbacks"; 6 | -------------------------------------------------------------------------------- /bot/src/utils/calcTVLPercent.ts: -------------------------------------------------------------------------------- 1 | import Big from "big.js"; 2 | 3 | export const calcTVLPercent = (a: number, b: number) => { 4 | const x = new Big(a); 5 | const y = new Big(b); 6 | 7 | return x.minus(y).div(y).mul(100).toPrecision(); 8 | }; 9 | -------------------------------------------------------------------------------- /bot/src/dao/resources.dao.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient, Prisma } from "@prisma/client"; 2 | 3 | export const createDao = (prisma: PrismaClient) => ({ 4 | getResource: (args: Prisma.ResourceFindUniqueArgs) => 5 | prisma.resource.findUnique(args), 6 | }); 7 | -------------------------------------------------------------------------------- /bot/src/utils/template.ts: -------------------------------------------------------------------------------- 1 | export function template(string: string, vars: { [key: string]: string }) { 2 | let s = string; 3 | for (const prop in vars) { 4 | s = s.replace(new RegExp("%{" + prop + "}", "g"), vars[prop]); 5 | } 6 | return s; 7 | } 8 | -------------------------------------------------------------------------------- /bot/src/utils/formatTokenPrice.ts: -------------------------------------------------------------------------------- 1 | import numeral from "numeral"; 2 | 3 | export const formatTokenPrice = (price?: number | string) => { 4 | if (!price) { 5 | return price; 6 | } 7 | return numeral(numeral(price).format("0.[00]", Math.floor)).value(); 8 | }; 9 | -------------------------------------------------------------------------------- /bot/src/constants/step.ts: -------------------------------------------------------------------------------- 1 | export const STEPS = { 2 | WALLET: "wallet", 3 | ADMIN: "admin", 4 | GOVERNANCE: "governance", 5 | TIMEZONE: "timezone", 6 | NOTIFICATION: "notification", 7 | BULK_IMPORT: "bulkImport", 8 | WALLET_PASSWORD: "walletPassword", 9 | }; 10 | -------------------------------------------------------------------------------- /server/types.ts: -------------------------------------------------------------------------------- 1 | interface postWalletRequest { 2 | Body: { 3 | wallet: string; 4 | name: string; 5 | iv: string; 6 | }; 7 | Params: { 8 | id: number; 9 | }; 10 | } 11 | 12 | interface getSendMessage { 13 | Params: { 14 | id: number; 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /bot/src/constants/regex.ts: -------------------------------------------------------------------------------- 1 | export const deleteAlarmRegex = /^deleteAlarm:/; 2 | // eslint-disable-next-line no-useless-escape 3 | export const alarmPriceRegex = /\d{1,2}([\.,][\d{1,2}])?/g; 4 | export const removeSpace = /\s+/g; 5 | export const cw20line = /^CW20 tokens: <\/b>.*\n\n?/gm; 6 | -------------------------------------------------------------------------------- /bot/src/dao/networks.dao.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient, Prisma } from "@prisma/client"; 2 | 3 | export const createDao = (prisma: PrismaClient) => ({ 4 | getNetwork: (args: Prisma.NetworkFindUniqueArgs) => 5 | prisma.network.findUnique(args), 6 | 7 | getAllNetworks: () => prisma.network.findMany(), 8 | }); 9 | -------------------------------------------------------------------------------- /bot/src/constants/proposalStatus.ts: -------------------------------------------------------------------------------- 1 | export const proposalStatus = { 2 | PROPOSAL_STATUS_VOTING_PERIOD: "PROPOSAL_STATUS_VOTING_PERIOD", 3 | PROPOSAL_STATUS_PASSED: "PROPOSAL_STATUS_PASSED", 4 | PROPOSAL_STATUS_REJECTED: "PROPOSAL_STATUS_REJECTED", 5 | PROPOSAL_STATUS_FAILED: "PROPOSAL_STATUS_FAILED", 6 | }; 7 | -------------------------------------------------------------------------------- /bot/src/context.ts: -------------------------------------------------------------------------------- 1 | import { AsyncLocalStorage } from "async_hooks"; 2 | import { User } from "@prisma/client"; 3 | import { Logger } from "pino"; 4 | 5 | export interface LocalContext { 6 | user?: User; 7 | logger?: Logger; 8 | } 9 | 10 | export const context = new AsyncLocalStorage(); 11 | -------------------------------------------------------------------------------- /bot/src/helpers/gracefulShutdownHandler.ts: -------------------------------------------------------------------------------- 1 | import { bot } from "@bot/bot"; 2 | import { logger } from "@bot/logger"; 3 | import { server } from "@server/server"; 4 | 5 | export const handleGracefulShutdown = async () => { 6 | logger.info("shutdown"); 7 | 8 | await bot.stop(); 9 | await server.close(); 10 | }; 11 | -------------------------------------------------------------------------------- /bot/src/utils/restRequest.ts: -------------------------------------------------------------------------------- 1 | import fetch, { BodyInit } from "node-fetch"; 2 | 3 | export const restRequest = async ( 4 | url: string, 5 | method = "GET", 6 | body?: BodyInit 7 | ) => 8 | await fetch(url, { 9 | method, 10 | headers: { Accept: "application/json", "Content-Type": "application/json" }, 11 | body, 12 | }); 13 | -------------------------------------------------------------------------------- /.example.bot.env: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | LOG_LEVEL=info 3 | CHECKPOINT_DISABLE=1 4 | DATABASE_URL=postgresql://bot:bot@postgres:5432/bot?schema=public 5 | REDIS_URL=redis://redis:6379/0 6 | BOT_SERVER_HOST=0.0.0.0 7 | BOT_SERVER_PORT=80 8 | BOT_ALLOWED_UPDATES=[] 9 | BOT_TOKEN=123:ABCABCD 10 | BOT_WEBHOOK=https://www.example.com/ 11 | BOT_ADMIN_USER_ID=1 -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base", "group:recommended", "group:allNonMajor"], 3 | "packageRules": [ 4 | { 5 | "description": "Automatically merge minor and patch-level updates", 6 | "matchUpdateTypes": ["minor", "patch", "digest"], 7 | "automerge": true, 8 | "automergeType": "branch" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /bot/src/menu/utils/keyboards.ts: -------------------------------------------------------------------------------- 1 | import { KEYBOARD } from "@bot/constants/keyboard"; 2 | import { InlineKeyboard } from "grammy"; 3 | 4 | export const agreementKeyboard = new InlineKeyboard() 5 | .add({ 6 | text: KEYBOARD.TEXT_YES, 7 | callback_data: KEYBOARD.CALLBACK_YES, 8 | }) 9 | .add({ text: KEYBOARD.TEXT_NO, callback_data: KEYBOARD.CALLBACK_NO }); 10 | -------------------------------------------------------------------------------- /bot/src/middlewares/updatesLogger.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Middleware } from "grammy"; 2 | 3 | import { logger } from "@bot/logger"; 4 | import { Context } from "@bot/types"; 5 | 6 | export const middleware = (): Middleware => (ctx, next) => { 7 | logger.debug({ 8 | msg: "update received", 9 | ...ctx.update, 10 | }); 11 | return next(); 12 | }; 13 | -------------------------------------------------------------------------------- /bot/src/transformers/apiCallsLogger.transformer.ts: -------------------------------------------------------------------------------- 1 | import { Transformer } from "grammy"; 2 | 3 | import { logger } from "@bot/logger"; 4 | 5 | export const transformer: Transformer = (prev, method, payload, signal) => { 6 | logger.debug({ 7 | msg: "bot api call", 8 | method, 9 | payload, 10 | }); 11 | return prev(method, payload, signal); 12 | }; 13 | -------------------------------------------------------------------------------- /bot/src/chains/agoric.ts: -------------------------------------------------------------------------------- 1 | import { Bech32Address } from "@keplr-wallet/cosmos"; 2 | 3 | export const agoricConfig = { 4 | network: "agoric", 5 | coingeckoId: "agoric", 6 | prefix: Bech32Address.defaultBech32Config("agoric"), 7 | primaryTokenUnit: "ubld", 8 | tokenUnits: { 9 | ubld: { 10 | display: "bld", 11 | exponent: 6, 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /bot/src/chains/desmos.ts: -------------------------------------------------------------------------------- 1 | import { Bech32Address } from "@keplr-wallet/cosmos"; 2 | 3 | export const desmosConfig = { 4 | network: "desmos", 5 | coingeckoId: "desmos", 6 | prefix: Bech32Address.defaultBech32Config("desmos"), 7 | primaryTokenUnit: "udsm", 8 | tokenUnits: { 9 | udsm: { 10 | display: "dsm", 11 | exponent: 6, 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /bot/src/chains/osmosis.ts: -------------------------------------------------------------------------------- 1 | import { Bech32Address } from "@keplr-wallet/cosmos"; 2 | 3 | export const osmoConfig = { 4 | network: "osmo", 5 | coingeckoId: "osmosis", 6 | prefix: Bech32Address.defaultBech32Config("osmo"), 7 | primaryTokenUnit: "uosmo", 8 | tokenUnits: { 9 | uosmo: { 10 | display: "osmo", 11 | exponent: 6, 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /bot/src/services/resources.service.ts: -------------------------------------------------------------------------------- 1 | import { resourceDao } from "@bot/dao"; 2 | 3 | export const resourcesService = () => { 4 | const getResource = async (resourceId: number) => { 5 | return await resourceDao.getResource({ 6 | where: { 7 | id: resourceId, 8 | }, 9 | }); 10 | }; 11 | 12 | return { 13 | getResource, 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /bot/src/chains/akash.ts: -------------------------------------------------------------------------------- 1 | import { Bech32Address } from "@keplr-wallet/cosmos"; 2 | 3 | export const akashConfig = { 4 | network: "akash", 5 | coingeckoId: "akash-network", 6 | prefix: Bech32Address.defaultBech32Config("akash"), 7 | primaryTokenUnit: "uakt", 8 | tokenUnits: { 9 | uakt: { 10 | display: "akt", 11 | exponent: 6, 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /bot/src/chains/band.ts: -------------------------------------------------------------------------------- 1 | import { Bech32Address } from "@keplr-wallet/cosmos"; 2 | 3 | export const bandConfig = { 4 | network: "band", 5 | coingeckoId: "band-protocol", 6 | prefix: Bech32Address.defaultBech32Config("band"), 7 | primaryTokenUnit: "uband", 8 | tokenUnits: { 9 | uband: { 10 | display: "band", 11 | exponent: 6, 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /bot/src/chains/comdex.ts: -------------------------------------------------------------------------------- 1 | import { Bech32Address } from "@keplr-wallet/cosmos"; 2 | 3 | export const comdexConfig = { 4 | network: "comdex", 5 | coingeckoId: "comdex", 6 | prefix: Bech32Address.defaultBech32Config("comdex"), 7 | primaryTokenUnit: "ucmdx", 8 | tokenUnits: { 9 | ucmdx: { 10 | display: "cmdx", 11 | exponent: 6, 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /bot/src/chains/cosmos.ts: -------------------------------------------------------------------------------- 1 | import { Bech32Address } from "@keplr-wallet/cosmos"; 2 | 3 | export const cosmosConfig = { 4 | network: "cosmos", 5 | coingeckoId: "cosmos", 6 | prefix: Bech32Address.defaultBech32Config("cosmos"), 7 | primaryTokenUnit: "uatom", 8 | tokenUnits: { 9 | uatom: { 10 | display: "atom", 11 | exponent: 6, 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /bot/src/chains/emoney.ts: -------------------------------------------------------------------------------- 1 | import { Bech32Address } from "@keplr-wallet/cosmos"; 2 | 3 | export const emoneyConfig = { 4 | network: "emoney", 5 | coingeckoId: "e-money", 6 | prefix: Bech32Address.defaultBech32Config("emoney"), 7 | primaryTokenUnit: "ungm", 8 | tokenUnits: { 9 | ungm: { 10 | display: "ngm", 11 | exponent: 6, 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /bot/src/chains/evmos.ts: -------------------------------------------------------------------------------- 1 | import { Bech32Address } from "@keplr-wallet/cosmos"; 2 | 3 | export const evmosConfig = { 4 | network: "evmos", 5 | coingeckoId: "evmos", 6 | prefix: Bech32Address.defaultBech32Config("evmos"), 7 | primaryTokenUnit: "aevmos", 8 | tokenUnits: { 9 | aevmos: { 10 | display: "evmos", 11 | exponent: 18, 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /bot/src/chains/juno.ts: -------------------------------------------------------------------------------- 1 | import { Bech32Address } from "@keplr-wallet/cosmos"; 2 | 3 | export const junoConfig = { 4 | network: "juno", 5 | coingeckoId: "juno-network", 6 | prefix: Bech32Address.defaultBech32Config("juno"), 7 | primaryTokenUnit: "ujuno", 8 | tokenUnits: { 9 | ujuno: { 10 | display: "juno", 11 | exponent: 6, 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /bot/src/chains/regen.ts: -------------------------------------------------------------------------------- 1 | import { Bech32Address } from "@keplr-wallet/cosmos"; 2 | 3 | export const regenConfig = { 4 | network: "regen", 5 | coingeckoId: "regen", 6 | prefix: Bech32Address.defaultBech32Config("regen"), 7 | primaryTokenUnit: "uregen", 8 | tokenUnits: { 9 | uregen: { 10 | display: "regen", 11 | exponent: 6, 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /bot/src/chains/rizon.ts: -------------------------------------------------------------------------------- 1 | import { Bech32Address } from "@keplr-wallet/cosmos"; 2 | 3 | export const rizonConfig = { 4 | network: "rizon", 5 | coingeckoId: "rizon", 6 | prefix: Bech32Address.defaultBech32Config("rizon"), 7 | primaryTokenUnit: "uatolo", 8 | tokenUnits: { 9 | uatolo: { 10 | display: "atolo", 11 | exponent: 6, 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /bot/src/chains/secret.ts: -------------------------------------------------------------------------------- 1 | import { Bech32Address } from "@keplr-wallet/cosmos"; 2 | 3 | export const secretConfig = { 4 | network: "secret", 5 | coingeckoId: "secret", 6 | prefix: Bech32Address.defaultBech32Config("secret"), 7 | primaryTokenUnit: "uscrt", 8 | tokenUnits: { 9 | uscrt: { 10 | display: "scrt", 11 | exponent: 6, 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /bot/src/chains/terra.ts: -------------------------------------------------------------------------------- 1 | import { Bech32Address } from "@keplr-wallet/cosmos"; 2 | 3 | export const terraConfig = { 4 | network: "terra", 5 | coingeckoId: "terra-luna", 6 | prefix: Bech32Address.defaultBech32Config("terra"), 7 | primaryTokenUnit: "uluna", 8 | tokenUnits: { 9 | uluna: { 10 | display: "lunc", 11 | exponent: 6, 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /bot/src/middlewares/setupLogger.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Middleware } from "grammy"; 2 | 3 | import { Context } from "@bot/types"; 4 | import { rawLogger } from "@bot/logger"; 5 | 6 | export const middleware = (): Middleware => (ctx, next) => { 7 | ctx.local.logger = rawLogger.child({ 8 | update_id: ctx.update.update_id, 9 | }); 10 | 11 | return next(); 12 | }; 13 | -------------------------------------------------------------------------------- /bot/src/chains/bitsong.ts: -------------------------------------------------------------------------------- 1 | import { Bech32Address } from "@keplr-wallet/cosmos"; 2 | 3 | export const bitsongConfig = { 4 | network: "bitsong", 5 | coingeckoId: "bitsong", 6 | prefix: Bech32Address.defaultBech32Config("bitsong"), 7 | primaryTokenUnit: "ubtsg", 8 | tokenUnits: { 9 | ubtsg: { 10 | display: "btsg", 11 | exponent: 6, 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /bot/src/chains/cheqd.ts: -------------------------------------------------------------------------------- 1 | import { Bech32Address } from "@keplr-wallet/cosmos"; 2 | 3 | export const cheqdConfig = { 4 | network: "cheqd", 5 | coingeckoId: "cheqd-network", 6 | prefix: Bech32Address.defaultBech32Config("cheqd"), 7 | primaryTokenUnit: "ncheq", 8 | tokenUnits: { 9 | ncheq: { 10 | display: "cheq", 11 | exponent: 9, 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /bot/src/chains/crescent.ts: -------------------------------------------------------------------------------- 1 | import { Bech32Address } from "@keplr-wallet/cosmos"; 2 | 3 | export const crescentConfig = { 4 | network: "cre", 5 | coingeckoId: "crescent-network", 6 | prefix: Bech32Address.defaultBech32Config("cre"), 7 | primaryTokenUnit: "ucre", 8 | tokenUnits: { 9 | ucre: { 10 | display: "cre", 11 | exponent: 6, 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /bot/src/chains/sifchain.ts: -------------------------------------------------------------------------------- 1 | import { Bech32Address } from "@keplr-wallet/cosmos"; 2 | 3 | export const sifchainConfig = { 4 | network: "sif", 5 | coingeckoId: "sifchain", 6 | prefix: Bech32Address.defaultBech32Config("sif"), 7 | primaryTokenUnit: "rowan", 8 | tokenUnits: { 9 | rowan: { 10 | display: "erowan", 11 | exponent: 18, 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /bot/src/chains/stargaze.ts: -------------------------------------------------------------------------------- 1 | import { Bech32Address } from "@keplr-wallet/cosmos"; 2 | 3 | export const stargazeConfig = { 4 | network: "stars", 5 | coingeckoId: "stargaze", 6 | prefix: Bech32Address.defaultBech32Config("stars"), 7 | primaryTokenUnit: "ustars", 8 | tokenUnits: { 9 | ustars: { 10 | display: "stars", 11 | exponent: 6, 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /bot/src/helpers/errorHandler.ts: -------------------------------------------------------------------------------- 1 | import { BotError } from "grammy"; 2 | import { Context } from "@bot/types"; 3 | import { logger } from "@bot/logger"; 4 | 5 | export const handleError = async (error: BotError) => { 6 | const { ctx } = error; 7 | const err = error.error; 8 | 9 | logger.error({ 10 | update_id: ctx.update.update_id, 11 | err, 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /bot/src/chains/likecoin.ts: -------------------------------------------------------------------------------- 1 | import { Bech32Address } from "@keplr-wallet/cosmos"; 2 | 3 | export const likecoinConfig = { 4 | network: "like", 5 | coingeckoId: "likecoin", 6 | prefix: Bech32Address.defaultBech32Config("like"), 7 | primaryTokenUnit: "nanolike", 8 | tokenUnits: { 9 | nanolike: { 10 | display: "like", 11 | exponent: 9, 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /bot/src/middlewares/setupLocalContext.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Middleware } from "grammy"; 2 | 3 | import { context, LocalContext } from "@bot/context"; 4 | import { Context } from "@bot/types"; 5 | 6 | export const middleware = (): Middleware => (ctx, next) => 7 | context.run({}, () => { 8 | ctx.local = context.getStore() as LocalContext; 9 | return next(); 10 | }); 11 | -------------------------------------------------------------------------------- /bot/src/chains/assetMantle.ts: -------------------------------------------------------------------------------- 1 | import { Bech32Address } from "@keplr-wallet/cosmos"; 2 | 3 | export const assetmantleConfig = { 4 | network: "mantle", 5 | coingeckoId: "assetmantle", 6 | prefix: Bech32Address.defaultBech32Config("mantle"), 7 | primaryTokenUnit: "umntl", 8 | tokenUnits: { 9 | umntl: { 10 | display: "mntl", 11 | exponent: 6, 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /bot/src/chains/provenance.ts: -------------------------------------------------------------------------------- 1 | import { Bech32Address } from "@keplr-wallet/cosmos"; 2 | 3 | export const provenanceConfig = { 4 | network: "pb", 5 | coingeckoId: "provenance-blockchain", 6 | prefix: Bech32Address.defaultBech32Config("pb"), 7 | primaryTokenUnit: "nhash", 8 | tokenUnits: { 9 | nhash: { 10 | display: "hash", 11 | exponent: 9, 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /bot/src/chains/chihuahua.ts: -------------------------------------------------------------------------------- 1 | import { Bech32Address } from "@keplr-wallet/cosmos"; 2 | 3 | export const chihuahuaConfig = { 4 | network: "chihuahua", 5 | coingeckoId: "chihuahua-token", 6 | prefix: Bech32Address.defaultBech32Config("chihuahua"), 7 | primaryTokenUnit: "uhuahua", 8 | tokenUnits: { 9 | uhuahua: { 10 | display: "HUAHUA", 11 | exponent: 6, 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /bot/src/chains/persistance.ts: -------------------------------------------------------------------------------- 1 | import { Bech32Address } from "@keplr-wallet/cosmos"; 2 | 3 | export const persistanceConfig = { 4 | network: "persistence", 5 | coingeckoId: "persistence", 6 | prefix: Bech32Address.defaultBech32Config("persistence"), 7 | primaryTokenUnit: "uxprt", 8 | tokenUnits: { 9 | uxprt: { 10 | display: "xprt", 11 | exponent: 6, 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /bot/src/features/help.feature.ts: -------------------------------------------------------------------------------- 1 | import { logHandle } from "@bot/helpers/logging"; 2 | import { router } from "@bot/middlewares"; 3 | import { en } from "@bot/constants/en"; 4 | import { ROUTE } from "@bot/constants/route"; 5 | 6 | export const feature = router.route(ROUTE.HELP); 7 | 8 | feature.command(en.help.command, logHandle(ROUTE.HELP), async (ctx) => { 9 | await ctx.reply(en.help.text); 10 | }); 11 | -------------------------------------------------------------------------------- /bot/src/features/start.feature.ts: -------------------------------------------------------------------------------- 1 | import { en } from "@bot/constants/en"; 2 | import { ROUTE, ROUTE_LOGS } from "@bot/constants/route"; 3 | import { logHandle } from "@bot/helpers/logging"; 4 | import { router } from "@bot/middlewares"; 5 | 6 | export const feature = router.route(ROUTE.START); 7 | 8 | feature.command(en.start.command, logHandle(ROUTE_LOGS.START), async (ctx) => { 9 | await ctx.reply(en.start.text); 10 | }); 11 | -------------------------------------------------------------------------------- /prisma/migrations/20211221124412_initial/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "users" ( 3 | "id" SERIAL NOT NULL, 4 | "telegram_id" BIGINT NOT NULL, 5 | "updated_at" TIMESTAMP(3) NOT NULL, 6 | "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 7 | 8 | CONSTRAINT "users_pkey" PRIMARY KEY ("id") 9 | ); 10 | 11 | -- CreateIndex 12 | CREATE UNIQUE INDEX "users_telegram_id_key" ON "users"("telegram_id"); 13 | -------------------------------------------------------------------------------- /bot/src/dao/alarms.dao.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient, Prisma } from "@prisma/client"; 2 | 3 | export const createDao = (prisma: PrismaClient) => ({ 4 | upsertAlarm: (args: Prisma.AlarmUpsertArgs) => { 5 | return prisma.alarm.upsert(args); 6 | }, 7 | 8 | getAlarm: (args: Prisma.AlarmFindUniqueArgs) => prisma.alarm.findUnique(args), 9 | 10 | getAllAlarms: (args?: Prisma.AlarmFindManyArgs) => 11 | prisma.alarm.findMany(args), 12 | }); 13 | -------------------------------------------------------------------------------- /bot/src/dao/notifications.dao.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient, Prisma } from "@prisma/client"; 2 | 3 | export const createDao = (prisma: PrismaClient) => ({ 4 | upsertNotification: (args: Prisma.NotificationUpsertArgs) => 5 | prisma.notification.upsert(args), 6 | 7 | getNotification: (args: Prisma.NotificationFindUniqueArgs) => 8 | prisma.notification.findUnique(args), 9 | 10 | getAllNotifications: () => prisma.notification.findMany(), 11 | }); 12 | -------------------------------------------------------------------------------- /bot/src/menu/notification/index.ts: -------------------------------------------------------------------------------- 1 | export { alarmMenu } from "./alarm.menu"; 2 | export { networksAlarmMenu } from "./networksAlarm.menu"; 3 | export { dailyReminderMenu } from "./dailyReminder.menu"; 4 | export { notificationMenu } from "./notification.menu"; 5 | export { proposalMenu } from "./proposal.menu"; 6 | export { networksReminderMenu } from "./networksReminder.menu"; 7 | export { networkTimeReminderMenu } from "./timeReminder.menu"; 8 | export { timezoneMenu } from "./timezone.menu"; 9 | -------------------------------------------------------------------------------- /bot/src/dao/users.dao.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient, Prisma } from "@prisma/client"; 2 | 3 | export const createDao = (prisma: PrismaClient) => ({ 4 | upsertUser: (args: Prisma.UserUpsertArgs) => prisma.user.upsert(args), 5 | 6 | updateUser: (args: Prisma.UserUpdateArgs) => prisma.user.update(args), 7 | 8 | getUser: (args: Prisma.UserFindUniqueArgs) => prisma.user.findUnique(args), 9 | 10 | getAllUser: () => prisma.user.findMany(), 11 | 12 | count: () => prisma.user.count(), 13 | }); 14 | -------------------------------------------------------------------------------- /bot/src/middlewares/setUser.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Middleware } from "grammy"; 2 | 3 | import { Context } from "@bot/types"; 4 | import { usersService } from "@bot/services"; 5 | 6 | export const middleware = (): Middleware => async (ctx, next) => { 7 | if (ctx.from?.is_bot === false) { 8 | const { upsertUser } = usersService(ctx); 9 | const { id: telegramId } = ctx.from; 10 | 11 | ctx.local.user = await upsertUser(telegramId); 12 | } 13 | 14 | return next(); 15 | }; 16 | -------------------------------------------------------------------------------- /bot/src/services/index.ts: -------------------------------------------------------------------------------- 1 | export { alarmsService } from "./alarms.service"; 2 | export { alarmPricesService } from "./alarmPrices.service"; 3 | export { networksInNotificationService } from "./networksInNotification.service"; 4 | export { notificationsService } from "./notifications.service"; 5 | export { resourcesService } from "./resources.service"; 6 | export { usersService } from "./users.service"; 7 | export { walletsService } from "./wallets.service"; 8 | export { networksService } from "./networks.service"; 9 | -------------------------------------------------------------------------------- /bot/src/metrics.ts: -------------------------------------------------------------------------------- 1 | import prometheus from "prom-client"; 2 | 3 | prometheus.collectDefaultMetrics(); 4 | 5 | export const metrics = { 6 | updatesCounter: new prometheus.Counter({ 7 | name: "bot_updates_count", 8 | help: "Count of updates received", 9 | labelNames: ["from_id", "chat_id"], 10 | }), 11 | updatesFailedCounter: new prometheus.Counter({ 12 | name: "bot_updates_failed_count", 13 | help: "Count of failed updates", 14 | labelNames: ["from_id", "chat_id"], 15 | }), 16 | }; 17 | -------------------------------------------------------------------------------- /bot/src/features/support.feature.ts: -------------------------------------------------------------------------------- 1 | import { logHandle } from "@bot/helpers/logging"; 2 | import { Context } from "@bot/types"; 3 | import { router } from "@bot/middlewares"; 4 | import { en } from "@bot/constants/en"; 5 | import { ROUTE, ROUTE_LOGS } from "@bot/constants/route"; 6 | 7 | export const feature = router.route(ROUTE.SUPPORT); 8 | 9 | feature.command( 10 | en.support.command, 11 | logHandle(ROUTE_LOGS.SUPPORT), 12 | async (ctx: Context) => { 13 | await ctx.reply(en.support.title); 14 | } 15 | ); 16 | -------------------------------------------------------------------------------- /bot/src/features/reset.feature.ts: -------------------------------------------------------------------------------- 1 | import { logHandle } from "@bot/helpers/logging"; 2 | import { router } from "@bot/middlewares"; 3 | import { en } from "@bot/constants/en"; 4 | 5 | export const feature = router.route("proposals"); 6 | 7 | feature.command( 8 | en.reset.command, 9 | logHandle("handle /proposals"), 10 | async (ctx) => { 11 | ctx.session.step = undefined; 12 | ctx.session.alarmNetwork = undefined; 13 | ctx.session.timezone = []; 14 | return await ctx.reply(en.reset.title); 15 | } 16 | ); 17 | -------------------------------------------------------------------------------- /bot/src/utils/getCW20tokens.ts: -------------------------------------------------------------------------------- 1 | import { CosmWasmClient } from "cosmwasm"; 2 | 3 | export async function getCW20tokens(tokenAddress: string, address: string) { 4 | try { 5 | const client = await CosmWasmClient.connect( 6 | "https://rpc-juno.itastakers.com:443" 7 | ); 8 | const token = await client.queryContractSmart(tokenAddress, { 9 | balance: { 10 | address, 11 | }, 12 | }); 13 | 14 | return token.balance; 15 | } catch (e) { 16 | console.log(e); 17 | return 0; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /bot/src/features/about.feature.ts: -------------------------------------------------------------------------------- 1 | import { logHandle } from "@bot/helpers/logging"; 2 | import { Context } from "@bot/types"; 3 | import { router } from "@bot/middlewares"; 4 | import { en } from "@bot/constants/en"; 5 | import { ROUTE, ROUTE_LOGS } from "@bot/constants/route"; 6 | 7 | export const feature = router.route(ROUTE.ABOUT); 8 | 9 | feature.command( 10 | en.about.command, 11 | logHandle(ROUTE_LOGS.ABOUT), 12 | async (ctx: Context) => { 13 | await ctx.reply(en.about.title, { disable_web_page_preview: true }); 14 | } 15 | ); 16 | -------------------------------------------------------------------------------- /bot/src/constants/api.ts: -------------------------------------------------------------------------------- 1 | const coingeckoApiHost = "https://api.coingecko.com/api/v3"; 2 | 3 | export const getTokenPrice = (queryParams: { [key: string]: string }) => { 4 | const params = new URLSearchParams(queryParams); 5 | return `${coingeckoApiHost}/simple/price?${params}`; 6 | }; 7 | 8 | export const getTokenPriceByDate = ( 9 | id: string, 10 | queryParams: { date: string; localization: string } 11 | ) => { 12 | const params = new URLSearchParams(queryParams); 13 | return `${coingeckoApiHost}/coins/${id}/history?${params}`; 14 | }; 15 | -------------------------------------------------------------------------------- /server/telegram.ts: -------------------------------------------------------------------------------- 1 | import { restRequest } from "@bot/utils/restRequest"; 2 | import { config } from "@bot/config"; 3 | 4 | const telegramBotKey = config.BOT_TOKEN; 5 | 6 | export const sendNotification = async ( 7 | text: string, 8 | parse_mode: string, 9 | chatId: number 10 | ) => { 11 | const endpoint = `https://api.telegram.org/bot${telegramBotKey}/sendMessage`; 12 | await restRequest( 13 | endpoint, 14 | "POST", 15 | JSON.stringify({ 16 | text, 17 | parse_mode, 18 | chat_id: chatId, 19 | }) 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /bot/src/features/proposal.feature.ts: -------------------------------------------------------------------------------- 1 | import { logHandle } from "@bot/helpers/logging"; 2 | import { router } from "@bot/middlewares"; 3 | import { en } from "@bot/constants/en"; 4 | import { networksProposalMenu } from "@bot/menu"; 5 | import { ROUTE, ROUTE_LOGS } from "@bot/constants/route"; 6 | 7 | export const feature = router.route(ROUTE.RESET); 8 | 9 | feature.command( 10 | en.proposals.command, 11 | logHandle(ROUTE_LOGS.RESET), 12 | async (ctx) => 13 | await ctx.reply(en.proposals.menu.title, { 14 | reply_markup: networksProposalMenu, 15 | }) 16 | ); 17 | -------------------------------------------------------------------------------- /bot/src/dao/alarmPrices.dao.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient, Prisma } from "@prisma/client"; 2 | 3 | export const createDao = (prisma: PrismaClient) => ({ 4 | createAlarmPrice: (args: Prisma.AlarmPriceCreateArgs) => 5 | prisma.alarmPrice.create(args), 6 | 7 | getAlarmPrice: (args: Prisma.AlarmPriceFindUniqueArgs) => 8 | prisma.alarmPrice.findUnique(args), 9 | 10 | getAllAlarmPrices: (args: Prisma.AlarmPriceFindManyArgs) => 11 | prisma.alarmPrice.findMany(args), 12 | 13 | removeAlarmPrice: (args: Prisma.AlarmPriceDeleteArgs) => 14 | prisma.alarmPrice.delete(args), 15 | }); 16 | -------------------------------------------------------------------------------- /bot/src/middlewares/collectMetrics.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Middleware } from "grammy"; 2 | 3 | import { Context } from "@bot/types"; 4 | import { metrics } from "@bot/metrics"; 5 | 6 | export const middleware = (): Middleware => async (ctx, next) => { 7 | try { 8 | metrics.updatesCounter.inc({ 9 | from_id: ctx.from?.id, 10 | chat_id: ctx.chat?.id, 11 | }); 12 | return await next(); 13 | } catch (e) { 14 | metrics.updatesFailedCounter.inc({ 15 | from_id: ctx.from?.id, 16 | chat_id: ctx.chat?.id, 17 | }); 18 | throw e; 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /bot/src/services/alarmPrices.service.ts: -------------------------------------------------------------------------------- 1 | import { alarmPricesDao } from "@bot/dao"; 2 | 3 | export function alarmPricesService() { 4 | const getAllAlarmPrices = async (alarmId: number) => 5 | await alarmPricesDao.getAllAlarmPrices({ 6 | where: { 7 | alarmId: alarmId, 8 | }, 9 | }); 10 | 11 | const removeAlarmPrice = async (alarmPriceId: number) => 12 | await alarmPricesDao.removeAlarmPrice({ 13 | where: { 14 | id: alarmPriceId, 15 | }, 16 | }); 17 | 18 | return { 19 | getAllAlarmPrices, 20 | removeAlarmPrice, 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /bot/src/features/resources.feature.ts: -------------------------------------------------------------------------------- 1 | import { logHandle } from "@bot/helpers/logging"; 2 | import { router } from "@bot/middlewares"; 3 | import { networksResourcesMenu } from "@bot/menu"; 4 | import { en } from "@bot/constants/en"; 5 | import { ROUTE, ROUTE_LOGS } from "@bot/constants/route"; 6 | 7 | export const feature = router.route(ROUTE.RESOURCES); 8 | 9 | feature.command( 10 | en.resources.command, 11 | logHandle(ROUTE_LOGS.RESOURCES), 12 | async (ctx) => { 13 | await ctx.reply(en.resources.menu.title, { 14 | reply_markup: networksResourcesMenu, 15 | }); 16 | } 17 | ); 18 | -------------------------------------------------------------------------------- /bot/src/features/statistic.feature.ts: -------------------------------------------------------------------------------- 1 | import { logHandle } from "@bot/helpers/logging"; 2 | import { router } from "@bot/middlewares"; 3 | import { networksStatisticMenu } from "@bot/menu"; 4 | import { en } from "@bot/constants/en"; 5 | import { ROUTE, ROUTE_LOGS } from "@bot/constants/route"; 6 | 7 | export const feature = router.route(ROUTE.STATISTIC); 8 | 9 | feature.command( 10 | en.statistic.command, 11 | logHandle(ROUTE_LOGS.STATISTIC), 12 | async (ctx) => { 13 | await ctx.reply(en.statistic.menu.title, { 14 | reply_markup: networksStatisticMenu, 15 | }); 16 | } 17 | ); 18 | -------------------------------------------------------------------------------- /bot/src/utils/getPnlDate.ts: -------------------------------------------------------------------------------- 1 | import dayjs from "dayjs"; 2 | import relativeTime from "dayjs/plugin/relativeTime"; 3 | dayjs.extend(relativeTime); 4 | 5 | export const getPnlDate = () => { 6 | const now = dayjs(); 7 | const template = "DD-MM-YYYY"; 8 | const dayAgo = now.subtract(1, "day").format(template); 9 | const sevenDayAgo = now.subtract(7, "day").format(template); 10 | const fourteenthDayAgo = now.subtract(14, "day").format(template); 11 | const thirtyDayAgo = now.subtract(30, "day").format(template); 12 | 13 | return [dayAgo, sevenDayAgo, fourteenthDayAgo, thirtyDayAgo]; 14 | }; 15 | -------------------------------------------------------------------------------- /bot/src/menu/index.ts: -------------------------------------------------------------------------------- 1 | export { walletMenu } from "./wallet.menu"; 2 | export { walletRemoveMenu } from "./walletRemove.menu"; 3 | export { networksStatisticMenu } from "./networksStatistic.menu"; 4 | export { networksResourcesMenu } from "./networksResources.menu"; 5 | export { networksProposalMenu } from "./networksProposals.menu"; 6 | export { assetsMenu } from "./assets.menu"; 7 | 8 | export { 9 | alarmMenu, 10 | networksAlarmMenu, 11 | notificationMenu, 12 | networksReminderMenu, 13 | dailyReminderMenu, 14 | networkTimeReminderMenu, 15 | timezoneMenu, 16 | proposalMenu, 17 | } from "./notification"; 18 | -------------------------------------------------------------------------------- /bot/src/middlewares/index.ts: -------------------------------------------------------------------------------- 1 | export { middleware as collectMetrics } from "./collectMetrics.middleware"; 2 | export { middleware as setUser } from "./setUser.middleware"; 3 | export { middleware as setupLocalContext } from "./setupLocalContext.middleware"; 4 | export { middleware as setupLogger } from "./setupLogger.middleware"; 5 | export { middleware as setupSession } from "./setupSession.middleware"; 6 | export { middleware as setupWalletsMigrate } from "./setupWalletsMigrate.middleware"; 7 | export { middleware as updatesLogger } from "./updatesLogger.middleware"; 8 | export { middleware as router } from "./setupRouter.middleware"; 9 | -------------------------------------------------------------------------------- /bot/src/middlewares/setupSession.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Middleware, session } from "grammy"; 2 | import { RedisAdapter } from "@grammyjs/storage-redis"; 3 | 4 | import { connection } from "@bot/redis"; 5 | import { Context } from "@bot/types"; 6 | 7 | const storage = new RedisAdapter({ 8 | instance: connection, 9 | }); 10 | 11 | export const middleware = (): Middleware => 12 | session({ 13 | initial: () => ({ 14 | step: "setup", 15 | timezones: [], 16 | alarmNetwork: undefined, 17 | walletPassword: "", 18 | isWalletsMigrated: false, 19 | }), 20 | storage, 21 | }); 22 | -------------------------------------------------------------------------------- /bot/src/utils/getFilterDenom.ts: -------------------------------------------------------------------------------- 1 | import _ from "lodash"; 2 | 3 | export const getDenom = ( 4 | list: { denom: string; amount: string | number }[] = [], 5 | denom: string 6 | ): { 7 | denom: string; 8 | amount: string | number; 9 | } => { 10 | const [selectedDenom] = list.filter((x) => x.denom === denom); 11 | let results: { 12 | denom: string; 13 | amount: string | number; 14 | } = { 15 | denom, 16 | amount: "0", 17 | }; 18 | if (!_.isEmpty(selectedDenom)) { 19 | results = { 20 | denom: _.get(selectedDenom, ["denom"], ""), 21 | amount: _.get(selectedDenom, ["amount"], "0"), 22 | }; 23 | } 24 | return results; 25 | }; 26 | -------------------------------------------------------------------------------- /bot/src/utils/nFormatter.ts: -------------------------------------------------------------------------------- 1 | export function nFormatter(num: number, digits: number) { 2 | const lookup = [ 3 | { value: 1, symbol: "" }, 4 | { value: 1e3, symbol: "k" }, 5 | { value: 1e6, symbol: "M" }, 6 | { value: 1e9, symbol: "G" }, 7 | { value: 1e12, symbol: "T" }, 8 | { value: 1e15, symbol: "P" }, 9 | { value: 1e18, symbol: "E" }, 10 | ]; 11 | const rx = /\.0+$|(\.[0-9]*[1-9])0+$/; 12 | const item = lookup 13 | .slice() 14 | .reverse() 15 | .find(function (item) { 16 | return num >= item.value; 17 | }); 18 | return item 19 | ? (num / item.value).toFixed(digits).replace(rx, "$1") + item.symbol 20 | : num.toFixed(2); 21 | } 22 | -------------------------------------------------------------------------------- /bot/src/config.ts: -------------------------------------------------------------------------------- 1 | import "dotenv/config"; 2 | import { cleanEnv, str, num, json } from "envalid"; 3 | 4 | export const config = cleanEnv(process.env, { 5 | NODE_ENV: str({ choices: ["development", "production"] }), 6 | LOG_LEVEL: str({ 7 | choices: ["trace", "debug", "info", "warn", "error", "fatal", "silent"], 8 | }), 9 | DATABASE_URL: str(), 10 | REDIS_URL: str(), 11 | BOT_SERVER_HOST: str({ 12 | default: "0.0.0.0", 13 | }), 14 | BOT_SERVER_PORT: num({ 15 | default: 80, 16 | }), 17 | BOT_ALLOWED_UPDATES: json({ 18 | default: [], 19 | }), 20 | BOT_TOKEN: str(), 21 | BOT_WEBHOOK: str(), 22 | BOT_ADMIN_USER_ID: num(), 23 | PORT: num(), 24 | UI_URL: str(), 25 | }); 26 | -------------------------------------------------------------------------------- /bot/src/utils/getBlocksPerYearReal.ts: -------------------------------------------------------------------------------- 1 | import { fetchLatestHeight, fetchBlock } from "@bot/api"; 2 | 3 | export async function getBlocksPerYearReal(api: string) { 4 | const latestHeight = await fetchLatestHeight(api); 5 | const block1 = latestHeight.height.header; 6 | const blockRange = Number(block1.height) > 10000 ? 10000 : 1; 7 | 8 | const nextBlock = await fetchBlock(api, Number(block1.height) - blockRange); 9 | const block2 = nextBlock.height.header; 10 | 11 | const yearMilisec = 31536000000; 12 | const blockMilisec = 13 | (new Date(block1.time).getTime() - new Date(block2.time).getTime()) / 14 | blockRange; 15 | return { blocksPerYear: Math.ceil(yearMilisec / blockMilisec) }; 16 | } 17 | -------------------------------------------------------------------------------- /bot/src/utils/validation.ts: -------------------------------------------------------------------------------- 1 | import { bech32 } from "bech32"; 2 | import { config } from "@bot/chains"; 3 | import { Wallet } from "@prisma/client"; 4 | 5 | export const validation = { 6 | isValidAddress: (address: string) => { 7 | try { 8 | const decoded = bech32.decode(address).words; 9 | return !!decoded; 10 | } catch { 11 | return false; 12 | } 13 | }, 14 | isValidChain: (address: string) => { 15 | const prefix = bech32.decode(address).prefix; 16 | return config.some(({ network }) => { 17 | return network === prefix; 18 | }); 19 | }, 20 | isDuplicateAddress: (userWallets: Wallet[], address: string) => { 21 | return userWallets.some((wallet) => wallet.address === address); 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /bot/src/dao/networksInNotification.dao.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient, Prisma } from "@prisma/client"; 2 | 3 | export const createDao = (prisma: PrismaClient) => ({ 4 | createNetworkInNotification: ( 5 | args: Prisma.NetworksInNotificationCreateArgs 6 | ) => prisma.networksInNotification.create(args), 7 | 8 | getNetworkInNotification: (args: Prisma.NetworksInNotificationFindManyArgs) => 9 | prisma.networksInNotification.findMany(args), 10 | 11 | getAllNetworkInNotification: ( 12 | args: Prisma.NetworksInNotificationFindManyArgs 13 | ) => prisma.networksInNotification.findMany(args), 14 | 15 | removeNetworkInNotification: ( 16 | args: Prisma.NetworksInNotificationDeleteArgs 17 | ) => prisma.networksInNotification.delete(args), 18 | }); 19 | -------------------------------------------------------------------------------- /bot/src/dao/wallets.dao.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient, Prisma } from "@prisma/client"; 2 | 3 | export const createDao = (prisma: PrismaClient) => ({ 4 | createWallet: (args: Prisma.WalletCreateArgs) => prisma.wallet.create(args), 5 | 6 | updateWallet: (args: Prisma.WalletUpdateArgs) => prisma.wallet.update(args), 7 | 8 | bulkCreateWallets: (data: Array) => 9 | prisma.wallet.createMany({ data, skipDuplicates: true }), 10 | 11 | getAllWallets: (args: Prisma.WalletFindManyArgs) => 12 | prisma.wallet.findMany(args), 13 | 14 | removeWallet: (args: Prisma.WalletDeleteArgs) => prisma.wallet.delete(args), 15 | 16 | removeAllWallet: (args: Prisma.WalletDeleteManyArgs) => 17 | prisma.wallet.deleteMany(args), 18 | }); 19 | -------------------------------------------------------------------------------- /bot/src/constants/route.ts: -------------------------------------------------------------------------------- 1 | export const ROUTE = { 2 | ABOUT: "about", 3 | ASSETS: "assets", 4 | HELP: "help", 5 | NOTIFICATION: "notification", 6 | PROPOSALS: "proposals", 7 | RESET: "reset", 8 | RESOURCES: "resources", 9 | START: "start", 10 | STATISTIC: "statistic", 11 | SUPPORT: "support", 12 | WALLET: "wallet", 13 | }; 14 | 15 | export const ROUTE_LOGS = { 16 | ABOUT: "handle /about", 17 | ASSETS: "handle /assets", 18 | HELP: "handle /help", 19 | NOTIFICATION: "handle /notification", 20 | PROPOSALS: "handle /proposals", 21 | RESET: "handle /reset", 22 | RESOURCES: "handle /resources", 23 | START: "handle /start", 24 | STATISTIC: "handle /statistic", 25 | SUPPORT: "handle /support", 26 | WALLET: "handle /wallet", 27 | }; 28 | -------------------------------------------------------------------------------- /bot/src/utils/getEmoji.ts: -------------------------------------------------------------------------------- 1 | export function getNumberEmoji(num: number) { 2 | const digits = num.toString().split(""); 3 | const digitWithEmoji = digits 4 | .map((item) => `${item}️⃣`) 5 | .join(""); 6 | 7 | return digitWithEmoji; 8 | } 9 | 10 | export function getPositiveOrNegativeEmoji(num: number | string) { 11 | let parsedNumber = num; 12 | if (typeof num === "string") { 13 | parsedNumber = num.substring(1); 14 | } 15 | return parsedNumber >= 0 ? `📈 ${num}` : `📉 ${num}`; 16 | } 17 | 18 | export function getFlagEmoji(countryCode: string) { 19 | const codePoints = countryCode 20 | .toUpperCase() 21 | .split("") 22 | .map((char) => 127397 + char.charCodeAt(0)); 23 | return String.fromCodePoint(...codePoints); 24 | } 25 | -------------------------------------------------------------------------------- /bot/src/utils/calculateApr.ts: -------------------------------------------------------------------------------- 1 | import Big from "big.js"; 2 | 3 | export function calculateRealAPR(params: { 4 | annualProvisions: number; 5 | communityTax: number; 6 | bondedTokens: number; 7 | blocksYearReal: number; 8 | }) { 9 | if (params.annualProvisions <= 0) { 10 | return 0; 11 | } 12 | 13 | const nominalAPR = Big(params.annualProvisions) 14 | .mul(Big(1).minus(params.communityTax)) 15 | .div(params.bondedTokens); 16 | 17 | const blockProvision = Big(params.annualProvisions).div( 18 | params.blocksYearReal 19 | ); 20 | const realProvision = Big(blockProvision).mul(params.blocksYearReal); 21 | 22 | return Big(nominalAPR) 23 | .mul(Big(realProvision).div(params.annualProvisions)) 24 | .mul(100) 25 | .toNumber(); 26 | } 27 | -------------------------------------------------------------------------------- /bot/src/api/requests/fetchProposal.ts: -------------------------------------------------------------------------------- 1 | import { ProposalItemResponse } from "@bot/types/general"; 2 | import { restRequest } from "@bot/utils"; 3 | 4 | export const fetchProposals = async (publicUrl: string) => { 5 | try { 6 | const req = await restRequest( 7 | `${publicUrl}cosmos/gov/v1beta1/proposals?pagination.limit=20&pagination.reverse=true` 8 | ); 9 | const res: { proposals: ProposalItemResponse } = await req.json(); 10 | 11 | return res.proposals.map((item) => ({ 12 | proposalId: item?.proposal_id, 13 | votingStartTime: item?.voting_start_time, 14 | title: item?.content?.title, 15 | description: item?.content?.description, 16 | status: item?.status, 17 | })); 18 | } catch (error) { 19 | return []; 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /bot/src/features/index.ts: -------------------------------------------------------------------------------- 1 | export { feature as botAdminFeature } from "./botAdmin.feature"; 2 | export { feature as startFeature } from "./start.feature"; 3 | export { feature as walletFeature } from "./wallet.feature"; 4 | export { feature as helpFeature } from "./help.feature"; 5 | export { feature as statisticFeature } from "./statistic.feature"; 6 | export { feature as proposalFeature } from "./proposal.feature"; 7 | export { feature as resourcesFeature } from "./resources.feature"; 8 | export { feature as notificationFeature } from "./notification.feature"; 9 | export { feature as assetsFeature } from "./assets.feature"; 10 | export { feature as aboutFeature } from "./about.feature"; 11 | export { feature as supportFeature } from "./support.feature"; 12 | export { feature as resetFeature } from "./reset.feature"; 13 | -------------------------------------------------------------------------------- /bot/src/logger.ts: -------------------------------------------------------------------------------- 1 | import pino, { Logger, LoggerOptions } from "pino"; 2 | import pretty from "pino-pretty"; 3 | 4 | import { config } from "@bot/config"; 5 | import { context } from "@bot/context"; 6 | 7 | const options: LoggerOptions = { 8 | level: config.LOG_LEVEL, 9 | }; 10 | 11 | export let rawLogger = pino(options); 12 | 13 | if (config.isDev) { 14 | rawLogger = pino( 15 | options, 16 | pretty({ 17 | ignore: "pid,hostname", 18 | colorize: true, 19 | translateTime: true, 20 | }) 21 | ); 22 | } 23 | 24 | export const logger: Logger = new Proxy(rawLogger, { 25 | get(target, property, receiver) { 26 | // eslint-disable-next-line no-param-reassign 27 | target = context.getStore()?.logger || target; 28 | return Reflect.get(target, property, receiver); 29 | }, 30 | }); 31 | -------------------------------------------------------------------------------- /bot/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { calcTVLPercent } from "./calcTVLPercent"; 2 | export { formatToken } from "./formatToken"; 3 | export { formatTokenPrice } from "./formatTokenPrice"; 4 | export { 5 | getNumberEmoji, 6 | getPositiveOrNegativeEmoji, 7 | getFlagEmoji, 8 | } from "./getEmoji"; 9 | export { getDenom } from "./getFilterDenom"; 10 | export { getPnlDate } from "./getPnlDate"; 11 | export { validation } from "./validation"; 12 | export { isNumber } from "./isNumber"; 13 | export { menuCreator } from "./menuCreator"; 14 | export { nFormatter } from "./nFormatter"; 15 | export { restRequest } from "./restRequest"; 16 | export { template } from "./template"; 17 | export { capitalizeFirstLetter } from "./capitalizeFirstLetter"; 18 | export { loadAddresses } from "./loadAddresses"; 19 | export { decrypt, encrypt } from "./crypto"; 20 | -------------------------------------------------------------------------------- /bot/src/helpers/logging.ts: -------------------------------------------------------------------------------- 1 | import { Middleware } from "grammy"; 2 | import { Chat, User } from "@grammyjs/types"; 3 | import { Context } from "@bot/types"; 4 | import { logger } from "@bot/logger"; 5 | 6 | interface LogMetadata { 7 | message_id: number | undefined; 8 | chat: Chat | undefined; 9 | peer: User | Chat | undefined; 10 | } 11 | 12 | export const getPeer = (ctx: Context): Chat | User | undefined => 13 | ctx.senderChat || ctx.from; 14 | 15 | export const getMetadata = (ctx: Context): LogMetadata => ({ 16 | message_id: ctx.msg?.message_id, 17 | chat: ctx.chat, 18 | peer: getPeer(ctx), 19 | }); 20 | 21 | export const logHandle = 22 | (name: string): Middleware => 23 | (ctx, next) => { 24 | logger.info({ 25 | msg: name, 26 | ...getMetadata(ctx), 27 | }); 28 | 29 | return next(); 30 | }; 31 | -------------------------------------------------------------------------------- /bot/src/features/assets.feature.ts: -------------------------------------------------------------------------------- 1 | import { logHandle } from "@bot/helpers/logging"; 2 | import { Context } from "@bot/types"; 3 | import { router } from "@bot/middlewares"; 4 | import { assetsMenu } from "@bot/menu"; 5 | import { en } from "@bot/constants/en"; 6 | import { walletsService } from "@bot/services"; 7 | import { ROUTE } from "@bot/constants/route"; 8 | 9 | export const feature = router.route(ROUTE.ASSETS); 10 | 11 | feature.command( 12 | en.assets.command, 13 | logHandle(ROUTE.ASSETS), 14 | async (ctx: Context) => { 15 | const { getAllUserWallets } = walletsService(ctx); 16 | const userWallets = await getAllUserWallets(); 17 | 18 | if (userWallets.length === 0) { 19 | return ctx.reply(en.wallet.emptyWallet); 20 | } 21 | 22 | await ctx.reply(en.assets.menu.title, { reply_markup: assetsMenu }); 23 | } 24 | ); 25 | -------------------------------------------------------------------------------- /bot/src/api/handlers/getProposals.ts: -------------------------------------------------------------------------------- 1 | import { fetchProposals } from "@bot/api"; 2 | import { proposalStatus } from "@bot/constants/proposalStatus"; 3 | import { ProposalItem } from "@bot/types/general"; 4 | 5 | export const getProposals = async (url: string) => { 6 | const proposals = await fetchProposals(url); 7 | 8 | return formatProposals(proposals); 9 | }; 10 | 11 | export const formatProposals = (proposals: Array) => { 12 | try { 13 | const activeProposals = proposals.filter( 14 | ({ status }) => status === proposalStatus.PROPOSAL_STATUS_VOTING_PERIOD 15 | ); 16 | 17 | return { 18 | activeProposals, 19 | proposals, 20 | }; 21 | } catch (e) { 22 | console.error("Error in formatProposals: " + e); 23 | 24 | return { 25 | activeProposals: [], 26 | proposals: [], 27 | }; 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /bot/src/types/context.ts: -------------------------------------------------------------------------------- 1 | import { Context as DefaultContext, SessionFlavor } from "grammy"; 2 | import { FluentContextFlavor } from "@grammyjs/fluent"; 3 | import { ParseModeContext } from "@grammyjs/parse-mode"; 4 | import { FileFlavor } from "@grammyjs/files"; 5 | 6 | import { LocalContext } from "@bot/context"; 7 | import { Network } from "@prisma/client"; 8 | import { Steps } from "./general"; 9 | 10 | export interface LocalContextFlavor { 11 | local: LocalContext; 12 | } 13 | 14 | export interface SessionData { 15 | timezone: string[]; 16 | alarmNetwork?: Network; 17 | step?: Steps; 18 | walletPassword: string; 19 | isWalletsMigrated: boolean; 20 | } 21 | 22 | export type Context = FileFlavor< 23 | DefaultContext & 24 | FluentContextFlavor & 25 | ParseModeContext & 26 | LocalContextFlavor & 27 | SessionFlavor 28 | >; 29 | -------------------------------------------------------------------------------- /bot/src/utils/formatToken.ts: -------------------------------------------------------------------------------- 1 | import { TokenUnit } from "@bot/types/general"; 2 | import Big from "big.js"; 3 | import _ from "lodash"; 4 | 5 | export const formatToken = ( 6 | value: number | string, 7 | tokenUnits: { 8 | display: string; 9 | exponent: number; 10 | }, 11 | denom: string 12 | ): TokenUnit => { 13 | if (typeof value !== "string" && isNaN(value)) { 14 | value = "0"; 15 | } 16 | 17 | if (typeof value === "number") { 18 | value = `${value}`; 19 | } 20 | 21 | const results: TokenUnit = { 22 | value, 23 | displayDenom: denom, 24 | baseDenom: denom, 25 | exponent: _.get(tokenUnits, ["exponent"], 0), 26 | }; 27 | 28 | const ratio = 10 ** tokenUnits.exponent; 29 | results.value = Big(value).div(ratio).toFixed(tokenUnits.exponent); 30 | results.displayDenom = tokenUnits.display; 31 | return results; 32 | }; 33 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "dom", 5 | "dom.iterable", 6 | "esnext" 7 | ], 8 | "esModuleInterop": true, 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "useUnknownInCatchVariables": true, 12 | "strict": true, 13 | "incremental": true, 14 | "module": "commonjs", 15 | "target": "es2018", 16 | "moduleResolution": "node", 17 | "sourceMap": true, 18 | "outDir": "dist", 19 | "baseUrl": "./", 20 | "skipLibCheck": true, 21 | "noEmit": false, 22 | "paths": { 23 | "@bot/*": [ 24 | "./bot/src/*" 25 | ], 26 | "@server/*": [ 27 | "./server/*" 28 | ] 29 | }, 30 | "jsx": "preserve" 31 | }, 32 | "include": [ 33 | "bot/**/*.ts", 34 | "server/**/*.ts", 35 | "server/**/*.png", 36 | "types/**/*.ts", 37 | "prisma/seed.ts" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /bot/src/menu/notification/alarm.menu.ts: -------------------------------------------------------------------------------- 1 | import { 2 | addAlarmCallback, 3 | deleteAlarmCallback, 4 | listAlarmsCallback, 5 | listAlarmsText, 6 | toggleAlarmActivity, 7 | } from "../callbacks"; 8 | import { menuCreator } from "@bot/utils"; 9 | import { en } from "@bot/constants/en"; 10 | 11 | const menuList = [ 12 | { 13 | text: en.notification.alarmMenu.add, 14 | callback: addAlarmCallback, 15 | }, 16 | { row: true }, 17 | { 18 | text: en.notification.alarmMenu.delete, 19 | callback: deleteAlarmCallback, 20 | }, 21 | { 22 | row: true, 23 | }, 24 | { 25 | text: en.notification.alarmMenu.list, 26 | callback: listAlarmsCallback, 27 | }, 28 | { 29 | row: true, 30 | }, 31 | { 32 | text: listAlarmsText, 33 | callback: toggleAlarmActivity, 34 | }, 35 | { 36 | row: true, 37 | }, 38 | { 39 | back: en.back, 40 | }, 41 | ]; 42 | 43 | export const alarmMenu = menuCreator("alarm", menuList); 44 | -------------------------------------------------------------------------------- /bot/src/menu/networksStatistic.menu.ts: -------------------------------------------------------------------------------- 1 | import { Menu, MenuRange } from "@grammyjs/menu"; 2 | import { networksService } from "@bot/services"; 3 | import { Context } from "@bot/types"; 4 | import { statisticCallback } from "@bot/menu/callbacks"; 5 | 6 | export const networksStatisticMenu = new Menu("statisticNetworks", { 7 | autoAnswer: false, 8 | }).dynamic(async () => { 9 | const range = new MenuRange(); 10 | 11 | const { getAllNetworks } = networksService(); 12 | const networks = await getAllNetworks(); 13 | 14 | if (networks.length > 0) { 15 | for (let i = 0; i < networks.length; i++) { 16 | const network = networks[i]; 17 | range.text(network.fullName, async (ctx) => { 18 | console.log(222, ctx.update.update_id); 19 | return statisticCallback(ctx, network); 20 | }); 21 | 22 | if ((i + 1) % 2 == 0) { 23 | range.row(); 24 | } 25 | } 26 | } 27 | 28 | range.row(); 29 | return range; 30 | }); 31 | -------------------------------------------------------------------------------- /bot/src/menu/notification/dailyReminder.menu.ts: -------------------------------------------------------------------------------- 1 | import { 2 | chooseNetworkCallback, 3 | chooseTimezoneCallback, 4 | chooseTimeCallback, 5 | isReminderActiveText, 6 | toggleReminderActivity, 7 | } from "../callbacks"; 8 | import { menuCreator } from "@bot/utils"; 9 | import { en } from "@bot/constants/en"; 10 | 11 | const menuList = [ 12 | { 13 | text: en.notification.reminderMenu.networks, 14 | callback: chooseNetworkCallback, 15 | }, 16 | { 17 | row: true, 18 | }, 19 | { 20 | text: en.notification.reminderMenu.time, 21 | callback: chooseTimeCallback, 22 | }, 23 | { 24 | row: true, 25 | }, 26 | { 27 | text: en.notification.reminderMenu.timezone, 28 | callback: chooseTimezoneCallback, 29 | }, 30 | { 31 | row: true, 32 | }, 33 | { 34 | text: isReminderActiveText, 35 | callback: toggleReminderActivity, 36 | }, 37 | { 38 | row: true, 39 | }, 40 | { 41 | back: en.back, 42 | }, 43 | ]; 44 | 45 | export const dailyReminderMenu = menuCreator("dailyReminder", menuList); 46 | -------------------------------------------------------------------------------- /bot/src/menu/wallet.menu.ts: -------------------------------------------------------------------------------- 1 | import { Menu, MenuRange } from "@grammyjs/menu"; 2 | import { Context } from "@bot/types"; 3 | import { en } from "@bot/constants/en"; 4 | import { config } from "@bot/config"; 5 | import { 6 | addManuallyCallback, 7 | bulkImportCallback, 8 | walletListCallback, 9 | deleteWalletCallback, 10 | bulkExportCallback, 11 | } from "@bot/menu/callbacks"; 12 | 13 | export const walletMenu = new Menu("wallets", { 14 | autoAnswer: false, 15 | }).dynamic(async (ctx) => { 16 | const range = new MenuRange(); 17 | 18 | range 19 | .webApp( 20 | en.wallet.menu.keplr, 21 | `${config.UI_URL}/warning?telegram-id=${ctx.from?.id}` 22 | ) 23 | .text(en.wallet.menu.manually, addManuallyCallback) 24 | .row() 25 | .text(en.wallet.menu.bulkImport, bulkImportCallback) 26 | .text(en.wallet.menu.bulkExport, bulkExportCallback) 27 | .row() 28 | .text(en.wallet.menu.list, walletListCallback) 29 | .text(en.wallet.menu.delete, deleteWalletCallback); 30 | 31 | return range; 32 | }); 33 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "es6": true, 5 | "browser": true, 6 | "node": true 7 | }, 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:prettier/recommended", 11 | "plugin:@typescript-eslint/recommended", 12 | "plugin:react/recommended" 13 | ], 14 | "plugins": [ 15 | "@typescript-eslint", 16 | "prettier", 17 | "import", 18 | "react" 19 | ], 20 | "parser": "@typescript-eslint/parser", 21 | "parserOptions": { 22 | "project": [ 23 | "./tsconfig.json" 24 | ] 25 | }, 26 | "rules": { 27 | "@typescript-eslint/strict-boolean-expressions": [ 28 | 1, 29 | { 30 | "allowString": false, 31 | "allowNumber": false 32 | } 33 | ], 34 | "react/react-in-jsx-scope": "off", 35 | "react/display-name": "off", 36 | "react/jsx-filename-extension": [ 37 | 0, 38 | { 39 | "extensions": [ 40 | ".js", 41 | ".jsx", 42 | ".ts", 43 | ".tsx" 44 | ] 45 | } 46 | ] 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /bot/src/menu/notification/notification.menu.ts: -------------------------------------------------------------------------------- 1 | import { dailyReminderMenu } from "@bot/menu"; 2 | import { Context } from "@bot/types"; 3 | import { proposalMenu, alarmMenu } from "@bot/menu"; 4 | import { menuCreator } from "@bot/utils/menuCreator"; 5 | import { en } from "@bot/constants/en"; 6 | 7 | const menuList = [ 8 | { 9 | text: en.notification.menu.reminder, 10 | callback: (ctx: Context) => 11 | ctx.reply(en.notification.reminderMenu.title, { 12 | reply_markup: dailyReminderMenu, 13 | }), 14 | }, 15 | { row: true }, 16 | { 17 | text: en.notification.menu.alarm, 18 | callback: (ctx: Context) => 19 | ctx.reply(en.notification.alarmMenu.title, { 20 | reply_markup: alarmMenu, 21 | }), 22 | }, 23 | { row: true }, 24 | { 25 | text: en.notification.menu.proposals, 26 | callback: (ctx: Context) => 27 | ctx.reply(en.notification.proposalMenu.title, { 28 | reply_markup: proposalMenu, 29 | }), 30 | }, 31 | ]; 32 | 33 | export const notificationMenu = menuCreator("notification", menuList); 34 | -------------------------------------------------------------------------------- /bot/src/menu/notification/timezone.menu.ts: -------------------------------------------------------------------------------- 1 | import { Menu, MenuRange } from "@grammyjs/menu"; 2 | import { usersService } from "@bot/services"; 3 | import { Context } from "@bot/types"; 4 | import { dailyReminderMenu } from "@bot/menu/notification/dailyReminder.menu"; 5 | 6 | export const timezoneMenu = new Menu("timezoneMenu", { 7 | autoAnswer: false, 8 | }).dynamic(async (ctx) => { 9 | const range = new MenuRange(); 10 | 11 | const timezones = ctx.session.timezone; 12 | 13 | if (timezones.length > 0) { 14 | for (const timezone of timezones) { 15 | range 16 | .text(timezone, async (ctx) => { 17 | ctx.session.timezone = []; 18 | const { updateUser } = usersService(ctx); 19 | const regex = /[a-zA-Z]|[/]/g; 20 | await updateUser({ 21 | timezone: timezone.match(regex)?.join(""), 22 | }); 23 | await ctx.reply("Timezone saved", { 24 | reply_markup: dailyReminderMenu, 25 | }); 26 | }) 27 | .row(); 28 | } 29 | } 30 | 31 | return range; 32 | }); 33 | -------------------------------------------------------------------------------- /bot/src/dao/index.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "@server/prisma"; 2 | import { createDao as createAlarmDao } from "./alarms.dao"; 3 | import { createDao as createAlarmPricesDao } from "./alarmPrices.dao"; 4 | import { createDao as createNetworkInNotificationDao } from "./networksInNotification.dao"; 5 | import { createDao as createNotificationDao } from "./notifications.dao"; 6 | import { createDao as createResourceDao } from "./resources.dao"; 7 | import { createDao as createUserDao } from "./users.dao"; 8 | import { createDao as createWalletDao } from "./wallets.dao"; 9 | import { createDao as createNetworkDao } from "./networks.dao"; 10 | 11 | export const alarmDao = createAlarmDao(prisma); 12 | export const networkInNotificationDao = createNetworkInNotificationDao(prisma); 13 | export const notificationDao = createNotificationDao(prisma); 14 | export const resourceDao = createResourceDao(prisma); 15 | export const userDao = createUserDao(prisma); 16 | export const walletDao = createWalletDao(prisma); 17 | export const networkDao = createNetworkDao(prisma); 18 | export const alarmPricesDao = createAlarmPricesDao(prisma); 19 | -------------------------------------------------------------------------------- /bot/src/menu/notification/networksReminder.menu.ts: -------------------------------------------------------------------------------- 1 | import { Menu, MenuRange } from "@grammyjs/menu"; 2 | import { networksService, networksInNotificationService } from "@bot/services"; 3 | import { Context } from "@bot/types"; 4 | import { en } from "@bot/constants/en"; 5 | 6 | export const networksReminderMenu = new Menu("reminderNetworks", { 7 | autoAnswer: false, 8 | }).dynamic(async (ctx) => { 9 | const range = new MenuRange(); 10 | 11 | const { getAllNetworks } = networksService(); 12 | const networks = await getAllNetworks(); 13 | for (let i = 0; i < networks.length; i++) { 14 | const network = networks[i]; 15 | 16 | const { isNetworkActive, updateReminder } = 17 | await networksInNotificationService({ 18 | ctx, 19 | network, 20 | }); 21 | 22 | range.text( 23 | isNetworkActive ? `🔔 ${network.fullName}` : `🔕 ${network.fullName}`, 24 | async (ctx) => { 25 | await updateReminder(); 26 | ctx.menu.update(); 27 | } 28 | ); 29 | 30 | if ((i + 1) % 2 == 0) { 31 | range.row(); 32 | } 33 | } 34 | 35 | range.row(); 36 | range.back(en.back); 37 | 38 | return range; 39 | }); 40 | -------------------------------------------------------------------------------- /bot/src/menu/notification/timeReminder.menu.ts: -------------------------------------------------------------------------------- 1 | import { Menu, MenuRange } from "@grammyjs/menu"; 2 | import { notificationsService } from "@bot/services"; 3 | import { Context } from "@bot/types"; 4 | 5 | const timeArr = [ 6 | "12:00 AM", 7 | "3:00 AM", 8 | "6:00 AM", 9 | "9:00 AM", 10 | "12:00 PM", 11 | "3:00 PM", 12 | "6:00 PM", 13 | "9:00 PM", 14 | ]; 15 | 16 | export const networkTimeReminderMenu = new Menu( 17 | "networkTimeReminder", 18 | { 19 | autoAnswer: false, 20 | } 21 | ).dynamic(async (ctx) => { 22 | const range = new MenuRange(); 23 | 24 | for (let i = 0; i < timeArr.length; i++) { 25 | const time = timeArr[i]; 26 | 27 | const { isNotificationTimeActive, updateNotificationReminderTime } = 28 | await notificationsService({ 29 | ctx, 30 | timeArr, 31 | }); 32 | const isActive = isNotificationTimeActive(time); 33 | 34 | range.text(isActive ? `✅ ${time}` : `❎ ${time}`, async (ctx) => { 35 | await updateNotificationReminderTime(time); 36 | ctx.menu.update(); 37 | }); 38 | 39 | if ((i + 1) % 2 == 0) { 40 | range.row(); 41 | } 42 | } 43 | range.row().back("<< Go back"); 44 | 45 | return range; 46 | }); 47 | -------------------------------------------------------------------------------- /bot/src/middlewares/setupWalletsMigrate.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Middleware } from "grammy"; 2 | import { Context } from "@bot/types"; 3 | import { walletDao } from "@bot/dao"; 4 | import { encrypt, validation } from "@bot/utils"; 5 | import { walletsService } from "@bot/services"; 6 | 7 | export const middleware = (): Middleware => async (ctx, next) => { 8 | const user = ctx.local.user; 9 | const { updateUserWallet } = walletsService(ctx); 10 | if (typeof user === "undefined" || ctx.session.isWalletsMigrated) 11 | return next(); 12 | 13 | const wallets = await walletDao.getAllWallets({ 14 | where: { 15 | userId: user.id, 16 | }, 17 | }); 18 | 19 | for (let i = 0; i < wallets.length; i++) { 20 | if (i + 1 === wallets.length) { 21 | ctx.session.isWalletsMigrated = true; 22 | } 23 | 24 | const address = wallets[i].address; 25 | if (validation.isValidAddress(address)) { 26 | const { iv, encryptedData } = encrypt( 27 | address, 28 | ctx.session.walletPassword 29 | ); 30 | 31 | updateUserWallet({ 32 | walletId: wallets[i].id, 33 | address: encryptedData, 34 | iv, 35 | }); 36 | } 37 | } 38 | 39 | return next(); 40 | }; 41 | -------------------------------------------------------------------------------- /bot/src/menu/notification/proposal.menu.ts: -------------------------------------------------------------------------------- 1 | import { Menu, MenuRange } from "@grammyjs/menu"; 2 | import { networksService, networksInNotificationService } from "@bot/services"; 3 | import { Context } from "@bot/types"; 4 | import dayjs from "dayjs"; 5 | import { en } from "@bot/constants/en"; 6 | 7 | export const proposalMenu = new Menu("governance", { 8 | autoAnswer: false, 9 | }).dynamic(async (ctx) => { 10 | const range = new MenuRange(); 11 | 12 | const { getAllNetworks } = networksService(); 13 | const networks = await getAllNetworks(); 14 | 15 | for (let i = 0; i < networks.length; i++) { 16 | const network = networks[i]; 17 | const { isGovActive, updateGovernance } = 18 | await networksInNotificationService({ 19 | ctx, 20 | network, 21 | }); 22 | const time = dayjs().toDate(); 23 | 24 | range.text( 25 | isGovActive ? `🔔 ${network.fullName}` : `🔕 ${network.fullName}`, 26 | async (ctx) => { 27 | await updateGovernance(time); 28 | ctx.menu.update(); 29 | } 30 | ); 31 | 32 | if ((i + 1) % 2 == 0) { 33 | range.row(); 34 | } 35 | } 36 | 37 | range.row(); 38 | range.back(en.back); 39 | 40 | return range; 41 | }); 42 | -------------------------------------------------------------------------------- /bot/src/menu/assets.menu.ts: -------------------------------------------------------------------------------- 1 | import { Menu, MenuRange } from "@grammyjs/menu"; 2 | import { Context } from "@bot/types"; 3 | import { walletsService } from "@bot/services"; 4 | import { assetsCallback, totalAmountCallback } from "@bot/menu/callbacks"; 5 | import { en } from "@bot/constants/en"; 6 | 7 | let currentUpdateId = 0; 8 | 9 | export const assetsMenu = new Menu("assets", { 10 | autoAnswer: false, 11 | }).dynamic(async (ctx) => { 12 | const range = new MenuRange(); 13 | 14 | const { getAllUserWallets } = walletsService(ctx); 15 | const userWallets = await getAllUserWallets(); 16 | 17 | for (let i = 0; i < userWallets.length; i++) { 18 | const wallet = userWallets[i]; 19 | range.text(wallet.name || wallet.address, async (ctx) => { 20 | const output = await assetsCallback(wallet); 21 | return ctx.reply(output); 22 | }); 23 | 24 | range.row(); 25 | } 26 | 27 | range.row(); 28 | range.text(en.assets.menu.all, async () => { 29 | if (currentUpdateId === ctx.update.update_id) { 30 | return; 31 | } 32 | currentUpdateId = ctx.update.update_id; 33 | await ctx.replyWithChatAction("typing"); 34 | 35 | const output = await totalAmountCallback(ctx); 36 | 37 | return ctx.reply(output); 38 | }); 39 | 40 | return range; 41 | }); 42 | -------------------------------------------------------------------------------- /bot/src/services/networks.service.ts: -------------------------------------------------------------------------------- 1 | import { networkDao } from "@bot/dao"; 2 | import { ChainInfo } from "@bot/types/general"; 3 | import { config } from "@bot/chains"; 4 | import { cosmosConfig } from "@bot/chains/cosmos"; 5 | 6 | export const networksService = () => { 7 | const getNetwork = async ({ 8 | networkId, 9 | name, 10 | }: { 11 | networkId?: number; 12 | name?: string; 13 | }) => { 14 | const network = (await networkDao.getNetwork({ 15 | where: { 16 | id: networkId, 17 | name, 18 | }, 19 | })) || { name: "", publicUrl: "", fullName: "", id: 0 }; 20 | const chain: ChainInfo = 21 | config.find((item) => item.network === network.name) || cosmosConfig; 22 | const { tokenUnits, primaryTokenUnit } = chain; 23 | const publicUrl = network?.publicUrl || ""; 24 | const denom = tokenUnits[primaryTokenUnit].display; 25 | 26 | return { 27 | network, 28 | publicUrl, 29 | denom, 30 | coingeckoId: chain.coingeckoId, 31 | }; 32 | }; 33 | 34 | const getAllNetworks = async () => { 35 | const networks = await networkDao.getAllNetworks(); 36 | 37 | return networks.sort((a, b) => a.fullName.localeCompare(b.fullName)); 38 | }; 39 | 40 | return { 41 | getNetwork, 42 | getAllNetworks, 43 | }; 44 | }; 45 | -------------------------------------------------------------------------------- /bot/src/api/requests/fetchTokenPrice.ts: -------------------------------------------------------------------------------- 1 | import { getPnlDate, restRequest } from "@bot/utils"; 2 | import { getTokenPriceByDate, getTokenPrice } from "@bot/constants/api"; 3 | 4 | export const fetchTokenPrice = async (apiId: string) => { 5 | const defaultReturnValue = { 6 | tokenPrice: {}, 7 | }; 8 | try { 9 | const response = await restRequest( 10 | getTokenPrice({ 11 | ids: apiId, 12 | vs_currencies: "usd", 13 | }) 14 | ); 15 | 16 | const data = await response.json(); 17 | 18 | return { 19 | tokenPrice: data, 20 | }; 21 | } catch (error) { 22 | return defaultReturnValue; 23 | } 24 | }; 25 | 26 | export const fetchTokenHistory = async (apiId: string) => { 27 | const defaultReturnValue = { 28 | tokenPrice: [], 29 | }; 30 | try { 31 | const dates = getPnlDate(); 32 | const promises = dates.map(async (date) => { 33 | const response = await restRequest( 34 | getTokenPriceByDate(apiId, { 35 | date, 36 | localization: "false", 37 | }) 38 | ); 39 | 40 | return await response.json(); 41 | }); 42 | const data = await Promise.all(promises); 43 | return { 44 | tokenPrice: Object.values(data), 45 | }; 46 | } catch (error) { 47 | return defaultReturnValue; 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /bot/src/services/users.service.ts: -------------------------------------------------------------------------------- 1 | import { userDao } from "@bot/dao"; 2 | import { Context } from "@bot/types"; 3 | 4 | export const usersService = (ctx?: Context) => { 5 | const user = ctx?.local.user || { 6 | telegramId: 0, 7 | }; 8 | 9 | const upsertUser = async (telegramId: number) => { 10 | return await userDao.upsertUser({ 11 | where: { 12 | telegramId: telegramId, 13 | }, 14 | create: { 15 | telegramId: telegramId, 16 | notification: { create: {} }, 17 | }, 18 | update: {}, 19 | }); 20 | }; 21 | 22 | const updateUser = async ({ timezone }: { timezone?: string }) => { 23 | return await userDao.updateUser({ 24 | where: { 25 | telegramId: user.telegramId, 26 | }, 27 | data: { 28 | timezone, 29 | }, 30 | }); 31 | }; 32 | 33 | const getUser = async ({ 34 | id, 35 | telegramId, 36 | }: { 37 | id?: number; 38 | telegramId?: number; 39 | }) => { 40 | const args = telegramId ? { telegramId } : { id }; 41 | return await userDao.getUser({ 42 | where: args, 43 | }); 44 | }; 45 | 46 | const getAllUser = async () => { 47 | return await userDao.getAllUser(); 48 | }; 49 | 50 | return { 51 | upsertUser, 52 | updateUser, 53 | getUser, 54 | getAllUser, 55 | }; 56 | }; 57 | -------------------------------------------------------------------------------- /server/run.ts: -------------------------------------------------------------------------------- 1 | import "module-alias/register"; 2 | 3 | import { bot } from "@bot/bot"; 4 | import { server } from "@server/server"; 5 | import { prisma } from "@server/prisma"; 6 | import { config } from "@bot/config"; 7 | import { logger } from "@bot/logger"; 8 | import { handleGracefulShutdown } from "@bot/helpers/gracefulShutdownHandler"; 9 | 10 | // Graceful shutdown 11 | prisma.$on("beforeExit", handleGracefulShutdown); 12 | 13 | const run = async () => { 14 | if (config.isProd) { 15 | server.listen({ port: config.PORT, host: config.BOT_SERVER_HOST }, () => { 16 | server.cron.startAllJobs(); 17 | 18 | bot.api 19 | .setWebhook(config.BOT_WEBHOOK, { 20 | allowed_updates: config.BOT_ALLOWED_UPDATES, 21 | }) 22 | .catch((err) => logger.error(err)); 23 | }); 24 | } else { 25 | server.listen( 26 | { host: config.BOT_SERVER_HOST, port: config.PORT }, 27 | (err) => { 28 | if (err) throw err; 29 | server.cron.startAllJobs(); 30 | console.log("Server listening on http://localhost:3000"); 31 | } 32 | ); 33 | 34 | bot.start({ 35 | allowed_updates: config.BOT_ALLOWED_UPDATES, 36 | onStart: ({ username }) => 37 | logger.info({ 38 | msg: "bot running...", 39 | username, 40 | }), 41 | }); 42 | } 43 | }; 44 | run(); 45 | -------------------------------------------------------------------------------- /bot/src/menu/walletRemove.menu.ts: -------------------------------------------------------------------------------- 1 | import { Menu, MenuRange } from "@grammyjs/menu"; 2 | import { walletsService } from "@bot/services"; 3 | import { Context } from "@bot/types"; 4 | import { template } from "@bot/utils"; 5 | import { en } from "@bot/constants/en"; 6 | 7 | export const walletRemoveMenu = new Menu("walletRemove", { 8 | autoAnswer: false, 9 | }).dynamic(async (ctx) => { 10 | await ctx.replyWithChatAction("typing"); 11 | const range = new MenuRange(); 12 | 13 | const { getAllUserWallets, removeUserWallet, removeAllUserWallet } = 14 | walletsService(ctx); 15 | const userWallets = await getAllUserWallets(); 16 | 17 | if (userWallets.length > 0) { 18 | for (let i = 0; i < userWallets.length; i++) { 19 | const currWallet = userWallets[i]; 20 | range 21 | .text(currWallet.name || currWallet.address, async (ctx) => { 22 | await removeUserWallet(currWallet.id); 23 | return ctx.reply( 24 | template(en.wallet.removedWallet, { 25 | address: currWallet.name || currWallet.address, 26 | }) 27 | ); 28 | }) 29 | .row(); 30 | } 31 | } 32 | 33 | range.text(en.wallet.menu.removeAll, async () => { 34 | const user = ctx.local.user; 35 | if (typeof user === "undefined") return; 36 | 37 | await removeAllUserWallet(user.id); 38 | 39 | return ctx.reply(en.wallet.removedAllWallets); 40 | }); 41 | 42 | return range; 43 | }); 44 | -------------------------------------------------------------------------------- /bot/src/menu/callbacks/dailyReminder.callbacks.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "@bot/types"; 2 | import { networksReminderMenu } from "@bot/menu/notification/networksReminder.menu"; 3 | import { networkTimeReminderMenu } from "@bot/menu/notification/timeReminder.menu"; 4 | import { notificationsService } from "@bot/services"; 5 | import { MenuFlavor } from "@grammyjs/menu"; 6 | import { en } from "@bot/constants/en"; 7 | 8 | export async function chooseNetworkCallback(ctx: Context) { 9 | await ctx.reply(en.notification.reminderMenu.chooseNetwork, { 10 | reply_markup: networksReminderMenu, 11 | }); 12 | } 13 | 14 | export async function chooseTimeCallback(ctx: Context) { 15 | await ctx.reply(en.notification.reminderMenu.chooseReminderTime, { 16 | reply_markup: networkTimeReminderMenu, 17 | }); 18 | } 19 | 20 | export async function chooseTimezoneCallback(ctx: Context) { 21 | ctx.session.step = "timezone"; 22 | await ctx.reply(en.notification.reminderMenu.fillCountry); 23 | } 24 | 25 | export async function isReminderActiveText(ctx: Context) { 26 | const { isReminderActive } = await notificationsService({ ctx }); 27 | 28 | return isReminderActive 29 | ? en.notification.reminderMenu.enabled 30 | : en.notification.reminderMenu.disabled; 31 | } 32 | 33 | export async function toggleReminderActivity(ctx: Context & MenuFlavor) { 34 | const { updateNotification } = await notificationsService({ ctx }); 35 | await updateNotification({ triggerReminderActivity: true }); 36 | 37 | ctx.menu.update(); 38 | } 39 | -------------------------------------------------------------------------------- /bot/src/menu/notification/networksAlarm.menu.ts: -------------------------------------------------------------------------------- 1 | import { Menu, MenuRange } from "@grammyjs/menu"; 2 | import { networksService } from "@bot/services"; 3 | import { Context } from "@bot/types"; 4 | import { getTokenPrice } from "@bot/api"; 5 | import { template } from "@bot/utils"; 6 | import { en } from "@bot/constants/en"; 7 | 8 | export const networksAlarmMenu = new Menu("alarmNetworks", { 9 | autoAnswer: false, 10 | }).dynamic(async () => { 11 | const range = new MenuRange(); 12 | 13 | const { getAllNetworks } = networksService(); 14 | const { getNetwork } = networksService(); 15 | const networks = await getAllNetworks(); 16 | 17 | if (networks.length > 0) { 18 | for (let i = 0; i < networks.length; i++) { 19 | const network = networks[i]; 20 | 21 | range.text(network.fullName, async (ctx) => { 22 | ctx.session.alarmNetwork = network; 23 | ctx.session.step = "notification"; 24 | const { coingeckoId } = await getNetwork({ 25 | networkId: network.id, 26 | }); 27 | 28 | const { price } = await getTokenPrice({ apiId: coingeckoId }); 29 | 30 | await ctx.reply( 31 | template(en.notification.alarmMenu.alarmPriceInput, { 32 | networkName: network.fullName, 33 | price: `${price}`, 34 | }) 35 | ); 36 | }); 37 | 38 | if ((i + 1) % 2 == 0) { 39 | range.row(); 40 | } 41 | } 42 | } 43 | 44 | range.row(); 45 | range.back(en.back); 46 | 47 | return range; 48 | }); 49 | -------------------------------------------------------------------------------- /bot/src/utils/menuCreator.ts: -------------------------------------------------------------------------------- 1 | import { Menu, MenuFlavor } from "@grammyjs/menu"; 2 | import { Context } from "@bot/types"; 3 | import { Message } from "@grammyjs/types"; 4 | 5 | type MenuItem = 6 | | { 7 | text: string; 8 | callback: (ctx: Context) => Promise; 9 | row?: boolean; 10 | back?: string; 11 | } 12 | | { 13 | text: (ctx: Context) => Promise; 14 | callback: ( 15 | ctx: Context & MenuFlavor 16 | ) => Promise; 17 | row?: boolean; 18 | back?: string; 19 | } 20 | | { 21 | text?: (ctx: Context) => Promise; 22 | callback?: (ctx: Context) => Promise; 23 | row: boolean; 24 | back?: string; 25 | } 26 | | { 27 | text?: (ctx: Context) => Promise; 28 | callback?: (ctx: Context) => Promise; 29 | row?: boolean; 30 | back: string; 31 | }; 32 | 33 | type MenuList = MenuItem[]; 34 | 35 | export const menuCreator = (id: string, menuList: MenuList) => { 36 | const menu = new Menu(id); 37 | 38 | for (const item of menuList) { 39 | if ( 40 | (typeof item.text === "string" || typeof item.text === "function") && 41 | typeof item.callback === "function" 42 | ) { 43 | menu.text(item.text, item.callback); 44 | } 45 | if (item.row) { 46 | menu.row(); 47 | } 48 | if (item.back) { 49 | menu.back(item.back); 50 | } 51 | } 52 | 53 | return menu; 54 | }; 55 | -------------------------------------------------------------------------------- /bot/src/chains/config.ts: -------------------------------------------------------------------------------- 1 | import { agoricConfig } from "./agoric"; 2 | import { akashConfig } from "./akash"; 3 | import { assetmantleConfig } from "./assetMantle"; 4 | import { chihuahuaConfig } from "./chihuahua"; 5 | import { cosmosConfig } from "./cosmos"; 6 | import { bandConfig } from "./band"; 7 | import { bitsongConfig } from "./bitsong"; 8 | import { cheqdConfig } from "./cheqd"; 9 | import { comdexConfig } from "./comdex"; 10 | import { crescentConfig } from "./crescent"; 11 | import { desmosConfig } from "./desmos"; 12 | import { emoneyConfig } from "./emoney"; 13 | import { evmosConfig } from "./evmos"; 14 | import { likecoinConfig } from "./likecoin"; 15 | import { osmoConfig } from "./osmosis"; 16 | import { persistanceConfig } from "./persistance"; 17 | import { provenanceConfig } from "./provenance"; 18 | import { regenConfig } from "./regen"; 19 | import { rizonConfig } from "./rizon"; 20 | import { secretConfig } from "./secret"; 21 | import { sifchainConfig } from "./sifchain"; 22 | import { stargazeConfig } from "./stargaze"; 23 | import { junoConfig } from "./juno"; 24 | import { terraConfig } from "./terra"; 25 | 26 | export const config = [ 27 | agoricConfig, 28 | akashConfig, 29 | assetmantleConfig, 30 | bandConfig, 31 | bitsongConfig, 32 | cheqdConfig, 33 | chihuahuaConfig, 34 | comdexConfig, 35 | cosmosConfig, 36 | crescentConfig, 37 | desmosConfig, 38 | emoneyConfig, 39 | evmosConfig, 40 | junoConfig, 41 | likecoinConfig, 42 | osmoConfig, 43 | persistanceConfig, 44 | provenanceConfig, 45 | regenConfig, 46 | rizonConfig, 47 | secretConfig, 48 | sifchainConfig, 49 | stargazeConfig, 50 | terraConfig, 51 | ]; 52 | -------------------------------------------------------------------------------- /bot/src/utils/crypto.ts: -------------------------------------------------------------------------------- 1 | import { 2 | randomBytes, 3 | createHash, 4 | createCipheriv, 5 | createDecipheriv, 6 | } from "crypto"; 7 | const algorithm = "aes-256-cbc"; 8 | 9 | export function encrypt(text: string, key: string) { 10 | const iv = randomBytes(16); 11 | const hash = getPasswordHash(key); 12 | const cipher = createCipheriv(algorithm, hash, iv); 13 | let encrypted = cipher.update(text); 14 | encrypted = Buffer.concat([encrypted, cipher.final()]); 15 | 16 | return { iv: iv.toString("hex"), encryptedData: encrypted.toString("hex") }; 17 | } 18 | 19 | export function decrypt( 20 | text: { 21 | iv: string; 22 | encryptedData: string; 23 | }, 24 | key: string 25 | ) { 26 | try { 27 | const hash = getPasswordHash(key); 28 | const iv = Buffer.from(text.iv, "hex"); 29 | const encryptedText = Buffer.from(text.encryptedData, "hex"); 30 | const decipher = createDecipheriv(algorithm, hash, iv); 31 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 32 | // @ts-ignore 33 | const buffer = Buffer.from(encryptedText, "base64").toString("hex"); 34 | const firstPart = decipher.update(buffer, "hex", "base64"); 35 | const finalPart = decipher.final("base64") || ""; 36 | const decrypted = `${firstPart}${finalPart}`; 37 | const buf = Buffer.from(decrypted, "base64"); 38 | return buf.toString("utf8"); 39 | } catch (e) { 40 | console.log(e); 41 | return "If you see this error, just remove current wallet and re-add it"; 42 | } 43 | } 44 | 45 | function getPasswordHash(password: string) { 46 | return createHash("sha256") 47 | .update(String(password)) 48 | .digest("base64") 49 | .substr(0, 32); 50 | } 51 | -------------------------------------------------------------------------------- /bot/src/menu/networksResources.menu.ts: -------------------------------------------------------------------------------- 1 | import { Menu, MenuRange } from "@grammyjs/menu"; 2 | import { networksService, resourcesService } from "@bot/services"; 3 | import { Context } from "@bot/types"; 4 | import { capitalizeFirstLetter, template } from "@bot/utils"; 5 | import { en } from "@bot/constants/en"; 6 | 7 | export const networksResourcesMenu = new Menu("networksResources", { 8 | autoAnswer: false, 9 | }).dynamic(async (ctx) => { 10 | await ctx.replyWithChatAction("typing"); 11 | const { getResource } = resourcesService(); 12 | const range = new MenuRange(); 13 | 14 | const { getAllNetworks } = networksService(); 15 | const networks = await getAllNetworks(); 16 | 17 | if (networks.length > 0) { 18 | for (let i = 0; i < networks.length; i++) { 19 | const network = networks[i]; 20 | range.text(`${network.fullName}`, async (ctx) => { 21 | let output = ""; 22 | 23 | const resource = (await getResource(network.resourceId)) || { 24 | site: "", 25 | discord: "", 26 | twitter: "", 27 | youtube: "", 28 | blog: "", 29 | github: "", 30 | telegram: "", 31 | }; 32 | for (const [item, link] of Object.entries(resource)) { 33 | if (typeof link === "string" && link.length > 0) { 34 | output += template(en.resources.menu.resourceItem, { 35 | item: capitalizeFirstLetter(item), 36 | link, 37 | }); 38 | } 39 | } 40 | 41 | return ctx.reply(output, { disable_web_page_preview: true }); 42 | }); 43 | if ((i + 1) % 2 == 0) { 44 | range.row(); 45 | } 46 | } 47 | } 48 | 49 | range.row(); 50 | 51 | return range; 52 | }); 53 | -------------------------------------------------------------------------------- /server/prisma.ts: -------------------------------------------------------------------------------- 1 | import { Prisma, PrismaClient } from "@prisma/client"; 2 | import { logger } from "@bot/logger"; 3 | 4 | const parseParameters = (parameters: string): unknown[] => { 5 | try { 6 | return JSON.parse(parameters); 7 | } catch { 8 | return []; 9 | } 10 | }; 11 | 12 | export const prisma = new PrismaClient({ 13 | log: [ 14 | { 15 | emit: "event", 16 | level: "query", 17 | }, 18 | { 19 | emit: "event", 20 | level: "error", 21 | }, 22 | { 23 | emit: "event", 24 | level: "info", 25 | }, 26 | { 27 | emit: "event", 28 | level: "warn", 29 | }, 30 | ], 31 | }); 32 | 33 | prisma.$on("query", (e: Prisma.QueryEvent) => { 34 | const parameters = parseParameters( 35 | e.params.replace( 36 | /\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.?\d* UTC/g, 37 | (date) => `"${date}"` 38 | ) 39 | ); 40 | const query = e.query.replace( 41 | /(\?|\$\d+)/g, 42 | (match, param, offset, string: string) => { 43 | const parameter = JSON.stringify(parameters.shift()); 44 | const previousChar = string.charAt(offset - 1); 45 | 46 | return (previousChar === "," ? " " : "") + parameter; 47 | } 48 | ); 49 | 50 | logger.debug({ 51 | msg: "database query", 52 | query, 53 | duration: e.duration, 54 | }); 55 | }); 56 | 57 | prisma.$on("error", (e: Prisma.LogEvent) => { 58 | logger.error({ 59 | msg: "database error", 60 | target: e.target, 61 | message: e.message, 62 | }); 63 | }); 64 | 65 | prisma.$on("info", (e: Prisma.LogEvent) => { 66 | logger.info({ 67 | msg: "database info", 68 | target: e.target, 69 | message: e.message, 70 | }); 71 | }); 72 | 73 | prisma.$on("warn", (e: Prisma.LogEvent) => { 74 | logger.warn({ 75 | msg: "database warning", 76 | target: e.target, 77 | message: e.message, 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /bot/src/menu/networksProposals.menu.ts: -------------------------------------------------------------------------------- 1 | import { Menu, MenuRange } from "@grammyjs/menu"; 2 | import { networksService } from "@bot/services"; 3 | import { Context } from "@bot/types"; 4 | import { getProposals } from "@bot/api"; 5 | import { en } from "@bot/constants/en"; 6 | import { template, getNumberEmoji } from "@bot/utils"; 7 | 8 | export const networksProposalMenu = new Menu("networksProposal", { 9 | autoAnswer: false, 10 | }).dynamic(async () => { 11 | const range = new MenuRange(); 12 | 13 | const { getAllNetworks } = networksService(); 14 | const networks = await getAllNetworks(); 15 | 16 | if (networks.length > 0) { 17 | for (let i = 0; i < networks.length; i++) { 18 | const network = networks[i]; 19 | range.text(network.fullName, async function (ctx) { 20 | let output = ""; 21 | const { activeProposals } = await getProposals(network.publicUrl); 22 | 23 | activeProposals.map(({ title, description, proposalId }, key) => { 24 | if (activeProposals.length > 1) { 25 | output += template(en.proposals.menu.proposalDescriptionTitle, { 26 | number: getNumberEmoji(key + 1), 27 | }); 28 | } 29 | const str = template(en.proposals.menu.proposalDescription, { 30 | title: title, 31 | description: description, 32 | }); 33 | output += str.replaceAll(/[\\]+[n]/gm, "\n"); 34 | 35 | if (output.length > 4000) { 36 | output = output.slice(0, 4000) + "... \n\n"; 37 | } 38 | 39 | if (network.keplrId) { 40 | output += template(en.proposals.menu.proposalDescriptionLink, { 41 | keplrId: `${network.keplrId}`, 42 | proposalId: `${proposalId}`, 43 | }); 44 | } 45 | 46 | ctx.reply(output, { disable_web_page_preview: true }); 47 | }); 48 | }); 49 | 50 | if ((i + 1) % 2 == 0) { 51 | range.row(); 52 | } 53 | } 54 | } 55 | 56 | range.row(); 57 | return range; 58 | }); 59 | -------------------------------------------------------------------------------- /bot/src/features/botAdmin.feature.ts: -------------------------------------------------------------------------------- 1 | import { isUserId } from "grammy-guard"; 2 | 3 | import { userDao } from "@bot/dao"; 4 | import { config } from "@bot/config"; 5 | import { logHandle } from "@bot/helpers/logging"; 6 | import { router } from "@bot/middlewares"; 7 | import { usersService } from "@bot/services"; 8 | 9 | export const feature = router 10 | .route("admin") 11 | .filter(isUserId(config.BOT_ADMIN_USER_ID)); 12 | 13 | feature 14 | .filter(isUserId(config.BOT_ADMIN_USER_ID)) 15 | .command("stats", logHandle("handle /stats"), async (ctx) => { 16 | await ctx.replyWithChatAction("typing"); 17 | 18 | const usersCount = await userDao.count(); 19 | 20 | const stats = `Users count: ${usersCount}`; 21 | 22 | return ctx.reply(stats); 23 | }); 24 | 25 | feature 26 | .filter(isUserId(config.BOT_ADMIN_USER_ID)) 27 | .command("updates", logHandle("handle updates"), async (ctx) => { 28 | ctx.session.step = "admin"; 29 | return ctx.reply("Write update message"); 30 | }); 31 | 32 | feature 33 | .filter((ctx) => { 34 | const isAdmin = isUserId(config.BOT_ADMIN_USER_ID); 35 | return isAdmin(ctx) && ctx.session.step === "admin"; 36 | }) 37 | .on("message:text", async (ctx) => { 38 | const message = ctx.message.text || ""; 39 | 40 | const { getAllUser } = usersService(); 41 | const users = await getAllUser(); 42 | 43 | for (const user of users) { 44 | await ctx.api.sendMessage(Number(user.telegramId), message); 45 | } 46 | }); 47 | 48 | feature 49 | .filter(isUserId(config.BOT_ADMIN_USER_ID)) 50 | .command("setcommands", logHandle("handle /setcommands"), async (ctx) => { 51 | await ctx.replyWithChatAction("typing"); 52 | 53 | // set private chat admin commands 54 | await ctx.api.setMyCommands( 55 | [ 56 | { 57 | command: "stats", 58 | description: "Stats", 59 | }, 60 | { 61 | command: "setcommands", 62 | description: "Set bot commands", 63 | }, 64 | ], 65 | { 66 | scope: { 67 | type: "chat", 68 | chat_id: config.BOT_ADMIN_USER_ID, 69 | }, 70 | } 71 | ); 72 | 73 | return ctx.reply("Commands updated"); 74 | }); 75 | -------------------------------------------------------------------------------- /bot/src/api/requests/fetchBalance.ts: -------------------------------------------------------------------------------- 1 | import { restRequest } from "@bot/utils"; 2 | 3 | export const fetchAvailableBalances = async (url: string, address: string) => { 4 | const defaultReturnValue = { 5 | accountBalances: { 6 | coins: [], 7 | }, 8 | }; 9 | try { 10 | const req = await restRequest( 11 | `${url}cosmos/bank/v1beta1/balances/${address}` 12 | ); 13 | const res = await req.json(); 14 | 15 | return { 16 | accountBalances: { 17 | coins: res.balances, 18 | }, 19 | }; 20 | } catch (error) { 21 | console.log(error); 22 | return defaultReturnValue; 23 | } 24 | }; 25 | 26 | export const fetchDelegationBalance = async (url: string, address: string) => { 27 | const defaultReturnValue = { 28 | delegationBalance: [], 29 | }; 30 | 31 | try { 32 | const req = await restRequest( 33 | `${url}cosmos/staking/v1beta1/delegations/${address}` 34 | ); 35 | const res = await req.json(); 36 | 37 | return { 38 | delegationBalance: res.delegation_responses, 39 | }; 40 | } catch (error) { 41 | console.log(error); 42 | return defaultReturnValue; 43 | } 44 | }; 45 | 46 | export const fetchUnbondingBalance = async (url: string, address: string) => { 47 | const defaultReturnValue = { 48 | unbondingBalance: { 49 | coins: [], 50 | }, 51 | }; 52 | 53 | try { 54 | const req = await restRequest( 55 | `${url}cosmos/staking/v1beta1/delegators/${address}/unbonding_delegations` 56 | ); 57 | const res = await req.json(); 58 | 59 | return { 60 | unbondingBalance: res.unbonding_responses, 61 | }; 62 | } catch (error) { 63 | console.log(error); 64 | return defaultReturnValue; 65 | } 66 | }; 67 | 68 | export const fetchRewards = async (url: string, address: string) => { 69 | const defaultReturnValue = { 70 | delegationRewards: [], 71 | }; 72 | try { 73 | const req = await restRequest( 74 | `${url}cosmos/distribution/v1beta1/delegators/${address}/rewards` 75 | ); 76 | const res = await req.json(); 77 | 78 | return { 79 | delegationRewards: res.rewards, 80 | }; 81 | } catch (error) { 82 | console.log(error); 83 | return defaultReturnValue; 84 | } 85 | }; 86 | -------------------------------------------------------------------------------- /bot/src/services/alarms.service.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "@bot/types"; 2 | import { alarmDao } from "@bot/dao"; 3 | import { getTokenPrice } from "@bot/api"; 4 | import { alarmPricesService } from "./alarmPrices.service"; 5 | import { networksService } from "./networks.service"; 6 | 7 | export async function alarmsService({ 8 | ctx, 9 | }: { 10 | ctx?: Context; 11 | } = {}) { 12 | const user = ctx?.local.user || { 13 | id: 0, 14 | }; 15 | const getAlarm = async (networkId: number) => 16 | await alarmDao.getAlarm({ 17 | where: { 18 | userId_networkId: { 19 | userId: user.id, 20 | networkId: networkId, 21 | }, 22 | }, 23 | }); 24 | 25 | const updateAlarmNetworks = async ( 26 | price: string, 27 | networkId: number, 28 | userId?: number 29 | ) => { 30 | const { getAllAlarmPrices } = alarmPricesService(); 31 | const { getNetwork } = networksService(); 32 | 33 | const alarm = (await getAlarm(networkId)) || { id: 0 }; 34 | const { coingeckoId } = await getNetwork({ networkId }); 35 | const alarmPrices = (await getAllAlarmPrices(alarm.id)) || { 36 | id: 0, 37 | }; 38 | 39 | const tokenPrices = await getTokenPrice({ apiId: coingeckoId }); 40 | const createAlarmPrice = { 41 | price: Number(price), 42 | coingeckoPrice: tokenPrices.price, 43 | }; 44 | const alarmPrice = alarmPrices.find((item) => item.price === Number(price)); 45 | 46 | await alarmDao.upsertAlarm({ 47 | where: { 48 | userId_networkId: { 49 | userId: userId ? userId : user.id, 50 | networkId: networkId, 51 | }, 52 | }, 53 | create: { 54 | networkId: networkId, 55 | userId: userId ? userId : user.id, 56 | alarmPrices: { 57 | create: createAlarmPrice, 58 | }, 59 | }, 60 | update: !alarmPrice?.id 61 | ? { 62 | alarmPrices: { 63 | create: createAlarmPrice, 64 | }, 65 | } 66 | : {}, 67 | }); 68 | }; 69 | 70 | const getAllAlarms = (all = false) => { 71 | const args = all ? {} : { where: { userId: user.id } }; 72 | return alarmDao.getAllAlarms(args); 73 | }; 74 | 75 | return { 76 | updateAlarmNetworks, 77 | getAllAlarms, 78 | getAlarm, 79 | }; 80 | } 81 | -------------------------------------------------------------------------------- /bot/src/menu/callbacks/wallet.callbacks.ts: -------------------------------------------------------------------------------- 1 | import { writeFileSync } from "fs"; 2 | import { Context } from "@bot/types"; 3 | import { en } from "@bot/constants/en"; 4 | import { walletsService } from "@bot/services"; 5 | import { getNumberEmoji, template } from "@bot/utils"; 6 | import { walletRemoveMenu } from "@bot/menu"; 7 | import { InputFile } from "grammy"; 8 | import { STEPS } from "@bot/constants/step"; 9 | import { Steps } from "@bot/types/general"; 10 | 11 | export async function addManuallyCallback(ctx: Context) { 12 | ctx.session.step = STEPS.WALLET as Steps; 13 | return ctx.reply(en.wallet.addAddress); 14 | } 15 | 16 | export async function bulkImportCallback(ctx: Context) { 17 | ctx.session.step = STEPS.BULK_IMPORT as Steps; 18 | return await ctx.replyWithPhoto( 19 | new InputFile("server/assets/csv_example.png"), 20 | { 21 | caption: en.wallet.addBulkWallet, 22 | } 23 | ); 24 | } 25 | 26 | export async function bulkExportCallback(ctx: Context) { 27 | const { getAllUserWallets } = walletsService(ctx); 28 | 29 | const userWallets = await getAllUserWallets(); 30 | 31 | let csv = "Address,Name" + "\r\n"; 32 | 33 | for (const i of userWallets) { 34 | csv += i.address + "," + i.name + "\r\n"; 35 | } 36 | 37 | writeFileSync("/tmp/addresses.csv", csv); 38 | 39 | await ctx.replyWithDocument(new InputFile("/tmp/addresses.csv")); 40 | } 41 | 42 | export async function walletListCallback(ctx: Context) { 43 | let wallets = ""; 44 | const { getAllUserWallets } = walletsService(ctx); 45 | const userWallets = await getAllUserWallets(); 46 | 47 | if (userWallets.length === 0) { 48 | return ctx.reply(en.wallet.emptyWallet); 49 | } 50 | 51 | userWallets.forEach(({ address, name }, key) => { 52 | return (wallets += 53 | template(en.wallet.showWallet, { 54 | number: getNumberEmoji(key + 1), 55 | name: name || "", 56 | address, 57 | }) + "\n\n"); 58 | }); 59 | 60 | await ctx.reply(wallets); 61 | } 62 | 63 | export async function deleteWalletCallback(ctx: Context) { 64 | const { getAllUserWallets } = walletsService(ctx); 65 | const userWallets = await getAllUserWallets(); 66 | 67 | if (userWallets.length === 0) { 68 | return ctx.reply(en.wallet.emptyWallet); 69 | } 70 | 71 | return ctx.reply(en.wallet.deleteWallet, { 72 | reply_markup: walletRemoveMenu, 73 | }); 74 | } 75 | -------------------------------------------------------------------------------- /bot/src/menu/callbacks/networkStatistic.callback.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "@bot/types"; 2 | import { Network } from "@prisma/client"; 3 | import { ChainInfo } from "@bot/types/general"; 4 | import { config } from "@bot/chains"; 5 | import { cosmosConfig } from "@bot/chains/cosmos"; 6 | import { getStatistic, getTokenPrice } from "@bot/api"; 7 | import { 8 | formatTokenPrice, 9 | getPositiveOrNegativeEmoji, 10 | nFormatter, 11 | template, 12 | } from "@bot/utils"; 13 | import { toNumber } from "lodash"; 14 | import { en } from "@bot/constants/en"; 15 | 16 | export async function statisticCallback(ctx: Context, network: Network) { 17 | const chain: ChainInfo = 18 | config.find((item) => item.network === network.name) || cosmosConfig; 19 | const { tokenUnits, primaryTokenUnit, coingeckoId } = chain; 20 | const publicUrl = network?.publicUrl || ""; 21 | const denom = tokenUnits[primaryTokenUnit].display; 22 | const prices = await getTokenPrice({ 23 | apiId: coingeckoId, 24 | isHistoryInclude: true, 25 | }); 26 | 27 | if (prices.price === 0) { 28 | return ctx.reply( 29 | template(en.statistic.menu.unknownPrice, { 30 | networkName: network.fullName, 31 | }) 32 | ); 33 | } 34 | 35 | const { communityPool, height, apr, inflation, bonded, unbonding, unbonded } = 36 | await getStatistic(publicUrl, denom, chain, primaryTokenUnit); 37 | 38 | const { first, seventh, fourteenth, thirty } = prices.PNL(1); 39 | 40 | return ctx.reply( 41 | template(en.statistic.menu.statisticDescription, { 42 | denom: `${communityPool?.displayDenom?.toUpperCase()}`, 43 | price: `${prices.price}`, 44 | apr: `${apr > 0 ? `${formatTokenPrice(apr)}%` : "<unknown>"}`, 45 | inflation: `${formatTokenPrice(inflation * 100)}`, 46 | height: `${height ?? 0}`, 47 | communityPool: `${nFormatter(toNumber(communityPool?.value), 2)}`, 48 | firstPercent: getPositiveOrNegativeEmoji(first.percent), 49 | seventhPercent: getPositiveOrNegativeEmoji(seventh.percent), 50 | fourteenthPercent: getPositiveOrNegativeEmoji(fourteenth.percent), 51 | thirtyPercent: getPositiveOrNegativeEmoji(thirty.percent), 52 | bonded: `${nFormatter(toNumber(bonded), 0)}`, 53 | unbonding: `${nFormatter(toNumber(unbonding), 0)}`, 54 | unbonded: `${nFormatter(toNumber(unbonded), 0)}`, 55 | }), 56 | { parse_mode: "HTML" } 57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /bot/src/utils/loadAddresses.ts: -------------------------------------------------------------------------------- 1 | import { createReadStream } from "fs"; 2 | import { parse } from "csv-parse"; 3 | import { validation } from "@bot/utils/validation"; 4 | import { en } from "@bot/constants/en"; 5 | import { bech32 } from "bech32"; 6 | import { template } from "@bot/utils/template"; 7 | import { networksService, usersService, walletsService } from "@bot/services"; 8 | import { Context } from "@bot/types"; 9 | import { encrypt } from "@bot/utils/crypto"; 10 | 11 | type TAddress = { 12 | address: string; 13 | name: string; 14 | networkId: number; 15 | userId: number; 16 | iv: string; 17 | }; 18 | 19 | export async function loadAddresses( 20 | path: string, 21 | ctx: Context 22 | ): Promise | void | string> { 23 | const addresses: Array = []; 24 | if (!ctx.from?.id) return; 25 | const { getAllUserWallets } = walletsService(ctx); 26 | const { getNetwork } = networksService(); 27 | const { getUser } = usersService(); 28 | const userWallets = await getAllUserWallets(); 29 | const user = await getUser({ telegramId: ctx.from.id }); 30 | 31 | if (!user?.id) return; 32 | 33 | try { 34 | const parser = createReadStream(path).pipe( 35 | parse({ delimiter: ",", from_line: 2 }) 36 | ); 37 | 38 | for await (const record of parser) { 39 | if (!record[0] || !record[1]) return en.wallet.incorrectCSV; 40 | 41 | const [address, name] = record; 42 | const parsedValue: string = address.replace(/\s+/g, ""); 43 | 44 | if (!validation.isValidAddress(parsedValue)) { 45 | return en.wallet.bulkImportAddressInvalid; 46 | } 47 | 48 | const prefix = bech32.decode(address).prefix; 49 | const { network } = await getNetwork({ name: prefix }); 50 | 51 | if (!validation.isValidChain(parsedValue)) { 52 | return template(en.wallet.bulkImportNetworkInvalid, { 53 | networkName: network.fullName, 54 | }); 55 | } 56 | 57 | if (validation.isDuplicateAddress(userWallets, parsedValue)) { 58 | return template(en.wallet.bulkImportDuplicateAddress, { 59 | walletAddress: parsedValue, 60 | }); 61 | } 62 | 63 | const { iv, encryptedData } = encrypt( 64 | parsedValue, 65 | ctx.session.walletPassword 66 | ); 67 | 68 | addresses.push({ 69 | address: encryptedData, 70 | name, 71 | networkId: network.id, 72 | userId: user.id, 73 | iv, 74 | }); 75 | } 76 | 77 | return addresses; 78 | } catch (e: any) { 79 | return e.message; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Serverless directories 108 | .serverless/ 109 | 110 | # FuseBox cache 111 | .fusebox/ 112 | 113 | # DynamoDB Local files 114 | .dynamodb/ 115 | 116 | # TernJS port file 117 | .tern-port 118 | 119 | # Stores VSCode versions used for testing VSCode extensions 120 | .vscode-test 121 | 122 | # yarn v2 123 | .yarn/cache 124 | .yarn/unplugged 125 | .yarn/build-state.yml 126 | .yarn/install-state.gz 127 | .pnp.* 128 | 129 | # Data 130 | data/ 131 | 132 | *.env -------------------------------------------------------------------------------- /bot/src/services/wallets.service.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "@bot/types"; 2 | import { walletDao } from "@bot/dao"; 3 | import { Prisma } from "@prisma/client"; 4 | import { decrypt } from "@bot/utils"; 5 | 6 | export const walletsService = (ctx?: Context) => { 7 | const user = ctx?.local.user || { 8 | id: 0, 9 | notificationId: 0, 10 | telegramId: 0, 11 | }; 12 | 13 | const createUserWallet = async ( 14 | { 15 | networkId, 16 | address, 17 | name, 18 | iv, 19 | }: { 20 | networkId: number; 21 | address: string; 22 | name: string; 23 | iv: string; 24 | }, 25 | userId?: number 26 | ) => { 27 | return await walletDao.createWallet({ 28 | data: { 29 | userId: !userId ? user.id : userId, 30 | name, 31 | networkId, 32 | address, 33 | iv, 34 | }, 35 | }); 36 | }; 37 | 38 | const updateUserWallet = async ({ 39 | walletId, 40 | address, 41 | iv, 42 | }: { 43 | walletId: number; 44 | address: string; 45 | iv: string; 46 | }) => { 47 | return await walletDao.updateWallet({ 48 | where: { 49 | id: walletId, 50 | }, 51 | data: { 52 | address, 53 | iv, 54 | }, 55 | }); 56 | }; 57 | 58 | const bulkCreateUserWallet = async ( 59 | data: Array 60 | ) => { 61 | return await walletDao.bulkCreateWallets(data); 62 | }; 63 | 64 | const getAllUserWallets = async (id?: number) => { 65 | const userId = id ?? user.id; 66 | const wallets = await walletDao.getAllWallets({ 67 | where: { 68 | userId: userId, 69 | }, 70 | }); 71 | 72 | return wallets.map((wallet) => { 73 | if (!wallet.iv || !ctx) return wallet; 74 | const encryptedWallet = decrypt( 75 | { iv: wallet.iv, encryptedData: wallet.address }, 76 | ctx.session.walletPassword 77 | ); 78 | 79 | return { 80 | ...wallet, 81 | address: encryptedWallet, 82 | }; 83 | }); 84 | }; 85 | 86 | const removeUserWallet = async (id: number) => { 87 | return await walletDao.removeWallet({ 88 | where: { 89 | id, 90 | }, 91 | }); 92 | }; 93 | 94 | const removeAllUserWallet = async (id: number) => { 95 | return await walletDao.removeAllWallet({ 96 | where: { 97 | userId: id, 98 | }, 99 | }); 100 | }; 101 | 102 | return { 103 | createUserWallet, 104 | bulkCreateUserWallet, 105 | getAllUserWallets, 106 | removeUserWallet, 107 | updateUserWallet, 108 | removeAllUserWallet, 109 | }; 110 | }; 111 | -------------------------------------------------------------------------------- /bot/src/services/networksInNotification.service.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "@bot/types"; 2 | import { Network } from "@prisma/client"; 3 | import { networkInNotificationDao, notificationDao } from "@bot/dao"; 4 | 5 | type Networks = { 6 | isNetworkActive: boolean; 7 | isGovActive: boolean; 8 | updateGovernance: (time: Date) => Promise; 9 | updateReminder: () => Promise; 10 | }; 11 | 12 | export async function networksInNotificationService({ 13 | ctx, 14 | network, 15 | }: { 16 | ctx: Context; 17 | network: Network; 18 | }): Promise { 19 | const user = ctx.local.user || { 20 | id: 0, 21 | notificationId: 0, 22 | }; 23 | 24 | const notification = (await notificationDao.getNotification({ 25 | where: { 26 | userId: user.id, 27 | }, 28 | })) || { 29 | id: 0, 30 | isReminderActive: false, 31 | notificationReminderTime: [""], 32 | }; 33 | 34 | const reminderNetwork = 35 | await networkInNotificationDao.getNetworkInNotification({ 36 | where: { 37 | reminderNetworkId: network.id, 38 | notificationId: notification.id, 39 | }, 40 | }); 41 | 42 | const governanceNetwork = 43 | await networkInNotificationDao.getNetworkInNotification({ 44 | where: { 45 | governanceNetworkId: network.id, 46 | notificationId: notification.id, 47 | }, 48 | }); 49 | 50 | const isNetworkActive = reminderNetwork.length > 0; 51 | const isGovActive = governanceNetwork.length > 0; 52 | 53 | const updateGovernance = async (time: Date) => { 54 | try { 55 | if (isGovActive) { 56 | await networkInNotificationDao.removeNetworkInNotification({ 57 | where: { 58 | governanceNetworkId: network.id, 59 | }, 60 | }); 61 | } else { 62 | await networkInNotificationDao.createNetworkInNotification({ 63 | data: { 64 | notificationId: notification.id, 65 | governanceNetworkId: network.id, 66 | governanceTimeStart: time, 67 | }, 68 | }); 69 | } 70 | } catch (e) { 71 | console.log(e); 72 | } 73 | }; 74 | 75 | const updateReminder = async () => { 76 | try { 77 | if (isNetworkActive) { 78 | await networkInNotificationDao.removeNetworkInNotification({ 79 | where: { 80 | reminderNetworkId: network.id, 81 | }, 82 | }); 83 | } else { 84 | await networkInNotificationDao.createNetworkInNotification({ 85 | data: { 86 | notificationId: notification.id, 87 | reminderNetworkId: network.id, 88 | }, 89 | }); 90 | } 91 | } catch (e) { 92 | console.log(e); 93 | } 94 | }; 95 | 96 | return { 97 | isNetworkActive, 98 | isGovActive, 99 | updateGovernance, 100 | updateReminder, 101 | }; 102 | } 103 | -------------------------------------------------------------------------------- /bot/src/menu/callbacks/alarm.callbacks.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "@bot/types"; 2 | import { networksAlarmMenu } from "@bot/menu"; 3 | import { 4 | alarmsService, 5 | alarmPricesService, 6 | networksService, 7 | notificationsService, 8 | } from "@bot/services"; 9 | import { InlineKeyboard } from "grammy"; 10 | import { MenuFlavor } from "@grammyjs/menu"; 11 | import _ from "lodash"; 12 | import { en } from "@bot/constants/en"; 13 | import { template } from "@bot/utils"; 14 | 15 | export async function addAlarmCallback(ctx: Context) { 16 | await ctx.reply(en.notification.alarmMenu.chooseNetworkTitle, { 17 | reply_markup: networksAlarmMenu, 18 | }); 19 | } 20 | 21 | export async function deleteAlarmCallback(ctx: Context) { 22 | const { getAllAlarms } = await alarmsService({ ctx }); 23 | const { getNetwork } = networksService(); 24 | const { getAllAlarmPrices } = alarmPricesService(); 25 | 26 | const alarms = await getAllAlarms(); 27 | 28 | const inlineKeyboard = new InlineKeyboard(); 29 | 30 | for await (const alarm of alarms) { 31 | const { network } = await getNetwork({ 32 | networkId: alarm.networkId, 33 | }); 34 | const alarmPrices = await getAllAlarmPrices(alarm.id); 35 | 36 | alarmPrices.forEach((item) => { 37 | inlineKeyboard 38 | .text( 39 | template(en.notification.alarmMenu.coinPrice, { 40 | name: network.fullName, 41 | price: `${item.price}`, 42 | }), 43 | `deleteAlarm:alarmPriceId=${item.id}` 44 | ) 45 | .row(); 46 | }); 47 | } 48 | 49 | await ctx.reply(en.notification.alarmMenu.removeWalletTitle, { 50 | reply_markup: inlineKeyboard, 51 | }); 52 | } 53 | export async function listAlarmsText(ctx: Context): Promise { 54 | const { isAlarmActive } = await notificationsService({ ctx }); 55 | 56 | return isAlarmActive 57 | ? en.notification.alarmMenu.enabled 58 | : en.notification.alarmMenu.disabled; 59 | } 60 | 61 | export async function listAlarmsCallback(ctx: Context) { 62 | let output = en.notification.alarmMenu.alarmList; 63 | const { getAllAlarms } = await alarmsService({ ctx }); 64 | const { getNetwork } = networksService(); 65 | const { getAllAlarmPrices } = alarmPricesService(); 66 | const alarms = await getAllAlarms(); 67 | 68 | for await (const alarm of alarms) { 69 | const { network } = await getNetwork({ 70 | networkId: alarm.networkId, 71 | }); 72 | const alarmPrices = await getAllAlarmPrices(alarm.id); 73 | const priceArr = _.map(alarmPrices, "price"); 74 | 75 | output += template(en.notification.alarmMenu.alarmListItem, { 76 | networkName: network.fullName, 77 | prices: `${priceArr.join("$, ")}`, 78 | }); 79 | } 80 | 81 | return ctx.reply(output); 82 | } 83 | 84 | export async function toggleAlarmActivity(ctx: Context & MenuFlavor) { 85 | const { updateNotification } = await notificationsService({ ctx }); 86 | await updateNotification({ triggerAlarmActivity: true }); 87 | 88 | ctx.menu.update(); 89 | } 90 | -------------------------------------------------------------------------------- /server/server.ts: -------------------------------------------------------------------------------- 1 | import fastify from "fastify"; 2 | import { BotError, webhookCallback } from "grammy"; 3 | import fastifyCors from "@fastify/cors"; 4 | import fastifyStatic from "@fastify/static"; 5 | import { register } from "prom-client"; 6 | import { bot } from "@bot/bot"; 7 | import { config } from "@bot/config"; 8 | import { logger } from "@bot/logger"; 9 | import { handleError } from "@bot/helpers/errorHandler"; 10 | import { networksService, walletsService, usersService } from "@bot/services"; 11 | import { bech32 } from "bech32"; 12 | import { cron } from "@server/cron"; 13 | import { sendNotification } from "@server/telegram"; 14 | import path from "path"; 15 | 16 | export const server = fastify({ 17 | logger, 18 | trustProxy: true, 19 | }); 20 | 21 | server.register(fastifyCors, { origin: "*" }); 22 | 23 | server.register(fastifyStatic, { 24 | root: path.join(__dirname, "assets"), 25 | prefix: "/assets/", 26 | }); 27 | 28 | cron(server); 29 | 30 | server.setErrorHandler(async (error, request, response) => { 31 | if (error instanceof BotError) { 32 | await handleError(error); 33 | 34 | response.code(200).send({}); 35 | } else { 36 | logger.error(error); 37 | 38 | response.status(500).send({ error: "Something went wrong" }); 39 | } 40 | }); 41 | 42 | server.post( 43 | `/${config.BOT_TOKEN}`, 44 | webhookCallback(bot, "fastify", { timeoutMilliseconds: 30000 }) 45 | ); 46 | 47 | server.get("/metrics", async (req, res) => { 48 | try { 49 | res.header("Content-Type", register.contentType); 50 | res.send(await register.metrics()); 51 | } catch (err) { 52 | res.status(500).send(err); 53 | } 54 | }); 55 | 56 | server.post("/update_wallet/:id", async (req, res) => { 57 | try { 58 | const { getNetwork } = networksService(); 59 | const { createUserWallet, getAllUserWallets } = walletsService(); 60 | const { getUser } = usersService(); 61 | const telegramId = Number(req.params.id); 62 | if (isNaN(telegramId)) { 63 | res.status(500).send({ error: "Please sign in via bot" }); 64 | return; 65 | // res.status(500).send({ message: "Please sign in via bot" }); 66 | } 67 | const user = await getUser({ telegramId }); 68 | const userWallets = await getAllUserWallets(user?.id); 69 | const address = req.body.wallet; 70 | const name = req.body.name; 71 | const iv = req.body.iv; 72 | 73 | if (userWallets.some((item) => item.address === address)) { 74 | return req.body; 75 | } 76 | 77 | const prefix = bech32.decode(address).prefix; 78 | const { network } = await getNetwork({ name: prefix }); 79 | 80 | await createUserWallet( 81 | { networkId: network.id, address, name, iv }, 82 | user?.id 83 | ); 84 | 85 | return req.body; 86 | } catch (err) { 87 | res.status(500).send(err); 88 | } 89 | }); 90 | 91 | server.get("/send_message/:id", async (req, res) => { 92 | try { 93 | const telegramId = Number(req.params.id); 94 | const message = "Perfect! Now you can use /assets command"; 95 | await sendNotification(message, "HTML", telegramId); 96 | return res.status(200); 97 | } catch (err) { 98 | res.status(500).send(err); 99 | } 100 | }); 101 | -------------------------------------------------------------------------------- /bot/src/services/notifications.service.ts: -------------------------------------------------------------------------------- 1 | import { usersService } from "@bot/services/index"; 2 | import { Context } from "@bot/types"; 3 | import { Network } from "@prisma/client"; 4 | import { notificationDao } from "@bot/dao"; 5 | 6 | type Notification = { 7 | isReminderActive: boolean; 8 | isAlarmActive: boolean; 9 | updateNotification: ({ 10 | triggerReminderActivity, 11 | triggerAlarmActivity, 12 | notificationReminderTime, 13 | }: { 14 | triggerReminderActivity?: boolean; 15 | triggerAlarmActivity?: boolean; 16 | notificationReminderTime?: string[]; 17 | }) => Promise; 18 | }; 19 | 20 | type NetworkTime = { 21 | isNotificationTimeActive: (time: string) => boolean; 22 | updateNotificationReminderTime: (time: string) => Promise; 23 | }; 24 | 25 | export async function notificationsService({ 26 | ctx, 27 | }: { 28 | ctx?: Context; 29 | }): Promise; 30 | export async function notificationsService({ 31 | ctx, 32 | timeArr, 33 | }: { 34 | ctx?: Context; 35 | timeArr?: string[]; 36 | }): Promise; 37 | export async function notificationsService({ 38 | ctx, 39 | timeArr, 40 | }: { 41 | ctx?: Context; 42 | network?: Network; 43 | timeArr?: string[]; 44 | }) { 45 | const user = ctx?.local.user || { 46 | id: 0, 47 | }; 48 | const notification = (await notificationDao.getNotification({ 49 | where: { 50 | userId: user.id, 51 | }, 52 | })) || { 53 | id: 0, 54 | isReminderActive: false, 55 | isAlarmActive: false, 56 | notificationReminderTime: [""], 57 | }; 58 | 59 | const isReminderActive = notification.isReminderActive || false; 60 | const isAlarmActive = notification.isAlarmActive || false; 61 | 62 | const updateNotification = async ({ 63 | triggerReminderActivity, 64 | triggerAlarmActivity, 65 | notificationReminderTime, 66 | }: { 67 | triggerReminderActivity?: boolean; 68 | triggerAlarmActivity?: boolean; 69 | notificationReminderTime?: string[]; 70 | }) => { 71 | await notificationDao.upsertNotification({ 72 | where: { 73 | userId: user.id, 74 | }, 75 | create: { 76 | userId: user.id, 77 | }, 78 | update: { 79 | isReminderActive: triggerReminderActivity 80 | ? !isReminderActive 81 | : undefined, 82 | isAlarmActive: triggerAlarmActivity ? !isAlarmActive : undefined, 83 | notificationReminderTime, 84 | }, 85 | }); 86 | }; 87 | 88 | if (timeArr && timeArr.length > 0) { 89 | const notificationTime = notification.notificationReminderTime; 90 | const isNotificationTimeActive = (time: string) => 91 | notificationTime?.includes(time); 92 | 93 | const updateNotificationReminderTime = async (time: string) => { 94 | let timeArr: string[]; 95 | 96 | if (notificationTime?.includes(time)) { 97 | timeArr = notificationTime.filter((item) => item !== time); 98 | } else { 99 | timeArr = [...notificationTime, time]; 100 | } 101 | 102 | await updateNotification({ notificationReminderTime: timeArr }); 103 | }; 104 | 105 | return { 106 | isNotificationTimeActive, 107 | updateNotificationReminderTime, 108 | }; 109 | } 110 | 111 | return { 112 | isReminderActive, 113 | isAlarmActive, 114 | updateNotification, 115 | }; 116 | } 117 | -------------------------------------------------------------------------------- /bot/src/bot.ts: -------------------------------------------------------------------------------- 1 | import { Bot } from "grammy"; 2 | import { limit as rateLimit } from "@grammyjs/ratelimiter"; 3 | import { apiThrottler } from "@grammyjs/transformer-throttler"; 4 | import { hydrateFiles } from "@grammyjs/files"; 5 | import { hydrateReply, parseMode } from "@grammyjs/parse-mode"; 6 | 7 | import { Context } from "@bot/types"; 8 | import { config } from "@bot/config"; 9 | import { connection as redis } from "./redis"; 10 | import { 11 | updatesLogger, 12 | setupSession, 13 | setupLocalContext, 14 | setupLogger, 15 | setUser, 16 | collectMetrics, 17 | router, 18 | setupWalletsMigrate, 19 | } from "@bot/middlewares"; 20 | import { apiCallsLogger } from "@bot/transformers"; 21 | import { 22 | botAdminFeature, 23 | helpFeature, 24 | startFeature, 25 | walletFeature, 26 | statisticFeature, 27 | proposalFeature, 28 | resourcesFeature, 29 | notificationFeature, 30 | assetsFeature, 31 | aboutFeature, 32 | supportFeature, 33 | resetFeature, 34 | } from "@bot/features"; 35 | import { handleError } from "@bot/helpers/errorHandler"; 36 | import { 37 | walletMenu, 38 | walletRemoveMenu, 39 | networksStatisticMenu, 40 | notificationMenu, 41 | dailyReminderMenu, 42 | alarmMenu, 43 | proposalMenu, 44 | networkTimeReminderMenu, 45 | networksReminderMenu, 46 | timezoneMenu, 47 | networksAlarmMenu, 48 | networksResourcesMenu, 49 | assetsMenu, 50 | networksProposalMenu, 51 | } from "@bot/menu"; 52 | import { en } from "./constants/en"; 53 | 54 | export const bot = new Bot(config.BOT_TOKEN); 55 | 56 | // Middlewares 57 | 58 | bot.api.config.use(apiThrottler()); 59 | bot.api.config.use(hydrateFiles(config.BOT_TOKEN)); 60 | bot.api.config.use(parseMode("HTML")); 61 | 62 | if (config.isDev) { 63 | bot.api.config.use(apiCallsLogger); 64 | bot.use(updatesLogger()); 65 | } 66 | 67 | // Menus 68 | 69 | bot.use(collectMetrics()); 70 | bot.use( 71 | rateLimit({ 72 | timeFrame: 1000, 73 | limit: 3, 74 | storageClient: redis, 75 | 76 | onLimitExceeded: () => { 77 | console.error("Too many request"); 78 | }, 79 | }) 80 | ); 81 | bot.use(hydrateReply); 82 | bot.use(setupSession()); 83 | bot.use(setupLocalContext()); 84 | bot.use(setupLogger()); 85 | bot.use(setUser()); 86 | bot.use(setupWalletsMigrate()); 87 | bot.use(walletMenu); 88 | bot.use(notificationMenu); 89 | bot.use(networksStatisticMenu); 90 | bot.use(networksResourcesMenu); 91 | bot.use(assetsMenu); 92 | bot.use(networksProposalMenu); 93 | walletMenu.register(walletRemoveMenu); 94 | notificationMenu.register(dailyReminderMenu); 95 | notificationMenu.register(alarmMenu); 96 | notificationMenu.register(proposalMenu); 97 | dailyReminderMenu.register(networksReminderMenu); 98 | dailyReminderMenu.register(networkTimeReminderMenu); 99 | dailyReminderMenu.register(timezoneMenu); 100 | alarmMenu.register(networksAlarmMenu); 101 | 102 | bot.use(helpFeature); 103 | bot.use(resetFeature); 104 | bot.use(statisticFeature); 105 | bot.use(botAdminFeature); 106 | bot.use(startFeature); 107 | bot.use(walletFeature); 108 | bot.use(assetsFeature); 109 | bot.use(proposalFeature); 110 | bot.use(resourcesFeature); 111 | bot.use(notificationFeature); 112 | bot.use(aboutFeature); 113 | bot.use(supportFeature); 114 | 115 | router.otherwise(async (ctx) => await ctx.reply(en.unknownRoute)); 116 | bot.use(router); 117 | 118 | if (config.isDev) { 119 | bot.catch(handleError); 120 | } 121 | -------------------------------------------------------------------------------- /bot/src/menu/callbacks/assets.callbacks.ts: -------------------------------------------------------------------------------- 1 | import { Wallet } from "@prisma/client"; 2 | import { getBalance, getTokenPrice } from "@bot/api"; 3 | import _, { toNumber } from "lodash"; 4 | import { networksService, walletsService } from "@bot/services"; 5 | import { 6 | getPositiveOrNegativeEmoji, 7 | getNumberEmoji, 8 | template, 9 | } from "@bot/utils"; 10 | import { en } from "@bot/constants/en"; 11 | import { cw20line } from "@bot/constants/regex"; 12 | import { Context } from "@bot/types"; 13 | 14 | export async function assetsCallback(wallet: Wallet, index?: number) { 15 | let output = ""; 16 | 17 | const { getNetwork } = networksService(); 18 | const { address, networkId } = wallet; 19 | const { coingeckoId, publicUrl, network } = await getNetwork({ 20 | networkId, 21 | }); 22 | 23 | const prices = await getTokenPrice({ 24 | apiId: coingeckoId, 25 | isHistoryInclude: true, 26 | }); 27 | const data = await getBalance(publicUrl, address, network.name); 28 | const { first, seventh, fourteenth, thirty } = prices.PNL( 29 | toNumber(data.total.value) 30 | ); 31 | const cw20str = data.cw20tokens 32 | .map((item) => `${item.displayDenom} — ${item.value} \n`) 33 | .join(""); 34 | const denomUppercase = data.available.displayDenom.toUpperCase(); 35 | 36 | output += template(en.assets.menu.walletDescription, { 37 | number: index ? getNumberEmoji(index) : "", 38 | address, 39 | denom: denomUppercase, 40 | available: data.available.value, 41 | delegate: data.delegate.value, 42 | unbonding: data.unbonding.value, 43 | reward: data.reward.value, 44 | totalCrypto: data.total.value, 45 | total: prices.totalFiat(toNumber(data.total.value)), 46 | firstAmount: getPositiveOrNegativeEmoji(`$${first.amount}`), 47 | seventhAmount: getPositiveOrNegativeEmoji(`$${seventh.amount}`), 48 | fourteenthAmount: getPositiveOrNegativeEmoji(`$${fourteenth.amount}`), 49 | thirtyAmount: getPositiveOrNegativeEmoji(`$${thirty.amount}`), 50 | firstPercent: first.percent, 51 | seventhPercent: seventh.percent, 52 | fourteenthPercent: fourteenth.percent, 53 | thirtyPercent: thirty.percent, 54 | cw20: cw20str, 55 | }); 56 | 57 | output = data.cw20tokens.length > 0 ? output : output.replace(cw20line, ""); 58 | 59 | return output; 60 | } 61 | 62 | export async function totalAmountCallback(ctx: Context) { 63 | const { getAllUserWallets } = walletsService(ctx); 64 | const userWallets = await getAllUserWallets(); 65 | 66 | let output = ""; 67 | const balances = []; 68 | 69 | for await (const wallet of userWallets) { 70 | const { getNetwork } = networksService(); 71 | const { address, networkId } = wallet; 72 | const { publicUrl, network } = await getNetwork({ 73 | networkId, 74 | }); 75 | 76 | const data = await getBalance(publicUrl, address, network.name, false); 77 | balances.push({ 78 | networkName: network.name, 79 | amount: Number(data.total.value), 80 | }); 81 | } 82 | 83 | const totalBalance = balances.reduce((accumulator: any, currentValue) => { 84 | let amount = _.get(currentValue, ["amount"], 0); 85 | 86 | if (accumulator[currentValue.networkName]) { 87 | amount += accumulator[currentValue.networkName]; 88 | } 89 | 90 | return { 91 | ...accumulator, 92 | [currentValue.networkName]: amount, 93 | }; 94 | }, {}); 95 | 96 | Object.entries(totalBalance).forEach(([key, value], index) => { 97 | output += template(en.assets.menu.total, { 98 | number: getNumberEmoji(index + 1), 99 | networkName: key, 100 | amount: `${value}`, 101 | }); 102 | }); 103 | 104 | return output; 105 | } 106 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cosmos-space-bot", 3 | "version": "0.0.1", 4 | "description": "Cosmos space bot", 5 | "main": "dist/run.js", 6 | "type": "commonjs", 7 | "scripts": { 8 | "format": "prettier --write \"src/**/*.ts\"", 9 | "lint": "eslint --ext .ts,.tsx ./bot/src ./server", 10 | "clean": "rimraf dist tsconfig.tsbuildinfo", 11 | "build": "npm run clean && tsc", 12 | "dev": "tsc-watch --onSuccess \"npm run start\"", 13 | "start": "node dist/server/run.js", 14 | "heroku-prebuild": "npm i --only=prod", 15 | "copyAssets": "copyfiles --error --up 1 server/assets/*.* dist/server", 16 | "postbuild": "npm run copyAssets" 17 | }, 18 | "author": "deptyped ", 19 | "license": "MIT", 20 | "private": true, 21 | "dependencies": { 22 | "@fastify/cors": "8.0.0", 23 | "@fastify/static": "6.5.0", 24 | "@grammyjs/files": "1.0.4", 25 | "@grammyjs/fluent": "1.0.3", 26 | "@grammyjs/menu": "1.1.2", 27 | "@grammyjs/parse-mode": "1.1.2", 28 | "@grammyjs/ratelimiter": "1.1.5", 29 | "@grammyjs/router": "2.0.0", 30 | "@grammyjs/storage-redis": "2.0.0", 31 | "@grammyjs/transformer-throttler": "1.1.2", 32 | "@grammyjs/types": "2.8.1", 33 | "@keplr-wallet/cosmos": "0.8.8", 34 | "@keplr-wallet/types": "0.8.8", 35 | "@prisma/client": "4.0.0", 36 | "@types/react": "18.0.15", 37 | "bech32": "2.0.0", 38 | "big.js": "6.2.0", 39 | "copyfiles": "2.4.1", 40 | "cosmwasm": "1.1.1", 41 | "countries-and-timezones": "3.3.0", 42 | "crypto": "1.0.0", 43 | "csv-parse": "5.3.0", 44 | "dayjs": "1.11.3", 45 | "dotenv": "16.0.1", 46 | "envalid": "7.3.1", 47 | "eslint-plugin-react": "7.30.1", 48 | "fastify": "4.2.0", 49 | "fastify-cron": "1.3.1", 50 | "fs-extra": "10.1.0", 51 | "grammy": "1.9.0", 52 | "grammy-guard": "0.3.1", 53 | "ioredis": "5.1.0", 54 | "lodash": "4.17.21", 55 | "module-alias": "2.2.2", 56 | "node-fetch": "2.6.7", 57 | "numeral": "2.0.6", 58 | "pino": "8.1.0", 59 | "pino-pretty": "8.1.0", 60 | "prom-client": "14.0.1", 61 | "react": "18.2.0", 62 | "react-dom": "18.2.0", 63 | "subscriptions-transport-ws": "0.11.0" 64 | }, 65 | "devDependencies": { 66 | "@babel/core": "7.18.9", 67 | "@babel/plugin-proposal-class-properties": "7.18.6", 68 | "@babel/plugin-transform-react-jsx": "7.18.6", 69 | "@babel/plugin-transform-runtime": "7.18.9", 70 | "@babel/preset-env": "7.18.9", 71 | "@babel/preset-react": "7.18.6", 72 | "@types/big.js": "6.1.5", 73 | "@types/debug": "4.1.7", 74 | "@types/fs-extra": "9.0.13", 75 | "@types/ioredis": "4.28.10", 76 | "@types/lodash": "4.14.182", 77 | "@types/node": "18.0.0", 78 | "@types/node-fetch": "2.6.2", 79 | "@types/numeral": "2.0.2", 80 | "@types/react-dom": "18.0.6", 81 | "@typescript-eslint/eslint-plugin": "5.30.0", 82 | "@typescript-eslint/parser": "5.30.0", 83 | "babel-preset-env": "1.7.0", 84 | "babel-preset-react": "6.24.1", 85 | "eslint": "8.18.0", 86 | "eslint-config-airbnb-base": "15.0.0", 87 | "eslint-config-prettier": "8.5.0", 88 | "eslint-import-resolver-node": "0.3.6", 89 | "eslint-import-resolver-typescript": "3.1.1", 90 | "eslint-module-utils": "2.7.3", 91 | "eslint-plugin-import": "2.26.0", 92 | "eslint-plugin-prettier": "4.1.0", 93 | "husky": "8.0.1", 94 | "lint-staged": "13.0.3", 95 | "prettier": "2.7.1", 96 | "prisma": "4.0.0", 97 | "rimraf": "3.0.2", 98 | "ts-node": "10.8.1", 99 | "tsc-watch": "5.0.3", 100 | "typescript": "4.7.4" 101 | }, 102 | "lint-staged": { 103 | "*.ts": "npm run lint" 104 | }, 105 | "prisma": { 106 | "seed": "ts-node prisma/seed.ts" 107 | }, 108 | "_moduleAliases": { 109 | "@bot": "dist/bot/src", 110 | "@server": "dist/server" 111 | }, 112 | "engines": { 113 | "node": "17.9.0", 114 | "npm": "8.11.0" 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /bot/src/features/notification.feature.ts: -------------------------------------------------------------------------------- 1 | import { logHandle } from "@bot/helpers/logging"; 2 | import { router } from "@bot/middlewares"; 3 | import { notificationMenu } from "@bot/menu"; 4 | import { getCountry } from "countries-and-timezones"; 5 | import { countries } from "@bot/constants/country"; 6 | import { timezoneMenu, alarmMenu } from "@bot/menu"; 7 | import _ from "lodash"; 8 | import { alarmPricesService, alarmsService } from "@bot/services"; 9 | import { agreementKeyboard } from "@bot/menu/utils"; 10 | import { isNumber } from "@bot/utils"; 11 | import { getFlagEmoji } from "@bot/utils/getEmoji"; 12 | import { en } from "@bot/constants/en"; 13 | import { ROUTE, ROUTE_LOGS } from "@bot/constants/route"; 14 | import { STEPS } from "@bot/constants/step"; 15 | import { Steps } from "@bot/types/general"; 16 | import { alarmPriceRegex, deleteAlarmRegex } from "@bot/constants/regex"; 17 | import { KEYBOARD } from "@bot/constants/keyboard"; 18 | 19 | export const feature = router.route(ROUTE.NOTIFICATION); 20 | 21 | feature.command( 22 | en.notification.command, 23 | logHandle(ROUTE_LOGS.NOTIFICATION), 24 | async (ctx) => 25 | await ctx.reply(en.notification.menu.title, { 26 | reply_markup: notificationMenu, 27 | }) 28 | ); 29 | 30 | feature 31 | .filter((ctx) => !_.isEmpty(ctx.session.alarmNetwork)) 32 | .on("message", async (ctx) => { 33 | const network = ctx.session.alarmNetwork; 34 | const price = ctx.message.text || ""; 35 | 36 | if (price.includes("$")) { 37 | await ctx.reply(en.notification.alarmMenu.incorrectNumber); 38 | return; 39 | } 40 | 41 | if (Number(price) < 0) { 42 | await ctx.reply(en.notification.alarmMenu.incorrectPrice); 43 | return; 44 | } 45 | 46 | if (!isNumber(price)) { 47 | await ctx.reply(en.notification.alarmMenu.incorrectPrice); 48 | return; 49 | } 50 | 51 | if (network) { 52 | const { updateAlarmNetworks } = await alarmsService({ ctx }); 53 | await updateAlarmNetworks(price, network.id); 54 | 55 | await ctx.reply(en.addMoreQuestion, { 56 | reply_markup: agreementKeyboard, 57 | }); 58 | } 59 | }); 60 | 61 | feature 62 | .filter((ctx) => ctx.session.step === STEPS.TIMEZONE) 63 | .on("message", async (ctx) => { 64 | const country = ctx.message.text || ""; 65 | 66 | const currentCountry = countries.find( 67 | ({ name, code }) => 68 | name.toLowerCase() === country.toLowerCase() || 69 | country.toLowerCase() === code.toLowerCase() 70 | ) || { 71 | code: "", 72 | }; 73 | 74 | if (!currentCountry.code) { 75 | return ctx.reply(en.notification.reminderMenu.incorrectCountry); 76 | } 77 | 78 | const parseCountry = getCountry(currentCountry.code); 79 | if (parseCountry?.timezones) { 80 | ctx.session.timezone = parseCountry.timezones.map( 81 | (item) => `${getFlagEmoji(currentCountry.code)} ${item}` 82 | ); 83 | 84 | return ctx.reply(en.notification.reminderMenu.chooseTimezone, { 85 | reply_markup: timezoneMenu, 86 | }); 87 | } 88 | }); 89 | 90 | feature 91 | .filter((ctx) => ctx.session.step === STEPS.NOTIFICATION) 92 | .callbackQuery( 93 | KEYBOARD.CALLBACK_YES, 94 | async (ctx) => await ctx.reply(en.notification.alarmMenu.addMorePrice) 95 | ); 96 | 97 | feature 98 | .filter((ctx) => ctx.session.step === STEPS.NOTIFICATION) 99 | .callbackQuery(KEYBOARD.CALLBACK_NO, async (ctx) => { 100 | ctx.session.alarmNetwork = undefined; 101 | 102 | await ctx.reply(en.notification.alarmMenu.alarmSaved, { 103 | reply_markup: alarmMenu, 104 | }); 105 | }); 106 | 107 | feature.callbackQuery(deleteAlarmRegex, async (ctx) => { 108 | const data = ctx.callbackQuery.data; 109 | 110 | const [alarmPriceId] = data.match(alarmPriceRegex) || []; 111 | const { removeAlarmPrice } = alarmPricesService(); 112 | await removeAlarmPrice(Number(alarmPriceId)); 113 | ctx.session.step = STEPS.NOTIFICATION as Steps; 114 | 115 | await ctx.reply(en.notification.alarmMenu.alarmRemoved, { 116 | reply_markup: notificationMenu, 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /bot/src/api/handlers/getTokenPrice.ts: -------------------------------------------------------------------------------- 1 | import Big from "big.js"; 2 | import { 3 | fetchTokenHistory, 4 | fetchTokenPrice as fetchTokenPriceApi, 5 | } from "@bot/api"; 6 | import { formatTokenPrice, calcTVLPercent } from "@bot/utils"; 7 | import { CoinHistoryResponse, TTokenPrice } from "@bot/types/general"; 8 | 9 | export async function getTokenPrice({ apiId }: { apiId: string }): Promise<{ 10 | price: number; 11 | }>; 12 | export async function getTokenPrice({ 13 | isHistoryInclude, 14 | apiId, 15 | }: { 16 | isHistoryInclude: boolean; 17 | apiId: string; 18 | }): Promise<{ 19 | price: number; 20 | totalFiat: (total: number) => string; 21 | PNL: any; 22 | }>; 23 | export async function getTokenPrice({ 24 | isHistoryInclude = false, 25 | apiId, 26 | }: { 27 | apiId: string; 28 | isHistoryInclude?: boolean; 29 | }) { 30 | const price: { 31 | tokenPrice: TTokenPrice; 32 | } = await fetchTokenPriceApi(apiId); 33 | 34 | const currentPrice = price.tokenPrice[apiId]?.usd; 35 | 36 | if (!isHistoryInclude) { 37 | return { price: currentPrice }; 38 | } 39 | 40 | const tokenHistory = await fetchTokenHistory(apiId); 41 | 42 | return { 43 | price: currentPrice, 44 | ...formatTokenHistory(tokenHistory.tokenPrice, price.tokenPrice, apiId), 45 | }; 46 | } 47 | 48 | export const formatTokenHistory = ( 49 | tokenHistory: Array, 50 | tokenPrice: TTokenPrice, 51 | apiId: string 52 | ) => { 53 | try { 54 | const currentPrice = tokenPrice[apiId]?.usd; 55 | const totalFiat = (total: number) => { 56 | return `${formatTokenPrice(currentPrice * total)}`; 57 | }; 58 | 59 | const PNL = (amount: number) => { 60 | const currentPrice = tokenPrice[apiId]?.usd; 61 | let firstDayPercent = "0"; 62 | 63 | if (tokenHistory[0]?.market_data?.current_price) { 64 | firstDayPercent = calcTVLPercent( 65 | currentPrice, 66 | tokenHistory[0].market_data.current_price.usd 67 | ); 68 | } 69 | 70 | let seventhDayPercent = "0"; 71 | if (tokenHistory[1]?.market_data?.current_price) { 72 | seventhDayPercent = calcTVLPercent( 73 | currentPrice, 74 | tokenHistory[1].market_data.current_price.usd 75 | ); 76 | } 77 | 78 | let fourteenthDayPercent = "0"; 79 | if (tokenHistory[2]?.market_data?.current_price) { 80 | fourteenthDayPercent = calcTVLPercent( 81 | currentPrice, 82 | tokenHistory[2].market_data.current_price.usd 83 | ); 84 | } 85 | 86 | let thirtyDayPercent = "0"; 87 | if (tokenHistory[3]?.market_data?.current_price) { 88 | thirtyDayPercent = calcTVLPercent( 89 | currentPrice, 90 | tokenHistory[3].market_data.current_price.usd 91 | ); 92 | } 93 | 94 | return { 95 | first: { 96 | percent: formatTokenPrice(firstDayPercent), 97 | amount: formatTokenPrice( 98 | Big(amount * currentPrice) 99 | .div(100) 100 | .mul(firstDayPercent) 101 | .toPrecision() 102 | ), 103 | }, 104 | seventh: { 105 | percent: formatTokenPrice(seventhDayPercent), 106 | amount: formatTokenPrice( 107 | Big(amount * currentPrice) 108 | .div(100) 109 | .mul(seventhDayPercent) 110 | .toPrecision() 111 | ), 112 | }, 113 | fourteenth: { 114 | percent: formatTokenPrice(fourteenthDayPercent), 115 | amount: formatTokenPrice( 116 | Big(amount * currentPrice) 117 | .div(100) 118 | .mul(fourteenthDayPercent) 119 | .toPrecision() 120 | ), 121 | }, 122 | thirty: { 123 | percent: formatTokenPrice(thirtyDayPercent), 124 | amount: formatTokenPrice( 125 | Big(amount * currentPrice) 126 | .div(100) 127 | .mul(thirtyDayPercent) 128 | .toPrecision() 129 | ), 130 | }, 131 | }; 132 | }; 133 | 134 | return { 135 | totalFiat, 136 | PNL, 137 | }; 138 | } catch (e) { 139 | return { 140 | totalFiat: 0, 141 | PNL: {}, 142 | }; 143 | } 144 | }; 145 | -------------------------------------------------------------------------------- /bot/src/features/wallet.feature.ts: -------------------------------------------------------------------------------- 1 | import { router } from "@bot/middlewares"; 2 | import { logHandle } from "@bot/helpers/logging"; 3 | import { Context } from "@bot/types"; 4 | import { walletMenu } from "@bot/menu"; 5 | import { networksService, walletsService } from "@bot/services"; 6 | import { bech32 } from "bech32"; 7 | import { agreementKeyboard } from "@bot/menu/utils"; 8 | import { en } from "@bot/constants/en"; 9 | import { template, validation, loadAddresses, encrypt } from "@bot/utils"; 10 | import { ROUTE, ROUTE_LOGS } from "@bot/constants/route"; 11 | import { STEPS } from "@bot/constants/step"; 12 | import { removeSpace } from "@bot/constants/regex"; 13 | import { KEYBOARD } from "@bot/constants/keyboard"; 14 | import { Steps } from "@bot/types/general"; 15 | 16 | export const feature = router.route(ROUTE.WALLET); 17 | 18 | feature.command( 19 | en.wallet.command, 20 | logHandle(ROUTE_LOGS.WALLET), 21 | async (ctx: Context) => { 22 | if (!ctx.session.walletPassword) { 23 | ctx.session.step = STEPS.WALLET_PASSWORD as Steps; 24 | 25 | await ctx.reply(en.wallet.emptyPassword); 26 | return; 27 | } 28 | 29 | await ctx.reply(en.wallet.menu.title, { 30 | reply_markup: walletMenu, 31 | disable_web_page_preview: true, 32 | }); 33 | } 34 | ); 35 | 36 | feature 37 | .filter((ctx) => ctx.session.step === STEPS.WALLET) 38 | .on("message:text", logHandle(ROUTE_LOGS.WALLET), async (ctx) => { 39 | await ctx.replyWithChatAction("typing"); 40 | const { getNetwork } = networksService(); 41 | const { createUserWallet, getAllUserWallets } = walletsService(ctx); 42 | 43 | const [address, name] = ctx.message.text.split("\n"); 44 | 45 | if (!address || !name) return ctx.reply(en.wallet.invalidFormat); 46 | 47 | const parsedValue = address.replace(removeSpace, ""); 48 | 49 | if (!validation.isValidAddress(parsedValue)) { 50 | return ctx.reply(en.wallet.invalidAddress); 51 | } 52 | 53 | const prefix = bech32.decode(address).prefix; 54 | const { network } = await getNetwork({ name: prefix }); 55 | const userWallets = await getAllUserWallets(); 56 | 57 | if (!validation.isValidChain(parsedValue)) { 58 | return ctx.reply( 59 | template(en.wallet.invalidNetwork, { networkName: network.fullName }) 60 | ); 61 | } 62 | 63 | if (validation.isDuplicateAddress(userWallets, parsedValue)) { 64 | return ctx.reply(en.wallet.duplicateAddress); 65 | } 66 | 67 | if (network) { 68 | const { iv, encryptedData } = encrypt( 69 | parsedValue, 70 | ctx.session.walletPassword 71 | ); 72 | 73 | await createUserWallet({ 74 | networkId: network.id, 75 | address: encryptedData, 76 | name, 77 | iv, 78 | }); 79 | } 80 | 81 | await ctx.reply(en.addMoreQuestion, { 82 | reply_markup: agreementKeyboard, 83 | }); 84 | }); 85 | 86 | feature 87 | .filter((ctx) => ctx.session.step === STEPS.WALLET_PASSWORD) 88 | .on("message:text", async (ctx) => { 89 | ctx.session.walletPassword = ctx.message.text; 90 | ctx.session.step = undefined; 91 | 92 | await ctx.reply( 93 | "Password was saved, now you can secure use wallet management" 94 | ); 95 | 96 | await ctx.reply(en.wallet.menu.title, { 97 | reply_markup: walletMenu, 98 | disable_web_page_preview: true, 99 | }); 100 | }); 101 | 102 | feature 103 | .filter((ctx) => ctx.session.step === STEPS.WALLET) 104 | .callbackQuery(KEYBOARD.CALLBACK_YES, async (ctx) => { 105 | await ctx.reply(en.wallet.addMore); 106 | }); 107 | 108 | feature 109 | .filter((ctx) => ctx.session.step === STEPS.WALLET) 110 | .callbackQuery(KEYBOARD.CALLBACK_NO, async (ctx) => { 111 | ctx.session.step = undefined; 112 | return ctx.reply(en.wallet.success); 113 | }); 114 | 115 | feature 116 | .filter((ctx) => ctx.session.step === STEPS.BULK_IMPORT) 117 | .on("message:document", logHandle(ROUTE_LOGS.WALLET), async (ctx) => { 118 | const { bulkCreateUserWallet } = walletsService(ctx); 119 | const file = await ctx.getFile(); 120 | const path = await file.download(); 121 | 122 | const result = await loadAddresses(path, ctx); 123 | 124 | if (typeof result === "string") return ctx.reply(result); 125 | 126 | if (Array.isArray(result)) { 127 | await bulkCreateUserWallet(result); 128 | 129 | await ctx.reply(en.wallet.successfulImport); 130 | } 131 | }); 132 | -------------------------------------------------------------------------------- /bot/src/chains/junoCW20.ts: -------------------------------------------------------------------------------- 1 | export const junoCW20 = [ 2 | { 3 | chain: "juno", 4 | contract_address: 5 | "juno1pqht3pkhr5fpyre2tw3ltrzc0kvxknnsgt04thym9l7n2rmxgw0sgefues", 6 | symbol: "DAO", 7 | decimal: 6, 8 | }, 9 | { 10 | chain: "juno", 11 | contract_address: 12 | "juno168ctmpyppk90d34p3jjy658zf5a5l3w8wk35wht6ccqj4mr0yv8s4j5awr", 13 | symbol: "NETA", 14 | decimal: 6, 15 | coinGeckoId: "neta", 16 | }, 17 | { 18 | chain: "juno", 19 | contract_address: 20 | "juno10rktvmllvgctcmhl5vv8kl3mdksukyqf2tdveh8drpn0sppugwwqjzz30z", 21 | symbol: "TCOS", 22 | decimal: 0, 23 | }, 24 | { 25 | chain: "juno", 26 | contract_address: 27 | "juno1pshrvuw5ng2q4nwcsuceypjkp48d95gmcgjdxlus2ytm4k5kvz2s7t9ldx", 28 | symbol: "HULC", 29 | }, 30 | { 31 | chain: "juno", 32 | contract_address: 33 | "juno1xmpenz0ykxfy8rxr3yc3d4dtqq4dpas4zz3xl6sh873us3vajlpshzp69d", 34 | symbol: "FOT", 35 | }, 36 | { 37 | chain: "juno", 38 | contract_address: 39 | "juno1dgg3qkqr2e7r2w6ka7l60zfmls6ezxjv6vjrwfvvlajr4graa99srldyss", 40 | symbol: "BFOT", 41 | decimal: 6, 42 | }, 43 | { 44 | chain: "juno", 45 | contract_address: 46 | "juno19azgt8q09r3hksgg3j293j3s82eupgrqsrnwxvaa4j35u955r69q39zmv5", 47 | symbol: "KING", 48 | decimal: 6, 49 | }, 50 | { 51 | chain: "juno", 52 | contract_address: 53 | "juno1wurfx334prlceydmda3aecldn2xh4axhqtly05n8gptgl69ee7msrewg6y", 54 | symbol: "TUCK", 55 | decimal: 3, 56 | }, 57 | { 58 | chain: "juno", 59 | contract_address: 60 | "juno1r4pzw8f9z0sypct5l9j906d47z998ulwvhvqe5xdwgy8wf84583sxwh0pa", 61 | symbol: "RAC", 62 | decimal: 6, 63 | coinGeckoId: "racoon", 64 | }, 65 | { 66 | chain: "juno", 67 | contract_address: 68 | "juno1g2g7ucurum66d42g8k5twk34yegdq8c82858gz0tq2fc75zy7khssgnhjl", 69 | symbol: "MARBLE", 70 | decimal: 3, 71 | coinGeckoId: "marble", 72 | }, 73 | { 74 | chain: "juno", 75 | contract_address: 76 | "juno1y9rf7ql6ffwkv02hsgd4yruz23pn4w97p75e2slsnkm0mnamhzysvqnxaq", 77 | symbol: "BLOCK", 78 | decimal: 6, 79 | }, 80 | { 81 | chain: "juno", 82 | contract_address: 83 | "juno1re3x67ppxap48ygndmrc7har2cnc7tcxtm9nplcas4v0gc3wnmvs3s807z", 84 | symbol: "HOPE", 85 | decimal: 6, 86 | }, 87 | { 88 | chain: "juno", 89 | contract_address: 90 | "juno15u3dt79t6sxxa3x3kpkhzsy56edaa5a66wvt3kxmukqjz2sx0hes5sn38g", 91 | symbol: "RAW", 92 | decimal: 6, 93 | }, 94 | { 95 | chain: "juno", 96 | contract_address: 97 | "juno1cltgm8v842gu54srmejewghnd6uqa26lzkpa635wzra9m9xuudkqa2gtcz", 98 | symbol: "FURY", 99 | decimal: 6, 100 | }, 101 | { 102 | chain: "juno", 103 | contract_address: 104 | "juno1sfwye65qxcfsc837gu5qcprcz7w49gkv3wnat04764ld76hy3arqs779tr", 105 | symbol: "DLA", 106 | decimal: 6, 107 | }, 108 | { 109 | chain: "juno", 110 | contract_address: 111 | "juno1rws84uz7969aaa7pej303udhlkt3j9ca0l3egpcae98jwak9quzq8szn2l", 112 | symbol: "PHMN", 113 | decimal: 6, 114 | }, 115 | { 116 | chain: "juno", 117 | contract_address: 118 | "juno1lnqhfvgr9x2u40z4d4u5ezep6cxk2kq7agd33evmjl5lfuwr52kszp0y57", 119 | symbol: "HNC", 120 | decimal: 6, 121 | }, 122 | { 123 | chain: "juno", 124 | contract_address: 125 | "juno14lycavan8gvpjn97aapzvwmsj8kyrvf644p05r0hu79namyj3ens87650k", 126 | symbol: "SGNL", 127 | decimal: 6, 128 | }, 129 | { 130 | chain: "juno", 131 | contract_address: 132 | "juno1dd0k0um5rqncfueza62w9sentdfh3ec4nw4aq4lk5hkjl63vljqscth9gv", 133 | symbol: "seJUNO", 134 | decimal: 6, 135 | }, 136 | { 137 | chain: "juno", 138 | contract_address: 139 | "juno1wwnhkagvcd3tjz6f8vsdsw5plqnw8qy2aj3rrhqr2axvktzv9q2qz8jxn3", 140 | symbol: "bJUNO", 141 | decimal: 6, 142 | }, 143 | { 144 | chain: "juno", 145 | contract_address: 146 | "juno1qsrercqegvs4ye0yqg93knv73ye5dc3prqwd6jcdcuj8ggp6w0us66deup", 147 | symbol: "LOOP", 148 | decimal: 6, 149 | }, 150 | { 151 | chain: "juno", 152 | contract_address: 153 | "juno147t4fd3tny6hws6rha9xs5gah9qa6g7hrjv9tuvv6ce6m25sy39sq6yv52", 154 | symbol: "DRGN", 155 | decimal: 6, 156 | }, 157 | { 158 | chain: "juno", 159 | contract_address: 160 | "juno19rqljkh95gh40s7qdx40ksx3zq5tm4qsmsrdz9smw668x9zdr3lqtg33mf", 161 | symbol: "SEASY", 162 | decimal: 6, 163 | }, 164 | ]; 165 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | previewFeatures = ["interactiveTransactions"] 4 | } 5 | 6 | datasource db { 7 | provider = "postgresql" 8 | url = env("DATABASE_URL") 9 | } 10 | 11 | model User { 12 | id Int @id @default(autoincrement()) 13 | wallets Wallet[] 14 | telegramId BigInt @unique @map("telegram_id") 15 | updatedAt DateTime @updatedAt @map("updated_at") 16 | createdAt DateTime @default(now()) @map("created_at") 17 | notification Notification? 18 | alarm Alarm[] 19 | timezone String @default("Europe/London") 20 | 21 | @@map(name: "users") 22 | } 23 | 24 | model Notification { 25 | id Int @id @default(autoincrement()) 26 | networks NetworksInNotification[] 27 | isReminderActive Boolean @default(false) @map("is_reminder_active") 28 | isAlarmActive Boolean @default(false) @map("is_alarm_active") 29 | notificationReminderTime String[] @map("notification_reminder_time") 30 | user User @relation(fields: [userId], references: [id]) 31 | userId Int @unique @map("user_id") 32 | 33 | @@map(name: "notifications") 34 | } 35 | 36 | model NetworksInNotification { 37 | id Int @id @default(autoincrement()) 38 | notification Notification? @relation(fields: [notificationId], references: [id]) 39 | notificationId Int? @map("notification_id") 40 | reminderNetwork Network? @relation("reminder_networks", fields: [reminderNetworkId], references: [id]) 41 | reminderNetworkId Int? @unique @map("reminder_network_id") 42 | governanceNetwork Network? @relation("governance_networks", fields: [governanceNetworkId], references: [id]) 43 | governanceNetworkId Int? @unique @map("governance_network_id") 44 | governanceTimeStart DateTime? @map("governance_time_start") 45 | 46 | @@map(name: "networks_in_notification") 47 | } 48 | 49 | model Alarm { 50 | id Int @id @default(autoincrement()) 51 | user User @relation(fields: [userId], references: [id]) 52 | userId Int @map("user_id") 53 | alarmPrices AlarmPrice[] 54 | network Network? @relation(fields: [networkId], references: [id]) 55 | networkId Int @unique @map("network_id") 56 | 57 | @@unique([userId, networkId]) 58 | @@map(name: "alarms") 59 | } 60 | 61 | model AlarmPrice { 62 | id Int @id @default(autoincrement()) 63 | price Float 64 | coingeckoPrice Float @map("coingecko_price") 65 | createdAt DateTime @default(now()) @map("created_at") 66 | alarm Alarm @relation(fields: [alarmId], references: [id]) 67 | alarmId Int @map("alarm_id") 68 | 69 | @@map(name: "alarm_prices") 70 | } 71 | 72 | model Wallet { 73 | id Int @id @default(autoincrement()) 74 | address String 75 | name String? 76 | network Network @relation(fields: [networkId], references: [id]) 77 | networkId Int @map("network_id") 78 | User User @relation(fields: [userId], references: [id]) 79 | userId Int @map("user_id") 80 | createdAt DateTime @default(now()) @map("created_at") 81 | iv String? 82 | 83 | @@map(name: "wallets") 84 | } 85 | 86 | model Network { 87 | id Int @id @default(autoincrement()) 88 | name String @unique 89 | fullName String @map("full_name") 90 | resource Resource? @relation(fields: [resourceId], references: [id]) 91 | resourceId Int @unique @map("resource_id") 92 | publicUrl String @map("public_url") 93 | keplrId String? @map("keplr_id") 94 | wallet Wallet[] 95 | Alarm Alarm[] 96 | governanceNotifications NetworksInNotification[] @relation("reminder_networks") 97 | reminderNotifications NetworksInNotification[] @relation("governance_networks") 98 | 99 | @@map(name: "networks") 100 | } 101 | 102 | model Resource { 103 | id Int @id @default(autoincrement()) 104 | site String 105 | discord String? 106 | twitter String 107 | youtube String? 108 | blog String? 109 | github String 110 | reddit String? 111 | telegram String? 112 | network Network[] 113 | 114 | @@map(name: "resources") 115 | } 116 | -------------------------------------------------------------------------------- /bot/src/api/requests/fetchStatistic.ts: -------------------------------------------------------------------------------- 1 | import { restRequest } from "@bot/utils"; 2 | 3 | export const fetchCommunityPool = async (url: string) => { 4 | const defaultReturnValue = { 5 | communityPool: [], 6 | }; 7 | try { 8 | const req = await restRequest( 9 | `${url}cosmos/distribution/v1beta1/community_pool` 10 | ); 11 | const res = await req.text(); 12 | 13 | return defaultReturnValue; 14 | } catch (error) { 15 | console.log(error); 16 | return defaultReturnValue; 17 | } 18 | }; 19 | 20 | export const fetchInflation = async (url: string, denom: string) => { 21 | const defaultReturnValue = { 22 | inflation: {}, 23 | }; 24 | try { 25 | if (denom === "evmos") { 26 | const req = await restRequest(`${url}evmos/inflation/v1/inflation_rate`); 27 | const res = await req.json(); 28 | 29 | return { 30 | inflation: Number(res.inflation_rate / 100 || 0), 31 | }; 32 | } 33 | const req = await restRequest(`${url}cosmos/mint/v1beta1/inflation`); 34 | const res = await req.json(); 35 | 36 | return { 37 | inflation: res.inflation, 38 | }; 39 | } catch (error) { 40 | console.log(error); 41 | return defaultReturnValue; 42 | } 43 | }; 44 | 45 | export const fetchSupply = async (url: string, denom: string) => { 46 | const defaultReturnValue = { 47 | supply: { 48 | amount: {}, 49 | }, 50 | }; 51 | try { 52 | const req = await restRequest(`${url}cosmos/bank/v1beta1/supply/${denom}`); 53 | const res = await req.json(); 54 | 55 | return { 56 | supply: res.amount, 57 | }; 58 | } catch (error) { 59 | console.log(error); 60 | return defaultReturnValue; 61 | } 62 | }; 63 | 64 | export const fetchPool = async (url: string) => { 65 | const defaultReturnValue = { 66 | pool: { 67 | bonded: "", 68 | notBonded: "", 69 | }, 70 | }; 71 | try { 72 | const req = await restRequest(`${url}cosmos/staking/v1beta1/pool`); 73 | const res = await req.json(); 74 | 75 | return { 76 | pool: { 77 | bonded: res.pool.bonded_tokens, 78 | notBonded: res.pool.not_bonded_tokens, 79 | }, 80 | }; 81 | } catch (error) { 82 | console.log(error); 83 | return defaultReturnValue; 84 | } 85 | }; 86 | 87 | export const fetchDistributionParams = async (url: string) => { 88 | const defaultReturnValue = { 89 | params: {}, 90 | }; 91 | try { 92 | const req = await restRequest(`${url}cosmos/distribution/v1beta1/params`); 93 | const res = await req.json(); 94 | 95 | return { 96 | params: res.params, 97 | }; 98 | } catch (error) { 99 | console.log(error); 100 | return defaultReturnValue; 101 | } 102 | }; 103 | 104 | export const fetchLatestHeight = async (publicUrl: string) => { 105 | const defaultReturnValue = { 106 | height: "", 107 | }; 108 | try { 109 | const req = await restRequest( 110 | `${publicUrl}cosmos/base/tendermint/v1beta1/blocks/latest` 111 | ); 112 | const res = await req.json(); 113 | 114 | return { 115 | height: res?.block, 116 | }; 117 | } catch (error) { 118 | console.log(error); 119 | return defaultReturnValue; 120 | } 121 | }; 122 | 123 | export const fetchBlock = async (publicUrl: string, block: number) => { 124 | const defaultReturnValue = { 125 | height: "", 126 | }; 127 | try { 128 | const req = await restRequest( 129 | `${publicUrl}cosmos/base/tendermint/v1beta1/blocks/${block}` 130 | ); 131 | const res = await req.json(); 132 | 133 | return { 134 | height: res?.block, 135 | }; 136 | } catch (error) { 137 | console.log(error); 138 | return defaultReturnValue; 139 | } 140 | }; 141 | 142 | export const fetchAnnualProvisions = async (publicUrl: string) => { 143 | const defaultReturnValue = { 144 | annualProvisions: "", 145 | }; 146 | try { 147 | const req = await restRequest( 148 | `${publicUrl}cosmos/mint/v1beta1/annual_provisions` 149 | ); 150 | console.log(123); 151 | const res = await req.text(); 152 | console.log(124, res); 153 | 154 | return defaultReturnValue; 155 | } catch (error) { 156 | console.log(error); 157 | return defaultReturnValue; 158 | } 159 | }; 160 | 161 | export const fetchNetworkStatistic = async (publicUrl: string) => { 162 | const defaultReturnValue = { 163 | networkStatistic: {}, 164 | }; 165 | try { 166 | const req = await restRequest(`${publicUrl}cosmos/mint/v1beta1/params`); 167 | const res = await req.json(); 168 | 169 | return { 170 | networkStatistic: res.params, 171 | }; 172 | } catch (error) { 173 | console.log(error); 174 | return defaultReturnValue; 175 | } 176 | }; 177 | -------------------------------------------------------------------------------- /bot/src/api/handlers/getStatistic.ts: -------------------------------------------------------------------------------- 1 | import _ from "lodash"; 2 | import { 3 | fetchCommunityPool, 4 | fetchDistributionParams, 5 | fetchInflation, 6 | fetchPool, 7 | fetchSupply, 8 | fetchLatestHeight, 9 | fetchAnnualProvisions, 10 | fetchNetworkStatistic, 11 | } from "@bot/api"; 12 | import numeral from "numeral"; 13 | import { ChainInfo, Coins, StatisticData } from "@bot/types/general"; 14 | import { formatToken } from "@bot/utils"; 15 | import { getBlocksPerYearReal } from "@bot/utils/getBlocksPerYearReal"; 16 | import { calculateRealAPR } from "@bot/utils/calculateApr"; 17 | 18 | export const getStatistic = async ( 19 | publicUrl: string, 20 | denom: string, 21 | chain: ChainInfo, 22 | tokenUnit: string 23 | ) => { 24 | console.log(66, publicUrl); 25 | const promises = [ 26 | fetchCommunityPool(publicUrl), 27 | fetchInflation(publicUrl, denom), 28 | fetchSupply(publicUrl, tokenUnit), 29 | fetchPool(publicUrl), 30 | fetchDistributionParams(publicUrl), 31 | fetchLatestHeight(publicUrl), 32 | fetchAnnualProvisions(publicUrl), 33 | fetchNetworkStatistic(publicUrl), 34 | getBlocksPerYearReal(publicUrl), 35 | ]; 36 | 37 | const [ 38 | communityPool, 39 | inflation, 40 | supply, 41 | pool, 42 | distributionParams, 43 | latestHeight, 44 | annualProvisions, 45 | networkStatistic, 46 | blocksPerYear, 47 | ] = await Promise.allSettled(promises); 48 | 49 | const formattedRawData: StatisticData = { 50 | communityPool: {}, 51 | inflation: {}, 52 | supply: { 53 | amount: 0, 54 | denom: "", 55 | }, 56 | pool: {}, 57 | distributionParams: {}, 58 | height: "", 59 | annualProvisions: "", 60 | networkStatistic: {}, 61 | blocksPerYear: 0, 62 | }; 63 | 64 | formattedRawData.communityPool = _.get( 65 | communityPool, 66 | ["value", "communityPool"], 67 | {} 68 | ); 69 | formattedRawData.inflation = _.get(inflation, ["value", "inflation"], 0); 70 | formattedRawData.supply = _.get(supply, ["value", "supply"], {}); 71 | formattedRawData.pool = _.get(pool, ["value", "pool"], {}); 72 | formattedRawData.distributionParams = _.get( 73 | distributionParams, 74 | ["value", "params"], 75 | {} 76 | ); 77 | formattedRawData.height = _.get( 78 | latestHeight, 79 | ["value", "height", "last_commit", "height"], 80 | "" 81 | ); 82 | formattedRawData.annualProvisions = _.get( 83 | annualProvisions, 84 | ["value", "annualProvisions"], 85 | "" 86 | ); 87 | formattedRawData.networkStatistic = _.get( 88 | networkStatistic, 89 | ["value", "networkStatistic"], 90 | "" 91 | ); 92 | formattedRawData.blocksPerYear = _.get( 93 | blocksPerYear, 94 | ["value", "blocksPerYear"], 95 | "" 96 | ); 97 | 98 | return formatStatisticsValues(formattedRawData, chain); 99 | }; 100 | 101 | const formatStatisticsValues = (data: StatisticData, chain: ChainInfo) => { 102 | try { 103 | const { primaryTokenUnit, tokenUnits } = chain; 104 | let communityPool; 105 | 106 | const [communityPoolCoin]: [Coins] = _.get( 107 | data, 108 | ["communityPool"], 109 | [] 110 | ).filter((x: Coins) => x.denom === chain.primaryTokenUnit); 111 | 112 | const inflation = _.get(data, ["inflation"], 0); 113 | 114 | const total = _.get(data, ["supply"], { 115 | amount: 0, 116 | }); 117 | 118 | const rawSupplyAmount = total.amount; 119 | 120 | const supply = formatToken( 121 | rawSupplyAmount, 122 | tokenUnits[primaryTokenUnit], 123 | primaryTokenUnit 124 | ); 125 | 126 | if ( 127 | communityPoolCoin && 128 | communityPoolCoin.denom === chain.primaryTokenUnit 129 | ) { 130 | communityPool = formatToken( 131 | communityPoolCoin.amount, 132 | tokenUnits[communityPoolCoin.denom], 133 | communityPoolCoin.denom 134 | ); 135 | } 136 | 137 | const bonded = _.get(data, ["pool", "bonded"], 1); 138 | const unbonding = _.get(data, ["pool", "notBonded"], 1); 139 | const unbonded = rawSupplyAmount - unbonding - bonded; 140 | 141 | const communityTax = _.get( 142 | data, 143 | ["distributionParams", "community_tax"], 144 | "0" 145 | ); 146 | 147 | const apr = calculateRealAPR({ 148 | annualProvisions: Number(data.annualProvisions), 149 | communityTax: communityTax, 150 | bondedTokens: bonded, 151 | blocksYearReal: data.blocksPerYear, 152 | }); 153 | 154 | return { 155 | supply, 156 | inflation, 157 | communityPool, 158 | apr: Number(apr), 159 | height: data.height, 160 | bonded: numeral( 161 | formatToken(bonded, tokenUnits[primaryTokenUnit], primaryTokenUnit) 162 | .value 163 | ).value(), 164 | unbonding: numeral( 165 | formatToken(unbonding, tokenUnits[primaryTokenUnit], primaryTokenUnit) 166 | .value 167 | ).value(), 168 | unbonded: numeral( 169 | formatToken(unbonded, tokenUnits[primaryTokenUnit], primaryTokenUnit) 170 | .value 171 | ).value(), 172 | }; 173 | } catch (e) { 174 | return { 175 | supply: 0, 176 | inflation: 0, 177 | communityPool: { 178 | displayDenom: "", 179 | value: 0, 180 | }, 181 | apr: 0, 182 | height: 0, 183 | bonded: 0, 184 | unbonding: 0, 185 | unbonded: 0, 186 | }; 187 | } 188 | }; 189 | -------------------------------------------------------------------------------- /bot/src/types/general.ts: -------------------------------------------------------------------------------- 1 | import { Bech32Config } from "@keplr-wallet/types"; 2 | 3 | export type TokenUnit = { 4 | displayDenom: string; 5 | baseDenom: string; 6 | exponent: number; 7 | value: string; 8 | }; 9 | 10 | export type Coins = { 11 | amount: string; 12 | denom: string; 13 | }; 14 | 15 | export type BalanceData = { 16 | accountBalances: { coins: Array }; 17 | delegationBalance: []; 18 | unbondingBalance: []; 19 | delegationRewards: Array<{ coins: Array; validatorAddress: string }>; 20 | }; 21 | 22 | export type ChainInfo = { 23 | network: string; 24 | prefix: Bech32Config; 25 | primaryTokenUnit: string; 26 | coingeckoId: string; 27 | tokenUnits: { 28 | [key: string]: { 29 | display: string; 30 | exponent: number; 31 | }; 32 | }; 33 | }; 34 | 35 | export type StatisticData = { 36 | communityPool: any; 37 | supply: { 38 | amount: number; 39 | denom: string; 40 | }; 41 | pool: any; 42 | distributionParams: any; 43 | inflation: any; 44 | height: ""; 45 | annualProvisions: ""; 46 | networkStatistic: any; 47 | blocksPerYear: 0; 48 | }; 49 | 50 | export type TTokenPrice = { 51 | [key: string]: { 52 | usd: number; 53 | }; 54 | }; 55 | 56 | export type TokenData = { 57 | tokenPrice: Array; 58 | tokenHistory: Array; 59 | }; 60 | 61 | export interface BasicCoin { 62 | id?: string; 63 | name?: string; 64 | symbol?: string; 65 | } 66 | 67 | export interface Image { 68 | thumb?: string; 69 | small?: string; 70 | large?: string; 71 | } 72 | 73 | export interface MarketData { 74 | current_price?: { [key: string]: number }; 75 | total_value_locked?: null; 76 | mcap_to_tvl_ratio?: null; 77 | fdv_to_tvl_ratio?: null; 78 | roi?: null; 79 | ath?: { [key: string]: number }; 80 | ath_change_percentage?: { [key: string]: number }; 81 | ath_date?: { [key: string]: Date }; 82 | atl?: { [key: string]: number }; 83 | atl_change_percentage?: { [key: string]: number }; 84 | atl_date?: { [key: string]: Date }; 85 | market_cap?: { [key: string]: number }; 86 | market_cap_rank?: number; 87 | fully_diluted_valuation?: any; 88 | total_volume?: { [key: string]: number }; 89 | high_24h?: { [key: string]: number }; 90 | low_24h?: { [key: string]: number }; 91 | price_change_24h?: number; 92 | price_change_percentage_24h?: number; 93 | price_change_percentage_7d?: number; 94 | price_change_percentage_14d?: number; 95 | price_change_percentage_30d?: number; 96 | price_change_percentage_60d?: number; 97 | price_change_percentage_200d?: number; 98 | price_change_percentage_1y?: number; 99 | market_cap_change_24h?: number; 100 | market_cap_change_percentage_24h?: number; 101 | price_change_24h_in_currency?: { [key: string]: number }; 102 | price_change_percentage_1h_in_currency?: { [key: string]: number }; 103 | price_change_percentage_24h_in_currency?: { [key: string]: number }; 104 | price_change_percentage_7d_in_currency?: { [key: string]: number }; 105 | price_change_percentage_14d_in_currency?: { [key: string]: number }; 106 | price_change_percentage_30d_in_currency?: { [key: string]: number }; 107 | price_change_percentage_60d_in_currency?: { [key: string]: number }; 108 | price_change_percentage_200d_in_currency?: { [key: string]: number }; 109 | price_change_percentage_1y_in_currency?: { [key: string]: number }; 110 | market_cap_change_24h_in_currency?: { [key: string]: number }; 111 | market_cap_change_percentage_24h_in_currency?: { [key: string]: number }; 112 | total_supply?: number; 113 | max_supply?: null; 114 | circulating_supply?: number; 115 | last_updated?: Date; 116 | } 117 | 118 | export interface CommunityData { 119 | facebook_likes?: null; 120 | twitter_followers?: number; 121 | reddit_average_posts_48h?: number; 122 | reddit_average_comments_48h?: number; 123 | reddit_subscribers?: number; 124 | reddit_accounts_active_48h?: number; 125 | telegram_channel_user_count?: number; 126 | } 127 | 128 | export interface CodeAdditionsDeletions4_Weeks { 129 | additions?: number; 130 | deletions?: number; 131 | } 132 | 133 | export interface DeveloperData { 134 | forks?: number; 135 | stars?: number; 136 | subscribers?: number; 137 | total_issues?: number; 138 | closed_issues?: number; 139 | pull_requests_merged?: number; 140 | pull_request_contributors?: number; 141 | code_additions_deletions_4_weeks?: CodeAdditionsDeletions4_Weeks; 142 | commit_count_4_weeks?: number; 143 | last_4_weeks_commit_activity_series?: number[]; 144 | } 145 | 146 | export interface PublicInterestStats { 147 | alexa_rank?: number; 148 | bing_matches?: null; 149 | } 150 | 151 | export interface CoinHistoryResponse extends BasicCoin { 152 | image: Image; 153 | market_data: MarketData; 154 | community_data: CommunityData; 155 | developer_data: DeveloperData; 156 | public_interest_stats: PublicInterestStats; 157 | } 158 | 159 | export type ProposalItem = { 160 | proposalId: number; 161 | votingStartTime: string; 162 | title: string; 163 | description: string; 164 | status: string; 165 | }; 166 | 167 | export type ProposalItemResponse = Array<{ 168 | proposal_id: number; 169 | voting_start_time: string; 170 | content: { 171 | title: string; 172 | description: string; 173 | }; 174 | status: string; 175 | }>; 176 | 177 | export type Cw20 = Array<{ 178 | symbol: string; 179 | decimal: number; 180 | balance: number; 181 | }>; 182 | 183 | export type Steps = 184 | | "wallet" 185 | | "admin" 186 | | "governance" 187 | | "timezone" 188 | | "notification" 189 | | "bulkImport" 190 | | "walletPassword"; 191 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

Cosmos space bot

5 | 6 | --- 7 | 8 |

Reach me out:

9 | 10 | [//]: # () 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Website - https://reactivestation.dev/ 23 | 24 | --- 25 | 26 |

About:

27 | 28 | ⚠️ The bot doesn't use any secure information, only your public address in encryption format, 29 | so no one except you has access to them. 30 | 31 |

Cosmos Space bot was developed to integrate the Cosmos Ecosystem into Telegram with many valuable 32 | features that help manage wallets and assets, including CW20 tokens on Juno, view network statistics, 33 | set up various types of notifications, and view useful resources. The bot currently supports the 34 | following networks: Agoric, 35 | Akash, AssetMantle, 36 | Band Protocol, Bitsong, 37 | Cheqd, Chihuahua, 38 | Comdex, Cosmos Hub, 39 | Crescent, Desmos, 40 | E-money, Evmos, 41 | Juno, LikeCoin, 42 | Osmosis, Persistence, 43 | Provenance Blockchain, Regen Network, 44 | Rizon, Secret network, 45 | Sifchain, Stargaze, 46 | Terra 47 |

48 | 49 |

Tech Stack:

50 |
    51 |
  • Node JS
  • 52 |
  • Typescript
  • 53 |
  • PostgresSQL
  • 54 |
  • Redis
  • 55 |
  • Prisma ORM
  • 56 |
  • Fastify
  • 57 |
  • GrammyJs
  • 58 |
59 | 60 |

Features and Roadmap:

61 |

Manage wallets

62 |
    63 |
  • - [x] Add wallet manually
  • 64 |
  • - [x] Add wallet via Keplr
  • 65 |
  • - [x] Add wallet via CSV
  • 66 |
  • - [x] Exports wallets in CSV
  • 67 |
  • - [x] List of Wallets
  • 68 |
  • - [x] Delete a wallet
  • 69 |
  • - [x] Wallet encryption
  • 70 |
  • - [x] Show Wallet assets
  • 71 |
  • - [x] CW20 tokens for Juno
  • 72 |
  • - [x] Total assets
  • 73 |
  • - [x] P&L for wallet
  • 74 |
  • - [ ] Bulk wallets delete
  • 75 |
  • - [ ] Import/Export wallets via .TXT
  • 76 |
  • - [ ] Wallets pagination
  • 77 |
  • - [ ] Add Juno CW20 tokens in total amount
  • 78 |
  • - [ ] Export total amount in .CSV
  • 79 |
  • - [ ] Add liquidity pools
  • 80 |
  • - [ ] Support interchain accounts in future
  • 81 |
  • - [ ] Integrate automatic wallets converter
  • 82 |
83 | 84 |

Network statistic and resources

85 |
    86 |
  • - [x] General networks statistic
  • 87 |
  • - [x] Price change
  • 88 |
  • - [x] Network resources
  • 89 |
  • - [x] Active proposals
  • 90 |
  • - [ ] Added more networks
  • 91 |
  • - [ ] Additional statistic params
  • 92 |
  • - [ ] Fix APR on some networks
  • 93 |
  • - [ ] Address converter between networks
  • 94 |
  • - [ ] Show user NFT which support Juno
  • 95 |
  • - [ ] DAO statistics
  • 96 |
97 | 98 |

Notification

99 |
    100 |
  • - [x] Daily price reminder
  • 101 |
  • - [x] Price alert
  • 102 |
  • - [x] Subscribe to proposal
  • 103 |
  • - [ ] Subscribe to new deposit on wallets
  • 104 |
  • - [ ] Add unbonding notification
  • 105 |
  • - [ ] Integrate twitter feed for official network
  • 106 |
  • - [ ] Support Juno CW20 tokens in price alert
  • 107 |
108 | 109 |

Additional

110 |
    111 |
  • - [ ] Integrate Faucet
  • 112 |
  • - [ ] Add multilingual
  • 113 |
114 | 115 |

Summary:

116 | 117 |
    118 |
  • I hope that this bot will be useful in the space ecosystem, add structure to the management 119 | of your funds, and notifications will help you never miss important events for you. 120 |
  • 121 |
  • For me, this is an advantageous experience that will help create valuable products and tools.
  • 122 |
  • Big thanks for help Posthuman ∞ DVS team
  • 123 |
124 | 125 |

Achivemets:

126 | 127 |
    128 |
  • 100 Active user
  • 129 |
  • Support 24 network
  • 130 |
131 | 132 |

License

133 | 134 |
    135 |
  • Open Source
  • 136 |
  • Apache-2.0 license
  • 137 |
138 | 139 |

Dependencies

140 | 143 | -------------------------------------------------------------------------------- /bot/src/api/handlers/getBalance.ts: -------------------------------------------------------------------------------- 1 | import _ from "lodash"; 2 | import Big from "big.js"; 3 | import { 4 | fetchAvailableBalances, 5 | fetchDelegationBalance, 6 | fetchUnbondingBalance, 7 | fetchRewards, 8 | } from "@bot/api"; 9 | import { getDenom, formatToken } from "@bot/utils"; 10 | import { BalanceData, ChainInfo, Cw20 } from "@bot/types/general"; 11 | import { config } from "@bot/chains"; 12 | import { cosmosConfig } from "@bot/chains/cosmos"; 13 | import { getCW20tokens } from "@bot/utils/getCW20tokens"; 14 | import { junoCW20 } from "@bot/chains/junoCW20"; 15 | 16 | export const getBalance = async ( 17 | publicUrl: string, 18 | address: string, 19 | prefix: string, 20 | isCW20Include = false 21 | ) => { 22 | const chain = 23 | config.find(({ network }) => network === prefix) || cosmosConfig; 24 | const cw20tokens: Cw20 = []; 25 | 26 | const promises = [ 27 | fetchAvailableBalances(publicUrl, address), 28 | fetchDelegationBalance(publicUrl, address), 29 | fetchUnbondingBalance(publicUrl, address), 30 | fetchRewards(publicUrl, address), 31 | ]; 32 | const [available, delegation, unbonding, rewards] = await Promise.allSettled( 33 | promises 34 | ); 35 | 36 | if (prefix === "juno" && isCW20Include) { 37 | await Promise.all( 38 | junoCW20.map(async (token) => { 39 | const balance: number = await getCW20tokens( 40 | token.contract_address, 41 | address 42 | ); 43 | cw20tokens.push({ 44 | symbol: token.symbol, 45 | decimal: token.decimal || 0, 46 | balance: Number(balance), 47 | }); 48 | }) 49 | ); 50 | } 51 | 52 | const formattedRawData: BalanceData = { 53 | accountBalances: { coins: [] }, 54 | delegationBalance: [], 55 | unbondingBalance: [], 56 | delegationRewards: [], 57 | }; 58 | formattedRawData.accountBalances = _.get( 59 | available, 60 | ["value", "accountBalances"], 61 | [] 62 | ); 63 | formattedRawData.delegationBalance = _.get( 64 | delegation, 65 | ["value", "delegationBalance"], 66 | [] 67 | ); 68 | formattedRawData.unbondingBalance = _.get( 69 | unbonding, 70 | ["value", "unbondingBalance"], 71 | [] 72 | ); 73 | formattedRawData.delegationRewards = _.get( 74 | rewards, 75 | ["value", "delegationRewards"], 76 | [] 77 | ); 78 | 79 | return formatAllBalance(formattedRawData, chain, cw20tokens); 80 | }; 81 | 82 | const formatAllBalance = ( 83 | data: BalanceData, 84 | chain: ChainInfo, 85 | cw20tokens: Cw20 86 | ) => { 87 | try { 88 | const { primaryTokenUnit, tokenUnits } = chain; 89 | const available = getDenom( 90 | _.get(data, ["accountBalances", "coins"], []), 91 | primaryTokenUnit 92 | ); 93 | const availableAmount = formatToken( 94 | available.amount, 95 | tokenUnits[primaryTokenUnit], 96 | primaryTokenUnit 97 | ); 98 | 99 | const delegate = data.delegationBalance.reduce((a, b) => { 100 | const coins = _.get(b, ["balance"], { amount: 0 }); 101 | 102 | return Big(a).plus(coins.amount).toPrecision(); 103 | }, "0"); 104 | const delegateAmount = formatToken( 105 | delegate, 106 | tokenUnits[primaryTokenUnit], 107 | primaryTokenUnit 108 | ); 109 | 110 | let unbonding = 0; 111 | 112 | if (data.unbondingBalance.length > 0) { 113 | data.unbondingBalance.forEach( 114 | (item: { entries: Array<{ balance: string }> }) => { 115 | if (item?.entries.length) { 116 | item?.entries.forEach(({ balance }) => { 117 | unbonding += Number(balance); 118 | }); 119 | } 120 | } 121 | ); 122 | } 123 | 124 | const unbondingAmount = formatToken( 125 | unbonding, 126 | tokenUnits[primaryTokenUnit], 127 | primaryTokenUnit 128 | ); 129 | 130 | const rewards = data.delegationRewards.reduce((a, b) => { 131 | const coins = _.get(b, ["reward"], []); 132 | const dsmCoins = getDenom(coins, primaryTokenUnit); 133 | 134 | return Big(a).plus(dsmCoins.amount).toPrecision(); 135 | }, "0"); 136 | 137 | const rewardsAmount = formatToken( 138 | rewards, 139 | tokenUnits[primaryTokenUnit], 140 | primaryTokenUnit 141 | ); 142 | 143 | const total = Big(availableAmount.value) 144 | .plus(delegateAmount.value) 145 | .plus(unbondingAmount.value) 146 | .plus(rewardsAmount.value) 147 | .toFixed(tokenUnits[primaryTokenUnit].exponent); 148 | 149 | const cw20 = cw20tokens 150 | .filter((item) => Number(item.balance) > 0) 151 | .map((item) => 152 | formatToken( 153 | item.balance, 154 | { exponent: item.decimal, display: item.symbol }, 155 | item.symbol 156 | ) 157 | ); 158 | 159 | return { 160 | available: availableAmount, 161 | delegate: delegateAmount, 162 | unbonding: unbondingAmount, 163 | reward: rewardsAmount, 164 | total: { 165 | value: total, 166 | displayDenom: availableAmount.displayDenom, 167 | baseDenom: availableAmount.baseDenom, 168 | exponent: availableAmount.exponent, 169 | }, 170 | cw20tokens: cw20, 171 | }; 172 | } catch (e) { 173 | console.error("Error in formatAllBalance: " + e); 174 | 175 | return { 176 | available: { 177 | value: "0", 178 | displayDenom: "", 179 | }, 180 | delegate: { 181 | value: "0", 182 | }, 183 | unbonding: { 184 | value: "0", 185 | }, 186 | reward: { 187 | value: "0", 188 | }, 189 | total: { 190 | value: "0", 191 | displayDenom: "", 192 | baseDenom: "", 193 | exponent: 0, 194 | }, 195 | cw20tokens: [], 196 | }; 197 | } 198 | }; 199 | -------------------------------------------------------------------------------- /server/cron.ts: -------------------------------------------------------------------------------- 1 | import fastifyCron from "fastify-cron"; 2 | import dayjs from "dayjs"; 3 | import utc from "dayjs/plugin/utc"; 4 | import timezone from "dayjs/plugin/timezone"; 5 | import localizedFormat from "dayjs/plugin/localizedFormat"; 6 | import { 7 | usersService, 8 | networksService, 9 | alarmsService, 10 | alarmPricesService, 11 | } from "@bot/services"; 12 | import { notificationDao, networkInNotificationDao } from "@bot/dao"; 13 | import { sendNotification } from "@server/telegram"; 14 | import { template } from "@bot/utils"; 15 | import { en } from "@bot/constants/en"; 16 | import { getProposals, getTokenPrice } from "@bot/api"; 17 | 18 | export function cron(server: any) { 19 | server.register(fastifyCron, { 20 | jobs: [ 21 | { 22 | cronTime: "0 * * * *", 23 | 24 | onTick: async () => { 25 | dayjs.extend(utc); 26 | dayjs.extend(timezone); 27 | dayjs.extend(localizedFormat); 28 | 29 | const { getUser } = usersService(); 30 | const { getNetwork } = networksService(); 31 | 32 | const notifications = await notificationDao.getAllNotifications(); 33 | 34 | for (const notification of notifications) { 35 | let prices = ""; 36 | const reminderNetworks = 37 | await networkInNotificationDao.getAllNetworkInNotification({ 38 | where: { 39 | notificationId: notification.id, 40 | }, 41 | }); 42 | const user = (await getUser({ 43 | id: notification.userId, 44 | })) || { 45 | timezone: "", 46 | telegramId: 0, 47 | }; 48 | 49 | const now = dayjs(); 50 | const userTime = dayjs.tz(now, user.timezone); 51 | prices += template(en.cron.reminderTitle, { 52 | date: userTime.format("LLL"), 53 | }); 54 | 55 | if (reminderNetworks.length === 0 && !notification.isReminderActive) 56 | return; 57 | 58 | for (const reminder of reminderNetworks) { 59 | const { network, coingeckoId } = await getNetwork({ 60 | networkId: Number(reminder.reminderNetworkId), 61 | }); 62 | const networkPrice = await getTokenPrice({ 63 | apiId: coingeckoId, 64 | }); 65 | 66 | prices += template(en.cron.reminderItem, { 67 | networkName: network.fullName, 68 | price: `${networkPrice.price}`, 69 | }); 70 | } 71 | 72 | if ( 73 | notification.notificationReminderTime.includes( 74 | userTime.format("LT") 75 | ) 76 | ) { 77 | sendNotification(prices, "HTML", Number(user.telegramId)); 78 | } 79 | } 80 | }, 81 | }, 82 | { 83 | cronTime: "*/2 * * * *", 84 | 85 | onTick: async () => { 86 | const { getAllAlarms } = await alarmsService(); 87 | const { getAllAlarmPrices, removeAlarmPrice } = alarmPricesService(); 88 | const { getNetwork } = networksService(); 89 | const { getUser } = usersService(); 90 | 91 | const alarms = await getAllAlarms(true); 92 | for (const alarm of alarms) { 93 | const { network, coingeckoId } = await getNetwork({ 94 | networkId: alarm.networkId, 95 | }); 96 | 97 | const networkPrice = await getTokenPrice({ 98 | apiId: coingeckoId, 99 | }); 100 | const alarmPrices = await getAllAlarmPrices(alarm.id); 101 | 102 | if (alarmPrices.length === 0) return; 103 | 104 | const user = await getUser({ 105 | id: alarm.userId, 106 | }); 107 | 108 | const sendMessage = async (id: number) => { 109 | await sendNotification( 110 | template(en.cron.alarmTitle, { 111 | networkName: network.fullName, 112 | price: `${networkPrice.price}`, 113 | }), 114 | "HTML", 115 | Number(user?.telegramId) 116 | ); 117 | await removeAlarmPrice(id); 118 | }; 119 | 120 | for (const price of alarmPrices) { 121 | const isPositive = price.price - price.coingeckoPrice > 0; 122 | 123 | if (isPositive && networkPrice.price > price.price) { 124 | await sendMessage(price.id); 125 | } 126 | 127 | if (!isPositive && networkPrice.price < price.price) { 128 | await sendMessage(price.id); 129 | } 130 | } 131 | } 132 | }, 133 | }, 134 | { 135 | cronTime: "* * * * *", 136 | 137 | onTick: async () => { 138 | const { getUser } = usersService(); 139 | const { getNetwork } = networksService(); 140 | 141 | const notifications = await notificationDao.getAllNotifications(); 142 | 143 | for (const notification of notifications) { 144 | const governanceNetworks = 145 | await networkInNotificationDao.getAllNetworkInNotification({ 146 | where: { 147 | notificationId: notification.id, 148 | }, 149 | }); 150 | 151 | const user = (await getUser({ 152 | id: notification.userId, 153 | })) || { 154 | timezone: "", 155 | telegramId: 0, 156 | }; 157 | 158 | const networks = governanceNetworks.filter( 159 | (item) => item.governanceNetworkId 160 | ); 161 | 162 | if (networks.length === 0) return; 163 | 164 | for (const network of networks) { 165 | const item = await getNetwork({ networkId: network.id }); 166 | const { activeProposals } = await getProposals(item.publicUrl); 167 | const proposals = activeProposals[0]; 168 | 169 | if (!proposals) return; 170 | 171 | if ( 172 | dayjs(network.governanceTimeStart).isBefore( 173 | dayjs(proposals.votingStartTime) 174 | ) 175 | ) { 176 | await sendNotification( 177 | template(en.cron.newProposal, { 178 | networkName: item.network.fullName, 179 | title: proposals.title, 180 | description: proposals.description, 181 | }), 182 | "HTML", 183 | Number(user.telegramId) 184 | ); 185 | 186 | await networkInNotificationDao.removeNetworkInNotification({ 187 | where: { 188 | governanceNetworkId: network.id, 189 | }, 190 | }); 191 | 192 | await networkInNotificationDao.createNetworkInNotification({ 193 | data: { 194 | notificationId: notification.id, 195 | governanceNetworkId: network.id, 196 | governanceTimeStart: dayjs().toDate(), 197 | }, 198 | }); 199 | } 200 | } 201 | } 202 | }, 203 | }, 204 | ], 205 | }); 206 | } 207 | -------------------------------------------------------------------------------- /bot/src/constants/en.ts: -------------------------------------------------------------------------------- 1 | export const en = { 2 | aboutBot: 3 | "Statistic, Personal assets, Notification in Cosmos-based networks.", 4 | descriptionBot: 5 | "This bot can send Crypto prices, Wallet(s) assets, and different types of notifications", 6 | start: { 7 | command: "start", 8 | text: 9 | "I'm the @CosmosSpaceBot \n\n" + 10 | "I have a lot of useful functions such as: \n\n" + 11 | "Manage wallets\n" + 12 | "View cryptocurrency assets\n" + 13 | "Network statistics\n" + 14 | "Show active proposals\n" + 15 | "List of resources about networks\n" + 16 | "Daily reminder\n" + 17 | "Price alert\n" + 18 | "Subscribe to networks proposals\n\n" + 19 | "/help get full command list \n\n" + 20 | "Reach Out to the Developer @ReactiveGuy", 21 | }, 22 | help: { 23 | command: "help", 24 | text: 25 | "/wallet - Manage wallets \n" + 26 | "/assets - View cryptocurrency Assets \n" + 27 | "/statistic - View one of the network statistic \n" + 28 | "/notification - Use notifications to get alerts \n" + 29 | "/proposals - View proposals in different chains" + 30 | "/resources - The list of resources about one of the Network \n" + 31 | "/help - Full command list \n" + 32 | "/about - About bot \n" + 33 | "/support - Support me \n", 34 | }, 35 | wallet: { 36 | command: "wallet", 37 | invalidFormat: 38 | "Format should be: (If you don't want, fill in /reset) \n\n" + 39 | "Address\n" + 40 | "Wallet name\n\n", 41 | invalidAddress: 42 | "Enter a valid address. If you don't want enter, fill in /reset", 43 | invalidNetwork: 44 | "Network %{networkName} is not supported, enter other chain. If you don't want enter, fill in /reset", 45 | duplicateAddress: 46 | "You already have this wallet. If you don't want enter, fill in /reset", 47 | addMore: "Add one more wallet", 48 | success: "Perfect! Use /assets command", 49 | menu: { 50 | title: 51 | "Choose the Action \n\n" + 52 | "Note: Your addresses stores in encrypted format,\n" + 53 | "so it's absolutely secure. You can check it from the " + 54 | "link", 55 | keplr: "🔑 Add Via Keplr", 56 | manually: "👇 Add Manually", 57 | bulkImport: "📁 Add via .csv", 58 | bulkExport: "📎 Export addresses", 59 | list: "💳 List of Wallets", 60 | delete: "🗑 Delete a wallet", 61 | removeAll: "Remove all wallets", 62 | }, 63 | addAddress: 64 | "Enter your address in format:\n\n" + "Address\n" + "Wallet name \n", 65 | addBulkWallet: 66 | "Send .csv file with wallet addresses, example of .csv file above", 67 | showWallet: "%{number} %{name} - %{address}", 68 | deleteWallet: "Choose the wallet that you want to remove", 69 | removedWallet: "Wallet %{address} was successful removed", 70 | removedAllWallets: "All wallets were removed", 71 | emptyWallet: "You don't have wallets, please add it", 72 | bulkImportAddressInvalid: 73 | "Check addresses in file and reload file. If you don't want, fill in /reset", 74 | bulkImportNetworkInvalid: 75 | "Network %{networkName} is not supported, fix and reload file. If you don't want, fill in /reset", 76 | bulkImportDuplicateAddress: 77 | "Wallet %{walletAddress} duplicated, remove it and reload file. If you don't want, fill in /reset", 78 | incorrectCSV: "Incorrect .csv file format, should be comma separator", 79 | successfulImport: "File was successful uploaded", 80 | emptyPassword: 81 | "Please enter a password, which will be stored locally in your telegram session and " + 82 | "is necessary to keep your wallets securely. " + 83 | "In the future, it will be used for decryption wallet addresses.", 84 | }, 85 | assets: { 86 | command: "assets", 87 | menu: { 88 | title: "Choose wallet(s)", 89 | all: "Total amount", 90 | walletDescription: 91 | "Wallet %{number}: %{address} \n" + 92 | "\nBalance in %{denom}: \n\n" + 93 | "👉 Available — %{available} \n\n" + 94 | "💸 Delegated — %{delegate} \n\n" + 95 | "🔐 Unbonding — %{unbonding} \n\n" + 96 | "🤑 Staking Reward — %{reward} \n\n" + 97 | "Total %{denom} — %{totalCrypto} \n" + 98 | "Total USD — 💲%{total} \n\n" + 99 | "CW20 tokens: \n%{cw20}\n" + 100 | "P&L: \n" + 101 | "▫️ For today %{firstAmount} (%{firstPercent}%) \n\n" + 102 | "▫️ In 7 days %{seventhAmount} (%{seventhPercent}%) \n\n" + 103 | "▫️ In 14 days %{fourteenthAmount} (%{fourteenthPercent}%) \n\n" + 104 | "▫️ In 30 days %{thirtyAmount} (%{thirtyPercent}%) \n\n", 105 | total: "%{number} %{networkName}%{amount} \n\n", 106 | }, 107 | }, 108 | notification: { 109 | command: "notification", 110 | menu: { 111 | title: "Choose the Action", 112 | reminder: "🗓 Set Up Daily Report Reminders", 113 | alarm: "⏰ Crypto price alert", 114 | proposals: "📝 Proposals", 115 | }, 116 | reminderMenu: { 117 | title: "Choose the action", 118 | networks: "🛰 Networks", 119 | time: "🕕 Time", 120 | timezone: "🌎 Timezone", 121 | enabled: " 🔔 Enabled", 122 | disabled: "🔕 Disabled", 123 | chooseNetwork: "Please, choose network", 124 | chooseReminderTime: "Please, choose alarm time", 125 | fillCountry: "Fill in the country to detect your timezone", 126 | incorrectCountry: 127 | "Incorrect name country, please fill full country name or country code, if you don't want enter, put in /reset", 128 | chooseTimezone: "Please, Choose timezone", 129 | }, 130 | alarmMenu: { 131 | title: "Choose the action", 132 | add: "➕ Add alert", 133 | delete: "➖ Delete alert", 134 | list: "📃 Alerts list", 135 | enabled: " 🔔 Enabled", 136 | disabled: "🔕 Disabled", 137 | chooseNetworkTitle: "Choose Network", 138 | alarmSaved: "Alarm saved", 139 | alarmRemoved: "Alarm removed", 140 | incorrectNumber: 141 | "Number should be without $, if you don't want enter, fill in /reset", 142 | incorrectPrice: 143 | "Price is incorrect, if you don't want enter, fill in /reset", 144 | positivePrice: 145 | "Price should be positive, if you don't want enter, fill in /reset", 146 | addMorePrice: "Add one more price alarm", 147 | coinPrice: "%{name} price - %{price}", 148 | removeWalletTitle: "Choose the wallet that you want to remove", 149 | alarmList: "You have alarms(s): \n", 150 | alarmListItem: "%{networkName} at price(s) — %{prices}$ \n", 151 | alarmPriceInput: 152 | "Current %{networkName} price - $%{price} is, please put alarm price", 153 | }, 154 | proposalMenu: { 155 | title: "Choose the network(s)", 156 | }, 157 | }, 158 | resources: { 159 | command: "resources", 160 | menu: { 161 | title: "Choose network", 162 | resourceItem: "🔘 %{item}: %{link} \n", 163 | }, 164 | }, 165 | statistic: { 166 | command: "statistic", 167 | menu: { 168 | title: "Choose the Network", 169 | statisticDescription: 170 | "%{denom} Price: 🔥 💲%{price} 🔥 \n\n" + 171 | "💸 APR - %{apr} \n\n" + 172 | "📊 Inflation - %{inflation}% \n\n" + 173 | "🔝 Height - %{height} \n\n" + 174 | "🌐 Community Pool - %{communityPool} \n\n" + 175 | "Price change:\n\n" + 176 | "▫️ For today - %{firstPercent}% \n\n" + 177 | "▫️ In 7 days - %{seventhPercent}% \n\n" + 178 | "▫️ In 14 days - %{fourteenthPercent}% \n\n" + 179 | "▫️ In 30 days - %{thirtyPercent}% \n\n" + 180 | "Tokenomics: \n\n" + 181 | "🔒 Bonded - %{bonded} \n\n" + 182 | "🔐 Unbonding - %{unbonding} \n\n" + 183 | "🔓 Unbonded - %{unbonded} \n", 184 | unknownPrice: "%{networkName} - <price is unknown>", 185 | }, 186 | }, 187 | proposals: { 188 | command: "proposals", 189 | menu: { 190 | title: "Choose the Network", 191 | proposalDescriptionTitle: "Proposal %{number} \n\n", 192 | proposalDescription: "%{title} \n\n" + "%{description} \n\n", 193 | proposalDescriptionLink: 194 | "https://wallet.keplr.app/chains/%{keplrId}/proposals/%{proposalId} \n\n", 195 | noProposal: "🙅‍♂️ No active proposal", 196 | }, 197 | }, 198 | cron: { 199 | reminderTitle: "⏰⏰⏰ Price reminder at time %{date} ⏰⏰⏰ \n\n", 200 | reminderItem: "%{networkName} — $%{price} \n", 201 | alarmTitle: "🚨🚨🚨 Alarm❗ %{networkName} price — $%{price} 🚨🚨🚨", 202 | newProposal: 203 | "🚨 New proposal from %{networkName}❗🚨 \n\n" + 204 | "%{title} \n\n" + 205 | "%{description}", 206 | }, 207 | support: { 208 | command: "support", 209 | title: 210 | "I'll be very pleased if you support me with some donation ❤️.\n" + 211 | "I'll continue develop useful tools for our Cosmos ecosystem. \n" + 212 | "ATOM: cosmos1te6z5n9mpz27wc2yyfrssdc88pztca9mzarmgd \n" + 213 | "ETH: 0xB2Df04F4536B99666E3968d14761bb890d002Df3 \n" + 214 | "BSC: 0xB2Df04F4536B99666E3968d14761bb890d002Df3", 215 | }, 216 | about: { 217 | command: "about", 218 | title: 219 | "I hope this bot you is useful for you. \n\n" + 220 | "If you have question or proposals how I can improve it " + 221 | "you can always reach me out @ReactiveGuy. \n" + 222 | "Here is full article " + 223 | "about the bot implementation and Github", 224 | }, 225 | reset: { 226 | command: "reset", 227 | title: "Step reseted", 228 | }, 229 | unknownRoute: 230 | "Sorry, I don't understand you, please use /help to get the full command list", 231 | addMoreQuestion: "Do you want add more?", 232 | back: "<< Go back", 233 | }; 234 | -------------------------------------------------------------------------------- /bot/src/constants/country.ts: -------------------------------------------------------------------------------- 1 | export const countries = [ 2 | { name: "Afghanistan", code: "AF" }, 3 | { name: "land Islands", code: "AX" }, 4 | { name: "Albania", code: "AL" }, 5 | { name: "Algeria", code: "DZ" }, 6 | { name: "American Samoa", code: "AS" }, 7 | { name: "AndorrA", code: "AD" }, 8 | { name: "Angola", code: "AO" }, 9 | { name: "Anguilla", code: "AI" }, 10 | { name: "Antarctica", code: "AQ" }, 11 | { name: "Antigua and Barbuda", code: "AG" }, 12 | { name: "Argentina", code: "AR" }, 13 | { name: "Armenia", code: "AM" }, 14 | { name: "Aruba", code: "AW" }, 15 | { name: "Australia", code: "AU" }, 16 | { name: "Austria", code: "AT" }, 17 | { name: "Azerbaijan", code: "AZ" }, 18 | { name: "Bahamas", code: "BS" }, 19 | { name: "Bahrain", code: "BH" }, 20 | { name: "Bangladesh", code: "BD" }, 21 | { name: "Barbados", code: "BB" }, 22 | { name: "Belarus", code: "BY" }, 23 | { name: "Belgium", code: "BE" }, 24 | { name: "Belize", code: "BZ" }, 25 | { name: "Benin", code: "BJ" }, 26 | { name: "Bermuda", code: "BM" }, 27 | { name: "Bhutan", code: "BT" }, 28 | { name: "Bolivia", code: "BO" }, 29 | { name: "Bosnia and Herzegovina", code: "BA" }, 30 | { name: "Botswana", code: "BW" }, 31 | { name: "Bouvet Island", code: "BV" }, 32 | { name: "Brazil", code: "BR" }, 33 | { name: "British Indian Ocean Territory", code: "IO" }, 34 | { name: "Brunei Darussalam", code: "BN" }, 35 | { name: "Bulgaria", code: "BG" }, 36 | { name: "Burkina Faso", code: "BF" }, 37 | { name: "Burundi", code: "BI" }, 38 | { name: "Cambodia", code: "KH" }, 39 | { name: "Cameroon", code: "CM" }, 40 | { name: "Canada", code: "CA" }, 41 | { name: "Cape Verde", code: "CV" }, 42 | { name: "Cayman Islands", code: "KY" }, 43 | { name: "Central African Republic", code: "CF" }, 44 | { name: "Chad", code: "TD" }, 45 | { name: "Chile", code: "CL" }, 46 | { name: "China", code: "CN" }, 47 | { name: "Christmas Island", code: "CX" }, 48 | { name: "Cocos (Keeling) Islands", code: "CC" }, 49 | { name: "Colombia", code: "CO" }, 50 | { name: "Comoros", code: "KM" }, 51 | { name: "Congo", code: "CG" }, 52 | { name: "Congo, The Democratic Republic of the", code: "CD" }, 53 | { name: "Cook Islands", code: "CK" }, 54 | { name: "Costa Rica", code: "CR" }, 55 | { name: "Cote D'Ivoire", code: "CI" }, 56 | { name: "Croatia", code: "HR" }, 57 | { name: "Cuba", code: "CU" }, 58 | { name: "Cyprus", code: "CY" }, 59 | { name: "Czech Republic", code: "CZ" }, 60 | { name: "Denmark", code: "DK" }, 61 | { name: "Djibouti", code: "DJ" }, 62 | { name: "Dominica", code: "DM" }, 63 | { name: "Dominican Republic", code: "DO" }, 64 | { name: "Ecuador", code: "EC" }, 65 | { name: "Egypt", code: "EG" }, 66 | { name: "El Salvador", code: "SV" }, 67 | { name: "Equatorial Guinea", code: "GQ" }, 68 | { name: "Eritrea", code: "ER" }, 69 | { name: "Estonia", code: "EE" }, 70 | { name: "Ethiopia", code: "ET" }, 71 | { name: "Falkland Islands (Malvinas)", code: "FK" }, 72 | { name: "Faroe Islands", code: "FO" }, 73 | { name: "Fiji", code: "FJ" }, 74 | { name: "Finland", code: "FI" }, 75 | { name: "France", code: "FR" }, 76 | { name: "French Guiana", code: "GF" }, 77 | { name: "French Polynesia", code: "PF" }, 78 | { name: "French Southern Territories", code: "TF" }, 79 | { name: "Gabon", code: "GA" }, 80 | { name: "Gambia", code: "GM" }, 81 | { name: "Georgia", code: "GE" }, 82 | { name: "Germany", code: "DE" }, 83 | { name: "Ghana", code: "GH" }, 84 | { name: "Gibraltar", code: "GI" }, 85 | { name: "Greece", code: "GR" }, 86 | { name: "Greenland", code: "GL" }, 87 | { name: "Grenada", code: "GD" }, 88 | { name: "Guadeloupe", code: "GP" }, 89 | { name: "Guam", code: "GU" }, 90 | { name: "Guatemala", code: "GT" }, 91 | { name: "Guernsey", code: "GG" }, 92 | { name: "Guinea", code: "GN" }, 93 | { name: "Guinea-Bissau", code: "GW" }, 94 | { name: "Guyana", code: "GY" }, 95 | { name: "Haiti", code: "HT" }, 96 | { name: "Heard Island and Mcdonald Islands", code: "HM" }, 97 | { name: "Holy See (Vatican City State)", code: "VA" }, 98 | { name: "Honduras", code: "HN" }, 99 | { name: "Hong Kong", code: "HK" }, 100 | { name: "Hungary", code: "HU" }, 101 | { name: "Iceland", code: "IS" }, 102 | { name: "India", code: "IN" }, 103 | { name: "Indonesia", code: "ID" }, 104 | { name: "Iran, Islamic Republic Of", code: "IR" }, 105 | { name: "Iraq", code: "IQ" }, 106 | { name: "Ireland", code: "IE" }, 107 | { name: "Isle of Man", code: "IM" }, 108 | { name: "Israel", code: "IL" }, 109 | { name: "Italy", code: "IT" }, 110 | { name: "Jamaica", code: "JM" }, 111 | { name: "Japan", code: "JP" }, 112 | { name: "Jersey", code: "JE" }, 113 | { name: "Jordan", code: "JO" }, 114 | { name: "Kazakhstan", code: "KZ" }, 115 | { name: "Kenya", code: "KE" }, 116 | { name: "Kiribati", code: "KI" }, 117 | { name: "Korea, Democratic People'S Republic of", code: "KP" }, 118 | { name: "Korea, Republic of", code: "KR" }, 119 | { name: "Kuwait", code: "KW" }, 120 | { name: "Kyrgyzstan", code: "KG" }, 121 | { name: "Lao People'S Democratic Republic", code: "LA" }, 122 | { name: "Latvia", code: "LV" }, 123 | { name: "Lebanon", code: "LB" }, 124 | { name: "Lesotho", code: "LS" }, 125 | { name: "Liberia", code: "LR" }, 126 | { name: "Libyan Arab Jamahiriya", code: "LY" }, 127 | { name: "Liechtenstein", code: "LI" }, 128 | { name: "Lithuania", code: "LT" }, 129 | { name: "Luxembourg", code: "LU" }, 130 | { name: "Macao", code: "MO" }, 131 | { name: "Macedonia, The Former Yugoslav Republic of", code: "MK" }, 132 | { name: "Madagascar", code: "MG" }, 133 | { name: "Malawi", code: "MW" }, 134 | { name: "Malaysia", code: "MY" }, 135 | { name: "Maldives", code: "MV" }, 136 | { name: "Mali", code: "ML" }, 137 | { name: "Malta", code: "MT" }, 138 | { name: "Marshall Islands", code: "MH" }, 139 | { name: "Martinique", code: "MQ" }, 140 | { name: "Mauritania", code: "MR" }, 141 | { name: "Mauritius", code: "MU" }, 142 | { name: "Mayotte", code: "YT" }, 143 | { name: "Mexico", code: "MX" }, 144 | { name: "Micronesia, Federated States of", code: "FM" }, 145 | { name: "Moldova, Republic of", code: "MD" }, 146 | { name: "Monaco", code: "MC" }, 147 | { name: "Mongolia", code: "MN" }, 148 | { name: "Montenegro", code: "ME" }, 149 | { name: "Montserrat", code: "MS" }, 150 | { name: "Morocco", code: "MA" }, 151 | { name: "Mozambique", code: "MZ" }, 152 | { name: "Myanmar", code: "MM" }, 153 | { name: "Namibia", code: "NA" }, 154 | { name: "Nauru", code: "NR" }, 155 | { name: "Nepal", code: "NP" }, 156 | { name: "Netherlands", code: "NL" }, 157 | { name: "Netherlands Antilles", code: "AN" }, 158 | { name: "New Caledonia", code: "NC" }, 159 | { name: "New Zealand", code: "NZ" }, 160 | { name: "Nicaragua", code: "NI" }, 161 | { name: "Niger", code: "NE" }, 162 | { name: "Nigeria", code: "NG" }, 163 | { name: "Niue", code: "NU" }, 164 | { name: "Norfolk Island", code: "NF" }, 165 | { name: "Northern Mariana Islands", code: "MP" }, 166 | { name: "Norway", code: "NO" }, 167 | { name: "Oman", code: "OM" }, 168 | { name: "Pakistan", code: "PK" }, 169 | { name: "Palau", code: "PW" }, 170 | { name: "Palestinian Territory, Occupied", code: "PS" }, 171 | { name: "Panama", code: "PA" }, 172 | { name: "Papua New Guinea", code: "PG" }, 173 | { name: "Paraguay", code: "PY" }, 174 | { name: "Peru", code: "PE" }, 175 | { name: "Philippines", code: "PH" }, 176 | { name: "Pitcairn", code: "PN" }, 177 | { name: "Poland", code: "PL" }, 178 | { name: "Portugal", code: "PT" }, 179 | { name: "Puerto Rico", code: "PR" }, 180 | { name: "Qatar", code: "QA" }, 181 | { name: "Reunion", code: "RE" }, 182 | { name: "Romania", code: "RO" }, 183 | { name: "Russian Federation", code: "RU" }, 184 | { name: "RWANDA", code: "RW" }, 185 | { name: "Saint Helena", code: "SH" }, 186 | { name: "Saint Kitts and Nevis", code: "KN" }, 187 | { name: "Saint Lucia", code: "LC" }, 188 | { name: "Saint Pierre and Miquelon", code: "PM" }, 189 | { name: "Saint Vincent and the Grenadines", code: "VC" }, 190 | { name: "Samoa", code: "WS" }, 191 | { name: "San Marino", code: "SM" }, 192 | { name: "Sao Tome and Principe", code: "ST" }, 193 | { name: "Saudi Arabia", code: "SA" }, 194 | { name: "Senegal", code: "SN" }, 195 | { name: "Serbia", code: "RS" }, 196 | { name: "Seychelles", code: "SC" }, 197 | { name: "Sierra Leone", code: "SL" }, 198 | { name: "Singapore", code: "SG" }, 199 | { name: "Slovakia", code: "SK" }, 200 | { name: "Slovenia", code: "SI" }, 201 | { name: "Solomon Islands", code: "SB" }, 202 | { name: "Somalia", code: "SO" }, 203 | { name: "South Africa", code: "ZA" }, 204 | { name: "South Georgia and the South Sandwich Islands", code: "GS" }, 205 | { name: "Spain", code: "ES" }, 206 | { name: "Sri Lanka", code: "LK" }, 207 | { name: "Sudan", code: "SD" }, 208 | { name: "Suriname", code: "SR" }, 209 | { name: "Svalbard and Jan Mayen", code: "SJ" }, 210 | { name: "Swaziland", code: "SZ" }, 211 | { name: "Sweden", code: "SE" }, 212 | { name: "Switzerland", code: "CH" }, 213 | { name: "Syrian Arab Republic", code: "SY" }, 214 | { name: "Taiwan, Province of China", code: "TW" }, 215 | { name: "Tajikistan", code: "TJ" }, 216 | { name: "Tanzania, United Republic of", code: "TZ" }, 217 | { name: "Thailand", code: "TH" }, 218 | { name: "Timor-Leste", code: "TL" }, 219 | { name: "Togo", code: "TG" }, 220 | { name: "Tokelau", code: "TK" }, 221 | { name: "Tonga", code: "TO" }, 222 | { name: "Trinidad and Tobago", code: "TT" }, 223 | { name: "Tunisia", code: "TN" }, 224 | { name: "Turkey", code: "TR" }, 225 | { name: "Turkmenistan", code: "TM" }, 226 | { name: "Turks and Caicos Islands", code: "TC" }, 227 | { name: "Tuvalu", code: "TV" }, 228 | { name: "Uganda", code: "UG" }, 229 | { name: "Ukraine", code: "UA" }, 230 | { name: "United Arab Emirates", code: "AE" }, 231 | { name: "United Kingdom", code: "GB" }, 232 | { name: "United States", code: "US" }, 233 | { name: "United States Minor Outlying Islands", code: "UM" }, 234 | { name: "Uruguay", code: "UY" }, 235 | { name: "Uzbekistan", code: "UZ" }, 236 | { name: "Vanuatu", code: "VU" }, 237 | { name: "Venezuela", code: "VE" }, 238 | { name: "Viet Nam", code: "VN" }, 239 | { name: "Virgin Islands, British", code: "VG" }, 240 | { name: "Virgin Islands, U.S.", code: "VI" }, 241 | { name: "Wallis and Futuna", code: "WF" }, 242 | { name: "Western Sahara", code: "EH" }, 243 | { name: "Yemen", code: "YE" }, 244 | { name: "Zambia", code: "ZM" }, 245 | { name: "Zimbabwe", code: "ZW" }, 246 | ]; 247 | --------------------------------------------------------------------------------