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