From d164f4784d5fa24bcee85c17496b8a4d7d818ba6 Mon Sep 17 00:00:00 2001 From: Meriem-BM Date: Sun, 18 Aug 2024 07:11:48 +0100 Subject: [PATCH 1/4] Fix: add instant trx check indpoint --- src/resolvers/draftDonationResolver.ts | 41 ++++++++++++++++++- .../cronJobs/checkQRTransactionJob.ts | 28 +++++++++++-- 2 files changed, 64 insertions(+), 5 deletions(-) diff --git a/src/resolvers/draftDonationResolver.ts b/src/resolvers/draftDonationResolver.ts index 66dd341d1..e5c1f1e2e 100644 --- a/src/resolvers/draftDonationResolver.ts +++ b/src/resolvers/draftDonationResolver.ts @@ -25,6 +25,7 @@ import { findRecurringDonationByProjectIdAndUserIdAndCurrency, } from '../repositories/recurringDonationRepository'; import { RecurringDonation } from '../entities/recurringDonation'; +import { checkTransactions } from '../services/cronJobs/checkQRTransactionJob'; const draftDonationEnabled = process.env.ENABLE_DRAFT_DONATION === 'true'; const draftRecurringDonationEnabled = @@ -362,6 +363,16 @@ export class DraftDonationResolver { .where('draftDonation.id = :id', { id }) .getOne(); + if (!draftDonation) return null; + + if ( + draftDonation.expiresAt && + new Date(draftDonation.expiresAt).getTime < new Date().getTime + ) { + await DraftDonation.update({ id }, { status: 'failed' }); + draftDonation.status = 'failed'; + } + return draftDonation; } @@ -424,7 +435,7 @@ export class DraftDonationResolver { throw new Error(translationErrorMessagesKeys.DRAFT_DONATION_NOT_FOUND); } - await DraftDonation.update({ id }, { expiresAt }); + await DraftDonation.update({ id }, { expiresAt, status: 'pending' }); return { ...draftDonation, @@ -437,4 +448,32 @@ export class DraftDonationResolver { return null; } } + + @Query(_returns => DraftDonation, { nullable: true }) + async fetchDaftDonationWithUpdatedStatus( + @Arg('id', _type => Int) id: number, + ): Promise { + try { + const draftDonation = await DraftDonation.createQueryBuilder( + 'draftDonation', + ) + .where('draftDonation.id = :id', { id }) + .getOne(); + + if (!draftDonation) return null; + + if (draftDonation.isQRDonation) { + await checkTransactions(draftDonation); + } + + return await DraftDonation.createQueryBuilder('draftDonation') + .where('draftDonation.id = :id', { id }) + .getOne(); + } catch (e) { + logger.error( + `Error in fetchDaftDonationWithUpdatedStatus - id: ${id} - error: ${e.message}`, + ); + return null; + } + } } diff --git a/src/services/cronJobs/checkQRTransactionJob.ts b/src/services/cronJobs/checkQRTransactionJob.ts index cfe23edba..a222364eb 100644 --- a/src/services/cronJobs/checkQRTransactionJob.ts +++ b/src/services/cronJobs/checkQRTransactionJob.ts @@ -20,7 +20,7 @@ const STELLAR_HORIZON_API = 'https://horizon.stellar.org'; const cronJobTime = (config.get('CHECK_QR_TRANSACTIONS_CRONJOB_EXPRESSION') as string) || - '0 */3 * * * *'; + '0 */1 * * * *'; async function getPendingDraftDonations() { return await DraftDonation.createQueryBuilder('draftDonation') @@ -41,10 +41,30 @@ const getToken = async ( }; // Check for transactions -async function checkTransactions(donation: DraftDonation): Promise { - const { toWalletAddress, amount, toWalletMemo } = donation; +export async function checkTransactions( + donation: DraftDonation, +): Promise { + const { toWalletAddress, amount, toWalletMemo, expiresAt, id } = donation; try { + if (!toWalletAddress || !amount) { + logger.debug(`Missing required fields for donation ID ${donation.id}`); + return; + } + + // Check if donation has expired + const now = new Date().getTime(); + const expiresAtDate = new Date(expiresAt!).getTime() + 1 * 60 * 1000; + + if (now > expiresAtDate) { + logger.debug(`Donation ID ${id} has expired. Updating status to expired`); + await updateDraftDonationStatus({ + donationId: id, + status: 'failed', + }); + return; + } + const response = await axios.get( `${STELLAR_HORIZON_API}/accounts/${toWalletAddress}/payments?limit=200&order=desc&join=transactions&include_failed=true`, ); @@ -56,7 +76,7 @@ async function checkTransactions(donation: DraftDonation): Promise { for (const transaction of transactions) { if ( transaction.asset_type === 'native' && - transaction.type === 'payment' && + ['payment', 'create_account'].includes(transaction.type) && Number(transaction.amount) === amount && transaction.to === toWalletAddress ) { From 53c5e4db21bbe530df99ace2528a37327aa184cb Mon Sep 17 00:00:00 2001 From: Meriem-BM Date: Mon, 19 Aug 2024 04:21:07 +0100 Subject: [PATCH 2/4] fix: update project statistics for Stellar chain --- src/services/chains/index.ts | 5 ++ .../chains/stellar/transactionService.ts | 64 +++++++++++++++++++ .../cronJobs/checkQRTransactionJob.ts | 31 ++++++--- 3 files changed, 91 insertions(+), 9 deletions(-) create mode 100644 src/services/chains/stellar/transactionService.ts diff --git a/src/services/chains/index.ts b/src/services/chains/index.ts index fb11ef0ef..42707a502 100644 --- a/src/services/chains/index.ts +++ b/src/services/chains/index.ts @@ -1,6 +1,7 @@ import { ChainType } from '../../types/network'; import { getSolanaTransactionInfoFromNetwork } from './solana/transactionService'; import { getEvmTransactionInfoFromNetwork } from './evm/transactionService'; +import {getStellarTransactionInfoFromNetwork} from './stellar/transactionService'; import { i18n, translationErrorMessagesKeys } from '../../utils/errorMessages'; import { logger } from '../../utils/logger'; import { NETWORK_IDS } from '../../provider'; @@ -83,6 +84,10 @@ export async function getTransactionInfoFromNetwork( return getSolanaTransactionInfoFromNetwork(input); } + if (input.chainType === ChainType.STELLAR) { + return getStellarTransactionInfoFromNetwork(input); + } + // If chain is not Solana, it's EVM for sure return getEvmTransactionInfoFromNetwork(input); } diff --git a/src/services/chains/stellar/transactionService.ts b/src/services/chains/stellar/transactionService.ts new file mode 100644 index 000000000..6fc61049b --- /dev/null +++ b/src/services/chains/stellar/transactionService.ts @@ -0,0 +1,64 @@ +import { + NetworkTransactionInfo, + TransactionDetailInput, + validateTransactionWithInputData, +} from '../index'; +import { + i18n, + translationErrorMessagesKeys, +} from '../../../utils/errorMessages'; +import axios from 'axios'; + +const STELLAR_HORIZON_API_URL = + process.env.STELLAR_HORIZON_API_URL || 'https://horizon.stellar.org'; + +const getStellarTransactionInfo = async ( + txHash: string, +): Promise => { + const NATIVE_STELLAR_ASSET_CODE = 'XLM'; + // Fetch transaction info from stellar network + + const response = await axios.get( + `${STELLAR_HORIZON_API_URL}/transactions/${txHash}/payments`, + ); + + const transaction = response.data._embedded.records[0]; + + if (!transaction) return null; + + // when a transaction is made to a newly created account, Stellar mark it as type 'create_account' + if (transaction.type === 'create_account') { + return { + hash: transaction.transaction_hash, + amount: Number(transaction.starting_balance), + from: transaction.source_account, + to: transaction.account, + currency: NATIVE_STELLAR_ASSET_CODE, + timestamp: transaction.created_at, + }; + } else if (transaction.type === 'payment') { + if (transaction.asset_type !== 'native') return null; + return { + hash: transaction.transaction_hash, + amount: Number(transaction.amount), + from: transaction.from, + to: transaction.to, + currency: NATIVE_STELLAR_ASSET_CODE, + timestamp: transaction.created_at, + }; + } else return null; +}; + +export async function getStellarTransactionInfoFromNetwork( + input: TransactionDetailInput, +): Promise { + let txData; + txData = await getStellarTransactionInfo(input.txHash); + if (!txData) { + throw new Error( + i18n.__(translationErrorMessagesKeys.TRANSACTION_NOT_FOUND), + ); + } + validateTransactionWithInputData(txData, input); + return txData; +} diff --git a/src/services/cronJobs/checkQRTransactionJob.ts b/src/services/cronJobs/checkQRTransactionJob.ts index a222364eb..5e6bfa541 100644 --- a/src/services/cronJobs/checkQRTransactionJob.ts +++ b/src/services/cronJobs/checkQRTransactionJob.ts @@ -14,6 +14,7 @@ import { CoingeckoPriceAdapter } from '../../adapters/price/CoingeckoPriceAdapte import { findUserById } from '../../repositories/userRepository'; import { relatedActiveQfRoundForProject } from '../qfRoundService'; import { QfRound } from '../../entities/qfRound'; +import { syncDonationStatusWithBlockchainNetwork } from '../donationService'; const STELLAR_HORIZON_API = (config.get('STELLAR_HORIZON_API_URL') as string) || @@ -74,13 +75,21 @@ export async function checkTransactions( if (transactions.length === 0) return; for (const transaction of transactions) { - if ( - transaction.asset_type === 'native' && - ['payment', 'create_account'].includes(transaction.type) && - Number(transaction.amount) === amount && - transaction.to === toWalletAddress - ) { - if (toWalletMemo && transaction.transaction.memo !== toWalletMemo) { + const isMatchingTransaction = + (transaction.asset_type === 'native' && + transaction.type === 'payment' && + transaction.to === toWalletAddress && + Number(transaction.amount) === amount) || + (transaction.type === 'create_account' && + transaction.account === toWalletAddress && + Number(transaction.starting_balance) === amount); + + if (isMatchingTransaction) { + if ( + toWalletMemo && + transaction.type === 'payment' && + transaction.transaction.memo !== toWalletMemo + ) { logger.debug( `Transaction memo does not match donation memo for donation ID ${donation.id}`, ); @@ -141,7 +150,7 @@ export async function checkTransactions( isTokenEligibleForGivback: token.isGivbackEligible, segmentNotified: false, toWalletAddress: donation.toWalletAddress, - donationAnonymous: !donation.userId, + donationAnonymous: false, transakId: '', token: donation.currency, valueUsd: donation.amount * tokenPrice, @@ -155,7 +164,7 @@ export async function checkTransactions( if (!returnedDonation) { logger.debug( - `Error creating donation for donation ID ${donation.id}`, + `Error creating donation for draft donation ID ${donation.id}`, ); return; } @@ -168,6 +177,10 @@ export async function checkTransactions( matchedDonationId: returnedDonation.id, }); + await syncDonationStatusWithBlockchainNetwork({ + donationId: returnedDonation.id, + }); + return; } } From b206f40998c4e099ae50b3bc9208d9f31cacbe4a Mon Sep 17 00:00:00 2001 From: Meriem-BM Date: Mon, 19 Aug 2024 04:29:37 +0100 Subject: [PATCH 3/4] fix: changes resolver name --- src/resolvers/draftDonationResolver.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/resolvers/draftDonationResolver.ts b/src/resolvers/draftDonationResolver.ts index b89ceff65..08f313bc7 100644 --- a/src/resolvers/draftDonationResolver.ts +++ b/src/resolvers/draftDonationResolver.ts @@ -464,7 +464,7 @@ export class DraftDonationResolver { } @Query(_returns => DraftDonation, { nullable: true }) - async fetchDaftDonationWithUpdatedStatus( + async verifyQRDonationTransaction( @Arg('id', _type => Int) id: number, ): Promise { try { From a720bb0510632016659407d2d01baa63ea812672 Mon Sep 17 00:00:00 2001 From: Meriem-BM Date: Mon, 19 Aug 2024 04:36:36 +0100 Subject: [PATCH 4/4] fix: lint errors --- src/services/chains/index.ts | 2 +- src/services/chains/stellar/transactionService.ts | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/services/chains/index.ts b/src/services/chains/index.ts index 42707a502..acb23f2bc 100644 --- a/src/services/chains/index.ts +++ b/src/services/chains/index.ts @@ -1,7 +1,7 @@ import { ChainType } from '../../types/network'; import { getSolanaTransactionInfoFromNetwork } from './solana/transactionService'; import { getEvmTransactionInfoFromNetwork } from './evm/transactionService'; -import {getStellarTransactionInfoFromNetwork} from './stellar/transactionService'; +import { getStellarTransactionInfoFromNetwork } from './stellar/transactionService'; import { i18n, translationErrorMessagesKeys } from '../../utils/errorMessages'; import { logger } from '../../utils/logger'; import { NETWORK_IDS } from '../../provider'; diff --git a/src/services/chains/stellar/transactionService.ts b/src/services/chains/stellar/transactionService.ts index 6fc61049b..c7bd62ff7 100644 --- a/src/services/chains/stellar/transactionService.ts +++ b/src/services/chains/stellar/transactionService.ts @@ -1,3 +1,4 @@ +import axios from 'axios'; import { NetworkTransactionInfo, TransactionDetailInput, @@ -7,7 +8,6 @@ import { i18n, translationErrorMessagesKeys, } from '../../../utils/errorMessages'; -import axios from 'axios'; const STELLAR_HORIZON_API_URL = process.env.STELLAR_HORIZON_API_URL || 'https://horizon.stellar.org'; @@ -52,8 +52,7 @@ const getStellarTransactionInfo = async ( export async function getStellarTransactionInfoFromNetwork( input: TransactionDetailInput, ): Promise { - let txData; - txData = await getStellarTransactionInfo(input.txHash); + const txData = await getStellarTransactionInfo(input.txHash); if (!txData) { throw new Error( i18n.__(translationErrorMessagesKeys.TRANSACTION_NOT_FOUND),