diff --git a/app.js b/app.ts similarity index 62% rename from app.js rename to app.ts index 2fbc6604..1809beec 100644 --- a/app.js +++ b/app.ts @@ -1,9 +1,11 @@ -require('dotenv').config(); -const { SocksProxyAgent } = require('socks-proxy-agent'); -const { start } = require('./bot'); -const mongoConnect = require('./db_connect'); +import * as dotenv from "dotenv"; +dotenv.config() +import { SocksProxyAgent } from "socks-proxy-agent"; +import { MainContext, start } from "./bot/start"; +import mongoConnect from './db_connect' const { resubscribeInvoices } = require('./ln'); -const logger = require('./logger'); +import logger from "./logger"; +import { Telegraf } from "telegraf"; const { delay } = require('./util'); (async () => { @@ -23,7 +25,7 @@ const { delay } = require('./util'); mongoose.connection .once('open', async () => { logger.info('Connected to Mongo instance.'); - let options = { handlerTimeout: 60000 }; + let options: Partial> = { handlerTimeout: 60000 }; if (process.env.SOCKS_PROXY_HOST) { const agent = new SocksProxyAgent(process.env.SOCKS_PROXY_HOST); options = { @@ -32,10 +34,11 @@ const { delay } = require('./util'); }, }; } - const bot = start(process.env.BOT_TOKEN, options); + const bot = start(String(process.env.BOT_TOKEN), options); // Wait 1 seconds before try to resubscribe hold invoices await delay(1000); await resubscribeInvoices(bot); }) - .on('error', error => logger.error(`Error connecting to Mongo: ${error}`)); + .on('error', (error: Error) => logger.error(`Error connecting to Mongo: ${error}`)); })(); + diff --git a/bot/messages.ts b/bot/messages.ts index 8bdf299c..a49ccac7 100644 --- a/bot/messages.ts +++ b/bot/messages.ts @@ -1,5 +1,5 @@ -const { TelegramError } = require('telegraf'); -const QR = require('qrcode'); +import { TelegramError } from 'telegraf' +import QR from 'qrcode'; const { getCurrency, numberFormat, @@ -12,7 +12,7 @@ const { decimalRound, getUserAge, } = require('../util'); -const logger = require('../logger'); +import logger from "../logger"; import { MainContext } from './start'; import { UserDocument } from '../models/user' import { IOrder } from '../models/order' @@ -109,7 +109,7 @@ const invoicePaymentRequestMessage = async ( } }; -const pendingSellMessage = async (ctx: MainContext, user: UserDocument, order: IOrder, channel: string, i18n: I18nContext) => { +const pendingSellMessage = async (ctx: Telegraf, user: UserDocument, order: IOrder, channel: string, i18n: I18nContext) => { try { const orderExpirationWindow = Number(process.env.ORDER_PUBLISHED_EXPIRATION_WINDOW) / 60 / 60; @@ -618,7 +618,7 @@ const publishBuyOrderMessage = async ( }; const publishSellOrderMessage = async ( - ctx: MainContext, + ctx: Telegraf, user: UserDocument, order: IOrder, i18n: I18nContext, @@ -1549,7 +1549,7 @@ const currencyNotSupportedMessage = async (ctx: MainContext, currencies: Array { +const notAuthorized = async (ctx: MainContext, tgId?: string) => { try { if (tgId) { await ctx.telegram.sendMessage(tgId, ctx.i18n.t('not_authorized')); @@ -1606,7 +1606,7 @@ const showConfirmationButtons = async (ctx: MainContext, orders: Array, } }; -module.exports = { +export { startMessage, initBotErrorMessage, invoicePaymentRequestMessage, diff --git a/bot/start.ts b/bot/start.ts index c522f625..cb5868b2 100644 --- a/bot/start.ts +++ b/bot/start.ts @@ -3,17 +3,16 @@ import { I18n, I18nContext } from '@grammyjs/i18n'; import { Message } from 'typegram' import { UserDocument } from '../models/user' import { FilterQuery } from 'mongoose'; - -const { limit } = require('@grammyjs/ratelimiter'); -const schedule = require('node-schedule'); -const { +import { limit } from "@grammyjs/ratelimiter" +import schedule from 'node-schedule'; +import { Order, User, PendingPayment, Community, Dispute, Config, -} = require('../models'); +} from '../models'; const { getCurrenciesWithPrice, deleteOrderFromChannel } = require('../util'); const { commandArgsMiddleware, @@ -56,7 +55,7 @@ const { validateInvoice, validateLightningAddress, } = require('./validations'); -const messages = require('./messages'); +import * as messages from './messages'; const { attemptPendingPayments, cancelOrders, @@ -66,7 +65,8 @@ const { deleteCommunity, nodeInfo, } = require('../jobs'); -const logger = require('../logger'); +import logger from "../logger"; +import { ICommunity, IUsernameId } from '../models/community'; export interface MainContext extends Context { match: Array | null; @@ -75,7 +75,7 @@ export interface MainContext extends Context { admin: UserDocument; } -interface OrderQuery { +export interface OrderQuery { status?: string; buyer_id?: string; seller_id?: string; @@ -125,6 +125,7 @@ const askForConfirmation = async (user: UserDocument, command: string) => { return []; } catch (error) { logger.error(error); + return null; } }; @@ -137,7 +138,7 @@ has the same condition. The problem mentioned above is similar to this issue: https://github.com/telegraf/telegraf/issues/1319#issuecomment-766360594 */ -const ctxUpdateAssertMsg = "ctx.update.message.text is not available."; +export const ctxUpdateAssertMsg = "ctx.update.message.text is not available."; const initialize = (botToken: string, options: Partial>): Telegraf => { const i18n = new I18n({ @@ -209,7 +210,7 @@ const initialize = (botToken: string, options: Partial'); if (!val) return; let config = await Config.findOne(); - if (!config) { + if (config === null) { config = new Config(); } config.maintenance = false; @@ -256,11 +257,11 @@ const initialize = (botToken: string, options: Partial el); + const [command, orderId] = params.filter((el: string) => el); if (!orderId) { const orders = await askForConfirmation(ctx.user, command); - if (!orders.length) return await ctx.reply(`${command} `); + if (orders === null || orders.length === 0) return await ctx.reply(`${command} `); return await messages.showConfirmationButtons(ctx, orders, command); } else if (!(await validateObjectId(ctx, orderId))) { @@ -283,7 +284,7 @@ const initialize = (botToken: string, options: Partial el); + const [command, orderId] = params.filter((el: string) => el); if (!orderId) { const orders = await askForConfirmation(ctx.user, command); - if (!orders.length) return await ctx.reply(`${command} `); + if (orders === null || orders.length === 0) return await ctx.reply(`${command} `); return await messages.showConfirmationButtons(ctx, orders, command); } else if (!(await validateObjectId(ctx, orderId))) { @@ -394,7 +396,7 @@ const initialize = (botToken: string, options: Partial el); + const [command, orderId] = params.filter((el: string) => el); if (!orderId) { const orders = await askForConfirmation(ctx.user, command); - if (!orders.length) return await ctx.reply(`${command} `); + if (orders === null || orders.length === 0) return await ctx.reply(`${command} `); return await messages.showConfirmationButtons(ctx, orders, command); } else if (!(await validateObjectId(ctx, orderId))) { @@ -554,7 +557,7 @@ const initialize = (botToken: string, options: Partial el.id !== user.id + if (community === null) throw Error("Community was not found in DB"); + community.banned_users = community.banned_users.toObject().filter( + (el: IUsernameId) => el.id !== user.id ); await community.save(); } else { @@ -821,6 +826,7 @@ const initialize = (botToken: string, options: Partial { try { const config = await Config.findOne({}); + if (config === null) throw Error("Config was not found in DB"); await messages.showInfoMessage(ctx, ctx.user, config); } catch (error) { logger.error(error); diff --git a/bot/validations.js b/bot/validations.ts similarity index 75% rename from bot/validations.js rename to bot/validations.ts index a3288bd2..e6ce3881 100644 --- a/bot/validations.js +++ b/bot/validations.ts @@ -1,18 +1,32 @@ +import { MainContext, OrderQuery, ctxUpdateAssertMsg } from "./start"; +import { ICommunity } from "../models/community"; +import { FilterQuery } from "mongoose"; +import { UserDocument } from "../models/user"; +import { IOrder } from "../models/order"; +import { Telegraf } from "telegraf"; + const { parsePaymentRequest } = require('invoices'); const { ObjectId } = require('mongoose').Types; -const messages = require('./messages'); -const { Order, User, Community } = require('../models'); +import * as messages from './messages'; +import { Order, User, Community } from '../models'; const { isIso4217, isDisputeSolver } = require('../util'); const { existLightningAddress } = require('../lnurl/lnurl-pay'); const logger = require('../logger'); // We look in database if the telegram user exists, // if not, it creates a new user -const validateUser = async (ctx, start) => { +const validateUser = async (ctx: MainContext, start: boolean) => { try { - const tgUser = ctx.update.callback_query - ? ctx.update.callback_query.from - : ctx.update.message.from; + let tgUser = null; + if (("callback_query" in ctx.update) && ctx.update.callback_query) { + tgUser = ctx.update.callback_query.from; + } + else if (("message" in ctx.update) && ctx.update.message) { + tgUser = ctx.update.message.from; + } + else { + throw new Error(ctxUpdateAssertMsg); + } // We need to make sure the user has a username if (!tgUser.username) { return await messages.nonHandleErrorMessage(ctx); @@ -45,15 +59,18 @@ const validateUser = async (ctx, start) => { } }; -const validateSuperAdmin = async (ctx, id) => { +const validateSuperAdmin = async (ctx: MainContext, id?: string) => { try { + if (!('message' in ctx.update) || !('text' in ctx.update.message)) { + throw new Error(ctxUpdateAssertMsg); + } const tgUserId = id || ctx.update.message.from.id; const user = await User.findOne({ tg_id: tgUserId }); // If the user never started the bot we can't send messages // to that user, so we do nothing - if (!user) return; + if (user === null) return; - if (!user.admin) return await messages.notAuthorized(ctx, tgUserId); + if (!user.admin) return await messages.notAuthorized(ctx, tgUserId.toString()); return user; } catch (error) { @@ -62,22 +79,26 @@ const validateSuperAdmin = async (ctx, id) => { } }; -const validateAdmin = async (ctx, id) => { +const validateAdmin = async (ctx: MainContext, id?: string) => { try { + if (!('message' in ctx.update) || !('text' in ctx.update.message)) { + throw new Error(ctxUpdateAssertMsg); + } const tgUserId = id || ctx.update.message.from.id; const user = await User.findOne({ tg_id: tgUserId }); // If the user never started the bot we can't send messages // to that user, so we do nothing - if (!user) return; + if (user === null) return; let community = null; if (user.default_community_id) community = await Community.findOne({ _id: user.default_community_id }); + if (community === null) throw Error("Community was not found in DB"); const isSolver = isDisputeSolver(community, user); if (!user.admin && !isSolver) - return await messages.notAuthorized(ctx, tgUserId); + return await messages.notAuthorized(ctx, tgUserId.toString()); return user; } catch (error) { @@ -86,7 +107,7 @@ const validateAdmin = async (ctx, id) => { } }; -const validateSellOrder = async ctx => { +const validateSellOrder = async (ctx: MainContext) => { try { const args = ctx.state.command.args; if (args.length < 4) { @@ -130,11 +151,11 @@ const validateSellOrder = async ctx => { return false; } - if (amount !== 0 && amount < process.env.MIN_PAYMENT_AMT) { + if (amount !== 0 && amount < Number(process.env.MIN_PAYMENT_AMT)) { await messages.mustBeGreatherEqThan( ctx, 'monto_en_sats', - process.env.MIN_PAYMENT_AMT + Number(process.env.MIN_PAYMENT_AMT) ); return false; } @@ -149,7 +170,7 @@ const validateSellOrder = async ctx => { return false; } - if (fiatAmount.some(x => x < 1)) { + if (fiatAmount.some((x: number) => x < 1)) { await messages.mustBeGreatherEqThan(ctx, 'monto_en_fiat', 1); return false; } @@ -174,7 +195,7 @@ const validateSellOrder = async ctx => { } }; -const validateBuyOrder = async ctx => { +const validateBuyOrder = async (ctx: MainContext) => { try { const args = ctx.state.command.args; if (args.length < 4) { @@ -216,11 +237,11 @@ const validateBuyOrder = async ctx => { return false; } - if (amount !== 0 && amount < process.env.MIN_PAYMENT_AMT) { + if (amount !== 0 && amount < Number(process.env.MIN_PAYMENT_AMT)) { await messages.mustBeGreatherEqThan( ctx, 'monto_en_sats', - process.env.MIN_PAYMENT_AMT + Number(process.env.MIN_PAYMENT_AMT) ); return false; } @@ -235,7 +256,7 @@ const validateBuyOrder = async ctx => { return false; } - if (fiatAmount.some(x => x < 1)) { + if (fiatAmount.some((x: number) => x < 1)) { await messages.mustBeGreatherEqThan(ctx, 'monto_en_fiat', 1); return false; } @@ -259,7 +280,7 @@ const validateBuyOrder = async ctx => { return false; } }; -const validateLightningAddress = async lightningAddress => { +const validateLightningAddress = async (lightningAddress: string) => { const pattern = /^[\w-.]+@([\w-]+.)+[\w-]{2,4}$/g; return ( @@ -267,18 +288,19 @@ const validateLightningAddress = async lightningAddress => { ); }; -const validateInvoice = async (ctx, lnInvoice) => { +const validateInvoice = async (ctx: MainContext, lnInvoice: string) => { try { const invoice = parsePaymentRequest({ request: lnInvoice }); const latestDate = new Date( - Date.now() + parseInt(process.env.INVOICE_EXPIRATION_WINDOW) + Date.now() + Number(process.env.INVOICE_EXPIRATION_WINDOW) ).toISOString(); - if (!!invoice.tokens && invoice.tokens < process.env.MIN_PAYMENT_AMT) { + if (!("MAIN_PAYMENT_AMT" in process.env)) throw Error("MIN_PAYMENT_AMT not found, please check .env file"); + if (!!invoice.tokens && invoice.tokens < Number(process.env.MIN_PAYMENT_AMT)) { await messages.minimunAmountInvoiceMessage(ctx); return false; } - if (new Date(invoice.expires_at) < latestDate) { + if (new Date(invoice.expires_at).toISOString() < latestDate) { await messages.minimunExpirationTimeInvoiceMessage(ctx); return false; } @@ -306,20 +328,20 @@ const validateInvoice = async (ctx, lnInvoice) => { } }; -const isValidInvoice = async (ctx, lnInvoice) => { +const isValidInvoice = async (ctx: MainContext, lnInvoice: string) => { try { const invoice = parsePaymentRequest({ request: lnInvoice }); const latestDate = new Date( - Date.now() + parseInt(process.env.INVOICE_EXPIRATION_WINDOW) + Date.now() + Number(process.env.INVOICE_EXPIRATION_WINDOW) ).toISOString(); - if (!!invoice.tokens && invoice.tokens < process.env.MIN_PAYMENT_AMT) { + if (!!invoice.tokens && invoice.tokens < Number(process.env.MIN_PAYMENT_AMT)) { await messages.invoiceMustBeLargerMessage(ctx); return { success: false, }; } - if (new Date(invoice.expires_at) < latestDate) { + if (new Date(invoice.expires_at).toISOString() < latestDate) { await messages.invoiceExpiryTooShortMessage(ctx); return { success: false, @@ -359,7 +381,7 @@ const isValidInvoice = async (ctx, lnInvoice) => { } }; -const isOrderCreator = (user, order) => { +const isOrderCreator = (user: UserDocument, order: IOrder) => { try { return user._id == order.creator_id; } catch (error) { @@ -368,7 +390,7 @@ const isOrderCreator = (user, order) => { } }; -const validateTakeSellOrder = async (ctx, bot, user, order) => { +const validateTakeSellOrder = async (ctx: MainContext, bot: Telegraf, user: UserDocument, order: IOrder) => { try { if (!order) { await messages.invalidOrderMessage(ctx, bot, user); @@ -397,10 +419,10 @@ const validateTakeSellOrder = async (ctx, bot, user, order) => { } }; -const validateTakeBuyOrder = async (ctx, bot, user, order) => { +const validateTakeBuyOrder = async (ctx: MainContext, bot: Telegraf, user: UserDocument, order: IOrder) => { try { if (!order) { - await messages.invalidOrderMessage(bot, user); + await messages.invalidOrderMessage(ctx, bot, user); return false; } if (isOrderCreator(user, order) && process.env.NODE_ENV === 'production') { @@ -422,9 +444,9 @@ const validateTakeBuyOrder = async (ctx, bot, user, order) => { } }; -const validateReleaseOrder = async (ctx, user, orderId) => { +const validateReleaseOrder = async (ctx: MainContext, user: UserDocument, orderId: string) => { try { - let where = { + let where: FilterQuery = { seller_id: user._id, status: 'WAITING_BUYER_INVOICE', _id: orderId, @@ -453,7 +475,7 @@ const validateReleaseOrder = async (ctx, user, orderId) => { } order = await Order.findOne(where); - if (!order) { + if (order === null) { await messages.notActiveOrderMessage(ctx); return false; } @@ -465,7 +487,7 @@ const validateReleaseOrder = async (ctx, user, orderId) => { } }; -const validateDisputeOrder = async (ctx, user, orderId) => { +const validateDisputeOrder = async (ctx: MainContext, user: UserDocument, orderId: string) => { try { const where = { $and: [ @@ -477,7 +499,7 @@ const validateDisputeOrder = async (ctx, user, orderId) => { const order = await Order.findOne(where); - if (!order) { + if (order === null) { await messages.notActiveOrderMessage(ctx); return false; } @@ -489,9 +511,9 @@ const validateDisputeOrder = async (ctx, user, orderId) => { } }; -const validateFiatSentOrder = async (ctx, user, orderId) => { +const validateFiatSentOrder = async (ctx: MainContext, user: UserDocument, orderId: string) => { try { - const where = { + const where: FilterQuery = { $and: [ { buyer_id: user._id }, { $or: [{ status: 'ACTIVE' }, { status: 'PAID_HOLD_INVOICE' }] }, @@ -502,7 +524,7 @@ const validateFiatSentOrder = async (ctx, user, orderId) => { where._id = orderId; } const order = await Order.findOne(where); - if (!order) { + if (order === null) { await messages.notActiveOrderMessage(ctx); return false; } @@ -525,7 +547,7 @@ const validateFiatSentOrder = async (ctx, user, orderId) => { }; // If a seller have an order with status FIAT_SENT, return false -const validateSeller = async (ctx, user) => { +const validateSeller = async (ctx: MainContext, user: UserDocument) => { try { const where = { seller_id: user._id, @@ -546,10 +568,13 @@ const validateSeller = async (ctx, user) => { } }; -const validateParams = async (ctx, paramNumber, errOutputString) => { +const validateParams = async (ctx: MainContext, paramNumber: number, errOutputString: string): Promise> => { try { + if (!('message' in ctx.update) || !('text' in ctx.update.message)) { + throw new Error(ctxUpdateAssertMsg); + } const paramsArray = ctx.update.message.text.split(' '); - const params = paramsArray.filter(el => el !== ''); + const params = paramsArray.filter((el: string) => el !== ''); if (params.length !== paramNumber) { await messages.customMessage( ctx, @@ -562,11 +587,11 @@ const validateParams = async (ctx, paramNumber, errOutputString) => { return params.slice(1); } catch (error) { logger.error(error); - return false; + return null; } }; -const validateObjectId = async (ctx, id) => { +const validateObjectId = async (ctx: MainContext, id: string) => { try { if (!ObjectId.isValid(id)) { await messages.notValidIdMessage(ctx); @@ -580,10 +605,10 @@ const validateObjectId = async (ctx, id) => { } }; -const validateUserWaitingOrder = async (ctx, bot, user) => { +const validateUserWaitingOrder = async (ctx: MainContext, bot: Telegraf, user: UserDocument) => { try { // If is a seller - let where = { + let where: FilterQuery = { seller_id: user._id, status: 'WAITING_PAYMENT', }; @@ -610,12 +635,12 @@ const validateUserWaitingOrder = async (ctx, bot, user) => { }; // We check if the user is banned from the community in the order -const isBannedFromCommunity = async (user, communityId) => { +const isBannedFromCommunity = async (user: UserDocument, communityId: string) => { try { if (!communityId) return false; const community = await Community.findOne({ _id: communityId }); if (!community) return false; - return community.banned_users.some(buser => buser.id == user._id); + return community.banned_users.toObject().some((buser: ICommunity) => buser.id == user._id); } catch (error) { logger.error(error); return false; diff --git a/db_connect.js b/db_connect.ts similarity index 81% rename from db_connect.js rename to db_connect.ts index e38fbd10..e93a8f7b 100644 --- a/db_connect.js +++ b/db_connect.ts @@ -1,5 +1,5 @@ -const mongoose = require('mongoose'); -const logger = require('./logger'); +import mongoose, { ConnectOptions } from "mongoose"; +import logger from "./logger"; // connect to database const credentials = process.env.DB_USER @@ -16,8 +16,8 @@ const connect = () => { mongoose.connect(MONGO_URI, { useNewUrlParser: true, useUnifiedTopology: true, - }); + } as ConnectOptions); return mongoose; }; -module.exports = connect; +export default connect; diff --git a/jobs/calculate_community_earnings.js b/jobs/calculate_community_earnings.ts similarity index 78% rename from jobs/calculate_community_earnings.js rename to jobs/calculate_community_earnings.ts index 56151d48..54e59b23 100644 --- a/jobs/calculate_community_earnings.js +++ b/jobs/calculate_community_earnings.ts @@ -1,5 +1,5 @@ -const { Order, Community } = require('../models'); -const logger = require('../logger'); +import { Order, Community } from '../models'; +import logger from "../logger"; const calculateEarnings = async () => { try { @@ -12,9 +12,9 @@ const calculateEarnings = async () => { for (const order of orders) { const amount = order.amount; const fee = order.fee; - const botFee = order.bot_fee || parseFloat(process.env.MAX_FEE); + const botFee = order.bot_fee || Number(process.env.MAX_FEE); const communityFeePercent = - order.community_fee || parseFloat(process.env.FEE_PERCENT); + order.community_fee || Number(process.env.FEE_PERCENT); const maxFee = amount * botFee; const communityFee = fee - maxFee * communityFeePercent; const earnings = earningsMap.get(order.community_id) || [0, 0]; @@ -27,6 +27,7 @@ const calculateEarnings = async () => { } for (const [communityId, earnings] of earningsMap) { const community = await Community.findById(communityId); + if (community === null) throw Error("Community was not found in DB"); const amount = Math.round(earnings[0]); community.earnings = community.earnings + amount; community.orders_to_redeem = community.orders_to_redeem + earnings[1]; @@ -36,9 +37,9 @@ const calculateEarnings = async () => { ); } } catch (error) { - const message = error.toString(); + const message = String(error); logger.error(`calculateEarnings catch error: ${message}`); } }; -module.exports = calculateEarnings; +export default calculateEarnings; diff --git a/jobs/cancel_orders.js b/jobs/cancel_orders.ts similarity index 85% rename from jobs/cancel_orders.js rename to jobs/cancel_orders.ts index 00cef019..0879a056 100644 --- a/jobs/cancel_orders.js +++ b/jobs/cancel_orders.ts @@ -1,15 +1,17 @@ -const { User, Order } = require('../models'); +import { Telegraf } from "telegraf"; +import { MainContext } from "../bot/start"; +import { User, Order } from "../models"; const { cancelShowHoldInvoice, cancelAddInvoice } = require('../bot/commands'); -const messages = require('../bot/messages'); -const { getUserI18nContext, holdInvoiceExpirationInSecs } = require('../util'); -const logger = require('../logger'); +import * as messages from "../bot/messages"; +import { getUserI18nContext, holdInvoiceExpirationInSecs } from '../util'; +import logger from "../logger"; -const cancelOrders = async bot => { +const cancelOrders = async (bot: Telegraf) => { try { const holdInvoiceTime = new Date(); holdInvoiceTime.setSeconds( holdInvoiceTime.getSeconds() - - parseInt(process.env.HOLD_INVOICE_EXPIRATION_WINDOW) + Number(process.env.HOLD_INVOICE_EXPIRATION_WINDOW) ); // We get the orders where the seller didn't pay the hold invoice before expired // or where the buyer didn't add the invoice @@ -45,6 +47,7 @@ const cancelOrders = async bot => { for (const order of activeOrders) { const buyerUser = await User.findOne({ _id: order.buyer_id }); const sellerUser = await User.findOne({ _id: order.seller_id }); + if (buyerUser === null || sellerUser === null) return; const i18nCtxBuyer = await getUserI18nContext(buyerUser); const i18nCtxSeller = await getUserI18nContext(sellerUser); // Instead of cancel this order we should send this to the admins @@ -70,7 +73,7 @@ const cancelOrders = async bot => { // Now we cancel orders expired // ============================== orderTime = new Date(); - let orderExpirationTime = parseInt( + let orderExpirationTime = Number( process.env.ORDER_PUBLISHED_EXPIRATION_WINDOW ); orderExpirationTime = orderExpirationTime + orderExpirationTime * 0.2; @@ -96,4 +99,4 @@ const cancelOrders = async bot => { } }; -module.exports = cancelOrders; +export default cancelOrders; diff --git a/jobs/communities.js b/jobs/communities.ts similarity index 70% rename from jobs/communities.js rename to jobs/communities.ts index e9ddb503..c57b5d49 100644 --- a/jobs/communities.js +++ b/jobs/communities.ts @@ -1,12 +1,15 @@ -const { Order, Community } = require('../models'); -const logger = require('../logger'); +import { Telegraf } from "telegraf"; +import { MainContext } from "../bot/start"; -const deleteCommunity = async bot => { +import { Order, Community } from '../models'; +import logger from "../logger"; + +const deleteCommunity = async (bot: Telegraf) => { try { const communities = await Community.find(); for (const community of communities) { // Delete communities with COMMUNITY_TTL days without a successful order - const days = 86400 * parseInt(process.env.COMMUNITY_TTL); + const days = 86400 * Number(process.env.COMMUNITY_TTL); const time = new Date(); time.setSeconds(time.getSeconds() - days); // If is a new community we don't do anything @@ -26,9 +29,9 @@ const deleteCommunity = async bot => { } } } catch (error) { - const message = error.toString(); + const message = String(error); logger.error(`deleteCommunity catch error: ${message}`); } }; -module.exports = deleteCommunity; +export default deleteCommunity; diff --git a/jobs/delete_published_orders.js b/jobs/delete_published_orders.ts similarity index 73% rename from jobs/delete_published_orders.js rename to jobs/delete_published_orders.ts index b4e284ae..18c2134a 100644 --- a/jobs/delete_published_orders.js +++ b/jobs/delete_published_orders.ts @@ -1,13 +1,16 @@ -const { Order } = require('../models'); +import { Telegraf } from "telegraf"; +import { MainContext } from "../bot/start"; + +import { Order } from '../models'; const { deleteOrderFromChannel } = require('../util'); -const logger = require('../logger'); +import logger from "../logger"; -const deleteOrders = async bot => { +const deleteOrders = async (bot: Telegraf) => { try { const windowTime = new Date(); windowTime.setSeconds( windowTime.getSeconds() - - parseInt(process.env.ORDER_PUBLISHED_EXPIRATION_WINDOW) + Number(process.env.ORDER_PUBLISHED_EXPIRATION_WINDOW) ); // We get the pending orders where time is expired const pendingOrders = await Order.find({ @@ -25,9 +28,9 @@ const deleteOrders = async bot => { await deleteOrderFromChannel(orderCloned, bot.telegram); } } catch (error) { - const message = error.toString(); + const message = String(error); logger.error(`deleteOrders catch error: ${message}`); } }; -module.exports = deleteOrders; +export default deleteOrders; diff --git a/jobs/index.js b/jobs/index.js deleted file mode 100644 index b784f178..00000000 --- a/jobs/index.js +++ /dev/null @@ -1,19 +0,0 @@ -const { - attemptPendingPayments, - attemptCommunitiesPendingPayments, -} = require('./pending_payments'); -const cancelOrders = require('./cancel_orders'); -const deleteOrders = require('./delete_published_orders'); -const calculateEarnings = require('./calculate_community_earnings'); -const deleteCommunity = require('./communities'); -const nodeInfo = require('./node_info'); - -module.exports = { - attemptPendingPayments, - cancelOrders, - deleteOrders, - calculateEarnings, - attemptCommunitiesPendingPayments, - deleteCommunity, - nodeInfo, -}; diff --git a/jobs/index.ts b/jobs/index.ts new file mode 100644 index 00000000..281f16f6 --- /dev/null +++ b/jobs/index.ts @@ -0,0 +1,19 @@ +import { + attemptPendingPayments, + attemptCommunitiesPendingPayments, +} from "./pending_payments"; +import cancelOrders from "./cancel_orders"; +import deleteOrders from "./delete_published_orders"; +import calculateEarnings from './calculate_community_earnings' +import deleteCommunity from './communities' +import nodeInfo from './node_info' + +export { + attemptPendingPayments, + cancelOrders, + deleteOrders, + calculateEarnings, + attemptCommunitiesPendingPayments, + deleteCommunity, + nodeInfo, +}; diff --git a/jobs/node_info.js b/jobs/node_info.ts similarity index 51% rename from jobs/node_info.js rename to jobs/node_info.ts index bad5d904..0caa7c5b 100644 --- a/jobs/node_info.js +++ b/jobs/node_info.ts @@ -1,10 +1,14 @@ -const { Config } = require('../models'); +import { Telegraf } from "telegraf"; +import { MainContext } from "../bot/start"; + +import { Config } from '../models'; const { getInfo } = require('../ln'); -const logger = require('../logger'); +import logger from "../logger"; -const info = async bot => { +const info = async (bot: Telegraf) => { try { const config = await Config.findOne({}); + if (config === null) throw Error("Config not found in DB"); const info = await getInfo(); if (info.is_synced_to_chain) { config.node_status = 'up'; @@ -12,9 +16,9 @@ const info = async bot => { config.node_uri = info.uris[0]; await config.save(); } catch (error) { - const message = error.toString(); + const message = String(error); logger.error(`node info catch error: ${message}`); } }; -module.exports = info; +export default info; diff --git a/jobs/pending_payments.ts b/jobs/pending_payments.ts index 80b79135..3cd8e295 100644 --- a/jobs/pending_payments.ts +++ b/jobs/pending_payments.ts @@ -1,13 +1,13 @@ -const { payRequest, isPendingPayment } = require('../ln'); -const { PendingPayment, Order, User, Community } = require('../models'); -const messages = require('../bot/messages'); -const { getUserI18nContext } = require('../util'); -const logger = require('../logger'); +import { PendingPayment, Order, User, Community } from '../models'; +import * as messages from '../bot/messages'; +import logger from "../logger"; import { Telegraf } from 'telegraf'; import { I18nContext } from '@grammyjs/i18n'; import { MainContext } from '../bot/start'; +const { payRequest, isPendingPayment } = require('../ln'); +const { getUserI18nContext } = require('../util'); -exports.attemptPendingPayments = async (bot: Telegraf): Promise => { +export const attemptPendingPayments = async (bot: Telegraf): Promise => { const pendingPayments = await PendingPayment.find({ paid: false, attempts: { $lt: process.env.PAYMENT_ATTEMPTS }, @@ -17,6 +17,7 @@ exports.attemptPendingPayments = async (bot: Telegraf): Promise): Promise): Promise): Promise): Promise => { +export const attemptCommunitiesPendingPayments = async (bot: Telegraf): Promise => { const pendingPayments = await PendingPayment.find({ paid: false, attempts: { $lt: process.env.PAYMENT_ATTEMPTS }, @@ -134,6 +139,7 @@ exports.attemptCommunitiesPendingPayments = async (bot: Telegraf): request: pending.payment_request, }); const user = await User.findById(pending.user_id); + if (user === null) throw Error("User was not found in DB"); const i18nCtx: I18nContext = await getUserI18nContext(user); // If the buyer's invoice is expired we let it know and don't try to pay again if (!!payment && payment.is_expired) { @@ -145,9 +151,10 @@ exports.attemptCommunitiesPendingPayments = async (bot: Telegraf): } const community = await Community.findById(pending.community_id); + if (community === null) throw Error("Community was not found in DB"); if (!!payment && !!payment.confirmed_at) { pending.paid = true; - pending.paid_at = new Date().toISOString(); + pending.paid_at = new Date(); // Reset the community's values community.earnings = 0; diff --git a/ln/connect.js b/ln/connect.ts similarity index 89% rename from ln/connect.js rename to ln/connect.ts index 167d5445..f82b5c04 100644 --- a/ln/connect.js +++ b/ln/connect.ts @@ -1,7 +1,7 @@ -const fs = require('fs'); -const path = require('path'); -const lightning = require('lightning'); -const logger = require('../logger'); +import * as fs from 'fs'; +import * as path from 'path'; +import * as lightning from "lightning"; +import logger from "../logger"; const { authenticatedLndGrpc } = lightning; @@ -47,4 +47,4 @@ const { lnd } = authenticatedLndGrpc({ socket, }); -module.exports = lnd; +export { lnd }; diff --git a/ln/hold_invoice.js b/ln/hold_invoice.ts similarity index 61% rename from ln/hold_invoice.js rename to ln/hold_invoice.ts index 1a36c39c..201d644c 100644 --- a/ln/hold_invoice.js +++ b/ln/hold_invoice.ts @@ -1,26 +1,26 @@ -const { createHash, randomBytes } = require('crypto'); -const lightning = require('lightning'); -const lnd = require('./connect'); -const logger = require('../logger'); +import { randomBytes, createHash } from 'crypto'; +import * as lightning from "lightning"; +import { lnd } from './connect' +import logger from "../logger"; -const createHoldInvoice = async ({ description, amount }) => { +const createHoldInvoice = async (description: string, amount: number ) => { try { const randomSecret = () => randomBytes(32); - const sha256 = buffer => createHash('sha256').update(buffer).digest('hex'); + const sha256 = (buffer: Buffer): string => createHash('sha256').update(buffer).digest('hex'); // We create a random secret const secret = randomSecret(); const expiresAt = new Date(); expiresAt.setSeconds(expiresAt.getSeconds() + 3600); const hash = sha256(secret); - const cltv_delta = parseInt(process.env.HOLD_INVOICE_CLTV_DELTA); + const cltv_delta = Number(process.env.HOLD_INVOICE_CLTV_DELTA); const { request, id } = await lightning.createHodlInvoice({ cltv_delta, lnd, description, id: hash, tokens: amount, - expires_at: expiresAt, + expires_at: expiresAt.toISOString(), }); // We sent back the response hash (id) to be used on testing @@ -30,7 +30,7 @@ const createHoldInvoice = async ({ description, amount }) => { } }; -const settleHoldInvoice = async ({ secret }) => { +const settleHoldInvoice = async ( { secret }: { secret: string } ) => { try { await lightning.settleHodlInvoice({ lnd, secret }); } catch (error) { @@ -38,7 +38,7 @@ const settleHoldInvoice = async ({ secret }) => { } }; -const cancelHoldInvoice = async ({ hash }) => { +const cancelHoldInvoice = async ( { hash }: { hash: string } ) => { try { await lightning.cancelHodlInvoice({ lnd, id: hash }); } catch (error) { @@ -46,7 +46,7 @@ const cancelHoldInvoice = async ({ hash }) => { } }; -const getInvoice = async ({ hash }) => { +const getInvoice = async ( { hash }: { hash: string } ) => { try { return await lightning.getInvoice({ lnd, id: hash }); } catch (error) { @@ -54,7 +54,7 @@ const getInvoice = async ({ hash }) => { } }; -module.exports = { +export { createHoldInvoice, settleHoldInvoice, cancelHoldInvoice, diff --git a/ln/index.js b/ln/index.ts similarity index 64% rename from ln/index.js rename to ln/index.ts index 109300f8..57372418 100644 --- a/ln/index.js +++ b/ln/index.ts @@ -1,16 +1,16 @@ -const { +import { createHoldInvoice, settleHoldInvoice, cancelHoldInvoice, getInvoice, -} = require('./hold_invoice'); -const subscribeInvoice = require('./subscribe_invoice'); +} from './hold_invoice'; +import subscribeInvoice from './subscribe_invoice'; const subscribeProbe = require('./subscribe_probe'); -const resubscribeInvoices = require('./resubscribe_invoices'); +import resubscribeInvoices from './resubscribe_invoices'; const { payRequest, payToBuyer, isPendingPayment } = require('./pay_request'); -const { getInfo } = require('./info'); +import { getInfo } from './info'; -module.exports = { +export { createHoldInvoice, subscribeInvoice, resubscribeInvoices, @@ -22,4 +22,4 @@ module.exports = { isPendingPayment, subscribeProbe, getInvoice, -}; +} diff --git a/ln/info.js b/ln/info.js deleted file mode 100644 index da731135..00000000 --- a/ln/info.js +++ /dev/null @@ -1,13 +0,0 @@ -const lightning = require('lightning'); -const lnd = require('./connect'); -const logger = require('../logger'); - -const getInfo = async () => { - try { - return await lightning.getWalletInfo({ lnd }); - } catch (error) { - logger.error(error); - } -}; - -module.exports = { getInfo }; diff --git a/ln/info.ts b/ln/info.ts new file mode 100644 index 00000000..179fb82e --- /dev/null +++ b/ln/info.ts @@ -0,0 +1,11 @@ +import * as lightning from "lightning"; +import { lnd } from './connect' +import logger from "../logger"; + +export const getInfo = async () => { + try { + return await lightning.getWalletInfo({ lnd }); + } catch (error) { + logger.error(error); + } +}; diff --git a/ln/resubscribe_invoices.js b/ln/resubscribe_invoices.ts similarity index 65% rename from ln/resubscribe_invoices.js rename to ln/resubscribe_invoices.ts index 644dbbba..33ae6e90 100644 --- a/ln/resubscribe_invoices.js +++ b/ln/resubscribe_invoices.ts @@ -1,13 +1,15 @@ -const { getInvoices } = require('lightning'); -const lnd = require('./connect'); +import { Telegraf } from 'telegraf'; +import { MainContext } from "../bot/start"; +import { getInvoices, GetInvoicesResult } from 'lightning'; +import { lnd } from './connect'; const subscribeInvoice = require('./subscribe_invoice'); -const { Order } = require('../models'); -const logger = require('../logger'); +import { Order } from '../models'; +import logger from "../logger"; -const resubscribeInvoices = async bot => { +const resubscribeInvoices = async (bot: Telegraf) => { try { let invoicesReSubscribed = 0; - const isHeld = invoice => !!invoice.is_held; + const isHeld = (invoice: any) => !!invoice.is_held; const unconfirmedInvoices = ( await getInvoices({ lnd, @@ -29,9 +31,9 @@ const resubscribeInvoices = async bot => { } logger.info(`Invoices resubscribed: ${invoicesReSubscribed}`); } catch (error) { - logger.error(`ResuscribeInvoice catch: ${error.toString()}`); + logger.error(`ResuscribeInvoice catch: ${String(error)}`); return false; } }; -module.exports = resubscribeInvoices; +export default resubscribeInvoices diff --git a/ln/subscribe_invoice.js b/ln/subscribe_invoice.ts similarity index 82% rename from ln/subscribe_invoice.js rename to ln/subscribe_invoice.ts index 2f96e36a..fe5a5142 100644 --- a/ln/subscribe_invoice.js +++ b/ln/subscribe_invoice.ts @@ -1,23 +1,27 @@ -const { subscribeToInvoice } = require('lightning'); -const { Order, User } = require('../models'); +import { Telegraf } from "telegraf"; +import { MainContext } from "../bot/start"; +import {subscribeToInvoice} from 'lightning' +import { Order, User } from '../models'; const { payToBuyer } = require('./pay_request'); -const lnd = require('./connect'); -const messages = require('../bot/messages'); +import { lnd } from "./connect"; +import * as messages from '../bot/messages'; const ordersActions = require('../bot/ordersActions'); const { getUserI18nContext, getEmojiRate, decimalRound } = require('../util'); -const logger = require('../logger'); +import logger from "../logger"; -const subscribeInvoice = async (bot, id, resub) => { +const subscribeInvoice = async (bot: Telegraf, id: string, resub: boolean) => { try { const sub = subscribeToInvoice({ id, lnd }); sub.on('invoice_updated', async invoice => { if (invoice.is_held && !resub) { const order = await Order.findOne({ hash: invoice.id }); + if (order === null) throw Error("Order was not found in DB"); logger.info( `Order ${order._id} Invoice with hash: ${id} is being held!` ); const buyerUser = await User.findOne({ _id: order.buyer_id }); const sellerUser = await User.findOne({ _id: order.seller_id }); + if (buyerUser === null || sellerUser === null) throw Error("buyer or seller was not found in DB"); order.status = 'ACTIVE'; // This is the i18n context we need to pass to the message const i18nCtxBuyer = await getUserI18nContext(buyerUser); @@ -47,11 +51,12 @@ const subscribeInvoice = async (bot, id, resub) => { rate ); } - order.invoice_held_at = Date.now(); + order.invoice_held_at = new Date(); order.save(); } if (invoice.is_confirmed) { const order = await Order.findOne({ hash: id }); + if (order === null) throw Error("Order was not found in DB"); logger.info( `Order ${order._id} - Invoice with hash: ${id} was settled!` ); @@ -59,6 +64,7 @@ const subscribeInvoice = async (bot, id, resub) => { await order.save(); const buyerUser = await User.findOne({ _id: order.buyer_id }); const sellerUser = await User.findOne({ _id: order.seller_id }); + if (buyerUser === null || sellerUser === null) throw Error("buyer and/or seller was not found in DB"); // We need two i18n contexts to send messages to each user const i18nCtxBuyer = await getUserI18nContext(buyerUser); const i18nCtxSeller = await getUserI18nContext(sellerUser); @@ -121,4 +127,4 @@ const subscribeInvoice = async (bot, id, resub) => { } }; -module.exports = subscribeInvoice; +export default subscribeInvoice; diff --git a/lnurl/lnurl-pay.js b/lnurl/lnurl-pay.ts similarity index 77% rename from lnurl/lnurl-pay.js rename to lnurl/lnurl-pay.ts index 45235898..4ebc2e99 100644 --- a/lnurl/lnurl-pay.js +++ b/lnurl/lnurl-pay.ts @@ -1,11 +1,11 @@ -const axios = require('axios').default; -const logger = require('../logger'); +import axios from 'axios'; +import logger from "../logger"; // { // pr: String, // bech32-serialized lightning invoice // routes: [], // an empty array // } -const resolvLightningAddress = async (address, amountMsat) => { +const resolvLightningAddress = async (address: string, amountMsat: number) => { const [user, domain] = address.split('@'); const lnAddressQuery = `https://${domain}/.well-known/lnurlp/${user}`; @@ -17,7 +17,7 @@ const resolvLightningAddress = async (address, amountMsat) => { } if ( - (lnAddressRes.minSendable > amountMsat) | + (lnAddressRes.minSendable > amountMsat) || (lnAddressRes.maxSendable < amountMsat) ) { logger.info('lnAddress invalid amount'); @@ -31,7 +31,7 @@ const resolvLightningAddress = async (address, amountMsat) => { return res; }; -const existLightningAddress = async address => { +const existLightningAddress = async (address: string) => { const [user, domain] = address.split('@'); const lnAddressQuery = `https://${domain}/.well-known/lnurlp/${user}`; @@ -49,7 +49,4 @@ const existLightningAddress = async address => { } }; -module.exports = { - resolvLightningAddress, - existLightningAddress, -}; +export { resolvLightningAddress, existLightningAddress } diff --git a/logger.js b/logger.ts similarity index 82% rename from logger.js rename to logger.ts index ada7ed8e..d5fe6e2a 100644 --- a/logger.js +++ b/logger.ts @@ -1,4 +1,4 @@ -const winston = require('winston'); +import * as winston from 'winston'; const level = process.env.LOG_LEVEL || 'notice'; @@ -9,9 +9,8 @@ const logger = winston.createLogger({ }), winston.format.colorize(), winston.format.printf(info => { - return `[${info.timestamp}] ${info.level}: ${info.message} ${ - info.stack ? info.stack : '' - }`; + return `[${info.timestamp}] ${info.level}: ${info.message} ${info.stack ? info.stack : '' + }`; }) ), levels: winston.config.syslog.levels, @@ -24,4 +23,5 @@ const logger = winston.createLogger({ exitOnError: false, }); -module.exports = logger; + +export default logger; diff --git a/models/community.ts b/models/community.ts index 51a5188d..4dcaf2fc 100644 --- a/models/community.ts +++ b/models/community.ts @@ -82,4 +82,4 @@ const CommunitySchema = new Schema({ }); -module.exports = mongoose.model('Community', CommunitySchema); +export default mongoose.model('Community', CommunitySchema); diff --git a/models/config.ts b/models/config.ts index f109d639..43dafece 100644 --- a/models/config.ts +++ b/models/config.ts @@ -13,5 +13,4 @@ const configSchema = new Schema({ node_uri: { type: String }, }); - -module.exports = mongoose.model('Config', configSchema); +export default mongoose.model('Config', configSchema); diff --git a/models/dispute.ts b/models/dispute.ts index 381a17e2..68f751c4 100644 --- a/models/dispute.ts +++ b/models/dispute.ts @@ -31,4 +31,4 @@ const DisputeSchema = new Schema({ created_at: { type: Date, default: Date.now }, }); -module.exports = mongoose.model('Dispute', DisputeSchema); +export default mongoose.model('Dispute', DisputeSchema); diff --git a/models/index.js b/models/index.js deleted file mode 100644 index 23dbfa94..00000000 --- a/models/index.js +++ /dev/null @@ -1,15 +0,0 @@ -const User = require('./user'); -const Order = require('./order'); -const PendingPayment = require('./pending_payment'); -const Community = require('./community'); -const Dispute = require('./dispute'); -const Config = require('./config'); - -module.exports = { - User, - Order, - PendingPayment, - Community, - Dispute, - Config, -}; diff --git a/models/index.ts b/models/index.ts new file mode 100644 index 00000000..6fd610a7 --- /dev/null +++ b/models/index.ts @@ -0,0 +1,15 @@ +import User from './user' +import Order from './order' +import PendingPayment from './pending_payment' +import Community from './community' +import Dispute from './dispute' +import Config from './config' + +export { + User, + Order, + PendingPayment, + Community, + Dispute, + Config, +}; diff --git a/models/order.ts b/models/order.ts index b1bbf66b..1a2f0411 100644 --- a/models/order.ts +++ b/models/order.ts @@ -123,4 +123,4 @@ const orderSchema = new Schema({ is_public: { type: Boolean, default: true }, }); -module.exports = mongoose.model('Order', orderSchema); +export default mongoose.model('Order', orderSchema); diff --git a/models/pending_payment.ts b/models/pending_payment.ts index f2a81f3b..f324815b 100644 --- a/models/pending_payment.ts +++ b/models/pending_payment.ts @@ -40,4 +40,4 @@ const PendingPaymentSchema = new Schema({ }); -module.exports = mongoose.model('PendingPayment', PendingPaymentSchema); +export default mongoose.model('PendingPayment', PendingPaymentSchema); diff --git a/models/user.ts b/models/user.ts index 1bc7f227..adc8f375 100644 --- a/models/user.ts +++ b/models/user.ts @@ -52,4 +52,4 @@ const UserSchema = new Schema({ default_community_id: { type: String }, }); -module.exports = mongoose.model('User', UserSchema); +export default mongoose.model('User', UserSchema); diff --git a/package-lock.json b/package-lock.json index c7e08854..1f512338 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "lnp2pbot", - "version": "0.8.5", + "version": "0.9.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "lnp2pbot", - "version": "0.8.5", + "version": "0.9.2", "license": "MIT", "dependencies": { "@grammyjs/i18n": "^0.5.1", @@ -27,6 +27,8 @@ }, "devDependencies": { "@types/node": "^20.5.0", + "@types/node-schedule": "^2.1.0", + "@types/qrcode": "^1.5.2", "chai": "^4.3.4", "chokidar": "^3.5.3", "eslint": "^8.15.0", @@ -1640,6 +1642,24 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.0.tgz", "integrity": "sha512-Mgq7eCtoTjT89FqNoTzzXg2XvCi5VMhRV6+I2aYanc6kQCBImeNaAYRs/DyoVqk1YEUJK5gN9VO7HRIdz4Wo3Q==" }, + "node_modules/@types/node-schedule": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/node-schedule/-/node-schedule-2.1.0.tgz", + "integrity": "sha512-NiTwl8YN3v/1YCKrDFSmCTkVxFDylueEqsOFdgF+vPsm+AlyJKGAo5yzX1FiOxPsZiN6/r8gJitYx2EaSuBmmg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/qrcode": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.2.tgz", + "integrity": "sha512-W4KDz75m7rJjFbyCctzCtRzZUj+PrUHV+YjqDp50sSRezTbrtEAIq2iTzC6lISARl3qw+8IlcCyljdcVJE0Wug==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/qs": { "version": "6.9.7", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", @@ -8462,6 +8482,24 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.0.tgz", "integrity": "sha512-Mgq7eCtoTjT89FqNoTzzXg2XvCi5VMhRV6+I2aYanc6kQCBImeNaAYRs/DyoVqk1YEUJK5gN9VO7HRIdz4Wo3Q==" }, + "@types/node-schedule": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/node-schedule/-/node-schedule-2.1.0.tgz", + "integrity": "sha512-NiTwl8YN3v/1YCKrDFSmCTkVxFDylueEqsOFdgF+vPsm+AlyJKGAo5yzX1FiOxPsZiN6/r8gJitYx2EaSuBmmg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/qrcode": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.2.tgz", + "integrity": "sha512-W4KDz75m7rJjFbyCctzCtRzZUj+PrUHV+YjqDp50sSRezTbrtEAIq2iTzC6lISARl3qw+8IlcCyljdcVJE0Wug==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/qs": { "version": "6.9.7", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", diff --git a/package.json b/package.json index 382c6827..c95d2bb7 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,8 @@ ], "devDependencies": { "@types/node": "^20.5.0", + "@types/qrcode": "^1.5.2", + "@types/node-schedule": "^2.1.0", "chai": "^4.3.4", "chokidar": "^3.5.3", "eslint": "^8.15.0", diff --git a/tests/bot_test.js b/tests/bot_test.js index df4ceca5..0feb9570 100644 --- a/tests/bot_test.js +++ b/tests/bot_test.js @@ -10,7 +10,7 @@ const ordersActions = require('../bot/ordersActions'); const testUser = require('./user'); const testOrder = require('./order'); const { getCurrenciesWithPrice } = require('../util'); -const mongoConnect = require('../db_connect'); +const mongoConnect = require('../db_connect').default; mongoConnect(); diff --git a/tsconfig.json b/tsconfig.json index 8039f57e..7b8d0663 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,8 @@ { "compilerOptions": { "strict": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "downlevelIteration": true } } diff --git a/util/fiatModel.ts b/util/fiatModel.ts index a722adc0..cc8d22e8 100644 --- a/util/fiatModel.ts +++ b/util/fiatModel.ts @@ -10,3 +10,7 @@ export interface IFiat { price?: boolean; locale?: string; } + +export interface IFiatCurrencies { + [key: string]: IFiat; +} diff --git a/util/index.js b/util/index.ts similarity index 73% rename from util/index.js rename to util/index.ts index 8cb13f63..a9165729 100644 --- a/util/index.js +++ b/util/index.ts @@ -1,18 +1,28 @@ -const axios = require('axios'); +import { I18nContext } from "@grammyjs/i18n"; +import { ICommunity, IOrderChannel } from "../models/community"; +import { IOrder } from "../models/order"; +import { UserDocument } from "../models/user"; +import { IFiatCurrencies, IFiat } from "./fiatModel"; +import { ILanguage, ILanguages } from "./languagesModel"; +import { Telegram } from "telegraf/typings/core/types/typegram"; +import axios from "axios"; +import fiatJson from './fiat.json'; +import languagesJson from './languages.json'; +import { Order, Community } from "../models"; +import logger from "../logger"; const { I18n } = require('@grammyjs/i18n'); -const currencies = require('./fiat.json'); -const languages = require('./languages.json'); -const { Order, Community } = require('../models'); -const logger = require('../logger'); + +const languages: ILanguages = languagesJson; +const currencies: IFiatCurrencies = fiatJson; // ISO 4217, all ISO currency codes are 3 letters but users can trade shitcoins -const isIso4217 = code => { +const isIso4217 = (code: string): boolean => { if (code.length < 3 || code.length > 5) { return false; } const alphabet = 'abcdefghijklmnopqrstuvwxyz'.split(''); - code = code.toLowerCase().split(''); - return code.every(letter => { + code = code.toLowerCase() + return code.split('').every(letter => { if (alphabet.indexOf(letter) == -1) { return false; } @@ -20,15 +30,15 @@ const isIso4217 = code => { }); }; -const getCurrency = code => { - if (!isIso4217(code)) return false; +const getCurrency = (code: string): (IFiat | null) => { + if (!isIso4217(code)) return null; const currency = currencies[code]; - if (!currency) return false; + if (!currency) return null; return currency; }; -const plural = n => { +const plural = (n: number): string => { if (n === 1) { return ''; } @@ -37,7 +47,7 @@ const plural = n => { // This function formats a number to locale strings. // If Iso code or locale code doesn´t exist, the function will return a number without format. -const numberFormat = (code, number) => { +const numberFormat = (code: string, number: number) => { if (!isIso4217(code)) return false; if (!currencies[code]) return number; @@ -53,7 +63,7 @@ const numberFormat = (code, number) => { // This function checks if the current buyer and seller were doing circular operations // In order to increase their trades_completed and volume_traded. // If we found those trades in the last 24 hours we decrease both variables to both users -const handleReputationItems = async (buyer, seller, amount) => { +const handleReputationItems = async (buyer: UserDocument, seller: UserDocument, amount: number) => { try { const yesterday = new Date(Date.now() - 86400000).toISOString(); const orders = await Order.find({ @@ -64,7 +74,7 @@ const handleReputationItems = async (buyer, seller, amount) => { }); if (orders.length > 0) { let totalAmount = 0; - orders.forEach(order => { + orders.forEach((order: IOrder) => { totalAmount += order.amount; }); const lastAmount = orders[orders.length - 1].amount; @@ -118,9 +128,10 @@ const handleReputationItems = async (buyer, seller, amount) => { } }; -const getBtcFiatPrice = async (fiatCode, fiatAmount) => { +const getBtcFiatPrice = async (fiatCode: string, fiatAmount: number) => { try { const currency = getCurrency(fiatCode); + if (currency === null) throw Error("Currency not found"); if (!currency.price) return; // Before hit the endpoint we make sure the code have only 3 chars const code = currency.code.substring(0, 3); @@ -130,13 +141,13 @@ const getBtcFiatPrice = async (fiatCode, fiatAmount) => { } const sats = (fiatAmount / response.data.btc) * 100000000; - return parseInt(sats); + return Number(sats); } catch (error) { logger.error(error); } }; -const getBtcExchangePrice = (fiatAmount, satsAmount) => { +const getBtcExchangePrice = (fiatAmount: number, satsAmount: number) => { try { const satsPerBtc = 1e8; const feeRate = (satsPerBtc * fiatAmount) / satsAmount; @@ -147,8 +158,8 @@ const getBtcExchangePrice = (fiatAmount, satsAmount) => { } }; -const objectToArray = object => { - const array = []; +const objectToArray = (object: any): any[] => { + const array: any[] = []; for (const i in object) array.push(object[i]); @@ -162,7 +173,7 @@ const getCurrenciesWithPrice = () => { return withPrice; }; -const getEmojiRate = rate => { +const getEmojiRate = (rate: number): string => { const star = '⭐'; const roundedRate = Math.round(rate); const output = []; @@ -173,7 +184,7 @@ const getEmojiRate = rate => { // Round number to exp decimal digits // Source: https://developer.mozilla.org/es/docs/Web/JavaScript/Reference/Global_Objects/Math/round#redondeo_decimal -const decimalRound = (value, exp) => { +const decimalRound = (value: number, exp: number): number => { if (typeof exp === 'undefined' || +exp === 0) { return Math.round(value); } @@ -184,27 +195,29 @@ const decimalRound = (value, exp) => { return NaN; } // Shift - value = value.toString().split('e'); - value = Math.round(+(value[0] + 'e' + (value[1] ? +value[1] - exp : -exp))); + let valueArr = value.toString().split('e'); + value = Math.round(+(valueArr[0] + 'e' + (valueArr[1] ? +valueArr[1] - exp : -exp))); // Shift back - value = value.toString().split('e'); - return +(value[0] + 'e' + (value[1] ? +value[1] + exp : exp)); + valueArr = value.toString().split('e'); + return +(valueArr[0] + 'e' + (valueArr[1] ? +valueArr[1] + exp : exp)); }; -const extractId = text => { +const extractId = (text: string): (string | null) => { const matches = text.match(/:([a-f0-9]{24}):$/); - - return matches[1]; + if (matches !== null){ + return matches?.[1]; + } + return null; }; // Clean strings that are going to be rendered with markdown -const sanitizeMD = text => { +const sanitizeMD = (text: any) => { if (!text) return ''; - return text.toString().replace(/(?=[|<>(){}[\]\-_!#.`=+])/g, '\\'); + return String(text).replace(/(?=[|<>(){}[\]\-_!#.`=+])/g, '\\'); }; -const secondsToTime = secs => { +const secondsToTime = (secs: number) => { const hours = Math.floor(secs / (60 * 60)); const divisor = secs % (60 * 60); @@ -216,9 +229,9 @@ const secondsToTime = secs => { }; }; -const isGroupAdmin = async (groupId, user, telegram) => { +const isGroupAdmin = async (groupId: string, user: UserDocument, telegram: Telegram) => { try { - const member = await telegram.getChatMember(groupId, user.tg_id); + const member = await telegram.getChatMember({chat_id: groupId, user_id: Number(user.tg_id)}); if ( member && (member.status === 'creator' || member.status === 'administrator') @@ -242,12 +255,12 @@ const isGroupAdmin = async (groupId, user, telegram) => { logger.error(error); return { success: false, - message: error.toString(), + message: String(error), }; } }; -const deleteOrderFromChannel = async (order, telegram) => { +const deleteOrderFromChannel = async (order: IOrder, telegram: Telegram) => { try { let channel = process.env.CHANNEL; if (order.community_id) { @@ -265,13 +278,13 @@ const deleteOrderFromChannel = async (order, telegram) => { } } } - await telegram.deleteMessage(channel, order.tg_channel_message1); + await telegram.deleteMessage({chat_id: channel!, message_id: Number(order.tg_channel_message1!)}); } catch (error) { logger.error(error); } }; -const getOrderChannel = async order => { +const getOrderChannel = async (order: IOrder) => { let channel = process.env.CHANNEL; if (order.community_id) { const community = await Community.findOne({ _id: order.community_id }); @@ -281,7 +294,7 @@ const getOrderChannel = async order => { if (community.order_channels.length === 1) { channel = community.order_channels[0].name; } else { - community.order_channels.forEach(async c => { + community.order_channels.forEach(async (c: IOrderChannel) => { if (c.type === order.type) { channel = c.name; } @@ -292,10 +305,11 @@ const getOrderChannel = async order => { return channel; }; -const getDisputeChannel = async order => { +const getDisputeChannel = async (order: IOrder) => { let channel = process.env.DISPUTE_CHANNEL; if (order.community_id) { const community = await Community.findOne({ _id: order.community_id }); + if (community === null) throw Error("Community was not found in DB"); channel = community.dispute_channel; } @@ -307,8 +321,13 @@ const getDisputeChannel = async order => { * @param {*} user * @returns i18n context */ -const getUserI18nContext = async user => { - const language = user.language || 'en'; +const getUserI18nContext = async (user: UserDocument) => { + let language = null; + if (!('language' in user)) { + language = 'en'; + } else { + language = user.language; + } const i18n = new I18n({ locale: language, defaultLanguageOnMissing: true, @@ -318,7 +337,7 @@ const getUserI18nContext = async user => { return i18n.createContext(user.lang); }; -const getDetailedOrder = (i18n, order, buyer, seller) => { +const getDetailedOrder = (i18n: I18nContext, order: IOrder, buyer: UserDocument, seller: UserDocument) => { try { const buyerUsername = buyer ? sanitizeMD(buyer.username) : ''; const buyerReputation = buyer @@ -336,7 +355,7 @@ const getDetailedOrder = (i18n, order, buyer, seller) => { createdAt = sanitizeMD(createdAt); takenAt = sanitizeMD(takenAt); const status = sanitizeMD(order.status); - const fee = order.fee ? parseInt(order.fee) : ''; + const fee = order.fee ? Number(order.fee) : ''; const creator = order.creator_id === buyerId ? buyerUsername : sellerUsername; const message = i18n.t('order_detail', { @@ -361,7 +380,7 @@ const getDetailedOrder = (i18n, order, buyer, seller) => { }; // We need to know if this user is a dispute solver for this community -const isDisputeSolver = (community, user) => { +const isDisputeSolver = (community: ICommunity, user: UserDocument) => { if (!community || !user) { return false; } @@ -371,37 +390,35 @@ const isDisputeSolver = (community, user) => { // Return the fee the bot will charge to the seller // this fee is a combination from the global bot fee and the community fee -const getFee = async (amount, communityId) => { - const maxFee = Math.round(amount * parseFloat(process.env.MAX_FEE)); +const getFee = async (amount: number, communityId: string) => { + const maxFee = Math.round(amount * Number(process.env.MAX_FEE)); if (!communityId) return maxFee; - const botFee = maxFee * parseFloat(process.env.FEE_PERCENT); + const botFee = maxFee * Number(process.env.FEE_PERCENT); let communityFee = Math.round(maxFee - botFee); const community = await Community.findOne({ _id: communityId }); + if (community === null) throw Error("Community was not found in DB"); communityFee = communityFee * (community.fee / 100); return botFee + communityFee; }; -const itemsFromMessage = str => { +const itemsFromMessage = (str: string) => { return str .split(' ') .map(e => e.trim()) .filter(e => !!e); }; -// Check if a number is int -const isInt = n => parseInt(n) === n; - // Check if a number is float -const isFloat = n => typeof n === 'number' && !isInt(n); +const isFloat = (n: number) => typeof n === 'number' && !Number.isInteger(n); // Returns an emoji flag for a language -const getLanguageFlag = code => { +const getLanguageFlag = (code: string): ILanguage => { return languages[code]; }; -const delay = time => { +const delay = (time: number) => { return new Promise(resolve => setTimeout(resolve, time)); }; @@ -409,9 +426,9 @@ const delay = time => { // and the hold invoice safety window in seconds const holdInvoiceExpirationInSecs = () => { const expirationTimeInSecs = - parseInt(process.env.HOLD_INVOICE_CLTV_DELTA) * 10 * 60; + Number(process.env.HOLD_INVOICE_CLTV_DELTA) * 10 * 60; const safetyWindowInSecs = - parseInt(process.env.HOLD_INVOICE_CLTV_DELTA_SAFETY_WINDOW) * 10 * 60; + Number(process.env.HOLD_INVOICE_CLTV_DELTA_SAFETY_WINDOW) * 10 * 60; return { expirationTimeInSecs, safetyWindowInSecs, @@ -419,7 +436,7 @@ const holdInvoiceExpirationInSecs = () => { }; // Returns the user age in days -const getUserAge = user => { +const getUserAge = (user: UserDocument) => { const userCreationDate = new Date(user.created_at); const today = new Date(); const ageInDays = Math.floor( @@ -434,13 +451,13 @@ const getUserAge = user => { * @param {*} i18n context * @returns String with the remaining time to expiration in format '1 hours 30 minutes' */ -const getTimeToExpirationOrder = (order, i18n) => { +const getTimeToExpirationOrder = (order: IOrder, i18n: I18nContext) => { const initialDateObj = new Date(order.created_at); - const timeToExpire = parseInt(process.env.ORDER_PUBLISHED_EXPIRATION_WINDOW); + const timeToExpire = Number(process.env.ORDER_PUBLISHED_EXPIRATION_WINDOW); initialDateObj.setSeconds(initialDateObj.getSeconds() + timeToExpire); const currentDateObj = new Date(); - const timeDifferenceMs = initialDateObj - currentDateObj; + const timeDifferenceMs = initialDateObj.valueOf() - currentDateObj.valueOf(); const totalSecondsRemaining = Math.floor(timeDifferenceMs / 1000); const textHour = i18n.t('hours'); const textMin = i18n.t('minutes'); @@ -454,7 +471,7 @@ const getTimeToExpirationOrder = (order, i18n) => { return `${hours} ${textHour} ${minutes} ${textMin}`; }; -module.exports = { +export { isIso4217, plural, getCurrency, @@ -477,7 +494,6 @@ module.exports = { isDisputeSolver, getFee, itemsFromMessage, - isInt, isFloat, getLanguageFlag, delay, diff --git a/util/languagesModel.ts b/util/languagesModel.ts new file mode 100644 index 00000000..fa159815 --- /dev/null +++ b/util/languagesModel.ts @@ -0,0 +1,9 @@ +export interface ILanguage { + name: string; + emoji: string; + code: string; +} + +export interface ILanguages { + [key: string]: ILanguage; +}