Skip to content

Commit

Permalink
Merge pull request #1767 from Giveth/feat/stellar_integration
Browse files Browse the repository at this point in the history
Fix: add instant trx check endpoint
  • Loading branch information
Meriem-BM authored Aug 19, 2024
2 parents df9c0ad + a720bb0 commit 2d263fd
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 13 deletions.
41 changes: 40 additions & 1 deletion src/resolvers/draftDonationResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
findRecurringDonationByProjectIdAndUserIdAndCurrency,
} from '../repositories/recurringDonationRepository';
import { RecurringDonation } from '../entities/recurringDonation';
import { checkTransactions } from '../services/cronJobs/checkQRTransactionJob';
import { findProjectById } from '../repositories/projectRepository';

const draftDonationEnabled = process.env.ENABLE_DRAFT_DONATION === 'true';
Expand Down Expand Up @@ -376,6 +377,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;
}

Expand Down Expand Up @@ -438,7 +449,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,
Expand All @@ -451,4 +462,32 @@ export class DraftDonationResolver {
return null;
}
}

@Query(_returns => DraftDonation, { nullable: true })
async verifyQRDonationTransaction(
@Arg('id', _type => Int) id: number,
): Promise<DraftDonation | null> {
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;
}
}
}
5 changes: 5 additions & 0 deletions src/services/chains/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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);
}
Expand Down
63 changes: 63 additions & 0 deletions src/services/chains/stellar/transactionService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import axios from 'axios';
import {
NetworkTransactionInfo,
TransactionDetailInput,
validateTransactionWithInputData,
} from '../index';
import {
i18n,
translationErrorMessagesKeys,
} from '../../../utils/errorMessages';

const STELLAR_HORIZON_API_URL =
process.env.STELLAR_HORIZON_API_URL || 'https://horizon.stellar.org';

const getStellarTransactionInfo = async (
txHash: string,
): Promise<NetworkTransactionInfo | null> => {
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<NetworkTransactionInfo> {
const txData = await getStellarTransactionInfo(input.txHash);
if (!txData) {
throw new Error(
i18n.__(translationErrorMessagesKeys.TRANSACTION_NOT_FOUND),
);
}
validateTransactionWithInputData(txData, input);
return txData;
}
57 changes: 45 additions & 12 deletions src/services/cronJobs/checkQRTransactionJob.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@ 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) ||
'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')
Expand All @@ -41,10 +42,30 @@ const getToken = async (
};

// Check for transactions
async function checkTransactions(donation: DraftDonation): Promise<void> {
const { toWalletAddress, amount, toWalletMemo } = donation;
export async function checkTransactions(
donation: DraftDonation,
): Promise<void> {
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`,
);
Expand All @@ -54,13 +75,21 @@ async function checkTransactions(donation: DraftDonation): Promise<void> {
if (transactions.length === 0) return;

for (const transaction of transactions) {
if (
transaction.asset_type === 'native' &&
transaction.type === 'payment' &&
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}`,
);
Expand Down Expand Up @@ -121,7 +150,7 @@ async function checkTransactions(donation: DraftDonation): Promise<void> {
isTokenEligibleForGivback: token.isGivbackEligible,
segmentNotified: false,
toWalletAddress: donation.toWalletAddress,
donationAnonymous: !donation.userId,
donationAnonymous: false,
transakId: '',
token: donation.currency,
valueUsd: donation.amount * tokenPrice,
Expand All @@ -135,7 +164,7 @@ async function checkTransactions(donation: DraftDonation): Promise<void> {

if (!returnedDonation) {
logger.debug(
`Error creating donation for donation ID ${donation.id}`,
`Error creating donation for draft donation ID ${donation.id}`,
);
return;
}
Expand All @@ -148,6 +177,10 @@ async function checkTransactions(donation: DraftDonation): Promise<void> {
matchedDonationId: returnedDonation.id,
});

await syncDonationStatusWithBlockchainNetwork({
donationId: returnedDonation.id,
});

return;
}
}
Expand Down

0 comments on commit 2d263fd

Please sign in to comment.