From e6f7e1bb2b166a1b08c42c9c0cc728556a371ff4 Mon Sep 17 00:00:00 2001 From: Vishtar Date: Fri, 5 Jul 2024 18:22:36 +0400 Subject: [PATCH 1/8] bypass rollback, fixed tests --- src/db/queries/index.ts | 1 + ...electFirstUserPurchaseByWalletAndJetton.ts | 17 +++++++++++ src/services/bot/types/TNotificationHandle.ts | 3 ++ src/services/bot/utils/getNotifications.ts | 10 +++++++ src/services/bot/utils/handleNotification.ts | 3 ++ src/utils/parseTxData/api.ts | 4 ++- tests/getNotifications.test.ts | 30 ++++++++++++++++--- 7 files changed, 63 insertions(+), 5 deletions(-) create mode 100644 src/db/queries/selectFirstUserPurchaseByWalletAndJetton.ts diff --git a/src/db/queries/index.ts b/src/db/queries/index.ts index 4fe12e6..9457a24 100644 --- a/src/db/queries/index.ts +++ b/src/db/queries/index.ts @@ -13,3 +13,4 @@ export { countUserWallets } from './countUserWallets' export { selectUserWallets } from './selectUserWallets' export { deleteUserWallets } from './deleteUserWallets' export { deleteUserWallet } from './deleteUserWallet' +export { selectFirstUserPurchaseByWalletAndJetton } from './selectFirstUserPurchaseByWalletAndJetton' diff --git a/src/db/queries/selectFirstUserPurchaseByWalletAndJetton.ts b/src/db/queries/selectFirstUserPurchaseByWalletAndJetton.ts new file mode 100644 index 0000000..9773643 --- /dev/null +++ b/src/db/queries/selectFirstUserPurchaseByWalletAndJetton.ts @@ -0,0 +1,17 @@ +import { asc, eq } from 'drizzle-orm' +import { userPurchases } from '../../db/schema' +import type { TDbConnection } from '../../types' + +export const selectFirstUserPurchaseByWalletAndJetton = async ( + db: TDbConnection, + jettonId: number, +) => { + const [firstPurchase] = await db + .select() + .from(userPurchases) + .where(eq(userPurchases.jettonId, jettonId)) + .orderBy(asc(userPurchases.timestamp)) + .limit(1) + + return firstPurchase +} diff --git a/src/services/bot/types/TNotificationHandle.ts b/src/services/bot/types/TNotificationHandle.ts index eec5e02..f8726c7 100644 --- a/src/services/bot/types/TNotificationHandle.ts +++ b/src/services/bot/types/TNotificationHandle.ts @@ -22,4 +22,7 @@ export type TNotificationHandle = { getLastAddressNotificationFromDB: ( jettonId: number, ) => Promise + getFirstAddressJettonPurchaseFromDB: ( + jettonId: number, + ) => Promise } diff --git a/src/services/bot/utils/getNotifications.ts b/src/services/bot/utils/getNotifications.ts index 7c6ee41..0907b80 100644 --- a/src/services/bot/utils/getNotifications.ts +++ b/src/services/bot/utils/getNotifications.ts @@ -6,6 +6,7 @@ export async function* getNotifications( handle: TNotificationHandle, ): AsyncGenerator { const { + getFirstAddressJettonPurchaseFromDB, getLastAddressJettonPurchaseFromDB, getLastAddressNotificationFromDB, getJettonsFromChain, @@ -27,6 +28,15 @@ export async function* getNotifications( const addressJettonsFromDb = await getJettonsFromDB(wallet.id) for (const jetton of addressJettonsFromDb) { if (!addressJettonsFromChainObj[jetton.token]) { + const firstPurchase = await getFirstAddressJettonPurchaseFromDB(jetton.id) + if (!firstPurchase) { + continue + } + const secondsFromPurchase = Date.now() / 1000 - firstPurchase.timestamp + // Estimated time to bypass a rollback + if (secondsFromPurchase <= 60) { + continue + } yield { userId: user.id, walletId: wallet.id, diff --git a/src/services/bot/utils/handleNotification.ts b/src/services/bot/utils/handleNotification.ts index 5b560ed..5a03f31 100644 --- a/src/services/bot/utils/handleNotification.ts +++ b/src/services/bot/utils/handleNotification.ts @@ -4,6 +4,7 @@ import TonWeb from 'tonweb' import { insertUserNotification, insertUserPurchase, + selectFirstUserPurchaseByWalletAndJetton, selectLastUserNotificationByWalletAndJetton, selectLastUserPurchaseByWalletAndJetton, selectUserSettings, @@ -34,6 +35,8 @@ export const handleNotification = async (bot: Telegraf) => { selectLastUserPurchaseByWalletAndJetton(db, jettonId), getLastAddressNotificationFromDB: (jettonId: number) => selectLastUserNotificationByWalletAndJetton(db, jettonId), + getFirstAddressJettonPurchaseFromDB: (jettonId: number) => + selectFirstUserPurchaseByWalletAndJetton(db, jettonId), } for await (const notification of getNotifications(handle)) { if (hiddenTickers.includes(notification.symbol)) { diff --git a/src/utils/parseTxData/api.ts b/src/utils/parseTxData/api.ts index fea0ec2..89f0a08 100644 --- a/src/utils/parseTxData/api.ts +++ b/src/utils/parseTxData/api.ts @@ -21,7 +21,9 @@ export const api = async (address: string) => { res[jettonInfo.symbol] = { ...jettonInfo, pnlPercentage, - chart: (slicedChart.length >= 2 ? slicedChart : chart).reverse().map(entity => [entity[0], normalizePrice(entity[1], jettonInfo.decimals)]), + chart: (slicedChart.length >= 2 ? slicedChart : chart) + .reverse() + .map(entity => [entity[0], normalizePrice(entity[1], jettonInfo.decimals)]), lastBuyTime, } } diff --git a/tests/getNotifications.test.ts b/tests/getNotifications.test.ts index d5440ca..efb6c22 100644 --- a/tests/getNotifications.test.ts +++ b/tests/getNotifications.test.ts @@ -29,6 +29,8 @@ describe('getNotifications', () => { mock.fn(), getLastAddressNotificationFromDB: mock.fn(), + getFirstAddressJettonPurchaseFromDB: + mock.fn(), } notifications.length = 0 }) @@ -191,6 +193,7 @@ describe('getNotifications', () => { jettonId: 1, symbol: 'JET', price: 200, + decimals: 9, action: ENotificationType.UP, // @ts-expect-error timestamp not everywhere timestamp: notifications[0].timestamp, @@ -218,7 +221,7 @@ describe('getNotifications', () => { handle.getLastAddressJettonPurchaseFromDB.mock.mockImplementation(() => Promise.resolve({ jettonId: 1, - timestamp: Date.now() - 20000, + timestamp: Date.now() - 10000, price: 100, }), ) @@ -229,6 +232,13 @@ describe('getNotifications', () => { price: 100, }), ) + handle.getFirstAddressJettonPurchaseFromDB.mock.mockImplementation(() => + Promise.resolve({ + jettonId: 1, + timestamp: Date.now() / 1000 - 20000, + price: 100, + }), + ) for await (const notification of getNotifications(handle as unknown as TNotificationHandle)) { notifications.push(notification) @@ -287,7 +297,14 @@ describe('getNotifications', () => { handle.getLastAddressNotificationFromDB.mock.mockImplementation(() => Promise.resolve({ jettonId: 1, - timestamp: Date.now() - 10000, + timestamp: Date.now() - 20000, + price: 100, + }), + ) + handle.getFirstAddressJettonPurchaseFromDB.mock.mockImplementation(() => + Promise.resolve({ + jettonId: 1, + timestamp: Date.now() / 1000 - 30000, price: 100, }), ) @@ -322,7 +339,6 @@ describe('getNotifications', () => { notifications.push(notification) } - assert.strictEqual(notifications.length, 3) assert.deepStrictEqual( [notifications[0].action, notifications[1].action, notifications[2].action], [ @@ -380,6 +396,13 @@ describe('getNotifications', () => { price: 100, }), ) + handle.getFirstAddressJettonPurchaseFromDB.mock.mockImplementation(() => + Promise.resolve({ + jettonId: 1, + timestamp: Date.now() / 1000 - 30000, + price: 100, + }), + ) for await (const notification of getNotifications(handle as unknown as TNotificationHandle)) { notifications.push(notification) @@ -419,7 +442,6 @@ describe('getNotifications', () => { notifications.push(notification) } - assert.strictEqual(notifications.length, 4) assert.deepStrictEqual( [ notifications[0].action, From 8ed3f83a0d25bb6d52938705380fc3cac49741cd Mon Sep 17 00:00:00 2001 From: Vishtar Date: Fri, 5 Jul 2024 18:38:27 +0400 Subject: [PATCH 2/8] renaming --- src/db/queries/index.ts | 6 +++--- ...etton.ts => selectFirstUserPurchaseByJettonId.ts} | 5 +---- ...on.ts => selectLastUserNotificationByJettonId.ts} | 5 +---- ...Jetton.ts => selectLastUserPurchaseByJettonId.ts} | 5 +---- src/services/bot/utils/handleNotification.ts | 12 ++++++------ 5 files changed, 12 insertions(+), 21 deletions(-) rename src/db/queries/{selectFirstUserPurchaseByWalletAndJetton.ts => selectFirstUserPurchaseByJettonId.ts} (75%) rename src/db/queries/{selectLastUserNotificationByWalletAndJetton.ts => selectLastUserNotificationByJettonId.ts} (76%) rename src/db/queries/{selectLastUserPurchaseByWalletAndJetton.ts => selectLastUserPurchaseByJettonId.ts} (75%) diff --git a/src/db/queries/index.ts b/src/db/queries/index.ts index 9457a24..c228fbf 100644 --- a/src/db/queries/index.ts +++ b/src/db/queries/index.ts @@ -1,7 +1,7 @@ export { insertUserAdress } from './insertUserAdress' export { upsertToken } from './upsertToken' -export { selectLastUserNotificationByWalletAndJetton } from './selectLastUserNotificationByWalletAndJetton' -export { selectLastUserPurchaseByWalletAndJetton } from './selectLastUserPurchaseByWalletAndJetton' +export { selectLastUserNotificationByJettonId } from './selectLastUserNotificationByJettonId' +export { selectLastUserPurchaseByJettonId } from './selectLastUserPurchaseByJettonId' export { deleteJettonByWallet } from './deleteJettonByWallet' export { insertUserPurchase } from './insertUserPurchase' export { insertUserNotification } from './insertUserNotification' @@ -13,4 +13,4 @@ export { countUserWallets } from './countUserWallets' export { selectUserWallets } from './selectUserWallets' export { deleteUserWallets } from './deleteUserWallets' export { deleteUserWallet } from './deleteUserWallet' -export { selectFirstUserPurchaseByWalletAndJetton } from './selectFirstUserPurchaseByWalletAndJetton' +export { selectFirstUserPurchaseByJettonId } from './selectFirstUserPurchaseByJettonId' diff --git a/src/db/queries/selectFirstUserPurchaseByWalletAndJetton.ts b/src/db/queries/selectFirstUserPurchaseByJettonId.ts similarity index 75% rename from src/db/queries/selectFirstUserPurchaseByWalletAndJetton.ts rename to src/db/queries/selectFirstUserPurchaseByJettonId.ts index 9773643..694ab79 100644 --- a/src/db/queries/selectFirstUserPurchaseByWalletAndJetton.ts +++ b/src/db/queries/selectFirstUserPurchaseByJettonId.ts @@ -2,10 +2,7 @@ import { asc, eq } from 'drizzle-orm' import { userPurchases } from '../../db/schema' import type { TDbConnection } from '../../types' -export const selectFirstUserPurchaseByWalletAndJetton = async ( - db: TDbConnection, - jettonId: number, -) => { +export const selectFirstUserPurchaseByJettonId = async (db: TDbConnection, jettonId: number) => { const [firstPurchase] = await db .select() .from(userPurchases) diff --git a/src/db/queries/selectLastUserNotificationByWalletAndJetton.ts b/src/db/queries/selectLastUserNotificationByJettonId.ts similarity index 76% rename from src/db/queries/selectLastUserNotificationByWalletAndJetton.ts rename to src/db/queries/selectLastUserNotificationByJettonId.ts index 1817296..1b1239c 100644 --- a/src/db/queries/selectLastUserNotificationByWalletAndJetton.ts +++ b/src/db/queries/selectLastUserNotificationByJettonId.ts @@ -2,10 +2,7 @@ import { desc, eq } from 'drizzle-orm' import { userNotifications } from '../../db/schema' import type { TDbConnection } from '../../types' -export const selectLastUserNotificationByWalletAndJetton = async ( - db: TDbConnection, - jettonId: number, -) => { +export const selectLastUserNotificationByJettonId = async (db: TDbConnection, jettonId: number) => { const [lastNotification] = await db .select() .from(userNotifications) diff --git a/src/db/queries/selectLastUserPurchaseByWalletAndJetton.ts b/src/db/queries/selectLastUserPurchaseByJettonId.ts similarity index 75% rename from src/db/queries/selectLastUserPurchaseByWalletAndJetton.ts rename to src/db/queries/selectLastUserPurchaseByJettonId.ts index 3d611cf..6b77270 100644 --- a/src/db/queries/selectLastUserPurchaseByWalletAndJetton.ts +++ b/src/db/queries/selectLastUserPurchaseByJettonId.ts @@ -2,10 +2,7 @@ import { desc, eq } from 'drizzle-orm' import { userPurchases } from '../../db/schema' import type { TDbConnection } from '../../types' -export const selectLastUserPurchaseByWalletAndJetton = async ( - db: TDbConnection, - jettonId: number, -) => { +export const selectLastUserPurchaseByJettonId = async (db: TDbConnection, jettonId: number) => { const [lastPurchase] = await db .select() .from(userPurchases) diff --git a/src/services/bot/utils/handleNotification.ts b/src/services/bot/utils/handleNotification.ts index 5a03f31..4c4b722 100644 --- a/src/services/bot/utils/handleNotification.ts +++ b/src/services/bot/utils/handleNotification.ts @@ -4,9 +4,9 @@ import TonWeb from 'tonweb' import { insertUserNotification, insertUserPurchase, - selectFirstUserPurchaseByWalletAndJetton, - selectLastUserNotificationByWalletAndJetton, - selectLastUserPurchaseByWalletAndJetton, + selectFirstUserPurchaseByJettonId, + selectLastUserNotificationByJettonId, + selectLastUserPurchaseByJettonId, selectUserSettings, upsertToken, } from '../../../db/queries' @@ -32,11 +32,11 @@ export const handleNotification = async (bot: Telegraf) => { getJettonsFromDB: walletId => db.select().from(tokens).where(eq(tokens.walletId, walletId)), getJettonsFromChain: getJettonsByAddress, getLastAddressJettonPurchaseFromDB: (jettonId: number) => - selectLastUserPurchaseByWalletAndJetton(db, jettonId), + selectLastUserPurchaseByJettonId(db, jettonId), getLastAddressNotificationFromDB: (jettonId: number) => - selectLastUserNotificationByWalletAndJetton(db, jettonId), + selectLastUserNotificationByJettonId(db, jettonId), getFirstAddressJettonPurchaseFromDB: (jettonId: number) => - selectFirstUserPurchaseByWalletAndJetton(db, jettonId), + selectFirstUserPurchaseByJettonId(db, jettonId), } for await (const notification of getNotifications(handle)) { if (hiddenTickers.includes(notification.symbol)) { From 3b33b7a58be779bf08217f8f6a9d1b1991bf11dc Mon Sep 17 00:00:00 2001 From: Vishtar Date: Mon, 8 Jul 2024 16:53:17 +0400 Subject: [PATCH 3/8] style fixes --- frontend/src/App.tsx | 12 +++++++++++- frontend/src/components/Charts/style.module.css | 8 -------- frontend/src/index.css | 5 +++-- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2997dc3..a6ab384 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,10 +1,12 @@ import { useEffect, useState } from 'react' import { useTonConnectModal, useTonConnectUI } from '@tonconnect/ui-react' +// TODO: replace '@tma.js/sdk-react' (deprecated) with '@telegram-apps/sdk-react' import { bindMiniAppCSSVars, bindThemeParamsCSSVars, useMiniApp, useThemeParams, + useViewport, } from '@tma.js/sdk-react' import { retrieveLaunchParams } from '@tma.js/sdk' @@ -22,10 +24,10 @@ export const App = () => { const [tonConnectUI] = useTonConnectUI() const themeParams = useThemeParams() const miniApp = useMiniApp() + const miniAppViewport = useViewport() const { mutate } = usePostData() const { t } = useTranslation() const [walletsCount, setWalletsCount] = useState(0) - miniApp.ready() const onClickLinkAnothgerWalletButton = async () => { if (tonConnectUI.connected) { @@ -41,6 +43,12 @@ export const App = () => { } } + useEffect(() => { + if (miniAppViewport) { + miniAppViewport.expand() + } + }, [miniAppViewport]) + useEffect(() => { return bindMiniAppCSSVars(miniApp, themeParams) }, [miniApp, themeParams]) @@ -56,6 +64,8 @@ export const App = () => { // }, [modal.state.status]); useEffect(() => { + miniApp.ready() + tonConnectUI.onStatusChange(wallet => { if (!wallet?.account.address) return const launchParams = retrieveLaunchParams() diff --git a/frontend/src/components/Charts/style.module.css b/frontend/src/components/Charts/style.module.css index d7e162d..93fdb08 100644 --- a/frontend/src/components/Charts/style.module.css +++ b/frontend/src/components/Charts/style.module.css @@ -1,11 +1,3 @@ .charts { width: 100%; } - -.yourJettonsHeader { - color: var(--tg-theme-text-color) !important; -} - -.symbolHeader { - color: white !important; -} diff --git a/frontend/src/index.css b/frontend/src/index.css index 0c3a726..5e4c0fc 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -9,10 +9,11 @@ html { .App { margin: 0 auto; padding: 1rem; + padding-bottom: 12px; max-width: 560px; width: 100%; - min-height: 100vh; - overflow-x: hidden; + min-height: var(--tg-viewport-height); + /* overflow-x: hidden; */ color: var(--tg-theme-text-color); display: flex; flex-direction: column; From a151e633a4e4b3a26ce490a7fec9438267b2ca5c Mon Sep 17 00:00:00 2001 From: Vishtar Date: Mon, 8 Jul 2024 19:13:03 +0400 Subject: [PATCH 4/8] fixes, minor changes --- .env.example | 1 + README.md | 2 +- frontend/src/types/TJettonData.ts | 3 +- frontend/src/utils.ts | 59 ------------- frontend/src/utils/badgeType.ts | 10 +++ frontend/src/utils/chartColor.ts | 9 ++ frontend/src/utils/formatDataToChart.ts | 12 +++ frontend/src/utils/index.ts | 5 ++ frontend/src/utils/normalizePrice.ts | 2 + frontend/src/utils/timeConverter.ts | 26 ++++++ src/db/queries/index.ts | 2 + src/db/queries/insertUserAdress.ts | 7 +- src/db/queries/insertUserPurchase.ts | 4 +- .../selectTokenByAddressAndWalletId.ts | 14 +++ src/db/queries/selectWalletById.ts | 7 ++ src/db/queries/upsertToken.ts | 7 +- src/services/bot/i18n/en.ts | 9 +- src/services/bot/i18n/ru.ts | 10 ++- src/services/bot/types/TNotificationHandle.ts | 4 +- src/services/bot/types/TNotifications.ts | 2 + src/services/bot/utils/getNotifications.ts | 23 ++--- .../bot/utils/handleCommandDisconnect.ts | 1 + src/services/bot/utils/handleNotification.ts | 41 ++++----- .../handleSuccessfulWalletLinkNotification.ts | 36 ++++---- src/services/web/handlers/onGetWalletData.ts | 6 +- src/types/TDbTransaction.ts | 9 ++ src/types/env.d.ts | 1 + src/types/index.ts | 1 + src/utils/parseTxData/api.ts | 16 ++-- tests/getNotifications.test.ts | 86 +++++++++++++++++-- 30 files changed, 272 insertions(+), 143 deletions(-) delete mode 100644 frontend/src/utils.ts create mode 100644 frontend/src/utils/badgeType.ts create mode 100644 frontend/src/utils/chartColor.ts create mode 100644 frontend/src/utils/formatDataToChart.ts create mode 100644 frontend/src/utils/index.ts create mode 100644 frontend/src/utils/normalizePrice.ts create mode 100644 frontend/src/utils/timeConverter.ts create mode 100644 src/db/queries/selectTokenByAddressAndWalletId.ts create mode 100644 src/db/queries/selectWalletById.ts create mode 100644 src/types/TDbTransaction.ts diff --git a/.env.example b/.env.example index 45582ed..5517805 100644 --- a/.env.example +++ b/.env.example @@ -8,6 +8,7 @@ TONAPI_TOKEN= NOTIFICATION_RATE_UP=2 NOTIFICATION_RATE_DOWN=0.5 LIMIT_WALLETS_FOR_USER=10 +SECONDS_FROM_PURCHASE_WITH_ROLLBACK_POSSIBILITY=60 # AMQP AMQP_ENDPOINT=amqp://localhost:5673 # PostgreSQL diff --git a/README.md b/README.md index 353980f..e899b3f 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,6 @@ It proactively notifies users of their gains via Telegram, making it easy to man ## Tests -All tests are located in the [tests](./tests) directory. Currently, there is one unit test (with 6 scenarios) for the main business function: +All tests are located in the [tests](./tests) directory. Currently, there is one unit test (with 7 scenarios) for the main business function: - [tests/getNotifications.test.ts](./tests/getNotifications.test.ts) diff --git a/frontend/src/types/TJettonData.ts b/frontend/src/types/TJettonData.ts index be5191e..d81f58e 100644 --- a/frontend/src/types/TJettonData.ts +++ b/frontend/src/types/TJettonData.ts @@ -2,7 +2,8 @@ export type TJettonData = { address: string symbol: string image?: string + decimals: number pnlPercentage: number - chart: [timestamp: number, price: number | string][] + chart: [timestamp: number, price: number][] lastBuyTime: number } diff --git a/frontend/src/utils.ts b/frontend/src/utils.ts deleted file mode 100644 index df36eee..0000000 --- a/frontend/src/utils.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { TChartData } from './types' - -export function timeConverter(UNIX_timestamp: number) { - const a = new Date(UNIX_timestamp * 1000) - const months = [ - 'Jan', - 'Feb', - 'Mar', - 'Apr', - 'May', - 'Jun', - 'Jul', - 'Aug', - 'Sep', - 'Oct', - 'Nov', - 'Dec', - ] - const year = a.getFullYear() - const month = months[a.getMonth()] - const date = a.getDate() - const hour = a.getHours() - const min = a.getMinutes() - const sec = a.getSeconds() - const time = - date + ' ' + month + ' ' + year + ' ' + hour + ':' + min + ':' + sec - return time -} - -export const reversePrice = (price: number) => 1 / price - -export const chartColor = (arr: TChartData[]) => { - if (Number(arr[0].Price) >= Number(arr.at(-1)!.Price)) { - return ['red'] - } else { - return ['emerald'] - } -} - -export const formatDataToChart = (input: { - chart: [number, number | string][] -}) => - input.chart.map(arr => { - return { - date: timeConverter(arr[0]), - Price: arr[1], - } - }) - -export const badgeType = (input: number) => { - if (input < 0) { - return 'moderateDecrease' - } - if (input > 0) { - return 'moderateIncrease' - } else { - return 'unchanged' - } -} diff --git a/frontend/src/utils/badgeType.ts b/frontend/src/utils/badgeType.ts new file mode 100644 index 0000000..b134385 --- /dev/null +++ b/frontend/src/utils/badgeType.ts @@ -0,0 +1,10 @@ +export const badgeType = (input: number) => { + if (input < 0) { + return 'moderateDecrease' + } + if (input > 0) { + return 'moderateIncrease' + } else { + return 'unchanged' + } +} diff --git a/frontend/src/utils/chartColor.ts b/frontend/src/utils/chartColor.ts new file mode 100644 index 0000000..abe22ec --- /dev/null +++ b/frontend/src/utils/chartColor.ts @@ -0,0 +1,9 @@ +import { TChartData } from '../types' + +export const chartColor = (arr: TChartData[]) => { + if (Number(arr[0].Price) >= Number(arr.at(-1)!.Price)) { + return ['red'] + } else { + return ['emerald'] + } +} diff --git a/frontend/src/utils/formatDataToChart.ts b/frontend/src/utils/formatDataToChart.ts new file mode 100644 index 0000000..23b41c8 --- /dev/null +++ b/frontend/src/utils/formatDataToChart.ts @@ -0,0 +1,12 @@ +import { normalizePrice, timeConverter } from '.' + +export const formatDataToChart = (input: { + chart: [number, number][] + decimals: number +}) => + input.chart.reverse().map(arr => { + return { + date: timeConverter(arr[0]), + Price: normalizePrice(arr[1], input.decimals), + } + }) diff --git a/frontend/src/utils/index.ts b/frontend/src/utils/index.ts new file mode 100644 index 0000000..f01d39c --- /dev/null +++ b/frontend/src/utils/index.ts @@ -0,0 +1,5 @@ +export { normalizePrice } from './normalizePrice' +export { timeConverter } from './timeConverter' +export { chartColor } from './chartColor' +export { formatDataToChart } from './formatDataToChart' +export { badgeType } from './badgeType' diff --git a/frontend/src/utils/normalizePrice.ts b/frontend/src/utils/normalizePrice.ts new file mode 100644 index 0000000..fcf2ee3 --- /dev/null +++ b/frontend/src/utils/normalizePrice.ts @@ -0,0 +1,2 @@ +export const normalizePrice = (price: number, decimals?: number) => + price > 0.01 ? Number(price.toFixed(2)) : price.toFixed(decimals || 20) diff --git a/frontend/src/utils/timeConverter.ts b/frontend/src/utils/timeConverter.ts new file mode 100644 index 0000000..234ff9e --- /dev/null +++ b/frontend/src/utils/timeConverter.ts @@ -0,0 +1,26 @@ +export function timeConverter(UNIX_timestamp: number) { + const a = new Date(UNIX_timestamp * 1000) + const months = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec', + ] + const year = a.getFullYear() + const month = months[a.getMonth()] + const date = a.getDate() + const hour = a.getHours() + const min = a.getMinutes() + const sec = a.getSeconds() + const time = + date + ' ' + month + ' ' + year + ' ' + hour + ':' + min + ':' + sec + return time +} diff --git a/src/db/queries/index.ts b/src/db/queries/index.ts index c228fbf..dad0884 100644 --- a/src/db/queries/index.ts +++ b/src/db/queries/index.ts @@ -14,3 +14,5 @@ export { selectUserWallets } from './selectUserWallets' export { deleteUserWallets } from './deleteUserWallets' export { deleteUserWallet } from './deleteUserWallet' export { selectFirstUserPurchaseByJettonId } from './selectFirstUserPurchaseByJettonId' +export { selectWalletById } from './selectWalletById' +export { selectTokenByAddressAndWalletId } from './selectTokenByAddressAndWalletId' diff --git a/src/db/queries/insertUserAdress.ts b/src/db/queries/insertUserAdress.ts index 230ad9a..3c86994 100644 --- a/src/db/queries/insertUserAdress.ts +++ b/src/db/queries/insertUserAdress.ts @@ -1,6 +1,9 @@ import { wallets } from '../../db/schema' -import type { TDbConnection } from '../../types' +import type { TDbConnection, TDbTransaction } from '../../types' -export const insertUserAdress = (db: TDbConnection, values: typeof wallets.$inferInsert) => { +export const insertUserAdress = ( + db: TDbConnection | TDbTransaction, + values: typeof wallets.$inferInsert, +) => { return db.insert(wallets).values(values).onConflictDoNothing().returning() } diff --git a/src/db/queries/insertUserPurchase.ts b/src/db/queries/insertUserPurchase.ts index 77b5325..ff20395 100644 --- a/src/db/queries/insertUserPurchase.ts +++ b/src/db/queries/insertUserPurchase.ts @@ -1,8 +1,8 @@ -import type { TDbConnection } from '../../types' +import type { TDbConnection, TDbTransaction } from '../../types' import { userPurchases } from '../schema' export const insertUserPurchase = ( - db: TDbConnection, + db: TDbConnection | TDbTransaction, values: typeof userPurchases.$inferInsert, ) => { return db.insert(userPurchases).values(values) diff --git a/src/db/queries/selectTokenByAddressAndWalletId.ts b/src/db/queries/selectTokenByAddressAndWalletId.ts new file mode 100644 index 0000000..3d92e5d --- /dev/null +++ b/src/db/queries/selectTokenByAddressAndWalletId.ts @@ -0,0 +1,14 @@ +import { and, eq } from 'drizzle-orm' +import type { TDbConnection } from '../../types' +import { tokens } from '../schema' + +export const selectTokenByAddressAndWalletId = ( + db: TDbConnection, + address: string, + walletId: number, +) => { + return db + .select() + .from(tokens) + .where(and(eq(tokens.token, address), eq(tokens.walletId, walletId))) +} diff --git a/src/db/queries/selectWalletById.ts b/src/db/queries/selectWalletById.ts new file mode 100644 index 0000000..6c9417c --- /dev/null +++ b/src/db/queries/selectWalletById.ts @@ -0,0 +1,7 @@ +import { eq } from 'drizzle-orm' +import type { TDbConnection } from '../../types' +import { wallets } from '../schema' + +export const selectWalletById = (db: TDbConnection, walletId: number) => { + return db.select().from(wallets).where(eq(wallets.id, walletId)) +} diff --git a/src/db/queries/upsertToken.ts b/src/db/queries/upsertToken.ts index 90dc3e0..2d59e6e 100644 --- a/src/db/queries/upsertToken.ts +++ b/src/db/queries/upsertToken.ts @@ -1,6 +1,9 @@ -import type { TDbConnection } from '../../types' +import type { TDbConnection, TDbTransaction } from '../../types' import { tokens } from '../schema' -export const upsertToken = (db: TDbConnection, values: typeof tokens.$inferInsert) => { +export const upsertToken = ( + db: TDbConnection | TDbTransaction, + values: typeof tokens.$inferInsert, +) => { return db.insert(tokens).values(values).onConflictDoNothing() } diff --git a/src/services/bot/i18n/en.ts b/src/services/bot/i18n/en.ts index d84aeea..60d7ff3 100644 --- a/src/services/bot/i18n/en.ts +++ b/src/services/bot/i18n/en.ts @@ -18,11 +18,14 @@ Send an address you want to watch or connect your own 👇 `, youNoLongerHaveJetton: (ticker: string) => `👋 You no longer hold $${jettonNamesWithSpecialCharacters[ticker] || ticker.toUpperCase()}, notifications for this jetton have been stopped.`, - detectedNewJetton: (ticker: string) => - `💎 New jetton found: $${jettonNamesWithSpecialCharacters[ticker] || ticker.toUpperCase()}. I will notify you when the price moves up or down by 2x.`, + detectedNewJetton: (ticker: string, wallet: string, price: number | string) => ` +💎 New jetton found: $${jettonNamesWithSpecialCharacters[ticker] || ticker.toUpperCase()}. Wallet: +${getEmojiForWallet(wallet)} \`${wallet}\` +💵 Current price: $${price} +📢 I will notify you when the price moves up or down by 2x.`, notification: { x2: (ticker: string, wallet: string, price: number | string) => ` -📈 $${jettonNamesWithSpecialCharacters[ticker] || ticker.toUpperCase()} made 2x! Wallet: +🚀 $${jettonNamesWithSpecialCharacters[ticker] || ticker.toUpperCase()} made 2x! Wallet: ${getEmojiForWallet(wallet)} \`${wallet}\` 💵 Current price: $${price} `, diff --git a/src/services/bot/i18n/ru.ts b/src/services/bot/i18n/ru.ts index c5745d0..cf01108 100644 --- a/src/services/bot/i18n/ru.ts +++ b/src/services/bot/i18n/ru.ts @@ -17,11 +17,15 @@ export const ru = { `, youNoLongerHaveJetton: (ticker: string) => `👋 Вы больше не холдите $${jettonNamesWithSpecialCharacters[ticker] || ticker.toUpperCase()}, уведомления для этого жетона остановлены.`, - detectedNewJetton: (ticker: string) => - `💎 Обнаружен новый жетон $${jettonNamesWithSpecialCharacters[ticker] || ticker.toUpperCase()}. Вы получите уведомление, когда его цена сделает 2x или упадёт вдвое.`, + detectedNewJetton: (ticker: string, wallet: string, price: number | string) => ` +💎 Обнаружен новый жетон $${jettonNamesWithSpecialCharacters[ticker] || ticker.toUpperCase()}. Кошелёк: +${getEmojiForWallet(wallet)} \`${wallet}\` +💵 Актуальная цена: $${price} +📢 Вы получите уведомление, когда его цена сделает 2x или упадёт вдвое. +`, notification: { x2: (ticker: string, wallet: string, price: number | string) => ` -📈 Жетон $${jettonNamesWithSpecialCharacters[ticker] || ticker.toUpperCase()} сделал x2! Адрес: +🚀 Жетон $${jettonNamesWithSpecialCharacters[ticker] || ticker.toUpperCase()} сделал x2! Адрес: ${getEmojiForWallet(wallet)} \`${wallet}\` 💵 Актуальная цена: $${price}`, x05: (ticker: string, wallet: string, price: number | string) => ` diff --git a/src/services/bot/types/TNotificationHandle.ts b/src/services/bot/types/TNotificationHandle.ts index f8726c7..e16aea6 100644 --- a/src/services/bot/types/TNotificationHandle.ts +++ b/src/services/bot/types/TNotificationHandle.ts @@ -18,11 +18,11 @@ export type TNotificationHandle = { > getLastAddressJettonPurchaseFromDB: ( jettonId: number, - ) => Promise + ) => Promise getLastAddressNotificationFromDB: ( jettonId: number, ) => Promise getFirstAddressJettonPurchaseFromDB: ( jettonId: number, - ) => Promise + ) => Promise } diff --git a/src/services/bot/types/TNotifications.ts b/src/services/bot/types/TNotifications.ts index f62887d..bc7ac36 100644 --- a/src/services/bot/types/TNotifications.ts +++ b/src/services/bot/types/TNotifications.ts @@ -13,6 +13,7 @@ export type TJettonRateNotification = TBaseNotification & { decimals: number action: ENotificationType.UP | ENotificationType.DOWN } + export type TNewJettonNotification = Omit & { jetton: string price: number @@ -20,6 +21,7 @@ export type TNewJettonNotification = Omit & { decimals: number action: ENotificationType.NEW_JETTON } + export type TNotHoldedJettonNotification = TBaseNotification & { action: ENotificationType.NOT_HOLD_JETTON_ANYMORE } diff --git a/src/services/bot/utils/getNotifications.ts b/src/services/bot/utils/getNotifications.ts index 0907b80..fc60f36 100644 --- a/src/services/bot/utils/getNotifications.ts +++ b/src/services/bot/utils/getNotifications.ts @@ -29,12 +29,11 @@ export async function* getNotifications( for (const jetton of addressJettonsFromDb) { if (!addressJettonsFromChainObj[jetton.token]) { const firstPurchase = await getFirstAddressJettonPurchaseFromDB(jetton.id) - if (!firstPurchase) { - continue - } const secondsFromPurchase = Date.now() / 1000 - firstPurchase.timestamp - // Estimated time to bypass a rollback - if (secondsFromPurchase <= 60) { + if ( + secondsFromPurchase <= + Number(process.env.SECONDS_FROM_PURCHASE_WITH_ROLLBACK_POSSIBILITY) + ) { continue } yield { @@ -49,15 +48,11 @@ export async function* getNotifications( const lastPurchase = await getLastAddressJettonPurchaseFromDB(jetton.id) const lastNotification = await getLastAddressNotificationFromDB(jetton.id) console.log({ lastPurchase, lastNotification }) - const newestTransactionInDb = - lastPurchase && lastNotification - ? lastPurchase.timestamp > lastNotification.timestamp - ? lastPurchase - : lastNotification - : lastPurchase || lastNotification - if (!newestTransactionInDb) { - continue - } + const newestTransactionInDb = lastNotification + ? lastPurchase.timestamp > lastNotification.timestamp + ? lastPurchase + : lastNotification + : lastPurchase const decimals = addressJettonsFromChainObj[jetton.token].decimals delete addressJettonsFromChainObj[jetton.token] const timestamp = Math.floor(Date.now() / 1000) diff --git a/src/services/bot/utils/handleCommandDisconnect.ts b/src/services/bot/utils/handleCommandDisconnect.ts index 4e21958..c83d992 100644 --- a/src/services/bot/utils/handleCommandDisconnect.ts +++ b/src/services/bot/utils/handleCommandDisconnect.ts @@ -31,6 +31,7 @@ export const handleCommandDisconnect = async ( if (!deletedWallet) { await ctx.reply(ctx.i18n.message.error()) await handleCommandDisconnect(db, ctx, '') + return } const address = new TonWeb.utils.Address(deletedWallet.address) const userFriendlyAddress = address.toString(true, true, true) diff --git a/src/services/bot/utils/handleNotification.ts b/src/services/bot/utils/handleNotification.ts index 4c4b722..fd50237 100644 --- a/src/services/bot/utils/handleNotification.ts +++ b/src/services/bot/utils/handleNotification.ts @@ -1,5 +1,5 @@ import type { Telegraf } from 'telegraf' -import { and, eq } from 'drizzle-orm' +import { eq } from 'drizzle-orm' import TonWeb from 'tonweb' import { insertUserNotification, @@ -7,13 +7,16 @@ import { selectFirstUserPurchaseByJettonId, selectLastUserNotificationByJettonId, selectLastUserPurchaseByJettonId, + selectTokenByAddressAndWalletId, selectUserSettings, + selectUserWallets, + selectWalletById, upsertToken, } from '../../../db/queries' import { ENotificationType } from '../constants' import type { TNotificationHandle, TTelegrafContext } from '../types' import { getDbConnection, getJettonsByAddress, normalizePrice } from '../../../utils' -import { tokens, users, wallets } from '../../../db/schema' +import { tokens, users } from '../../../db/schema' import { i18n } from '../i18n' import { getNotifications } from '.' import { getPrice } from '../../../utils/parseTxData' @@ -28,7 +31,7 @@ export const handleNotification = async (bot: Telegraf) => { }, getPrice, getUsersInDb: () => db.select().from(users), - getWalletsInDb: userId => db.select().from(wallets).where(eq(wallets.userId, userId)), + getWalletsInDb: userId => selectUserWallets(db, userId), getJettonsFromDB: walletId => db.select().from(tokens).where(eq(tokens.walletId, walletId)), getJettonsFromChain: getJettonsByAddress, getLastAddressJettonPurchaseFromDB: (jettonId: number) => @@ -45,33 +48,31 @@ export const handleNotification = async (bot: Telegraf) => { console.log({ notification }) const userSettings = await selectUserSettings(db, notification.userId) if (notification.action === ENotificationType.NEW_JETTON) { + const [wallet] = await selectWalletById(db, notification.walletId) + const address = new TonWeb.utils.Address(wallet.address) + const walletUserFriendly = address.toString(true, true, true) await upsertToken(db, { token: notification.jetton, walletId: notification.walletId, ticker: notification.symbol, }) - const [jetton] = await db - .select() - .from(tokens) - .where( - and(eq(tokens.token, notification.jetton), eq(tokens.walletId, notification.walletId)), - ) - const purchase = await insertUserPurchase(db, { + const [jetton] = await selectTokenByAddressAndWalletId( + db, + notification.jetton, + notification.walletId, + ) + await insertUserPurchase(db, { timestamp: notification.timestamp, jettonId: jetton.id, price: `${notification.price}`, }) - console.log( - { insertedJetton: jetton, purchase }, - { - timestamp: notification.timestamp, - jettonId: jetton.id, - price: `${notification.price}`, - }, - ) await bot.telegram.sendMessage( notification.userId, - i18n(userSettings?.languageCode).message.detectedNewJetton(notification.symbol), + i18n(userSettings?.languageCode).message.detectedNewJetton( + notification.symbol, + walletUserFriendly, + notification.price, + ), ) continue } @@ -84,7 +85,7 @@ export const handleNotification = async (bot: Telegraf) => { continue } - const [wallet] = await db.select().from(wallets).where(eq(wallets.id, notification.walletId)) + const [wallet] = await selectWalletById(db, notification.walletId) const address = new TonWeb.utils.Address(wallet.address) const walletUserFriendly = address.toString(true, true, true) const text = diff --git a/src/services/bot/utils/handleSuccessfulWalletLinkNotification.ts b/src/services/bot/utils/handleSuccessfulWalletLinkNotification.ts index 8b2d791..740c5ca 100644 --- a/src/services/bot/utils/handleSuccessfulWalletLinkNotification.ts +++ b/src/services/bot/utils/handleSuccessfulWalletLinkNotification.ts @@ -47,25 +47,27 @@ export const handleSuccessfulWalletLinkNotification = async ( timestamp: Math.floor(Date.now() / 1000), }) } - const [wallet] = await insertUserAdress(db, payload) - for (const jetton of jettonsForDb) { - await upsertToken(db, { - token: jetton.token, - walletId: wallet.id, - ticker: jetton.ticker, - }) - if (jetton.price) { - const [insertedToken] = await db - .select() - .from(tokens) - .where(and(eq(tokens.token, jetton.token), eq(tokens.walletId, wallet.id))) - await insertUserPurchase(db, { - timestamp: Math.floor(Date.now() / 1000), - jettonId: insertedToken.id, - price: `${jetton.price}`, + await db.transaction(async tx => { + const [wallet] = await insertUserAdress(tx, payload) + for (const jetton of jettonsForDb) { + await upsertToken(tx, { + token: jetton.token, + walletId: wallet.id, + ticker: jetton.ticker, }) + if (jetton.price) { + const [insertedToken] = await tx + .select() + .from(tokens) + .where(and(eq(tokens.token, jetton.token), eq(tokens.walletId, wallet.id))) + await insertUserPurchase(tx, { + timestamp: Math.floor(Date.now() / 1000), + jettonId: insertedToken.id, + price: `${jetton.price}`, + }) + } } - } + }) const address = new TonWeb.utils.Address(payload.address) const userFriendlyAddress = address.toString(true, true, true) await bot.telegram.sendMessage( diff --git a/src/services/web/handlers/onGetWalletData.ts b/src/services/web/handlers/onGetWalletData.ts index 55b6e03..c59cba3 100644 --- a/src/services/web/handlers/onGetWalletData.ts +++ b/src/services/web/handlers/onGetWalletData.ts @@ -1,4 +1,3 @@ -import { hiddenTickers } from '../../../constants' import { selectUserWallets } from '../../../db/queries' import { getDbConnection } from '../../../utils' import { api } from '../../../utils/parseTxData' @@ -22,10 +21,7 @@ unknown, for (const wallet of userWallets) { const result = await api(wallet.address) Object.entries(result).forEach(([ticker, info]) => { - if ( - !hiddenTickers.includes(info.symbol) && - (!uniqJettonsObject[ticker] || uniqJettonsObject[ticker].lastBuyTime < info.lastBuyTime) - ) { + if (!uniqJettonsObject[ticker] || uniqJettonsObject[ticker].lastBuyTime < info.lastBuyTime) { uniqJettonsObject[ticker] = info } }) diff --git a/src/types/TDbTransaction.ts b/src/types/TDbTransaction.ts new file mode 100644 index 0000000..e4e27b2 --- /dev/null +++ b/src/types/TDbTransaction.ts @@ -0,0 +1,9 @@ +import type { ExtractTablesWithRelations } from 'drizzle-orm' +import type { PgTransaction } from 'drizzle-orm/pg-core' +import type { PostgresJsQueryResultHKT } from 'drizzle-orm/postgres-js' + +export type TDbTransaction = PgTransaction< +PostgresJsQueryResultHKT, +Record, +ExtractTablesWithRelations> +> diff --git a/src/types/env.d.ts b/src/types/env.d.ts index 6bf53c6..3ab4786 100644 --- a/src/types/env.d.ts +++ b/src/types/env.d.ts @@ -14,6 +14,7 @@ type EnvKeys = | 'POSTGRES_DB' | 'AMQP_ENDPOINT' | 'LIMIT_WALLETS_FOR_USER' + | 'SECONDS_FROM_PURCHASE_WITH_ROLLBACK_POSSIBILITY' declare global { namespace NodeJS { diff --git a/src/types/index.ts b/src/types/index.ts index a6db7d5..f509624 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -2,3 +2,4 @@ export type { TDbConnection } from './TDbConnection' export type { TDeepTypeInObject } from './TDeepTypeInObject' export type { TSuccessfulWalletLinkNotificationCh } from './TSuccessfulWalletLinkNotificationCh' export { TJetton } from './TJetton' +export { TDbTransaction } from './TDbTransaction' diff --git a/src/utils/parseTxData/api.ts b/src/utils/parseTxData/api.ts index 89f0a08..2fee98d 100644 --- a/src/utils/parseTxData/api.ts +++ b/src/utils/parseTxData/api.ts @@ -1,5 +1,5 @@ import { getAddressPnL, getChart, getJettonsByAddress } from '.' -import { normalizePrice } from '..' +import { filterHiddenJettons } from '../../services/bot/utils' export const api = async (address: string) => { const jettons = await getJettonsByAddress(address) @@ -7,23 +7,25 @@ export const api = async (address: string) => { string, (typeof jettons)[number] & { pnlPercentage?: number - chart: [timestamp: number, price: number | string][] + chart: [timestamp: number, price: number][] lastBuyTime: number } > = {} - for (const jettonInfo of jettons) { + for (const jettonInfo of filterHiddenJettons(jettons)) { const addressPnl = await getAddressPnL(address, jettonInfo.address) if (!addressPnl) continue const { pnlPercentage, lastBuyTime } = addressPnl const chart = await getChart(jettonInfo.address, lastBuyTime) const slicedChart = chart.filter(x => x[0] >= lastBuyTime) - // console.log(chart, lastBuyTime, slicedChart) + if (slicedChart.length < 2) { + throw new Error( + `slicedChart.length < 2: ${JSON.stringify(chart)}. Wallet: ${jettonInfo.address}, lastBuyTime: ${lastBuyTime}`, + ) + } res[jettonInfo.symbol] = { ...jettonInfo, pnlPercentage, - chart: (slicedChart.length >= 2 ? slicedChart : chart) - .reverse() - .map(entity => [entity[0], normalizePrice(entity[1], jettonInfo.decimals)]), + chart: slicedChart, lastBuyTime, } } diff --git a/tests/getNotifications.test.ts b/tests/getNotifications.test.ts index efb6c22..aefd51d 100644 --- a/tests/getNotifications.test.ts +++ b/tests/getNotifications.test.ts @@ -385,14 +385,14 @@ describe('getNotifications', () => { handle.getLastAddressJettonPurchaseFromDB.mock.mockImplementation(() => Promise.resolve({ jettonId: 1, - timestamp: Date.now() - 20000, + timestamp: Date.now() / 1000 - 20000, price: 100, }), ) handle.getLastAddressNotificationFromDB.mock.mockImplementation(() => Promise.resolve({ jettonId: 1, - timestamp: Date.now() - 10000, + timestamp: Date.now() / 1000 - 10000, price: 100, }), ) @@ -435,9 +435,6 @@ describe('getNotifications', () => { }, ]), ) - handle.getLastAddressJettonPurchaseFromDB.mock.mockImplementation(() => - Promise.resolve(undefined), - ) for await (const notification of getNotifications(handle as unknown as TNotificationHandle)) { notifications.push(notification) } @@ -457,4 +454,83 @@ describe('getNotifications', () => { ], ) }) + + it('should handle rollbacks', async () => { + handle.getUsersInDb.mock.mockImplementation(() => + Promise.resolve([ + { + id: 1, + username: 'testUser', + timestamp: 1, + }, + ]), + ) + handle.getWalletsInDb.mock.mockImplementation(() => + Promise.resolve([{ id: 1, userId: 1, address: 'wallet1' }]), + ) + handle.getJettonsFromDB.mock.mockImplementation(() => Promise.resolve([])) + handle.getJettonsFromChain.mock.mockImplementation(() => + Promise.resolve([ + { + address: 'jetton1', + symbol: 'JET', + decimals: 9, + }, + ]), + ) + handle.getPrice.mock.mockImplementation(() => Promise.resolve(100)) + // handle.getLastAddressJettonPurchaseFromDB.mock.mockImplementation(() => + // Promise.resolve(undefined), + // ) + // handle.getLastAddressNotificationFromDB.mock.mockImplementation(() => + // Promise.resolve({ + // jettonId: 1, + // timestamp: Date.now() / 1000 - 10000, + // price: 100, + // }), + // ) + for await (const notification of getNotifications(handle as unknown as TNotificationHandle)) { + notifications.push(notification) + } + assert.equal(notifications.length, 1) + assert.equal(notifications[0].action, ENotificationType.NEW_JETTON) + notifications.length = 0 + + handle.getJettonsFromDB.mock.mockImplementation(() => + Promise.resolve([ + { + id: 1, + token: 'jetton1', + walletId: 1, + ticker: 'JET', + }, + ]), + ) + handle.getJettonsFromChain.mock.mockImplementation(() => Promise.resolve([])) + handle.getFirstAddressJettonPurchaseFromDB.mock.mockImplementation(() => + Promise.resolve({ + jettonId: 1, + timestamp: + Date.now() / 1000 - Number(process.env.SECONDS_FROM_PURCHASE_WITH_ROLLBACK_POSSIBILITY), + price: 100, + }), + ) + for await (const notification of getNotifications(handle as unknown as TNotificationHandle)) { + notifications.push(notification) + } + assert.equal(notifications.length, 0) + + handle.getFirstAddressJettonPurchaseFromDB.mock.mockImplementation(() => + Promise.resolve({ + jettonId: 1, + timestamp: Date.now() / 1000 - 10000, + price: 100, + }), + ) + for await (const notification of getNotifications(handle as unknown as TNotificationHandle)) { + notifications.push(notification) + } + assert.equal(notifications.length, 1) + assert.equal(notifications[0].action, ENotificationType.NOT_HOLD_JETTON_ANYMORE) + }) }) From 0371678f953083deb0824826557107b9feea76e6 Mon Sep 17 00:00:00 2001 From: Vishtar Date: Mon, 8 Jul 2024 19:18:35 +0400 Subject: [PATCH 5/8] fixed test --- src/services/bot/types/TNotificationHandle.ts | 1 + src/services/bot/utils/getNotifications.ts | 6 ++---- src/services/bot/utils/handleNotification.ts | 1 + tests/getNotifications.test.ts | 17 ++++------------- 4 files changed, 8 insertions(+), 17 deletions(-) diff --git a/src/services/bot/types/TNotificationHandle.ts b/src/services/bot/types/TNotificationHandle.ts index e16aea6..992694a 100644 --- a/src/services/bot/types/TNotificationHandle.ts +++ b/src/services/bot/types/TNotificationHandle.ts @@ -5,6 +5,7 @@ export type TNotificationHandle = { top: number bottom: number } + secondForPossibleRollback: number getUsersInDb: () => Promise<(typeof users.$inferSelect)[]> getPrice: (jetton: string) => Promise getWalletsInDb: (userId: number) => Promise<(typeof wallets.$inferSelect)[]> diff --git a/src/services/bot/utils/getNotifications.ts b/src/services/bot/utils/getNotifications.ts index fc60f36..dfc48a4 100644 --- a/src/services/bot/utils/getNotifications.ts +++ b/src/services/bot/utils/getNotifications.ts @@ -9,6 +9,7 @@ export async function* getNotifications( getFirstAddressJettonPurchaseFromDB, getLastAddressJettonPurchaseFromDB, getLastAddressNotificationFromDB, + secondForPossibleRollback, getJettonsFromChain, getJettonsFromDB, getWalletsInDb, @@ -30,10 +31,7 @@ export async function* getNotifications( if (!addressJettonsFromChainObj[jetton.token]) { const firstPurchase = await getFirstAddressJettonPurchaseFromDB(jetton.id) const secondsFromPurchase = Date.now() / 1000 - firstPurchase.timestamp - if ( - secondsFromPurchase <= - Number(process.env.SECONDS_FROM_PURCHASE_WITH_ROLLBACK_POSSIBILITY) - ) { + if (secondsFromPurchase <= secondForPossibleRollback) { continue } yield { diff --git a/src/services/bot/utils/handleNotification.ts b/src/services/bot/utils/handleNotification.ts index fd50237..ac760bc 100644 --- a/src/services/bot/utils/handleNotification.ts +++ b/src/services/bot/utils/handleNotification.ts @@ -29,6 +29,7 @@ export const handleNotification = async (bot: Telegraf) => { top: Number(process.env.NOTIFICATION_RATE_UP), bottom: Number(process.env.NOTIFICATION_RATE_DOWN), }, + secondForPossibleRollback: Number(process.env.SECONDS_FROM_PURCHASE_WITH_ROLLBACK_POSSIBILITY), getPrice, getUsersInDb: () => db.select().from(users), getWalletsInDb: userId => selectUserWallets(db, userId), diff --git a/tests/getNotifications.test.ts b/tests/getNotifications.test.ts index aefd51d..339b0da 100644 --- a/tests/getNotifications.test.ts +++ b/tests/getNotifications.test.ts @@ -6,11 +6,12 @@ import type { TNotification } from '../src/services/bot/types/TNotifications' import { ENotificationType } from '../src/services/bot/constants' import { getNotifications } from '../src/services/bot/utils' -type THandleCallbackKeys = keyof Omit +type THandleCallbackKeys = keyof Omit describe('getNotifications', () => { let handle: Record> & { rates: TNotificationHandle['rates'] + secondForPossibleRollback: TNotificationHandle['secondForPossibleRollback'] } const notifications: TNotification[] = [] @@ -20,6 +21,7 @@ describe('getNotifications', () => { top: 2, bottom: 0.5, }, + secondForPossibleRollback: 60, getPrice: mock.fn(), getUsersInDb: mock.fn(), getWalletsInDb: mock.fn(), @@ -479,16 +481,6 @@ describe('getNotifications', () => { ]), ) handle.getPrice.mock.mockImplementation(() => Promise.resolve(100)) - // handle.getLastAddressJettonPurchaseFromDB.mock.mockImplementation(() => - // Promise.resolve(undefined), - // ) - // handle.getLastAddressNotificationFromDB.mock.mockImplementation(() => - // Promise.resolve({ - // jettonId: 1, - // timestamp: Date.now() / 1000 - 10000, - // price: 100, - // }), - // ) for await (const notification of getNotifications(handle as unknown as TNotificationHandle)) { notifications.push(notification) } @@ -510,8 +502,7 @@ describe('getNotifications', () => { handle.getFirstAddressJettonPurchaseFromDB.mock.mockImplementation(() => Promise.resolve({ jettonId: 1, - timestamp: - Date.now() / 1000 - Number(process.env.SECONDS_FROM_PURCHASE_WITH_ROLLBACK_POSSIBILITY), + timestamp: Date.now() / 1000 - handle.secondForPossibleRollback, price: 100, }), ) From 68f4cb192de110186155bdbdc6675f2cd0b831e4 Mon Sep 17 00:00:00 2001 From: Vishtar Date: Mon, 8 Jul 2024 19:49:24 +0400 Subject: [PATCH 6/8] wip --- frontend/src/components/Charts/index.tsx | 16 ++++++--------- frontend/src/index.css | 2 +- src/services/bot/i18n/en.ts | 2 ++ src/services/bot/i18n/ru.ts | 2 ++ src/services/bot/utils/handleNotification.ts | 3 +++ .../handleSuccessfulWalletLinkNotification.ts | 20 +++++++++++++++++-- 6 files changed, 32 insertions(+), 13 deletions(-) diff --git a/frontend/src/components/Charts/index.tsx b/frontend/src/components/Charts/index.tsx index 8b8cc51..f7816eb 100644 --- a/frontend/src/components/Charts/index.tsx +++ b/frontend/src/components/Charts/index.tsx @@ -36,7 +36,7 @@ export const Charts = (props: { if (isLoading) { return ( - + ) @@ -45,7 +45,7 @@ export const Charts = (props: { if (!data) { return ( -

{t('label.error')}

+

{t('label.error')}

) } @@ -53,7 +53,7 @@ export const Charts = (props: { if (!data.jettons.length) { return ( -

{t('label.noJettons')}

+

{t('label.noJettons')}

) } @@ -61,9 +61,7 @@ export const Charts = (props: { if (data) { return (
-

+

{t('label.yourJettons')}

{data @@ -82,15 +80,13 @@ export const Charts = (props: { className="shadow rounded-full max-w-full h-auto align-middle border-none" />
-

+

{obj.symbol}

{obj.pnlPercentage !== 0 ? (
-

{}

+

{}

'Connect Wallet', + openApp: () => `Open App`, + open: () => `Open`, link: () => 'Connect', }, command: { diff --git a/src/services/bot/i18n/ru.ts b/src/services/bot/i18n/ru.ts index cf01108..eec5516 100644 --- a/src/services/bot/i18n/ru.ts +++ b/src/services/bot/i18n/ru.ts @@ -69,6 +69,8 @@ ${disconnectCOmmandList} }, button: { linkWallet: () => 'Подключить кошелёк', + openApp: () => `Открыть приложение`, + open: () => `Открыть`, link: () => 'Подключить', }, command: { diff --git a/src/services/bot/utils/handleNotification.ts b/src/services/bot/utils/handleNotification.ts index ac760bc..6a7864b 100644 --- a/src/services/bot/utils/handleNotification.ts +++ b/src/services/bot/utils/handleNotification.ts @@ -74,6 +74,9 @@ export const handleNotification = async (bot: Telegraf) => { walletUserFriendly, notification.price, ), + { + parse_mode: 'Markdown' + } ) continue } diff --git a/src/services/bot/utils/handleSuccessfulWalletLinkNotification.ts b/src/services/bot/utils/handleSuccessfulWalletLinkNotification.ts index 740c5ca..f9cd285 100644 --- a/src/services/bot/utils/handleSuccessfulWalletLinkNotification.ts +++ b/src/services/bot/utils/handleSuccessfulWalletLinkNotification.ts @@ -1,4 +1,4 @@ -import type { Telegraf } from 'telegraf' +import { Markup, type Telegraf } from 'telegraf' import TonWeb from 'tonweb' import type { TTelegrafContext } from '../types' import type { TDbConnection, TSuccessfulWalletLinkNotificationCh } from '../../../types' @@ -70,7 +70,7 @@ export const handleSuccessfulWalletLinkNotification = async ( }) const address = new TonWeb.utils.Address(payload.address) const userFriendlyAddress = address.toString(true, true, true) - await bot.telegram.sendMessage( + const message = await bot.telegram.sendMessage( payload.userId, i18n(userSettings?.languageCode).message.newWalletConnected( userFriendlyAddress, @@ -80,8 +80,24 @@ export const handleSuccessfulWalletLinkNotification = async ( ), { parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: [[ + Markup.button.webApp(i18n(userSettings?.languageCode).button.openApp(), process.env.TELEGRAM_BOT_WEB_APP), + ]], + } }, ) + await bot.telegram.pinChatMessage(payload.userId, message.message_id) + await bot.telegram.setChatMenuButton({ + chatId: payload.userId, + menuButton: { + type: 'web_app', + text: i18n(userSettings?.languageCode).button.open(), + web_app: { + url: process.env.TELEGRAM_BOT_WEB_APP, + }, + } + }) } return true } From e5cd40950b9a8fb3d38ea962b20cb9288df8b3c3 Mon Sep 17 00:00:00 2001 From: Vishtar Date: Mon, 8 Jul 2024 21:46:55 +0400 Subject: [PATCH 7/8] disabled NOT_HOLDED_JETTON_ANYMORE notifications --- src/services/bot/utils/getNotifications.ts | 10 +- src/services/bot/utils/handleNotification.ts | 14 +- .../handleSuccessfulWalletLinkNotification.ts | 15 +- tests/getNotifications.test.ts | 130 +++++++++--------- 4 files changed, 87 insertions(+), 82 deletions(-) diff --git a/src/services/bot/utils/getNotifications.ts b/src/services/bot/utils/getNotifications.ts index dfc48a4..7c4e1f3 100644 --- a/src/services/bot/utils/getNotifications.ts +++ b/src/services/bot/utils/getNotifications.ts @@ -29,11 +29,11 @@ export async function* getNotifications( const addressJettonsFromDb = await getJettonsFromDB(wallet.id) for (const jetton of addressJettonsFromDb) { if (!addressJettonsFromChainObj[jetton.token]) { - const firstPurchase = await getFirstAddressJettonPurchaseFromDB(jetton.id) - const secondsFromPurchase = Date.now() / 1000 - firstPurchase.timestamp - if (secondsFromPurchase <= secondForPossibleRollback) { - continue - } + // const firstPurchase = await getFirstAddressJettonPurchaseFromDB(jetton.id) + // const secondsFromPurchase = Date.now() / 1000 - firstPurchase.timestamp + // if (secondsFromPurchase <= secondForPossibleRollback) { + // continue + // } yield { userId: user.id, walletId: wallet.id, diff --git a/src/services/bot/utils/handleNotification.ts b/src/services/bot/utils/handleNotification.ts index 6a7864b..56918dc 100644 --- a/src/services/bot/utils/handleNotification.ts +++ b/src/services/bot/utils/handleNotification.ts @@ -75,17 +75,17 @@ export const handleNotification = async (bot: Telegraf) => { notification.price, ), { - parse_mode: 'Markdown' - } + parse_mode: 'Markdown', + }, ) continue } if (notification.action === ENotificationType.NOT_HOLD_JETTON_ANYMORE) { - await db.delete(tokens).where(eq(tokens.id, notification.jettonId)) - await bot.telegram.sendMessage( - notification.userId, - i18n(userSettings?.languageCode).message.youNoLongerHaveJetton(notification.symbol), - ) + // await db.delete(tokens).where(eq(tokens.id, notification.jettonId)) + // await bot.telegram.sendMessage( + // notification.userId, + // i18n(userSettings?.languageCode).message.youNoLongerHaveJetton(notification.symbol), + // ) continue } diff --git a/src/services/bot/utils/handleSuccessfulWalletLinkNotification.ts b/src/services/bot/utils/handleSuccessfulWalletLinkNotification.ts index f9cd285..255786a 100644 --- a/src/services/bot/utils/handleSuccessfulWalletLinkNotification.ts +++ b/src/services/bot/utils/handleSuccessfulWalletLinkNotification.ts @@ -81,10 +81,15 @@ export const handleSuccessfulWalletLinkNotification = async ( { parse_mode: 'Markdown', reply_markup: { - inline_keyboard: [[ - Markup.button.webApp(i18n(userSettings?.languageCode).button.openApp(), process.env.TELEGRAM_BOT_WEB_APP), - ]], - } + inline_keyboard: [ + [ + Markup.button.webApp( + i18n(userSettings?.languageCode).button.openApp(), + process.env.TELEGRAM_BOT_WEB_APP, + ), + ], + ], + }, }, ) await bot.telegram.pinChatMessage(payload.userId, message.message_id) @@ -96,7 +101,7 @@ export const handleSuccessfulWalletLinkNotification = async ( web_app: { url: process.env.TELEGRAM_BOT_WEB_APP, }, - } + }, }) } return true diff --git a/tests/getNotifications.test.ts b/tests/getNotifications.test.ts index 339b0da..7ef3ffa 100644 --- a/tests/getNotifications.test.ts +++ b/tests/getNotifications.test.ts @@ -457,71 +457,71 @@ describe('getNotifications', () => { ) }) - it('should handle rollbacks', async () => { - handle.getUsersInDb.mock.mockImplementation(() => - Promise.resolve([ - { - id: 1, - username: 'testUser', - timestamp: 1, - }, - ]), - ) - handle.getWalletsInDb.mock.mockImplementation(() => - Promise.resolve([{ id: 1, userId: 1, address: 'wallet1' }]), - ) - handle.getJettonsFromDB.mock.mockImplementation(() => Promise.resolve([])) - handle.getJettonsFromChain.mock.mockImplementation(() => - Promise.resolve([ - { - address: 'jetton1', - symbol: 'JET', - decimals: 9, - }, - ]), - ) - handle.getPrice.mock.mockImplementation(() => Promise.resolve(100)) - for await (const notification of getNotifications(handle as unknown as TNotificationHandle)) { - notifications.push(notification) - } - assert.equal(notifications.length, 1) - assert.equal(notifications[0].action, ENotificationType.NEW_JETTON) - notifications.length = 0 + // it('should handle rollbacks', async () => { + // handle.getUsersInDb.mock.mockImplementation(() => + // Promise.resolve([ + // { + // id: 1, + // username: 'testUser', + // timestamp: 1, + // }, + // ]), + // ) + // handle.getWalletsInDb.mock.mockImplementation(() => + // Promise.resolve([{ id: 1, userId: 1, address: 'wallet1' }]), + // ) + // handle.getJettonsFromDB.mock.mockImplementation(() => Promise.resolve([])) + // handle.getJettonsFromChain.mock.mockImplementation(() => + // Promise.resolve([ + // { + // address: 'jetton1', + // symbol: 'JET', + // decimals: 9, + // }, + // ]), + // ) + // handle.getPrice.mock.mockImplementation(() => Promise.resolve(100)) + // for await (const notification of getNotifications(handle as unknown as TNotificationHandle)) { + // notifications.push(notification) + // } + // assert.equal(notifications.length, 1) + // assert.equal(notifications[0].action, ENotificationType.NEW_JETTON) + // notifications.length = 0 - handle.getJettonsFromDB.mock.mockImplementation(() => - Promise.resolve([ - { - id: 1, - token: 'jetton1', - walletId: 1, - ticker: 'JET', - }, - ]), - ) - handle.getJettonsFromChain.mock.mockImplementation(() => Promise.resolve([])) - handle.getFirstAddressJettonPurchaseFromDB.mock.mockImplementation(() => - Promise.resolve({ - jettonId: 1, - timestamp: Date.now() / 1000 - handle.secondForPossibleRollback, - price: 100, - }), - ) - for await (const notification of getNotifications(handle as unknown as TNotificationHandle)) { - notifications.push(notification) - } - assert.equal(notifications.length, 0) + // handle.getJettonsFromDB.mock.mockImplementation(() => + // Promise.resolve([ + // { + // id: 1, + // token: 'jetton1', + // walletId: 1, + // ticker: 'JET', + // }, + // ]), + // ) + // handle.getJettonsFromChain.mock.mockImplementation(() => Promise.resolve([])) + // handle.getFirstAddressJettonPurchaseFromDB.mock.mockImplementation(() => + // Promise.resolve({ + // jettonId: 1, + // timestamp: Date.now() / 1000 - handle.secondForPossibleRollback, + // price: 100, + // }), + // ) + // for await (const notification of getNotifications(handle as unknown as TNotificationHandle)) { + // notifications.push(notification) + // } + // assert.equal(notifications.length, 0) - handle.getFirstAddressJettonPurchaseFromDB.mock.mockImplementation(() => - Promise.resolve({ - jettonId: 1, - timestamp: Date.now() / 1000 - 10000, - price: 100, - }), - ) - for await (const notification of getNotifications(handle as unknown as TNotificationHandle)) { - notifications.push(notification) - } - assert.equal(notifications.length, 1) - assert.equal(notifications[0].action, ENotificationType.NOT_HOLD_JETTON_ANYMORE) - }) + // handle.getFirstAddressJettonPurchaseFromDB.mock.mockImplementation(() => + // Promise.resolve({ + // jettonId: 1, + // timestamp: Date.now() / 1000 - 10000, + // price: 100, + // }), + // ) + // for await (const notification of getNotifications(handle as unknown as TNotificationHandle)) { + // notifications.push(notification) + // } + // assert.equal(notifications.length, 1) + // assert.equal(notifications[0].action, ENotificationType.NOT_HOLD_JETTON_ANYMORE) + // }) }) From 580b23e5d6fbbec42e66ae51f978f7e28f2687f6 Mon Sep 17 00:00:00 2001 From: Vishtar Date: Mon, 8 Jul 2024 21:47:19 +0400 Subject: [PATCH 8/8] README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e899b3f..353980f 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,6 @@ It proactively notifies users of their gains via Telegram, making it easy to man ## Tests -All tests are located in the [tests](./tests) directory. Currently, there is one unit test (with 7 scenarios) for the main business function: +All tests are located in the [tests](./tests) directory. Currently, there is one unit test (with 6 scenarios) for the main business function: - [tests/getNotifications.test.ts](./tests/getNotifications.test.ts)