From 05b63ff49d7377974c15999184cd2dd8b75b3462 Mon Sep 17 00:00:00 2001 From: Carlos Date: Sun, 21 Jan 2024 00:09:35 -0500 Subject: [PATCH] add backup service import cronjob --- config/example.env | 4 + config/test.env | 5 +- src/server/bootstrap.ts | 5 + .../cronJobs/backupDonationImportJob.ts | 303 ++++++++++++++++++ 4 files changed, 315 insertions(+), 2 deletions(-) create mode 100644 src/services/cronJobs/backupDonationImportJob.ts diff --git a/config/example.env b/config/example.env index 6c9116d29..c67492d85 100644 --- a/config/example.env +++ b/config/example.env @@ -246,3 +246,7 @@ LOST_DONATIONS_NETWORK_ID= SOLANA_CHAIN_ID=103 DISABLE_NOTIFICATION_CENTER= +ENABLE_IMPORT_DONATION_BACKUP=false +DONATION_SAVE_BACKUP_API_URL= +DONATION_SAVE_BACKUP_API_SECRET= +DONATION_SAVE_BACKUP_CRONJOB_EXPRESSION= diff --git a/config/test.env b/config/test.env index e37dcedd3..c96e5d191 100644 --- a/config/test.env +++ b/config/test.env @@ -210,6 +210,7 @@ LOST_DONATIONS_NETWORK_ID=10 DISABLE_NOTIFICATION_CENTER=false - -DONATION_SAVE_BACKUP_API_URL=https://eu-central-1.aws.data.mongodb-api.com/app/data-jyoly/endpoint/data/v1/action +DONATION_SAVE_BACKUP_CRONJOB_EXPRESSION= +ENABLE_IMPORT_DONATION_BACKUP=false +DONATION_SAVE_BACKUP_API_URL= DONATION_SAVE_BACKUP_API_SECRET= \ No newline at end of file diff --git a/src/server/bootstrap.ts b/src/server/bootstrap.ts index 8e062debc..5e3c5e96b 100644 --- a/src/server/bootstrap.ts +++ b/src/server/bootstrap.ts @@ -69,6 +69,7 @@ import { runCheckActiveStatusOfQfRounds } from '../services/cronJobs/checkActive import { runUpdateProjectCampaignsCacheJob } from '../services/cronJobs/updateProjectCampaignsCacheJob'; import { corsOptions } from './cors'; import { runSyncLostDonations } from '../services/cronJobs/importLostDonationsJob'; +import { runSyncBackupServiceDonations } from '../services/cronJobs/backupDonationImportJob'; Resource.validate = validate; @@ -340,6 +341,10 @@ export async function bootstrap() { runSyncLostDonations(); } + if ((config.get('ENABLE_IMPORT_DONATION_BACKUP') as string) === 'true') { + runSyncBackupServiceDonations(); + } + if ( (config.get('FILL_POWER_SNAPSHOT_BALANCE_SERVICE_ACTIVE') as string) === 'true' diff --git a/src/services/cronJobs/backupDonationImportJob.ts b/src/services/cronJobs/backupDonationImportJob.ts new file mode 100644 index 000000000..809f3a3cb --- /dev/null +++ b/src/services/cronJobs/backupDonationImportJob.ts @@ -0,0 +1,303 @@ +import { + getNotImportedDonationsFromBackup, + markDonationAsImported, +} from '../../adapters/donationSaveBackup/donationSaveBackupAdapter'; +import config from '../../config'; + +import { logger } from '../../utils/logger'; +import { schedule } from 'node-cron'; +import { detectAddressChainType } from '../../utils/networks'; +import { + createDonationQueryValidator, + validateWithJoiSchema, +} from '../../utils/validators/graphqlQueryValidators'; +import { i18n, translationErrorMessagesKeys } from '../../utils/errorMessages'; +import { findProjectById } from '../../repositories/projectRepository'; +import { ProjStatus, Project } from '../../entities/project'; +import { Token } from '../../entities/token'; +import { + isTokenAcceptableForProject, + updateDonationPricesAndValues, +} from '../donationService'; +import { findProjectRecipientAddressByNetworkId } from '../../repositories/projectAddressRepository'; +import { + findUserByWalletAddress, + setUserAsReferrer, +} from '../../repositories/userRepository'; +import { ChainType } from '../../types/network'; +import { Donation } from '../../entities/donation'; +import { getChainvineReferralInfoForDonation } from '../chainvineReferralService'; +import { relatedActiveQfRoundForProject } from '../qfRoundService'; +import { NETWORK_IDS } from '../../provider'; + +const cronJobTime = + (config.get('DONATION_SAVE_BACKUP_CRONJOB_EXPRESSION') as string) || + '0 0 * * 0'; + +export const runSyncBackupServiceDonations = () => { + logger.debug('importBackupServiceDonations() has been called'); + schedule(cronJobTime, async () => { + await importBackupServiceDonations(); + }); +}; + +// Mock Mongo Methods to write a test +export const importBackupServiceDonations = async () => { + const limit = 10; + let skip = 0; + let donations = await getNotImportedDonationsFromBackup(limit, skip); + while (donations.length > 0) { + for (const donation of donations) { + try { + await createBackupDonation(donation); + await markDonationAsImported(donation._id); + } catch (e) { + logger.error(`donation error with id ${donation._id}: `, e); + logger.error('donation error with params: ', JSON.parse(donation)); + } + } + skip += limit; + donations = await getNotImportedDonationsFromBackup(limit, skip); + } +}; + +// Same logic as the donationResolver CreateDonation() mutation +const createBackupDonation = async (donationData: any) => { + if (!donationData?.token?.address) return; // test donations + + const donorUser = await findUserByWalletAddress(donationData.walletAddress); + if (!donorUser) { + throw new Error(i18n.__(translationErrorMessagesKeys.UN_AUTHORIZED)); + } + + const chainType = detectAddressChainType(donationData.walletAddress); + + const validDataInput = { + amount: donationData.amount, + transactionId: donationData.txHash, + transactionNetworkId: donationData.chainId, + anonymous: donationData.anonymous, + tokenAddress: donationData.token.address, + token: donationData.symbol, + projectId: donationData.tokenId, + nonce: donationData.nonce, + transakId: null, // TODO: remove this column it's unused + referrerId: donationData.chainvineReferred, + safeTransactionId: donationData.safeTransactionId, + chainType, + }; + + validateJoiSchema(validDataInput); + + const project = await findProjectById(donationData.projectId); + + if (!project) + throw new Error(i18n.__(translationErrorMessagesKeys.PROJECT_NOT_FOUND)); + if (project.status.id !== ProjStatus.active) { + throw new Error( + i18n.__( + translationErrorMessagesKeys.JUST_ACTIVE_PROJECTS_ACCEPT_DONATION, + ), + ); + } + + // Validate token + const tokenInDb = await Token.findOne({ + where: { + networkId: donationData.chainId, + symbol: donationData.symbol, + }, + }); + + const [isCustomToken, isTokenEligibleForGivback] = await validateProjectToken( + project, + tokenInDb, + ); + const projectRelatedAddress = await validateProjectRecipientAddress( + project, + donationData.chainId, + ); + + let toAddress = projectRelatedAddress.address; + let fromAddress = donorUser.walletAddress!; + let transactionTx = donationData.txHash; + + // Keep the lowerCase flow the same as before if it's EVM + if (chainType === ChainType.EVM) { + toAddress = toAddress.toLowerCase(); + fromAddress = fromAddress.toLowerCase(); + transactionTx = transactionTx.toLowerCase() as string; + } + + const donation = await Donation.create({ + amount: Number(donationData.amount), + transactionId: transactionTx, + isFiat: Boolean(donationData?.transakId), + transactionNetworkId: donationData.chainId, + currency: donationData.symbol, + user: donorUser, + tokenAddress: donationData.token.address, + nonce: donationData.nonce, + project, + isTokenEligibleForGivback, + isCustomToken, + isProjectVerified: project.verified, + createdAt: new Date(), + segmentNotified: false, + toWalletAddress: toAddress, + fromWalletAddress: fromAddress, + anonymous: Boolean(donationData.anonymous), + safeTransactionId: donationData.safeTransactionId, + chainType: chainType as ChainType, + }); + + // TODO: this is not correct naming, please add as chainvineReferrerId to mongo SCHEMA + // I assume the id goes in that field, looks like a boolean + if (donationData.chainvineReferred) { + // Fill referrer data if referrerId is valid + await setChainvineParamsOnDonation( + donation, + project, + donationData.chainvineReferred, + ); + } + + // Setup QfRound + const activeQfRoundForProject = await relatedActiveQfRoundForProject( + project.id, + ); + if ( + activeQfRoundForProject && + activeQfRoundForProject.isEligibleNetwork(donation.transactionNetworkId) + ) { + donation.qfRound = activeQfRoundForProject; + } + await donation.save(); + + // set chain network to fetch price + await fillDonationCurrencyValues(donation, project, tokenInDb); + logger.info(`Donation with Id ${donation.id} has been processed succesfully`); +}; + +const fillDonationCurrencyValues = async ( + donation: Donation, + project: Project, + token: Token | null, +) => { + let priceChainId; + switch (donation.transactionNetworkId) { + case NETWORK_IDS.ROPSTEN: + priceChainId = NETWORK_IDS.MAIN_NET; + break; + case NETWORK_IDS.GOERLI: + priceChainId = NETWORK_IDS.MAIN_NET; + break; + case NETWORK_IDS.OPTIMISM_GOERLI: + priceChainId = NETWORK_IDS.OPTIMISTIC; + break; + case NETWORK_IDS.MORDOR_ETC_TESTNET: + priceChainId = NETWORK_IDS.ETC; + break; + default: + priceChainId = donation.transactionNetworkId; + break; + } + + await updateDonationPricesAndValues( + donation, + project, + token, + donation.currency, + priceChainId, + donation.amount, + ); +}; + +const setChainvineParamsOnDonation = async ( + donation: Donation, + project: Project, + referredId: string, +) => { + try { + const { + referralStartTimestamp, + isReferrerGivbackEligible, + referrerWalletAddress, + } = await getChainvineReferralInfoForDonation({ + referrerId: referredId, // like this + fromAddress: donation.fromWalletAddress, + donorUserId: donation.userId, + projectVerified: project.verified, + }); + + donation.isReferrerGivbackEligible = isReferrerGivbackEligible; + donation.referrerWallet = referrerWalletAddress; + donation.referralStartTimestamp = referralStartTimestamp; + await setUserAsReferrer(referrerWalletAddress); + + await donation.save(); + } catch (e) { + logger.error('get chainvine wallet address error', e); + } +}; + +const validateProjectRecipientAddress = async ( + project: Project, + networkId: number, +) => { + const projectRelatedAddress = await findProjectRecipientAddressByNetworkId({ + projectId: project.id, + networkId, + }); + if (!projectRelatedAddress) { + throw new Error( + i18n.__( + translationErrorMessagesKeys.THERE_IS_NO_RECIPIENT_ADDRESS_FOR_THIS_NETWORK_ID_AND_PROJECT, + ), + ); + } + + return projectRelatedAddress; +}; + +const validateProjectToken = async ( + project: Project, + tokenInDb?: Token | null, +) => { + const isCustomToken = !Boolean(tokenInDb); + let isTokenEligibleForGivback = false; + if (isCustomToken && !project.organization.supportCustomTokens) { + throw new Error(i18n.__(translationErrorMessagesKeys.TOKEN_NOT_FOUND)); + } else if (tokenInDb) { + const acceptsToken = await isTokenAcceptableForProject({ + projectId: project.id, + tokenId: tokenInDb.id, + }); + if (!acceptsToken && !project.organization.supportCustomTokens) { + throw new Error( + i18n.__( + translationErrorMessagesKeys.PROJECT_DOES_NOT_SUPPORT_THIS_TOKEN, + ), + ); + } + isTokenEligibleForGivback = tokenInDb.isGivbackEligible; + } + + return [isCustomToken, isTokenEligibleForGivback]; +}; + +const validateJoiSchema = validDataInput => { + try { + validateWithJoiSchema(validDataInput, createDonationQueryValidator); + } catch (e) { + logger.error('Error on validating createDonation input', validDataInput); + // Joi alternatives does not handle custom errors, have to catch them. + if (e.message.includes('does not match any of the allowed types')) { + throw new Error( + i18n.__(translationErrorMessagesKeys.INVALID_TRANSACTION_ID), + ); + } else { + throw e; // Rethrow the original error + } + } +};