Skip to content

Commit

Permalink
add backup service import cronjob
Browse files Browse the repository at this point in the history
  • Loading branch information
CarlosQ96 committed Jan 21, 2024
1 parent 40a51df commit f103a88
Show file tree
Hide file tree
Showing 4 changed files with 314 additions and 2 deletions.
4 changes: 4 additions & 0 deletions config/example.env
Original file line number Diff line number Diff line change
Expand Up @@ -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=
5 changes: 3 additions & 2 deletions config/test.env
Original file line number Diff line number Diff line change
Expand Up @@ -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=
5 changes: 5 additions & 0 deletions src/server/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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'
Expand Down
302 changes: 302 additions & 0 deletions src/services/cronJobs/backupDonationImportJob.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
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, please add a chainvineReferrerId to mongo SCHEMA
if (donationData.chainvineReferred) {
// Fill referrer data if referrerId is valid
await setChainvineParamsOnDonation(
donation,
project,
donationData.chainvineReffererId,
);
}

// 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
}
}
};

0 comments on commit f103a88

Please sign in to comment.