├── .env.example ├── .eslintrc ├── .gitignore ├── README.md ├── bot.js ├── config.js ├── constants.js ├── data ├── addresses.json └── excludedAddresses.json ├── handlers ├── addAddress.js ├── changeListPage.js ├── clearExceptions.js ├── deleteAddress.js ├── editExceptions.js ├── editMinAmount.js ├── editTag.js ├── editUndoKeyboard.js ├── handleEdit.js ├── handleExceptions.js ├── handleMinAmount.js ├── openAddress.js ├── openAddressNotifications.js ├── resetMinAmount.js ├── sendAddressesList.js ├── sendWelcome.js ├── turnNotifications.js └── undoAddressDelete.js ├── i18n.js ├── index.js ├── keyboards ├── addressMenu.js ├── addressNotifications.js ├── addressesList.js ├── backToAddress.js ├── backToNotifications.js ├── editExceptions.js ├── editMinAmount.js ├── openAddress.js └── undoDelete.js ├── locales └── en.yaml ├── middlewares ├── auth.js ├── blockDetection.js └── session.js ├── migrations └── notifyMigrate.js ├── models ├── address.js ├── counters.js ├── sessions.js └── user.js ├── monitors ├── addresses.js ├── pool.js ├── scan.js └── scanPrice.js ├── package-lock.json ├── package.json ├── repositories ├── address.js └── user.js ├── scenes ├── editExceptions.js ├── editMinAmount.js └── editTag.js ├── services ├── pagination.js ├── session.js ├── ton.js └── transactionProcessor.js ├── syncIndexes.js ├── utils ├── big.js ├── escapeHTML.js ├── formatAddress.js ├── formatBalance.js ├── formatBigNumberStr.js ├── formatNotificationsMenu.js ├── formatTag.js ├── formatTransactionPrice.js ├── formatTransactionValue.js ├── log.js └── sleep.js └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | BOT_TOKEN = 1958... 2 | MONGODB_URI = mongodb://127.0.0.1:27017/ton-notify-dev 3 | TON_NODE_URL = https://testnet.toncenter.com/api/v2/jsonRPC 4 | TON_INDEX_URL = https://testnet.toncenter.com/api/index 5 | TON_NODE_API_KEY = ... 6 | TON_INDEX_API_KEY = ... 7 | NODE_ENV = development 8 | SYNCHRONIZER_INTERVAL = 5 9 | NOTIFICATIONS_CHANNEL_ID = -100... 10 | MIN_TRANSACTION_AMOUNT = 50000 11 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "rules": { 4 | "arrow-body-style": "off", 5 | "class-methods-use-this": "off", 6 | "linebreak-style": "off", 7 | "operator-linebreak": "off", 8 | "semi": ["error", "never"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | .idea 107 | 108 | # Lock file 109 | pnpm-lock.yaml -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ton-notify-bot 2 | 🚀 The bot can notify users about new events of TON addresses. 3 | -------------------------------------------------------------------------------- /bot.js: -------------------------------------------------------------------------------- 1 | const { Telegraf, Composer, Stage } = require('telegraf') 2 | 3 | const i18n = require('./i18n') 4 | 5 | const config = require('./config') 6 | const log = require('./utils/log') 7 | 8 | const editTagScene = require('./scenes/editTag') 9 | const editMinAmountScene = require('./scenes/editMinAmount') 10 | const editExceptions = require('./scenes/editExceptions') 11 | 12 | const sendWelcome = require('./handlers/sendWelcome') 13 | const addAddress = require('./handlers/addAddress') 14 | const sendAddressesList = require('./handlers/sendAddressesList') 15 | const changeListPage = require('./handlers/changeListPage') 16 | const openAddress = require('./handlers/openAddress') 17 | const handleEdit = require('./handlers/handleEdit') 18 | const handleMinAmount = require('./handlers/handleMinAmount') 19 | const handleExceptions = require('./handlers/handleExceptions') 20 | const openAddressNotifications = require('./handlers/openAddressNotifications') 21 | const turnNotifications = require('./handlers/turnNotifications') 22 | const deleteAddress = require('./handlers/deleteAddress') 23 | const undoAddressDelete = require('./handlers/undoAddressDelete') 24 | const editUndoKeyboard = require('./handlers/editUndoKeyboard') 25 | 26 | const session = require('./middlewares/session') 27 | const blockDetection = require('./middlewares/blockDetection') 28 | const auth = require('./middlewares/auth') 29 | 30 | const bot = new Telegraf(config.get('bot.token')) 31 | 32 | const payloadRegex = /^(\w|-){48}/ 33 | 34 | const stage = new Stage([editTagScene, editMinAmountScene, editExceptions]) 35 | 36 | stage.start( 37 | Composer.optional( 38 | ({ startPayload }) => startPayload && payloadRegex.test(startPayload), 39 | Composer.tap(addAddress), 40 | Stage.leave(), 41 | ), 42 | ) 43 | 44 | stage.start(Composer.tap(sendWelcome), Stage.leave()) 45 | 46 | stage.hears(/(^(\w|-){48}(:.+)*$)/, Composer.tap(addAddress), Stage.leave()) 47 | 48 | stage.command('list', Composer.tap(sendAddressesList), Stage.leave()) 49 | 50 | bot.use(session, i18n) 51 | 52 | bot.use(blockDetection, auth) 53 | 54 | bot.use(stage) 55 | 56 | bot.catch((err) => log.error(`Handle update error: ${err}`)) 57 | 58 | bot.on( 59 | 'callback_query', 60 | Composer.tap((ctx) => ctx.answerCbQuery()), 61 | ) 62 | 63 | bot.action(/(?<=^list_)\d+/, changeListPage) 64 | 65 | bot.action(/(?<=^open_).+/, openAddress) 66 | 67 | bot.action(/(?<=^edit_).+/, handleEdit) 68 | 69 | bot.action(/(?<=^notify_exceptions_).+$/, handleExceptions) 70 | 71 | bot.action(/(?<=^notify_min_amout_).+$/, handleMinAmount) 72 | 73 | bot.action(/(?<=^notify_)(.+)_(on|off)$/, turnNotifications) 74 | 75 | bot.action(/(?<=^notify_).+/, openAddressNotifications) 76 | 77 | bot.action(/(?<=^delete_).+/, deleteAddress) 78 | 79 | bot.action(/(?<=^undo_).+/, Composer.tap(undoAddressDelete), openAddress) 80 | 81 | bot.action(/(?<=^open-list-)(.+)-(\d+)$/, Composer.tap(editUndoKeyboard), sendAddressesList) 82 | 83 | module.exports = (options) => bot.launch(options).then(() => log.info('bot was launched')) 84 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | require('dotenv') 2 | .config({ path: __dirname + '/.env' }) 3 | 4 | const convict = require('convict') 5 | 6 | const config = convict({ 7 | env: { 8 | doc: 'The application environment.', 9 | format: ['production', 'development'], 10 | default: 'development', 11 | env: 'NODE_ENV', 12 | }, 13 | db: { 14 | doc: 'Mongo connection url.', 15 | format: String, 16 | default: 'mongodb://localhost:27017/ton-notify', 17 | env: 'MONGODB_URI', 18 | }, 19 | bot: { 20 | token: { 21 | doc: 'Telegram Bot token.', 22 | format: String, 23 | default: '', 24 | env: 'BOT_TOKEN', 25 | }, 26 | notifications_channel_id: { 27 | doc: 'Notifications channel ID.', 28 | format: String, 29 | default: '', 30 | env: 'NOTIFICATIONS_CHANNEL_ID', 31 | }, 32 | }, 33 | min_transaction_amount: { 34 | doc: 'Minimum amount of a transaction to send a notification to the channel', 35 | format: Number, 36 | default: 1000, 37 | env: 'MIN_TRANSACTION_AMOUNT', 38 | }, 39 | ton: { 40 | node: { 41 | doc: 'TON Node API URL.', 42 | format: String, 43 | default: 'https://testnet.toncenter.com/api/v2/jsonRPC', 44 | env: 'TON_NODE_URL', 45 | }, 46 | index: { 47 | doc: 'TON Index API URL.', 48 | format: String, 49 | default: 'https://testnet.toncenter.com/api/index', 50 | env: 'TON_INDEX_URL', 51 | }, 52 | node_key: { 53 | doc: 'TON Node API Key.', 54 | format: String, 55 | default: '', 56 | env: 'TON_NODE_API_KEY', 57 | }, 58 | index_key: { 59 | doc: 'TON Index API Key.', 60 | format: String, 61 | default: '', 62 | env: 'TON_INDEX_API_KEY', 63 | }, 64 | }, 65 | synchronizer: { 66 | interval: { 67 | doc: 'Sync interval in seconds (expected block generation time).', 68 | format: Number, 69 | default: 5, 70 | env: 'SYNCHRONIZER_INTERVAL', 71 | }, 72 | }, 73 | }) 74 | 75 | config.validate({ allowed: 'strict' }) 76 | 77 | module.exports = config 78 | -------------------------------------------------------------------------------- /constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | PAGINATION_LIMIT: 10, 3 | } 4 | -------------------------------------------------------------------------------- /data/addresses.json: -------------------------------------------------------------------------------- 1 | { 2 | "EQDCH6vT0MvVp0bBYNjoONpkgb51NMPNOJXFQWG54XoIAs5Y": "CAT Services", 3 | "EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N": "TON Foundation", 4 | "EQAhE3sLxHZpsyZ_HecMuwzvXHKLjYx4kEUehhOy2JmCcHCT": "TON Ecosystem Reserve", 5 | "Ef8zMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzM0vF": "Elector Contract", 6 | "Ef9VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVbxn": "Config Contract", 7 | "Ef8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAU": "System", 8 | "Ef80UXx731GHxVr0-LYf3DIViMerdo3uJLAG3ykQZFjXz2kW": "Log tests Contract", 9 | 10 | "EQCtiv7PrMJImWiF2L5oJCgPnzp-VML2CAt5cbn1VsKAxLiE": "CryptoBot", 11 | "EQDd3NPNrWCvTA1pOJ9WetUdDCY_pJaNZVq0JMaara-TIp90": "Wallet Bot", 12 | "EQBDanbCeUqI4_v-xrnAN0_I2wRvEIaLg1Qg2ZN5c6Zl1KOh": "Wallet Bot", 13 | "EQBfAN7LfaUYgXZNw5Wc7GBgkEX2yhuJ5ka95J1JJwXXf4a8": "OKX", 14 | "EQCzFTXpNNsFu8IgJnRnkDyBCL2ry8KgZYiDi3Jt31ie8EIQ": "FTX", 15 | "EQBX63RAdgShn34EAFMV73Cut7Z15lUZd1hnVva68SEl7sxi": "MEXC", 16 | "EQB5lISMH8vLxXpqWph7ZutCS4tU4QdZtrUUpmtgDCsO73JR": "EXMO", 17 | "EQCNGVeTuq2aCMRtw1OuvpmTQdq9B3IblyXxnhirw9ENkhLa": "EXMO Cold Storage 1", 18 | "EQAmq4rnY6OnwwZ9iCt7Ac1dNyVMuHaPV7akfAACjv_HuO5H": "EXMO Cold Storage 2", 19 | "EQABMMdzRuntgt9nfRB61qd1wR-cGPagXA3ReQazVYUNrT7p": "EXMO Deposit", 20 | 21 | "Ef9NXAIQs12t2qIZ-sRZ26D977H65Ol6DQeXc5_gUNaUys5r": "BSC Bridge", 22 | "EQAHI1vGuw7d4WG-CtfDrWqEPNtmUuKjKFEFeJmZaqqfWTvW": "BSC Bridge Collector", 23 | "Ef8OvX_5ynDgbp4iqJIvWudSEanWo0qAlOjhWHtga9u2YjVp": "BSC Bridge Governance", 24 | "Ef_dJMSh8riPi3BTUTtcxsWjG8RLKnLctNjAM4rw8NN-xWdr": "ETH Bridge", 25 | "EQCuzvIOXLjH2tv35gY4tzhIvXCqZWDuK9kUhFGXKLImgxT5": "ETH Bridge Collector", 26 | "Ef87m7_QrVM4uXAPCDM4DuF9Rj5Rwa5nHubwiQG96JmyAjQY": "ETH Bridge Governance", 27 | "Ef_P2CJw784O1qVd8Qbn8RCQc4EgxAs8Ra-M3bDhZn3OfzRb": "Bridge Oracle 0", 28 | "Ef8DfObDUrNqz66pr_7xMbUYckUFbIIvRh1FSNeVSLWrvo1M": "Bridge Oracle 1", 29 | "Ef8JKqx4I-XECLuVhTqeY1WMgbgTp8Ld3mzN-JUogBF4ZEW-": "Bridge Oracle 2", 30 | "Ef8voAFh-ByCeKD3SZhjMNzioqCmDOK6S6IaeefTwYmRhgsn": "Bridge Oracle 3", 31 | "Ef_uJVTTToU8b3o7-Jr5pcUqenxWzDNYpyklvhl73KSIA17M": "Bridge Oracle 4", 32 | "Ef93olLWqh1OuBSTOnJKWZ4NwxNq_ELK55_h_laNPVwxcEro": "Bridge Oracle 5", 33 | "Ef_iUPZdKLOCrqcNpDuFGNEmiuBwMB18TBXNjDimewpDExgn": "Bridge Oracle 6", 34 | "Ef_tTGGToGmONePskH_Y6ZG-QLV9Kcg5DIXeKwBvCX4YifKa": "Bridge Oracle 7", 35 | "Ef94L53akPw-4gOk2uQOenUyDYLOaif2g2uRoiu1nv0cWYMC": "Bridge Oracle 8", 36 | 37 | "Ef-VAFf1Wd3fXd-mQhDw5lNsVdIZv2_H1yhbdzXCFfIe9p95": "CAT Validator 1", 38 | "Ef-p4N7wkBQcce3Awcm06a1EV2VsFPYR7GtRczlYP0G2C1Pm": "CAT Validator 2", 39 | "Ef86ziqX4uPh-ZcrOK8bWszUzfNhHg_SPnvf9CQOnElFINEE": "CAT Validator 3", 40 | "Ef8vk8p6nogM_JKMhpqXnffFrzikOzGjIUNLP8sdIasIb8DV": "CAT Validator 4", 41 | 42 | "Ef9wm_whwjPFe7H4jvP-ODhluiZFm0Tb2Gj-67zqS31hCaWC": "CAT Staking Pool 1", 43 | "Ef-BHO0nH49EnLUetZIAkLkssgCyDwcXBnbp22--naUWz8VY": "CAT Staking Pool 2", 44 | "Ef8iu8EiNOP2MczVvHseFi-CrGO1C4v6MkSSOgVZcESNGfT7": "CAT Staking Pool 3", 45 | "Ef8AeKBMQKW-PB8-RDeyJQSsxQQr5oEwbQeBEwE2BKDiFA_U": "CAT Staking Pool 4", 46 | "Ef-DxWkExr12iOSi0vJfT5TKCUG9W3-eWInm1yT5oEJISJkl": "CAT Staking Pool 5", 47 | "Ef8z0gek-K888pl61tyErw96TfnthjV9lZ7UDVFXlad9HEGS": "CAT Staking Pool 6", 48 | "Ef98be2ASo4xA_t2Q2VIH0cdpYKylDyfhL3nL7Byjz180VaU": "CAT Staking Pool 7", 49 | "Ef8h5pd9_ZuWJOlilBH6a_LOACxl_R6DJbWHhut7QLDLWSgf": "CAT Staking Pool 8", 50 | "Ef8gQpp7pKD9GzBrcr3ju9faPjEWHPerhZ4tFpSiDoDUINxn": "Very First Pool #1", 51 | "Ef_dodh2I8BjpvxJIrKG5owo5_C1RZlzkZtXC3HGLuNZE0Sa": "Very First Pool #2", 52 | "Ef9Qhifu_o6WiS3JzJEailMuDtlDqmy55eCGDJ5cYSTGD4BW": "Global Net AC Pool #1", 53 | "Ef-pcGkDL4qjf44vN8iD8-yrjs-wVWOi5LVQudbDKbukHQvb": "Global Net AC Pool #2", 54 | "Ef9IJWfn0qDrh4S8CBJsZUzAPrxQkH3C_JanVrQWjOZ5LndS": "Fastnet Pool A #1", 55 | "Ef__38zm-M_kTn6OgIs5DKVQy2qnjJIF50xZ203regWrI-yh": "Fastnet Pool A #2", 56 | "Ef8_6eSCSeJyCeLM7uCxYjeirtQ4Zo8OEp0W4HJYJY4IC0zK": "Fastnet Pool B #1", 57 | "Ef_nHR5IUKCBf_qHlIjCsUWuo6bbrh174f_aJfN6zIAnBF9n": "Fastnet Pool B #2", 58 | "EQAUgVXUBJC7c72oEaQGowLvWhnf-nKghL7zBX8FSqrSXkp4": "TonStake Deposit", 59 | "Ef9qXoe6qX5kboeTbXXdxNOcqCA53Oi9LYsY66l4FuNLZRWx": "TonStake Validator", 60 | "EQAOCN7KlgGzTp6YjW6d_fm_ibJEUe0VwFyKNnZVzlL4Jda3": "TonStake Withdrawal", 61 | 62 | "EQAAFhjXzKuQ5N0c96nsdZQWATcJm909LYSaCAvWFxVJP80D": "Whales Pool", 63 | "EQBeNwQShukLyOWjKWZ0Oxoe5U3ET-ApQIWYeC4VLZ4tmeTm": "Whales Withdraw 1", 64 | "EQAQwQc4N7k_2q1ZQoTOi47_e5zyVCdEDrL8aCdi4UcTZef4": "Whales Withdraw 2", 65 | "EQDQA68_iHZrDEdkqjJpXcVqEM3qQC9u0w4nAhYJ4Ddsjttc": "Whales Withdraw 3", 66 | "EQCr1U4EVmSWpx2sunO1jhtHveatorjfDpttMCCkoa0JyD1P": "Whales Withdraw 4", 67 | "EQAB_3oC0MH1r4fz1kztk6Nhq9GFQnrBUgObzrhyAXjzzjrc": "Whales Withdraw 5", 68 | "EQCz4NlftqOJlDZFerutRjy8bpDuNkLuLFn9pHsnK-mfXuZ0": "TON Coin Pool Fund", 69 | "EQCUp88072pLUGNQCXXXDFJM3C5v9GXTjV7ou33Mj3r0Xv2W": "TON Coin Pool Rewards", 70 | "EQCUp88072pLUGNQCXXXDFJM3C5v9GXTjV7ou33Mj3r0Xv2W": "TON Coin Pool Withdraw 1", 71 | "EQAW6gzsWc-zqY7Z9rquxxeOA4Y6QMB09skcBXDnnuL3EK8L": "TON Coin Pool Withdraw 2", 72 | "EQDfbIxBNnGdCSYyKu0SxPtdzVrUBWlc73LXHia4fLt2Ia8i": "TON Universe Pool", 73 | "EQC2T_EwQ9Gvv7mKWdKP_fVqalEBBa5wWBn2I1BgeIcfS3Rb": "TON Universe Pool Withdraw 1", 74 | "EQCW1v0RwP31xMrDgUu0Qa18Z0YssSulsrhoDtZHpJ6p5s3J": "TON Universe Pool Withdraw 2", 75 | "EQA3hKVrpll-jVAtA1KjfpaMJ7YGg44M6DQuBdBX9G5xWxOS": "TON Pool", 76 | "EQA4FvvNgz9GThubbM4F4CQBZR26uP6UH13CoEODBAOXJAs0": "TON Pool Withdraw 1", 77 | "EQBN5uMyngPBKG2mwXmfqj3rZWP2tLMaY9NsmUOiJUZ3r9_f": "TON Pool withdraw 2", 78 | 79 | "EQCjk1hh952vWaE9bRguFkAhDAL5jj3xj9p0uPWrFBq_GEMS": "Getgems Marketplace", 80 | "EQBYTuYbLf8INxFtD8tQeNk5ZLy-nAX9ahQbG_yl1qQ-GEMS": "Getgems Sales", 81 | "EQDrLq-X6jKZNHAScgghh0h1iog3StK71zn8dcmrOj8jPWRA": "Disintar Marketplace", 82 | "EQA8sc2WlFb7VIpK_777JIX9vrYMz3FvPogd2OdhxR18e-Hg": "Rich Cats Fund", 83 | "EQDe1lrwD7d5ntSQuAPtQ2kUp_BSa8a4tNMMNak21zQXPSUa": "Rich Cats", 84 | "EQCJTkhd1W2wztkVNp_dsKBpv2SIoUWoIyzI7mQrbSrj_NSh": "TON Diamonds", 85 | "EQANKN8ZnM0OzYOENTkOEg7VVgFog5fBWdCtqQro1MRmU5_2": "Animals Red List NFT", 86 | "EQCRMjhmUVkjiYvj9d-Yotr4OT2ekPoKU9Hmq0EHTokRO6EK": "TON Earth", 87 | "EQASOUQL3Pok0fuUHvc_d0lY4K7Z9VnGE_VLot3GAeSNVeF5": "TON Guys NFT", 88 | 89 | "EQA5Pxp_EC9pTlxrvO59D1iqBqodajojullgf07ENKa22oSN": "TelePay", 90 | "EQBNaV2nd9-OGDmWXNip6SizsygyGrhd7CQ-hkJ6xm7b6NhC": "OTC Market", 91 | "EQCtBAFC02qgf2jKf6SrLNiRxTZaHut7pRpbXZoasOx2EnXs": "Tonometr Bot", 92 | "EQCR1zBW4DUjLwmq-CQqHVHuqYtqW-u_isDJ5SHQKhpL2wQV": "Morgenshtern", 93 | "EQCpDaCVY7Z0Ckt_aMoJ-9t2sANcwQFFChbi55uYXruzilrn": "Morgenshtern Private", 94 | "EQBd3OeCL1nRhTVSWlKFzYDO_u8_Xf-N1CmaiVfhKdh_wUzR": "Subscriptions" 95 | } 96 | -------------------------------------------------------------------------------- /data/excludedAddresses.json: -------------------------------------------------------------------------------- 1 | [ 2 | "EQCkR1cGmnsE45N4K0otPl5EnxnRakmGqeJUNua5fkWhales", 3 | "Ef8W1vCpA1tr9xr6QSXSxcVSdn1Sm7SYX_PCWQdClaWhales", 4 | "EQCOj4wEjXUR59Kq0KeXUJouY5iAcujkmwJGsYX7qPnITEAM", 5 | "Ef8Ogh4NObupw39Cx8xrjI_nJkdV7UVSKYZkxOVcFOiUTEAM", 6 | "Ef8zMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzM0vF" 7 | ] 8 | -------------------------------------------------------------------------------- /handlers/addAddress.js: -------------------------------------------------------------------------------- 1 | const { Extra } = require('telegraf') 2 | const AddressRepository = require('../repositories/address') 3 | const ton = require('../services/ton') 4 | const getOpenAddressKeyboard = require('../keyboards/openAddress') 5 | const getAddressMenuKeyboard = require('../keyboards/addressMenu') 6 | const formatAddress = require('../utils/formatAddress') 7 | const formatTag = require('../utils/formatTag') 8 | const { PAGINATION_LIMIT } = require('../constants') 9 | 10 | module.exports = async (ctx) => { 11 | const [address = ctx.startPayload, tag] = ctx.match ? ctx.match[0].split(':') : [] 12 | 13 | try { 14 | await ton.node.send('getAddressInformation', { address }) 15 | } catch (err) { 16 | return false 17 | } 18 | 19 | const addressRepository = new AddressRepository() 20 | const userId = ctx.from.id 21 | 22 | const addressAddedText = ctx.i18n.t('address.added', { 23 | address, 24 | tag, 25 | formatAddress, 26 | formatTag, 27 | }) 28 | 29 | try { 30 | const { _id } = await addressRepository.create({ 31 | user_id: userId, 32 | address, 33 | tag, 34 | }) 35 | 36 | return ctx.replyWithHTML( 37 | addressAddedText, 38 | Extra.markup(getOpenAddressKeyboard(_id, !!tag, ctx.i18n)).webPreview(false), 39 | ) 40 | } catch (err) { 41 | if (err.code !== 11000) { 42 | throw err 43 | } 44 | 45 | const { 46 | _id, 47 | notifications, 48 | tag: oldTag, 49 | is_deleted: isDeleted, 50 | } = await addressRepository.getOneByAddress(address, { user_id: userId }) 51 | 52 | if (isDeleted) { 53 | await addressRepository.updateOneById(_id, { 54 | is_deleted: false, 55 | tag: tag || '', 56 | }) 57 | 58 | return ctx.replyWithHTML( 59 | addressAddedText, 60 | Extra.markup(getOpenAddressKeyboard(_id, !!tag, ctx.i18n)).webPreview(false), 61 | ) 62 | } 63 | 64 | const addressPage = await addressRepository.getAddressPaginationPage( 65 | ctx.from.id, 66 | address, 67 | PAGINATION_LIMIT, 68 | ) 69 | 70 | return ctx.replyWithHTML( 71 | ctx.i18n.t('address.chosen', { 72 | address, 73 | formatAddress, 74 | tag: oldTag, 75 | }), 76 | Extra.markup( 77 | getAddressMenuKeyboard({ _id, notifications, address }, ctx.me, addressPage, ctx.i18n), 78 | ), 79 | ) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /handlers/changeListPage.js: -------------------------------------------------------------------------------- 1 | const { Extra } = require('telegraf') 2 | const AddressRepository = require('../repositories/address') 3 | const pagination = require('../services/pagination') 4 | const getAddressesListKeyboard = require('../keyboards/addressesList') 5 | const { PAGINATION_LIMIT } = require('../constants') 6 | 7 | module.exports = async (ctx) => { 8 | const [offsetStr] = ctx.match 9 | const offset = Number(offsetStr) || 0 10 | 11 | const addressRepository = new AddressRepository() 12 | // eslint-disable-next-line camelcase 13 | const { addresses, total_count } = await pagination( 14 | ctx.from.id, 15 | addressRepository, 16 | offset * PAGINATION_LIMIT, 17 | ) 18 | 19 | if (!addresses) { 20 | return false 21 | } 22 | 23 | const paginationOptions = { current: offset, total_count } 24 | return ctx.editMessageText( 25 | ctx.i18n.t('list.chooseAddress'), 26 | Extra.markup(getAddressesListKeyboard(addresses, paginationOptions)), 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /handlers/clearExceptions.js: -------------------------------------------------------------------------------- 1 | const { Extra } = require('telegraf') 2 | const AddressRepository = require('../repositories/address') 3 | const getAddressNotificationsKeyboard = require('../keyboards/addressNotifications') 4 | const getNotificationsMenu = require('../utils/formatNotificationsMenu') 5 | 6 | module.exports = async (ctx) => { 7 | const addressId = ctx.scene.state.address_id 8 | 9 | const addressRepository = new AddressRepository() 10 | await addressRepository.clearExceptions(addressId) 11 | 12 | const { _id, notifications } = await addressRepository.getOneById(addressId) 13 | 14 | ctx.scene.leave() 15 | 16 | return ctx.editMessageText( 17 | getNotificationsMenu(notifications, ctx.i18n), 18 | Extra.HTML() 19 | .webPreview(false) 20 | .markup(getAddressNotificationsKeyboard({ _id, notifications }, ctx.i18n)), 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /handlers/deleteAddress.js: -------------------------------------------------------------------------------- 1 | const { Extra } = require('telegraf') 2 | const AddressRepository = require('../repositories/address') 3 | const getUndoDeleteKeyboard = require('../keyboards/undoDelete') 4 | const formatAddress = require('../utils/formatAddress') 5 | const { PAGINATION_LIMIT } = require('../constants') 6 | 7 | module.exports = async (ctx) => { 8 | const [addressId] = ctx.match 9 | 10 | const addressRepository = new AddressRepository() 11 | const { _id, address, tag } = await addressRepository.getOneById(addressId) 12 | 13 | const addressPage = await addressRepository.getAddressPaginationPage( 14 | ctx.from.id, 15 | address, 16 | PAGINATION_LIMIT, 17 | ) 18 | 19 | const { addresses } = await addressRepository.paginationByUserId( 20 | ctx.from.id, 21 | addressPage * PAGINATION_LIMIT, 22 | PAGINATION_LIMIT, 23 | { is_deleted: false }, 24 | ) 25 | 26 | const returnPage = (addresses.length - 1) < 1 ? addressPage - 1 : addressPage 27 | 28 | await addressRepository.softDeleteOne(addressId) 29 | 30 | return ctx.editMessageText( 31 | ctx.i18n.t('address.deleted', { 32 | tag, 33 | address, 34 | formatAddress, 35 | }), 36 | Extra 37 | .webPreview(false) 38 | .HTML() 39 | .markup(getUndoDeleteKeyboard(_id, false, returnPage, ctx.i18n)), 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /handlers/editExceptions.js: -------------------------------------------------------------------------------- 1 | const { Extra } = require('telegraf') 2 | const AddressRepository = require('../repositories/address') 3 | const getAddressNotificationsKeyboard = require('../keyboards/addressNotifications') 4 | const getNotificationsMenu = require('../utils/formatNotificationsMenu') 5 | 6 | module.exports = async (ctx) => { 7 | const addressId = ctx.scene.state.address_id 8 | const rawListOfExceptions = ctx.message.text 9 | .split(',') 10 | .map( 11 | (v) => v.trim().replace(/\n/g, ''), 12 | ) 13 | 14 | const fullListOfExceptions = [...new Set(rawListOfExceptions)] 15 | 16 | const rawExceptions = [] 17 | const rawInclusion = [] 18 | 19 | const regexClearType = /^[+-]/; 20 | fullListOfExceptions.forEach((rawText) => { 21 | const textType = /^-/.test(rawText) ? '-' : '+' 22 | const text = rawText.replace(regexClearType, '').trim() 23 | 24 | if (textType === '-') { 25 | rawExceptions.push(text) 26 | } else { 27 | rawInclusion.push(text) 28 | } 29 | }) 30 | 31 | const exceptions = [...new Set(rawExceptions)] 32 | const inclusion = [...new Set(rawInclusion)] 33 | 34 | const addressRepository = new AddressRepository() 35 | await addressRepository.updateExceptions( 36 | addressId, 37 | exceptions, inclusion, 38 | ) 39 | 40 | const { _id, notifications } = await addressRepository.getOneById(addressId) 41 | 42 | ctx.scene.leave() 43 | 44 | return ctx.replyWithHTML( 45 | getNotificationsMenu(notifications, ctx.i18n), 46 | Extra.HTML() 47 | .webPreview(false) 48 | .markup(getAddressNotificationsKeyboard({ _id, notifications }, ctx.i18n)), 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /handlers/editMinAmount.js: -------------------------------------------------------------------------------- 1 | const { Extra } = require('telegraf') 2 | const { Big } = require('../utils/big') 3 | const AddressRepository = require('../repositories/address') 4 | const getBackToNotificationsKeyboard = require('../keyboards/backToNotifications') 5 | const getAddressNotificationsKeyboard = require('../keyboards/addressNotifications') 6 | const getNotificationsMenu = require('../utils/formatNotificationsMenu') 7 | const ton = require('../services/ton') 8 | const log = require('../utils/log') 9 | 10 | module.exports = async (ctx) => { 11 | const addressId = ctx.scene.state.address_id 12 | const minAmount = ctx.message.text 13 | 14 | try { 15 | // eslint-disable-next-line no-new 16 | const val = new Big(minAmount) 17 | if (val.lt(0)) { 18 | throw new Error('Value less than 0') 19 | } 20 | if (val.gt(5e9)) { 21 | throw new Error('Value more than 5 000 000 000') 22 | } 23 | const nanoAmount = ton.utils.toNano(val.toString()).toString(10) 24 | 25 | const addressRepository = new AddressRepository() 26 | await addressRepository.updateMinAmount(addressId, nanoAmount) 27 | 28 | const { _id, notifications } = await addressRepository.getOneById(addressId) 29 | 30 | ctx.scene.leave() 31 | 32 | return ctx.replyWithHTML( 33 | getNotificationsMenu(notifications, ctx.i18n), 34 | Extra.HTML() 35 | .webPreview(false) 36 | .markup(getAddressNotificationsKeyboard({ _id, notifications }, ctx.i18n)), 37 | ) 38 | } catch (error) { 39 | log.error(`Editing minimal amount error: ${error}`) 40 | return ctx.replyWithHTML( 41 | ctx.i18n.t('address.notifications.invalid'), 42 | Extra.HTML().webPreview(false).markup(getBackToNotificationsKeyboard(addressId, ctx.i18n)), 43 | ) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /handlers/editTag.js: -------------------------------------------------------------------------------- 1 | const { Extra } = require('telegraf') 2 | const AddressRepository = require('../repositories/address') 3 | const getAddressMenuKeyboard = require('../keyboards/addressMenu') 4 | const formatAddress = require('../utils/formatAddress') 5 | const { PAGINATION_LIMIT } = require('../constants') 6 | 7 | module.exports = async (ctx) => { 8 | const addressId = ctx.scene.state.address_id 9 | const tag = ctx.message.text 10 | 11 | const addressRepository = new AddressRepository() 12 | await addressRepository.updateTag(addressId, tag) 13 | const { _id, notifications, address } = await addressRepository.getOneById(addressId) 14 | 15 | const addressPage = await addressRepository.getAddressPaginationPage( 16 | ctx.from.id, 17 | address, 18 | PAGINATION_LIMIT, 19 | ) 20 | 21 | return ctx.replyWithHTML( 22 | ctx.i18n.t('address.chosen', { 23 | tag, 24 | address, 25 | formatAddress, 26 | }), 27 | Extra 28 | .webPreview(false) 29 | .markup( 30 | getAddressMenuKeyboard( 31 | { _id, notifications, address }, 32 | ctx.me, 33 | addressPage, 34 | ctx.i18n, 35 | ), 36 | ), 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /handlers/editUndoKeyboard.js: -------------------------------------------------------------------------------- 1 | const getUndoDeleteKeyboard = require('../keyboards/undoDelete') 2 | 3 | module.exports = (ctx) => { 4 | const addressId = ctx.match[1] 5 | 6 | return ctx.editMessageReplyMarkup(getUndoDeleteKeyboard(addressId, true, null, ctx.i18n)) 7 | } 8 | -------------------------------------------------------------------------------- /handlers/handleEdit.js: -------------------------------------------------------------------------------- 1 | const { Extra } = require('telegraf') 2 | const AddressRepository = require('../repositories/address') 3 | const getBackToAddressKeyboard = require('../keyboards/backToAddress') 4 | const formatAddress = require('../utils/formatAddress') 5 | 6 | module.exports = async (ctx) => { 7 | const [addressId] = ctx.match 8 | 9 | const addressRepository = new AddressRepository() 10 | const { _id, address, is_deleted: isDeleted } = await addressRepository.getOneById(addressId) 11 | 12 | if (isDeleted) { 13 | return false 14 | } 15 | 16 | await ctx.editMessageText( 17 | ctx.i18n.t('address.sendTag', { address, formatAddress }), 18 | Extra.HTML().webPreview(false).markup(getBackToAddressKeyboard(_id, ctx.i18n)), 19 | ) 20 | 21 | return ctx.scene.enter('editTag', { address_id: addressId }) 22 | } 23 | -------------------------------------------------------------------------------- /handlers/handleExceptions.js: -------------------------------------------------------------------------------- 1 | const { Extra } = require('telegraf') 2 | const AddressRepository = require('../repositories/address') 3 | const getEditExceptionsKeyboard = require('../keyboards/editExceptions') 4 | 5 | module.exports = async (ctx) => { 6 | const [addressId] = ctx.match 7 | 8 | const addressRepository = new AddressRepository() 9 | const { 10 | _id, is_deleted: isDeleted, 11 | notifications, 12 | } = await addressRepository.getOneById(addressId) 13 | 14 | if (isDeleted) { 15 | return ctx.answerCbQuery() 16 | } 17 | 18 | const { exceptions, inclusion } = notifications 19 | 20 | const totalList = [] 21 | inclusion.forEach((inclus) => { 22 | totalList.push(`+${inclus}`) 23 | }) 24 | exceptions.forEach((exception) => { 25 | totalList.push(`-${exception}`) 26 | }) 27 | 28 | await ctx.editMessageText( 29 | ctx.i18n.t('address.notifications.editExceptions', { 30 | current: totalList.length 31 | ? ctx.i18n.t('address.notifications.currentList', { list: totalList.join(', ') }) 32 | : ctx.i18n.t('address.notifications.zeroExceptions'), 33 | }), 34 | Extra.HTML() 35 | .webPreview(false) 36 | .markup(getEditExceptionsKeyboard(_id, notifications, ctx.i18n)), 37 | ) 38 | 39 | return ctx.scene.enter('editExceptions', { address_id: addressId }) 40 | } 41 | -------------------------------------------------------------------------------- /handlers/handleMinAmount.js: -------------------------------------------------------------------------------- 1 | const { Extra } = require('telegraf') 2 | const AddressRepository = require('../repositories/address') 3 | const getEditMinAmountKeyboard = require('../keyboards/editMinAmount') 4 | const formatAddress = require('../utils/formatAddress') 5 | 6 | module.exports = async (ctx) => { 7 | const [addressId] = ctx.match 8 | 9 | const addressRepository = new AddressRepository() 10 | const { 11 | _id, address, tag, 12 | is_deleted: isDeleted, 13 | notifications, 14 | } = await addressRepository.getOneById(addressId) 15 | 16 | if (isDeleted) { 17 | return ctx.answerCbQuery() 18 | } 19 | 20 | await ctx.editMessageText( 21 | ctx.i18n.t('address.notifications.editMinAmount', { 22 | tag, 23 | address, 24 | formatAddress, 25 | }), 26 | Extra.HTML().webPreview(false).markup(getEditMinAmountKeyboard(_id, notifications, ctx.i18n)), 27 | ) 28 | 29 | return ctx.scene.enter('editMinAmount', { address_id: addressId }) 30 | } 31 | -------------------------------------------------------------------------------- /handlers/openAddress.js: -------------------------------------------------------------------------------- 1 | const { Extra } = require('telegraf') 2 | const AddressRepository = require('../repositories/address') 3 | const { PAGINATION_LIMIT } = require('../constants') 4 | const getAddressMenuKeyboard = require('../keyboards/addressMenu') 5 | const formatAddress = require('../utils/formatAddress') 6 | 7 | module.exports = async (ctx) => { 8 | const [addressId] = ctx.match 9 | 10 | const addressRepository = new AddressRepository() 11 | const address = await addressRepository.getOneById(addressId) 12 | const addressString = address.address 13 | 14 | const addressPage = await addressRepository.getAddressPaginationPage( 15 | ctx.from.id, 16 | addressString, 17 | PAGINATION_LIMIT, 18 | ) 19 | 20 | return ctx.editMessageText( 21 | ctx.i18n.t('address.chosen', { 22 | address: addressString, 23 | formatAddress, 24 | tag: address.tag, 25 | }), 26 | Extra.HTML() 27 | .webPreview(false) 28 | .markup(getAddressMenuKeyboard(address, ctx.me, addressPage, ctx.i18n)), 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /handlers/openAddressNotifications.js: -------------------------------------------------------------------------------- 1 | const { Extra } = require('telegraf') 2 | const AddressRepository = require('../repositories/address') 3 | const getAddressNotificationsKeyboard = require('../keyboards/addressNotifications') 4 | const getNotificationsMenu = require('../utils/formatNotificationsMenu') 5 | 6 | module.exports = async (ctx) => { 7 | const [addressId] = ctx.match 8 | 9 | const addressRepository = new AddressRepository() 10 | const address = await addressRepository.getOneById(addressId) 11 | 12 | return ctx.editMessageText( 13 | getNotificationsMenu(address.notifications, ctx.i18n), 14 | Extra.HTML() 15 | .webPreview(false) 16 | .markup(getAddressNotificationsKeyboard(address, ctx.i18n)), 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /handlers/resetMinAmount.js: -------------------------------------------------------------------------------- 1 | const { Extra } = require('telegraf') 2 | const AddressRepository = require('../repositories/address') 3 | const getAddressNotificationsKeyboard = require('../keyboards/addressNotifications') 4 | const getNotificationsMenu = require('../utils/formatNotificationsMenu') 5 | 6 | module.exports = async (ctx) => { 7 | const addressId = ctx.scene.state.address_id 8 | 9 | const addressRepository = new AddressRepository() 10 | await addressRepository.resetMinAmount(addressId) 11 | 12 | const { _id, notifications } = await addressRepository.getOneById(addressId) 13 | 14 | ctx.scene.leave() 15 | 16 | return ctx.editMessageText( 17 | getNotificationsMenu(notifications, ctx.i18n), 18 | Extra.HTML() 19 | .webPreview(false) 20 | .markup(getAddressNotificationsKeyboard({ _id, notifications }, ctx.i18n)), 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /handlers/sendAddressesList.js: -------------------------------------------------------------------------------- 1 | const { Extra } = require('telegraf') 2 | const AddressRepository = require('../repositories/address') 3 | const pagination = require('../services/pagination') 4 | const getAddressesListKeyboard = require('../keyboards/addressesList') 5 | const { PAGINATION_LIMIT } = require('../constants') 6 | 7 | module.exports = async (ctx) => { 8 | const addressesPage = ctx.match && ctx.match[2] ? Number(ctx.match[2]) : 0 9 | 10 | const addressRepository = new AddressRepository() 11 | // eslint-disable-next-line camelcase 12 | const { addresses, total_count } = await pagination( 13 | ctx.from.id, 14 | addressRepository, 15 | addressesPage * PAGINATION_LIMIT, 16 | ) 17 | 18 | if (!addresses) { 19 | return ctx.replyWithHTML(ctx.i18n.t('list.empty')) 20 | } 21 | 22 | const paginationOptions = { current: addressesPage, total_count } 23 | return ctx.replyWithHTML( 24 | ctx.i18n.t('list.chooseAddress'), 25 | Extra.markup(getAddressesListKeyboard(addresses, paginationOptions)).webPreview( 26 | false, 27 | ), 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /handlers/sendWelcome.js: -------------------------------------------------------------------------------- 1 | const { Extra } = require('telegraf') 2 | 3 | module.exports = ({ replyWithHTML, i18n }) => { 4 | return replyWithHTML(i18n.t('welcome'), Extra.webPreview(false)) 5 | } 6 | -------------------------------------------------------------------------------- /handlers/turnNotifications.js: -------------------------------------------------------------------------------- 1 | const AddressRepository = require('../repositories/address') 2 | const getAddressNotificationsKeyboard = require('../keyboards/addressNotifications') 3 | 4 | module.exports = async (ctx) => { 5 | const addressId = ctx.match[1] 6 | const state = ctx.match[2] 7 | 8 | const notifications = (state === 'on') 9 | 10 | const addressRepository = new AddressRepository() 11 | 12 | if (notifications) { 13 | await addressRepository.turnOnNotifications(addressId) 14 | } else { 15 | await addressRepository.turnOfNotifications(addressId) 16 | } 17 | 18 | const address = await addressRepository.getOneById(addressId) 19 | 20 | return ctx.editMessageReplyMarkup( 21 | getAddressNotificationsKeyboard(address, ctx.i18n), 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /handlers/undoAddressDelete.js: -------------------------------------------------------------------------------- 1 | const AddressRepository = require('../repositories/address') 2 | 3 | module.exports = async (ctx) => { 4 | const [addressId] = ctx.match 5 | 6 | const addressRepository = new AddressRepository() 7 | return addressRepository.restoreOne(addressId) 8 | } 9 | -------------------------------------------------------------------------------- /i18n.js: -------------------------------------------------------------------------------- 1 | const TelegrafI18n = require('telegraf-i18n') 2 | const path = require('path') 3 | 4 | module.exports = new TelegrafI18n({ 5 | defaultLanguage: 'en', 6 | directory: path.resolve(__dirname, 'locales'), 7 | }) 8 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const config = require('./config') 3 | const log = require('./utils/log') 4 | const startBot = require('./bot') 5 | 6 | mongoose 7 | .connect(config.get('db')) 8 | .then(() => startBot()) 9 | .catch((err) => log.error(`Bot initialization error: ${err}`)) 10 | -------------------------------------------------------------------------------- /keyboards/addressMenu.js: -------------------------------------------------------------------------------- 1 | const { Markup: m } = require('telegraf') 2 | 3 | module.exports = (address, botUsername, addressPage, i18n) => { 4 | const { _id, address: addressId, notifications } = address 5 | 6 | const notificationsState = notifications.is_enabled 7 | ? i18n.t('buttons.notifications.On') 8 | : i18n.t('buttons.notifications.Off') 9 | 10 | return m.inlineKeyboard( 11 | [ 12 | [ 13 | m.callbackButton( 14 | i18n.t('buttons.notifications.text', { state: notificationsState }), 15 | `notify_${_id}`, 16 | ), 17 | m.callbackButton(i18n.t('buttons.editTag'), `edit_${_id}`), 18 | ], 19 | [ 20 | m.switchToChatButton( 21 | i18n.t('buttons.shareAddress'), 22 | i18n.t('address.share', { address: addressId, username: botUsername }), 23 | ), 24 | m.callbackButton(i18n.t('buttons.deleteAddress'), `delete_${_id}`), 25 | ], 26 | [ 27 | m.callbackButton(i18n.t('buttons.backToList'), `list_${addressPage}`), 28 | ], 29 | ], 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /keyboards/addressNotifications.js: -------------------------------------------------------------------------------- 1 | const { Markup: m } = require('telegraf') 2 | const ton = require('../services/ton') 3 | 4 | module.exports = (address, i18n) => { 5 | const { _id, notifications } = address 6 | 7 | const { is_enabled: isEnabled, min_amount: minAmout, exceptions, inclusion } = notifications 8 | 9 | const exceptionsButton = exceptions.length || inclusion.length 10 | ? i18n.t('buttons.notifications.editExceptions') 11 | : i18n.t('buttons.notifications.addExceptions') 12 | 13 | const stringAmount = String(minAmout) 14 | return m.inlineKeyboard( 15 | [ 16 | m.callbackButton( 17 | i18n.t('buttons.notifications.send', { state: isEnabled ? 'Yes' : 'No' }), 18 | `notify_${_id}_${isEnabled ? 'off' : 'on'}`, 19 | ), 20 | m.callbackButton( 21 | i18n.t( 22 | 'buttons.notifications.minAmount', 23 | { 24 | state: stringAmount === '0' ? 'OFF' : `${ton.utils.fromNano(stringAmount)} TON`, 25 | }, 26 | ), 27 | `notify_min_amout_${_id}`, 28 | !isEnabled, 29 | ), 30 | m.callbackButton( 31 | exceptionsButton, 32 | `notify_exceptions_${_id}`, 33 | !isEnabled, 34 | ), 35 | m.callbackButton(i18n.t('buttons.backToAddress'), `open_${_id}`), 36 | ], 37 | { columns: 1 }, 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /keyboards/addressesList.js: -------------------------------------------------------------------------------- 1 | const { Markup: m } = require('telegraf') 2 | const { PAGINATION_LIMIT } = require('../constants') 3 | const formatAddress = require('../utils/formatAddress') 4 | 5 | const generatePaginationKeyboard = (current, count, prefix) => { 6 | const keyboard = [] 7 | 8 | if (count <= 5) { 9 | return Array.from({ length: count }, (_, index) => index + 1).map((index) => { 10 | const text = current + 1 === index ? `· ${index} ·` : `${index}` 11 | return m.callbackButton(text, `${prefix}${index - 1}`) 12 | }) 13 | } 14 | 15 | let one = 1 16 | if (current === 0) { 17 | one = '· 1 ·' 18 | } else if (current > 2) { 19 | one = '« 1' 20 | } 21 | keyboard.push(m.callbackButton(one, `${prefix}0`)) 22 | 23 | let two = current <= 2 ? 2 : current 24 | let actionTwo 25 | if (current === 1) { 26 | two = '· 2 ·' 27 | actionTwo = 1 28 | } else if (current >= 3) { 29 | actionTwo = current - 1 30 | two = `‹ ${two}` 31 | } 32 | 33 | if ((current + 1 === count - 1) || (current + 1 === count)) { 34 | actionTwo = count - 4 35 | two = `‹ ${count - 3}` 36 | } 37 | 38 | if (!actionTwo) { 39 | actionTwo = 1 40 | } 41 | 42 | keyboard.push(m.callbackButton(two, `${prefix}${actionTwo}`)) 43 | 44 | let actionThree 45 | let three = current > 2 ? current + 1 : 3 46 | if (current === 2) { 47 | actionThree = 2 48 | three = '· 3 ·' 49 | } else if (current > 2) { 50 | actionThree = current 51 | three = `· ${current + 1} ·` 52 | } 53 | 54 | if ((current + 1 === count - 1) || (current + 1 === count)) { 55 | actionThree = count - 3 56 | three = count - 2 57 | } 58 | 59 | if (!actionThree) { 60 | actionThree = 2 61 | } 62 | 63 | keyboard.push(m.callbackButton(three, `${prefix}${actionThree}`)) 64 | 65 | let actionFour 66 | let four 67 | if (current <= 2) { 68 | four = '4 ›' 69 | } else { 70 | four = `${current + 2}` 71 | if (!(current + 2 === count - 1)) { 72 | four += ' ›' 73 | } 74 | } 75 | 76 | if ((current + 1 === count - 1)) { 77 | four = `· ${(count - 1)} ·` 78 | } 79 | 80 | if (current + 1 === count) { 81 | four = count - 1 82 | } 83 | 84 | actionFour = (current + 1 === count) || (current + 1 === count - 1) ? count - 2 : current + 1 85 | 86 | if (current <= 2) { 87 | actionFour = 3 88 | } 89 | 90 | keyboard.push(m.callbackButton(four, `${prefix}${actionFour}`)) 91 | 92 | let five = count 93 | if (!(current + 2 === count - 1)) { 94 | five += ' »' 95 | } 96 | 97 | if (current + 1 === count - 1) { 98 | five = count 99 | } 100 | 101 | if (current + 1 === count) { 102 | five = `· ${count} ·` 103 | } 104 | 105 | keyboard.push(m.callbackButton(five, `${prefix}${count - 1}`)) 106 | 107 | return keyboard 108 | } 109 | 110 | module.exports = (addresses, pagination) => { 111 | const buttons = addresses.map(({ _id, address, tag }) => { 112 | const text = tag ? `${tag}: ${formatAddress(address)}` : formatAddress(address) 113 | return [m.callbackButton(text, `open_${_id}`)] 114 | }) 115 | 116 | const { current, total_count: totalCount } = pagination 117 | const pagesCount = Math.ceil(totalCount / PAGINATION_LIMIT) 118 | 119 | const addressesListKeyboard = [...buttons] 120 | if (pagesCount > 1) { 121 | addressesListKeyboard.push( 122 | generatePaginationKeyboard(current, pagesCount, 'list_'), 123 | ) 124 | } 125 | 126 | return m.inlineKeyboard(addressesListKeyboard) 127 | } 128 | -------------------------------------------------------------------------------- /keyboards/backToAddress.js: -------------------------------------------------------------------------------- 1 | const { Markup: m } = require('telegraf') 2 | 3 | module.exports = (addressId, i18n) => { 4 | return m.inlineKeyboard([m.callbackButton(i18n.t('buttons.backToAddress'), `open_${addressId}`)]) 5 | } 6 | -------------------------------------------------------------------------------- /keyboards/backToNotifications.js: -------------------------------------------------------------------------------- 1 | const { Markup: m } = require('telegraf') 2 | 3 | module.exports = (addressId, i18n) => { 4 | return m.inlineKeyboard([m.callbackButton(i18n.t('buttons.backToNotifications'), `notify_${addressId}`)]) 5 | } 6 | -------------------------------------------------------------------------------- /keyboards/editExceptions.js: -------------------------------------------------------------------------------- 1 | const { Markup: m } = require('telegraf') 2 | 3 | module.exports = (addressId, notifications, i18n) => { 4 | return m.inlineKeyboard( 5 | [ 6 | m.callbackButton( 7 | i18n.t('buttons.notifications.clearExceptions'), 8 | 'clear_exceptions', 9 | !notifications.exceptions.length, 10 | ), 11 | m.callbackButton(i18n.t('buttons.backToNotifications'), `notify_${addressId}`), 12 | ], 13 | { columns: 1 }, 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /keyboards/editMinAmount.js: -------------------------------------------------------------------------------- 1 | const { Markup: m } = require('telegraf') 2 | 3 | module.exports = (addressId, notifications, i18n) => { 4 | return m.inlineKeyboard( 5 | [ 6 | m.callbackButton( 7 | i18n.t('buttons.notifications.resetMinAmount'), 8 | 'reset_min_amount', 9 | String(notifications.min_amount) === '0', 10 | ), 11 | m.callbackButton(i18n.t('buttons.backToNotifications'), `notify_${addressId}`), 12 | ], 13 | { columns: 1 }, 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /keyboards/openAddress.js: -------------------------------------------------------------------------------- 1 | const { Markup: m } = require('telegraf') 2 | 3 | module.exports = (addressId, isHideSetTagButton, i18n) => { 4 | return m.inlineKeyboard([ 5 | m.callbackButton(i18n.t('buttons.setTag'), `edit_${addressId}`, isHideSetTagButton), 6 | m.callbackButton(i18n.t('buttons.openAddress'), `open_${addressId}`), 7 | ]) 8 | } 9 | -------------------------------------------------------------------------------- /keyboards/undoDelete.js: -------------------------------------------------------------------------------- 1 | const { Markup: m } = require('telegraf') 2 | 3 | module.exports = (addressId, isHideOpenButton, returnPage, i18n) => { 4 | return m.inlineKeyboard( 5 | [ 6 | m.callbackButton(i18n.t('buttons.undo'), `undo_${addressId}`), 7 | m.callbackButton( 8 | i18n.t('buttons.openAddressesList'), 9 | `open-list-${addressId}-${returnPage}`, 10 | isHideOpenButton, 11 | ), 12 | ], 13 | { columns: 1 }, 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /locales/en.yaml: -------------------------------------------------------------------------------- 1 | welcome: | 2 | I send you instant notifications about any event of the TON blockchain. 3 | 4 | Send me an address like address:tag to get notifications for it. 5 | 6 | /list — manage your alerts 7 | 8 | Powered by @tonbase. 9 | 10 | transaction: 11 | message: | 12 | 🏷 ${type} 👤 ${fromTag}${fromBalance} ➜ ${toTag}${toBalance} 13 | 14 | 💎 ${value} TON${price} 15 | 16 | ${comment} 17 | channelMessage: | 18 | 💸 ${fromTag}${fromBalance} ➜ ${toTag}${toBalance} 19 | 20 | 💎 ${value} TON${price} 21 | 22 | ${comment} 23 | comment: 💬 ${text} 24 | accountBalance: " · ${value}" 25 | price: " · $${value}" 26 | receive: Receive 27 | send: Send 28 | 29 | address: 30 | added: | 31 | ${formatAddress(address)}${formatTag(tag)} was added. 32 | 33 | You'll get notified about all events of this address. 34 | chosen: | 35 | Here it is: ${tag} ${formatAddress(address)}. 36 | 37 | What do you want to do with the address? 38 | sendTag: | 39 | Send me a tag for this address: ${formatAddress(address)} 40 | 41 | For example: My Address and etc. 42 | deleted: | 43 | The address ${tag} ${formatAddress(address)} was deleted. 44 | share: | 45 | t.me/${username}?start=${address} 46 | 47 | Use the above link to receive instant notifications for all events of this address 👆 48 | notifications: 49 | menu: | 50 | Here you can set notifications. 51 | 52 | ${inclusion} 53 | ${exceptions} 54 | invalid: | 55 | Invalid number. 56 | editMinAmount: | 57 | Send me a minimal amount for this address: ${tag} ${formatAddress(address)} 58 | 59 | For example: 0.1 and etc. 60 | zeroExceptions: "Exceptions are disabled." 61 | exceptionsList: "Current exceptions: ${list}" 62 | editExceptions: | 63 | Here you can add exclusions for notifications. Send me words separated by commas to turn off notifications with the payload of transactions. 64 | For example: +cashback, -ton.events, -ads. 65 | 66 | ${current} 67 | zeroInclusion: "Inclusion are disabled." 68 | inclusionList: "Current inclusion: ${list}" 69 | currentList: "Current: ${list}" 70 | 71 | 72 | 73 | list: 74 | chooseAddress: | 75 | Choose an address from the list below: 76 | empty: | 77 | 😔 You have no addresses added. 78 | 79 | Send me a TON address to get instant notifications for its events. 80 | 81 | Also, you can send me address:tag to add it with the tag once. 82 | 83 | buttons: 84 | notifications: 85 | text: "Notifications: ${state}" 86 | On: ON 87 | Off: Off 88 | send: "Send notifications: ${state}" 89 | minAmount: "Min. amount: ${state}" 90 | resetMinAmount: Reset 91 | addExceptions: Add exceptions 92 | editExceptions: Edit exceptions 93 | clearExceptions: Clear 94 | openAddress: Open Address 95 | nextPage: Next » 96 | prevPage: « Prev 97 | editTag: Edit Tag 98 | setTag: Set Tag 99 | shareAddress: Share Address 100 | deleteAddress: Delete Address 101 | undo: Undo 102 | backToAddress: « Back to Address 103 | backToNotifications: « Back to notifications 104 | backToList: « Back to Address list 105 | openAddressesList: Open Adress List 106 | -------------------------------------------------------------------------------- /middlewares/auth.js: -------------------------------------------------------------------------------- 1 | const UserRepository = require('../repositories/user') 2 | 3 | module.exports = async (ctx, next) => { 4 | const { from } = ctx 5 | 6 | if (!from) { 7 | return next() 8 | } 9 | 10 | const params = { 11 | user_id: from.id, 12 | first_name: from.first_name, 13 | last_name: from.last_name || '', 14 | language_code: from.language_code || '', 15 | last_activity_at: new Date(), 16 | } 17 | const userRepository = new UserRepository() 18 | 19 | try { 20 | const user = await userRepository.create(params) 21 | ctx.user = user.toJSON() 22 | } catch (err) { 23 | if (err.code !== 11000) { 24 | throw err 25 | } 26 | 27 | const user = await userRepository.getOneAndUpdateByTgId(from.id, params) 28 | ctx.user = user.toJSON() 29 | } 30 | 31 | return next() 32 | } 33 | -------------------------------------------------------------------------------- /middlewares/blockDetection.js: -------------------------------------------------------------------------------- 1 | const UserRepository = require('../repositories/user') 2 | 3 | module.exports = async (ctx, next) => { 4 | if (ctx.update?.my_chat_member?.chat?.type !== 'private') { 5 | return next() 6 | } 7 | if (!ctx.update?.my_chat_member?.new_chat_member?.status) { 8 | return next() 9 | } 10 | 11 | const userId = ctx.update.my_chat_member.chat.id 12 | const userRepository = new UserRepository() 13 | 14 | if (ctx.update?.my_chat_member?.new_chat_member?.status === 'kicked') { 15 | return userRepository.getOneAndUpdateByTgId(userId, { 16 | is_blocked: true, 17 | }) 18 | } else if (ctx.update?.my_chat_member?.new_chat_member?.status === 'member') { 19 | return userRepository.getOneAndUpdateByTgId(userId, { 20 | is_blocked: false, 21 | is_deactivated: false, 22 | }) 23 | } 24 | 25 | return next() 26 | } 27 | -------------------------------------------------------------------------------- /middlewares/session.js: -------------------------------------------------------------------------------- 1 | const MongooseSession = require('../services/session') 2 | 3 | module.exports = new MongooseSession() 4 | -------------------------------------------------------------------------------- /migrations/notifyMigrate.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const config = require('../config') 3 | const AddressModel = require('../models/address') 4 | const log = require('../utils/log') 5 | 6 | mongoose 7 | .connect(config.get('db')) 8 | .then(async () => { 9 | await AddressModel.updateMany( 10 | { 11 | 'notifications.is_enabled': { $exists: false }, 12 | }, 13 | [ 14 | { 15 | $set: { 16 | 'notifications.is_enabled': { 17 | $cond: { 18 | if: { $eq: ['$notifications', true] }, 19 | then: true, 20 | else: false, 21 | }, 22 | }, 23 | }, 24 | }, 25 | ], 26 | ) 27 | log.info('Migration completed') 28 | process.exit(1) 29 | }) 30 | -------------------------------------------------------------------------------- /models/address.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | 3 | const schemaTypes = mongoose.Schema.Types 4 | const addressSchema = new mongoose.Schema( 5 | { 6 | user_id: { type: Number, unique: false, required: true }, 7 | address: { type: String, unique: false, required: true }, 8 | tag: { type: String, default: '' }, 9 | is_deleted: { type: Boolean, default: false }, 10 | notifications: { 11 | is_enabled: { type: Boolean, default: true }, 12 | min_amount: { 13 | type: schemaTypes.Decimal128, 14 | default: 0, 15 | }, 16 | exceptions: { type: [String], default: [] }, 17 | inclusion: { type: [String], default: [] }, 18 | }, 19 | counters: { 20 | send_coins: { type: Number, default: 0 }, 21 | }, 22 | }, 23 | { 24 | versionKey: false, 25 | timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' }, 26 | }, 27 | ) 28 | 29 | addressSchema.index({ user_id: 1, address: 1 }, { unique: true }) 30 | 31 | module.exports = mongoose.model('address', addressSchema) 32 | -------------------------------------------------------------------------------- /models/counters.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | 3 | const countersSchema = new mongoose.Schema( 4 | { 5 | name: { 6 | type: String, 7 | required: true, 8 | unique: true, 9 | }, 10 | data: { 11 | type: Object, 12 | }, 13 | }, 14 | { 15 | versionKey: false, 16 | timestamps: { 17 | createdAt: 'created_at', 18 | updatedAt: 'updated_at', 19 | }, 20 | }, 21 | ) 22 | 23 | module.exports = mongoose.model('counters', countersSchema) 24 | -------------------------------------------------------------------------------- /models/sessions.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | 3 | // indexes: 4 | // {user_id: 1, chat_id: 1} 5 | 6 | const sessions = new mongoose.Schema({ 7 | user_id: Number, 8 | chat_id: Number, 9 | data: {}, 10 | }, { 11 | versionKey: false, 12 | autoIndex: false, 13 | }) 14 | sessions.index({user_id: 1, chat_id: 1}) 15 | 16 | module.exports = mongoose.model('sessions', sessions) 17 | -------------------------------------------------------------------------------- /models/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | 3 | const userSchema = new mongoose.Schema( 4 | { 5 | user_id: { type: Number, required: true, unique: true }, 6 | first_name: { type: String, required: true }, 7 | last_name: { type: String }, 8 | language_code: { type: String }, 9 | is_deactivated: { type: Boolean, default: false }, 10 | is_blocked: { type: Boolean, default: false }, 11 | language: { type: String, default: 'en' }, 12 | last_activity_at: Date, 13 | }, 14 | { 15 | versionKey: false, 16 | timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' }, 17 | }, 18 | ) 19 | 20 | module.exports = mongoose.model('user', userSchema) 21 | -------------------------------------------------------------------------------- /monitors/addresses.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') 2 | 3 | const log = require('../utils/log') 4 | const { sleep } = require('../utils/sleep') 5 | 6 | const knownAccounts = {} 7 | 8 | const updateAddresses = async () => { 9 | try { 10 | const { data } = await axios.get('https://address-book.tonscan.org/addresses.json') 11 | 12 | for (const adr in data) { 13 | const address = data[adr] 14 | const title = (typeof address === 'string') ? address : 15 | (address?.name ? `${address.tonIcon ? `${address.tonIcon} ` : ''}${address.name}` : null) 16 | 17 | if (!title) { 18 | continue; 19 | } 20 | 21 | knownAccounts[adr] = title 22 | } 23 | } catch (err) { 24 | log.error(`Updating book of addresses error: ${err}`) 25 | } 26 | 27 | await sleep(60 * 60 * 1000) // 1 hour 28 | updateAddresses() 29 | } 30 | 31 | updateAddresses() 32 | 33 | function getTitleByAddress(address) { 34 | return knownAccounts[address] || false 35 | } 36 | 37 | module.exports = getTitleByAddress 38 | -------------------------------------------------------------------------------- /monitors/pool.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') 2 | 3 | const log = require('../utils/log') 4 | const { sleep } = require('../utils/sleep') 5 | 6 | const pools = [] 7 | 8 | const updatePool = async () => { 9 | try { 10 | const { data } = await axios.get('https://tonapi.io/v2/staking/pools?include_unverified=true') 11 | 12 | pools.splice(0, pools.length) 13 | pools.push(data.pools) 14 | } catch (err) { 15 | log.error(`Pool scan error: ${err}`) 16 | } 17 | 18 | await sleep(5 * 60 * 1000) 19 | updatePool() 20 | } 21 | 22 | updatePool() 23 | 24 | module.exports = () => pools 25 | -------------------------------------------------------------------------------- /monitors/scan.js: -------------------------------------------------------------------------------- 1 | f = (...args) => console.log(...args) 2 | j = (obj) => f(JSON.stringify(obj, null, 2)) 3 | 4 | const { Big } = require('../utils/big') 5 | const mongoose = require('mongoose') 6 | 7 | const config = require('../config') 8 | const ton = require('../services/ton') 9 | const log = require('../utils/log') 10 | const Counters = require('../models/counters') 11 | const transactionProcessor = require('../services/transactionProcessor') 12 | const { sleep } = require('../utils/sleep') 13 | 14 | let IS_RUNNING = false 15 | 16 | const addTransactionToQueue = async (transaction, seqno) => { 17 | const inMsg = transaction?.in_msg 18 | const outMsg = transaction?.out_msgs?.[0] 19 | const message = inMsg?.source && inMsg?.destination ? inMsg : outMsg 20 | 21 | if (!message || !new Big(message?.value).gt(0)) { 22 | return false 23 | } 24 | 25 | const comment = message?.comment || '' 26 | 27 | return transactionProcessor({ 28 | from: message.source, 29 | to: message.destination, 30 | value: ton.utils.fromNano(message.value.toString()), 31 | nanoValue: message.value, 32 | comment, 33 | raw: transaction, 34 | }, { 35 | seqno, hash: message.hash, 36 | }) 37 | } 38 | 39 | const scanAddresses = async () => { 40 | if (IS_RUNNING) { 41 | log.warn('Scan is running') 42 | return false 43 | } 44 | IS_RUNNING = true 45 | 46 | const lastCheckedBlockFilter = { name: 'lastCheckedBlock' } 47 | 48 | const lastEnqueuedMaster = await Counters.findOne(lastCheckedBlockFilter) 49 | const masterchainInfo = await ton.node.send('getMasterchainInfo', {}) 50 | const lastSeqno = masterchainInfo.last.seqno 51 | 52 | let currentSeqno = lastEnqueuedMaster?.data?.seqno 53 | 54 | if (!currentSeqno || currentSeqno === 0) { 55 | await Counters.findOneAndUpdate( 56 | lastCheckedBlockFilter, 57 | { data: { seqno: lastSeqno } }, 58 | { upsert: true }, 59 | ) 60 | currentSeqno = lastSeqno 61 | } 62 | 63 | currentSeqno += 1 // to skip check one block twice 64 | 65 | log.info(`Enqueue master blocks ${currentSeqno}-${lastSeqno}`) 66 | 67 | const excludedTransactionTypes = ['trans_tick_tock'] 68 | 69 | for (let seqno = currentSeqno; seqno < lastSeqno; seqno++) { 70 | const transactionsList = await ton.getTransactionsByMasterchainSeqno(seqno) 71 | const filteredTransactionsList = transactionsList 72 | .filter((t) => !excludedTransactionTypes 73 | .includes(t.transaction_type) 74 | ) 75 | 76 | log.info(`Received ${filteredTransactionsList.length} transactions on seqno: ${seqno}`) 77 | for (const [index, transaction] of filteredTransactionsList.entries()) { 78 | transaction.address = new ton.utils.Address(transaction.account) 79 | .toString(true, true, true, false) 80 | 81 | // log.info(`Adding transaction #${Number(index) + 1} ${transaction 82 | // .transaction_type} to queue (${transaction.address})`) 83 | 84 | addTransactionToQueue(transaction, seqno) 85 | } 86 | 87 | await Counters.findOneAndUpdate(lastCheckedBlockFilter, { data: { seqno } }) 88 | 89 | await sleep(100) 90 | } 91 | 92 | IS_RUNNING = false 93 | } 94 | 95 | mongoose 96 | .connect(config.get('db')) 97 | .then(() => 98 | setInterval( 99 | () => 100 | scanAddresses().catch((err) => { 101 | IS_RUNNING = false 102 | console.error(err) 103 | log.error(`Scan adresses error: ${err}`) 104 | }), 105 | config.get('synchronizer.interval') * 1000, 106 | ), 107 | ) 108 | .catch((err) => log.error(`Monitor initialization error: ${err}`)) 109 | -------------------------------------------------------------------------------- /monitors/scanPrice.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') 2 | 3 | const log = require('../utils/log') 4 | const { sleep } = require('../utils/sleep') 5 | 6 | let price = 0 7 | 8 | const scanPrice = async () => { 9 | try { 10 | const { data: { data: [{ last }] } } = await axios 11 | .get('https://www.okx.com/api/v5/market/ticker?instId=TON-USDT-SWAP') 12 | price = last 13 | } catch (err) { 14 | log.error(`Price scan error: ${err}`) 15 | } 16 | 17 | await sleep(60 * 1000) 18 | scanPrice() 19 | } 20 | 21 | scanPrice() 22 | 23 | module.exports = () => price 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ton-notify-bot", 3 | "version": "0.0.1", 4 | "description": "🚀 The bot can notify users about new events of TON addresses.", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "dev:bot": "nodemon -V -L index.js", 9 | "dev:scan": "nodemon -V -L monitors/scan.js" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/tonbase/ton-notify-bot.git" 14 | }, 15 | "author": "", 16 | "license": "ISC", 17 | "bugs": { 18 | "url": "https://github.com/tonbase/ton-notify-bot/issues" 19 | }, 20 | "homepage": "https://github.com/tonbase/ton-notify-bot#readme", 21 | "dependencies": { 22 | "axios": "^1.3.4", 23 | "big.js": "^6.1.1", 24 | "chalk": "^4.1.2", 25 | "convict": "^5.2.1", 26 | "dotenv": "^10.0.0", 27 | "lru-cache": "^8.0.4", 28 | "mongoose": "^6.0.5", 29 | "node-fetch": "^2.6.2", 30 | "telegraf": "^3.39.0", 31 | "telegraf-i18n": "^6.6.0", 32 | "tonweb": "^0.0.60", 33 | "winston": "^3.3.3" 34 | }, 35 | "devDependencies": { 36 | "eslint": "^7.32.0", 37 | "eslint-config-airbnb-base": "^14.2.1", 38 | "eslint-plugin-import": "^2.22.1", 39 | "eslint-plugin-unicorn": "^26.0.1" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /repositories/address.js: -------------------------------------------------------------------------------- 1 | const AddressModel = require('../models/address') 2 | 3 | class AddressRepository { 4 | getOneById(id) { 5 | return AddressModel.findById(id) 6 | } 7 | 8 | getOneByAddress(address, filter = {}) { 9 | return AddressModel.findOne({ address, ...filter }) 10 | } 11 | 12 | getByAddress(address, filter = {}) { 13 | const wrapped = Array.isArray(address) ? address : [address] 14 | return AddressModel.find({ address: { $in: wrapped }, ...filter }) 15 | } 16 | 17 | async getAddressPaginationPage(userId, address, pagination) { 18 | const result = await AddressModel.aggregate([ 19 | { 20 | $facet: { 21 | addresses: [ 22 | { $match: { user_id: userId, is_deleted: false } }, 23 | { $skip: 0 }, 24 | ], 25 | }, 26 | }, 27 | ]) 28 | 29 | const { addresses } = result[0] 30 | const index = addresses.findIndex((v) => v.address === address) 31 | if (index === -1) { 32 | return 1 33 | } 34 | 35 | return Math.floor(index / pagination) 36 | } 37 | 38 | async paginationByUserId(userId, offset = 0, limit, filter = {}) { 39 | const result = await AddressModel.aggregate([ 40 | { 41 | $facet: { 42 | addresses: [ 43 | { $match: { user_id: userId, ...filter } }, 44 | { $skip: offset }, 45 | { $limit: limit }, 46 | ], 47 | total_count: [{ $match: { user_id: userId, ...filter } }, { $count: 'count' }], 48 | }, 49 | }, 50 | ]) 51 | 52 | const { addresses, total_count: totalCount } = result[0] 53 | return { addresses, total_count: totalCount[0] && totalCount[0].count } 54 | } 55 | 56 | create(address) { 57 | return AddressModel.create(address) 58 | } 59 | 60 | updateOneById(addressId, update) { 61 | return AddressModel.updateOne({ _id: addressId }, { $set: update }) 62 | } 63 | 64 | updateTag(addressId, tag) { 65 | return AddressModel.updateOne({ _id: addressId }, { $set: { tag } }) 66 | } 67 | 68 | updateMinAmount(addressId, amount) { 69 | return AddressModel.updateOne( 70 | { _id: addressId }, 71 | { $set: { 'notifications.min_amount': amount } }, 72 | ) 73 | } 74 | 75 | resetMinAmount(addressId) { 76 | return AddressModel.updateOne( 77 | { _id: addressId }, 78 | { $set: { 'notifications.min_amount': '0' } }, 79 | ) 80 | } 81 | 82 | updateExceptions(addressId, exceptions, inclusion) { 83 | return AddressModel.updateOne( 84 | { _id: addressId }, 85 | { 86 | $set: { 87 | 'notifications.exceptions': exceptions, 88 | 'notifications.inclusion': inclusion, 89 | }, 90 | }, 91 | ) 92 | } 93 | 94 | clearExceptions(addressId) { 95 | return AddressModel.updateOne( 96 | { _id: addressId }, 97 | { 98 | $set: { 99 | 'notifications.exceptions': [], 100 | 'notifications.inclusion': [], 101 | }, 102 | }, 103 | ) 104 | } 105 | 106 | incSendCoinsCounter(addressId, value) { 107 | return AddressModel.updateOne({ _id: addressId }, { $inc: { 'counters.send_coins': value } }) 108 | } 109 | 110 | turnOnNotifications(addressId) { 111 | return AddressModel.updateOne( 112 | { _id: addressId }, 113 | { $set: { 'notifications.is_enabled': true } }, 114 | ) 115 | } 116 | 117 | turnOfNotifications(addressId) { 118 | return AddressModel.updateOne( 119 | { _id: addressId }, 120 | { $set: { 'notifications.is_enabled': false } }, 121 | ) 122 | } 123 | 124 | softDeleteOne(addressId) { 125 | return AddressModel.updateOne({ _id: addressId }, { $set: { is_deleted: true } }) 126 | } 127 | 128 | restoreOne(addressId) { 129 | return AddressModel.updateOne({ _id: addressId }, { $set: { is_deleted: false } }) 130 | } 131 | } 132 | 133 | module.exports = AddressRepository 134 | -------------------------------------------------------------------------------- /repositories/user.js: -------------------------------------------------------------------------------- 1 | const UserModel = require('../models/user') 2 | 3 | class UserRepository { 4 | getOneById(id) { 5 | return UserModel.findById(id) 6 | } 7 | 8 | getOneByTgId(userId) { 9 | return UserModel.findOne({ user_id: userId }) 10 | } 11 | 12 | getByTgId(userId, filter = {}) { 13 | return UserModel.find({ user_id: userId, ...filter }) 14 | } 15 | 16 | create(user) { 17 | return UserModel.create(user) 18 | } 19 | 20 | getOneAndUpdateByTgId(userId, update) { 21 | return UserModel.findOneAndUpdate({ user_id: userId }, { $set: update }) 22 | } 23 | 24 | softDeleteOne(userId) { 25 | return UserModel.updateOne( 26 | { user_id: userId }, 27 | { $set: { is_deactivated: true } }, 28 | ) 29 | } 30 | 31 | restoreOne(userId) { 32 | return UserModel.updateOne( 33 | { user_id: userId }, 34 | { $set: { is_deactivated: false } }, 35 | ) 36 | } 37 | } 38 | 39 | module.exports = UserRepository 40 | -------------------------------------------------------------------------------- /scenes/editExceptions.js: -------------------------------------------------------------------------------- 1 | const { Composer, BaseScene } = require('telegraf') 2 | const clearExceptions = require('../handlers/clearExceptions') 3 | const editExceptions = require('../handlers/editExceptions') 4 | 5 | const editExceptionsScene = new BaseScene('editExceptions') 6 | 7 | editExceptionsScene.action('clear_exceptions', Composer.tap(clearExceptions)) 8 | 9 | editExceptionsScene.on('text', Composer.tap(editExceptions)) 10 | 11 | module.exports = editExceptionsScene 12 | -------------------------------------------------------------------------------- /scenes/editMinAmount.js: -------------------------------------------------------------------------------- 1 | const { Composer, BaseScene } = require('telegraf') 2 | const editMinAmount = require('../handlers/editMinAmount') 3 | const resetMinAmount = require('../handlers/resetMinAmount') 4 | 5 | const editMinAmountScene = new BaseScene('editMinAmount') 6 | 7 | editMinAmountScene.action('reset_min_amount', Composer.tap(resetMinAmount)) 8 | 9 | editMinAmountScene.on('text', Composer.tap(editMinAmount)) 10 | 11 | module.exports = editMinAmountScene 12 | -------------------------------------------------------------------------------- /scenes/editTag.js: -------------------------------------------------------------------------------- 1 | const { Composer, BaseScene, Stage } = require('telegraf') 2 | const editTag = require('../handlers/editTag') 3 | 4 | const editTagScene = new BaseScene('editTag') 5 | 6 | editTagScene.on('text', Composer.tap(editTag)) 7 | 8 | editTagScene.use(Composer.tap(Stage.leave())) 9 | 10 | module.exports = editTagScene 11 | -------------------------------------------------------------------------------- /services/pagination.js: -------------------------------------------------------------------------------- 1 | const { PAGINATION_LIMIT } = require('../constants') 2 | 3 | module.exports = async (userId, addressRepository, offset) => { 4 | const { addresses, total_count } = 5 | await addressRepository.paginationByUserId( 6 | userId, 7 | offset, 8 | PAGINATION_LIMIT, 9 | { is_deleted: false }, 10 | ) 11 | 12 | if (!addresses.length) { 13 | return {} 14 | } 15 | 16 | return { addresses, total_count } 17 | } 18 | -------------------------------------------------------------------------------- /services/session.js: -------------------------------------------------------------------------------- 1 | const Sessions = require('../models/sessions') 2 | 3 | class MongoSession { 4 | constructor(options) { 5 | this.options = { 6 | property: 'session', 7 | getSessionKey: (ctx) => ctx.from && ctx.chat && {user_id: ctx.from.id, chat_id: ctx.chat.id}, 8 | store: {}, 9 | ...options, 10 | } 11 | } 12 | 13 | getSessionKey(...v) { 14 | return this.options.getSessionKey(...v) 15 | } 16 | 17 | // eslint-disable-next-line class-methods-use-this 18 | saveSession(key, session) { 19 | if (!session || Object.keys(session).length === 0) { 20 | return this.clearSession(key) 21 | } 22 | return Sessions.updateOne(key, {$set: {data: session}}, {upsert: true}) 23 | } 24 | 25 | // eslint-disable-next-line class-methods-use-this 26 | clearSession(key) { 27 | return Sessions.deleteOne(key) 28 | } 29 | 30 | // eslint-disable-next-line class-methods-use-this 31 | async getSession(key) { 32 | const session = await Sessions.findOne(key) 33 | if (!session) { 34 | return {} 35 | } 36 | return session.data 37 | } 38 | 39 | middleware() { 40 | return async (ctx, next) => { 41 | if (!ctx.chat || ctx.chat.type !== 'private') { 42 | return next() 43 | } 44 | const key = this.getSessionKey(ctx) 45 | if (!key) { 46 | return next() 47 | } 48 | let session = await this.getSession(key) 49 | Object.defineProperty(ctx, this.options.property, { 50 | get() { return session }, 51 | set(newValue) { session = {...newValue} } 52 | }) 53 | return next().then(() => this.saveSession(key, session)) 54 | } 55 | } 56 | } 57 | 58 | module.exports = MongoSession 59 | -------------------------------------------------------------------------------- /services/ton.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') 2 | const Tonweb = require('tonweb') 3 | 4 | const config = require('../config') 5 | 6 | const { 7 | HttpProvider, 8 | utils, 9 | } = Tonweb 10 | 11 | class TON { 12 | constructor(nodeUrl, indexUrl, nodeApiKey, indexApiKey) { 13 | this.index = axios.create({ 14 | baseURL: indexUrl, 15 | headers: { 'X-API-Key': indexApiKey }, 16 | }) 17 | this.node = new HttpProvider(nodeUrl, { apiKey: nodeApiKey }) 18 | this.tonweb = new Tonweb(this.node) 19 | this.utils = utils 20 | } 21 | 22 | async getTransactionsByMasterchainSeqno(seqno) { 23 | const response = await this.index.get('/getTransactionsByMasterchainSeqno', { 24 | params: { 25 | seqno, 26 | include_msg_body: false, 27 | }, 28 | }) 29 | 30 | if (response.status !== 200) { 31 | throw new Error('getTransactionsByMasterchainSeqno failed') 32 | } 33 | 34 | return response.data 35 | } 36 | } 37 | 38 | module.exports = new TON( 39 | config.get('ton.node'), 40 | config.get('ton.index'), 41 | config.get('ton.node_key'), 42 | config.get('ton.index_key'), 43 | ) 44 | -------------------------------------------------------------------------------- /services/transactionProcessor.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-continue */ 2 | /* eslint-disable no-await-in-loop */ 3 | const { Telegram, Extra } = require('telegraf') 4 | const LRUCache = require('lru-cache') 5 | const { promisify } = require('util') 6 | const { Big } = require('../utils/big') 7 | const config = require('../config') 8 | const log = require('../utils/log') 9 | const i18n = require('../i18n') 10 | const ton = require('./ton') 11 | const getPrice = require('../monitors/scanPrice') 12 | const AddressRepository = require('../repositories/address') 13 | const UserRepository = require('../repositories/user') 14 | const formatAddress = require('../utils/formatAddress') 15 | const formatTransactionValue = require('../utils/formatTransactionValue') 16 | const formatBalance = require('../utils/formatBalance') 17 | const formatTransactionPrice = require('../utils/formatTransactionPrice') 18 | const escapeHTML = require('../utils/escapeHTML') 19 | const getTitleByAddress = require('../monitors/addresses') 20 | const excludedAddresses = require('../data/excludedAddresses.json') 21 | const getPools = require('../monitors/pool') 22 | 23 | const timeout = promisify(setTimeout) 24 | 25 | const telegram = new Telegram(config.get('bot.token')) 26 | 27 | const addressRepository = new AddressRepository() 28 | const userRepository = new UserRepository() 29 | 30 | const NOTIFICATIONS_CHANNEL_ID = config.get('bot.notifications_channel_id') 31 | const MIN_TRANSACTION_AMOUNT = config.get('min_transaction_amount') 32 | 33 | const cache = new LRUCache({ 34 | ttl: 30 * 60 * 1000, // 30 minutes 35 | max: 1000, 36 | }) 37 | 38 | async function getBalance(address, seqno) { 39 | const cacheBalanceKey = `${address}:${seqno}` 40 | 41 | const cachedBalance = cache.get(cacheBalanceKey) 42 | if (cachedBalance) { 43 | return cachedBalance 44 | } 45 | 46 | const balance = await ton.node.getBalance(address) 47 | cache.set(cachedBalance, balance) 48 | return balance 49 | } 50 | 51 | async function sendTransactionMessage(addresses, transaction, transactionMeta) { 52 | const transactionSeqno = transactionMeta.seqno 53 | const transactionHash = transactionMeta.hash 54 | 55 | const fromBalance = await getBalance(transaction.from, transactionSeqno) 56 | const toBalance = await getBalance(transaction.to, transactionSeqno) 57 | 58 | const formattedFromBalance = 59 | fromBalance || fromBalance === 0 ? formatBalance(ton.utils.fromNano(fromBalance)) : '' 60 | const formattedToBalance = 61 | toBalance || toBalance === 0 ? formatBalance(ton.utils.fromNano(toBalance)) : '' 62 | 63 | const formattedTransactionValue = formatTransactionValue(transaction.value) 64 | 65 | const comment = transaction.comment ? escapeHTML(transaction.comment) : '' 66 | 67 | const transactionPrice = getPrice() 68 | ? formatTransactionPrice(new Big(transaction.value).mul(getPrice())) 69 | : '' 70 | 71 | // eslint-disable-next-line no-restricted-syntax, object-curly-newline 72 | for (const { _id, address, tag, user_id: userId } of addresses) { 73 | try { 74 | const user = await userRepository.getByTgId(userId) 75 | 76 | if (user.is_blocked || user.is_deactivated) { 77 | continue 78 | } 79 | 80 | const from = 81 | address === transaction.from 82 | ? { address, tag, user_id: userId } 83 | : addresses.find((el) => el.address === transaction.from && el.user_id === userId) 84 | const to = 85 | address === transaction.to 86 | ? { address, tag, user_id: userId } 87 | : addresses.find((el) => el.address === transaction.to && el.user_id === userId) 88 | 89 | const type = i18n.t( 90 | user.language, 91 | address === transaction.from ? 'transaction.send' : 'transaction.receive', 92 | ) 93 | 94 | const fromTag = from && from.tag ? from.tag : transaction.fromDefaultTag 95 | const toTag = to && to.tag ? to.tag : transaction.toDefaultTag 96 | 97 | const rawMessageText = i18n.t(user.language, 'transaction.message', { 98 | type, 99 | from: transaction.from, 100 | to: transaction.to, 101 | fromTag, 102 | toTag, 103 | fromBalance: 104 | formattedFromBalance && 105 | i18n.t(user.language, 'transaction.accountBalance', { value: formattedFromBalance }), 106 | toBalance: 107 | formattedToBalance && 108 | i18n.t(user.language, 'transaction.accountBalance', { value: formattedToBalance }), 109 | value: formattedTransactionValue, 110 | price: 111 | transactionPrice && 112 | i18n.t(user.language, 'transaction.price', { value: transactionPrice }), 113 | comment: comment && i18n.t(user.language, 'transaction.comment', { text: comment }), 114 | hash: transactionHash, 115 | }) 116 | 117 | await telegram.sendMessage(userId, rawMessageText, Extra.HTML().webPreview(false)) 118 | 119 | await addressRepository.incSendCoinsCounter(_id, 1) 120 | } catch (err) { 121 | if (err.code === 403) { 122 | log.error(`Transaction notification sending error: ${err}`) 123 | addressRepository.updateOneById(_id, { notifications: false }).then(() => { 124 | log.error(`Disable notification for ${address}`) 125 | }) 126 | } 127 | } 128 | await timeout(200) 129 | } 130 | 131 | if (transaction.sendToChannel) { 132 | const rawMessageText = i18n.t('en', 'transaction.channelMessage', { 133 | from: transaction.from, 134 | to: transaction.to, 135 | fromTag: transaction.fromDefaultTag, 136 | toTag: transaction.toDefaultTag, 137 | fromBalance: 138 | formattedFromBalance && 139 | i18n.t('en', 'transaction.accountBalance', { value: formattedFromBalance }), 140 | toBalance: 141 | formattedToBalance && 142 | i18n.t('en', 'transaction.accountBalance', { value: formattedToBalance }), 143 | value: formattedTransactionValue, 144 | price: transactionPrice && i18n.t('en', 'transaction.price', { value: transactionPrice }), 145 | comment: comment && i18n.t('en', 'transaction.comment', { text: comment }), 146 | hash: transactionHash, 147 | }) 148 | 149 | await telegram.sendMessage( 150 | NOTIFICATIONS_CHANNEL_ID, 151 | rawMessageText, 152 | Extra.HTML().webPreview(false), 153 | ).catch((err) => { 154 | log.error(`Transaction notification sending error: ${err}`) 155 | }) 156 | } 157 | } 158 | 159 | function checkIsPoolTransaction(transaction) { 160 | const inDestinationAddress = transaction.in_msg?.destination 161 | 162 | const pools = getPools() 163 | 164 | const isDestination = pools.find((pool) => pool.address === inDestinationAddress) 165 | 166 | if(isDestination && transaction.out_msgs.length === 0) { 167 | return true 168 | } 169 | 170 | const outSourceAddress = transaction.out_msgs[0]?.source 171 | 172 | if (!inDestinationAddress || !outSourceAddress) { 173 | return false 174 | } 175 | 176 | const isSource = pools.find((pool) => pool.address === outSourceAddress) 177 | 178 | return isDestination && isSource 179 | } 180 | 181 | module.exports = async (data, meta) => { 182 | const transaction = data 183 | const transactionHash = meta.hash 184 | const transactionSeqno = meta.seqno 185 | 186 | transaction.fromDefaultTag = getTitleByAddress(transaction.from) || 187 | formatAddress(transaction.from) 188 | transaction.toDefaultTag = getTitleByAddress(transaction.to) || formatAddress(transaction.to) 189 | 190 | if (excludedAddresses.includes(transaction.from) || excludedAddresses.includes(transaction.to)) { 191 | log.info(`Ignored ${excludedAddresses.includes(transaction.from) ? transaction.fromDefaultTag : transaction.toDefaultTag}`) 192 | return false 193 | } 194 | 195 | if (checkIsPoolTransaction(transaction.raw)) { 196 | log.info('Ignored pool transaction') 197 | return false 198 | } 199 | 200 | if (cache.get(transactionHash) !== undefined) { 201 | return false 202 | } 203 | cache.set(transactionHash, data) 204 | 205 | const addresses = await addressRepository.getByAddress([transaction.from, transaction.to], { 206 | is_deleted: false, 207 | 'notifications.is_enabled': true, 208 | $expr: { $gte: [transaction.nanoValue, '$notifications.min_amount'] }, 209 | }) 210 | 211 | const filteredAddresses = addresses.filter((v) => { 212 | const { exceptions, inclusion } = v.notifications 213 | 214 | if (exceptions.includes(transaction.comment)) { 215 | return false 216 | } 217 | 218 | if (inclusion.length) { 219 | return inclusion.includes(transaction.comment) 220 | } 221 | 222 | return true 223 | }) 224 | 225 | transaction.sendToChannel = (new Big(transaction.value).gte(MIN_TRANSACTION_AMOUNT)) 226 | 227 | if (filteredAddresses.length || transaction.sendToChannel) { 228 | // log.info(`Sending notify to users(${filteredAddresses.length}) or to channel(${transaction.sendToChannel ? '+' : '-'})`) 229 | await sendTransactionMessage( 230 | filteredAddresses, 231 | transaction, 232 | { 233 | hash: transactionHash, 234 | seqno: transactionSeqno, 235 | }, 236 | ) 237 | } 238 | 239 | await timeout(500) 240 | 241 | return true 242 | } 243 | -------------------------------------------------------------------------------- /syncIndexes.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const mongoose = require('mongoose') 3 | 4 | const config = require('./config') 5 | 6 | const modelsPath = './models' 7 | 8 | // eslint-disable-next-line import/no-dynamic-require 9 | const models = fs.readdirSync(modelsPath).map((v) => require(`${modelsPath}/${v}`)) 10 | 11 | void (async () => { 12 | await mongoose.connect(config.get('db')) 13 | 14 | for (const model of models) { 15 | console.log(`Updating indexes for ${model.modelName}`) 16 | await model.syncIndexes({background: true}) 17 | console.log(await model.listIndexes()) 18 | } 19 | console.log('done') 20 | process.exit(0) 21 | })() 22 | -------------------------------------------------------------------------------- /utils/big.js: -------------------------------------------------------------------------------- 1 | const Big = require('big.js') 2 | 3 | Big.DP = 40 4 | Big.PE = 100 5 | Big.NE = -100 6 | Big.RM = 0 7 | 8 | module.exports = { 9 | Big, 10 | } 11 | -------------------------------------------------------------------------------- /utils/escapeHTML.js: -------------------------------------------------------------------------------- 1 | const escapedChars = { 2 | '"': '"', 3 | '&': '&', 4 | '<': '<', 5 | '>': '>', 6 | } 7 | 8 | module.exports = (string) => { 9 | const chars = [...string] 10 | return chars.map((char) => escapedChars[char] || char).join('') 11 | } 12 | -------------------------------------------------------------------------------- /utils/formatAddress.js: -------------------------------------------------------------------------------- 1 | module.exports = (address) => `${address.slice(0, 5)}…${address.slice(-5)}` 2 | -------------------------------------------------------------------------------- /utils/formatBalance.js: -------------------------------------------------------------------------------- 1 | const { Big } = require('./big') 2 | const formatBigNumberStr = require('./formatBigNumberStr') 3 | 4 | module.exports = (value) => { 5 | const balance = new Big(value) 6 | if (balance.gte(1)) { 7 | return formatBigNumberStr(balance.toFixed(0, 0)) 8 | } 9 | const str = balance.toFixed(9, 0) 10 | let index = 1 11 | for (let i = str.length - 1; i >= 0; i--) { 12 | if (str[i] !== '0' && Number(str[i])) { 13 | index = i + 1 14 | break 15 | } 16 | } 17 | return str.slice(0, index) 18 | } 19 | -------------------------------------------------------------------------------- /utils/formatBigNumberStr.js: -------------------------------------------------------------------------------- 1 | module.exports = (str) => str.replace(/\B(? { 2 | const { exceptions, inclusion } = notifications 3 | const exceptionsList = exceptions.length 4 | ? i18n.t('address.notifications.exceptionsList', { list: exceptions.join(', ') }) 5 | : i18n.t('address.notifications.zeroExceptions') 6 | 7 | const inclusionList = inclusion.length 8 | ? i18n.t('address.notifications.inclusionList', { list: inclusion.join(', ') }) 9 | : i18n.t('address.notifications.zeroInclusion') 10 | 11 | return i18n.t( 12 | 'address.notifications.menu', 13 | { exceptions: exceptionsList, inclusion: inclusionList }, 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /utils/formatTag.js: -------------------------------------------------------------------------------- 1 | module.exports = (tag) => (tag ? ` (${tag})` : '') 2 | -------------------------------------------------------------------------------- /utils/formatTransactionPrice.js: -------------------------------------------------------------------------------- 1 | const formatBigNumberStr = require('./formatBigNumberStr') 2 | 3 | module.exports = (price) => { 4 | if (price.lt(0.01)) { 5 | return '' 6 | } 7 | return price.gte(1) ? formatBigNumberStr(price.toFixed(0, 0)) : price.toFixed(2, 0) 8 | } 9 | -------------------------------------------------------------------------------- /utils/formatTransactionValue.js: -------------------------------------------------------------------------------- 1 | const { Big } = require('./big') 2 | const formatBigNumberStr = require('./formatBigNumberStr') 3 | 4 | module.exports = (str) => { 5 | if (new Big(str).gte(10)) { 6 | str = new Big(str).toFixed(0, 0) 7 | } 8 | return formatBigNumberStr(str) 9 | } 10 | -------------------------------------------------------------------------------- /utils/log.js: -------------------------------------------------------------------------------- 1 | const winston = require('winston') 2 | const chalk = require('chalk') 3 | 4 | module.exports = winston.createLogger({ 5 | level: 'debug', 6 | transports: [ 7 | new winston.transports.Console(), 8 | ], 9 | format: winston.format.combine( 10 | winston.format.timestamp(), 11 | winston.format.printf((info) => { 12 | const color = winston.format.colorize.Colorizer.allColors[info.level] 13 | return [ 14 | chalk[color](`${info.level[0].toUpperCase()} `), 15 | `${info.timestamp} `, 16 | info.message, 17 | ].join('') 18 | }), 19 | ), 20 | }) 21 | -------------------------------------------------------------------------------- /utils/sleep.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)), 3 | } 4 | --------------------------------------------------------------------------------