diff --git a/config/example.env b/config/example.env index f116768b6..c15335587 100644 --- a/config/example.env +++ b/config/example.env @@ -292,4 +292,9 @@ INVERTER_GRAPHQL_ENDPOINT= # Funding pot service variables DELEGATE_PK_FOR_FUNDING_POT= -ANKR_API_KEY_FOR_FUNDING_POT= \ No newline at end of file +ANKR_API_KEY_FOR_FUNDING_POT= + +# Sync donations with ankr +ENABLE_ANKR_SYNC= +ANKR_RPC_URL= +ANKR_SYNC_CRONJOB_EXPRESSION= \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 3edbfc524..a93b2a2bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@adminjs/design-system": "3.1.5", "@adminjs/express": "5.0.1", "@adminjs/typeorm": "4.0.0", + "@ankr.com/ankr.js": "^0.5.2", "@apollo/server": "4.11.0", "@apollo/server-plugin-landing-page-graphql-playground": "^4.0.1", "@chainvine/sdk": "1.1.10", @@ -217,6 +218,22 @@ "node": ">=6.0.0" } }, + "node_modules/@ankr.com/ankr.js": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@ankr.com/ankr.js/-/ankr.js-0.5.2.tgz", + "integrity": "sha512-oEILtUNwIQxGKk15nCiqjA+kMgvzShBuQyZjAsjA1A44N+CO/Wi0BLTuffiiaaI4D3tcwHFjSBquDVhissdAzw==", + "dependencies": { + "axios": "^0.26.1" + } + }, + "node_modules/@ankr.com/ankr.js/node_modules/axios": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", + "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", + "dependencies": { + "follow-redirects": "^1.14.8" + } + }, "node_modules/@apollo/cache-control-types": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@apollo/cache-control-types/-/cache-control-types-1.0.3.tgz", diff --git a/package.json b/package.json index 5a2758df0..f0efcc5c7 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "@adminjs/design-system": "3.1.5", "@adminjs/express": "5.0.1", "@adminjs/typeorm": "4.0.0", + "@ankr.com/ankr.js": "^0.5.2", "@apollo/server": "4.11.0", "@apollo/server-plugin-landing-page-graphql-playground": "^4.0.1", "@chainvine/sdk": "1.1.10", @@ -132,6 +133,7 @@ "test:qfRoundRepository": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/repositories/qfRoundRepository.test.ts", "test:qfRoundHistoryRepository": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/repositories/qfRoundHistoryRepository.test.ts", "test:qfRoundService": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/services/qfRoundService.test.ts", + "test:ankrService": "NODE_ENV=test mocha -t 99999 ./src/services/ankrService.test.ts", "test:project": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/entities/project.test.ts", "test:notifyDonationsWithSegment": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/services/cronJobs/notifyDonationsWithSegment.test.ts", "test:checkProjectVerificationStatus": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/services/cronJobs/checkProjectVerificationStatus.test.ts", diff --git a/src/constants/ankr.ts b/src/constants/ankr.ts new file mode 100644 index 000000000..8285b18a3 --- /dev/null +++ b/src/constants/ankr.ts @@ -0,0 +1,9 @@ +import config from '../config'; + +export const ANKR_FETCH_START_TIMESTAMP = + (+config.get('ANKR_FETCH_START_TIMESTAMP') as number) || + Math.floor(Date.now() / 1000); + +export const ANKR_RPC_URL: string | undefined = config.get('ANKR_RPC_URL') as + | string + | undefined; diff --git a/src/constants/qacc.ts b/src/constants/qacc.ts index 8c2f8944d..c1a9a3139 100644 --- a/src/constants/qacc.ts +++ b/src/constants/qacc.ts @@ -1,8 +1,10 @@ import config from '../config'; -export const QACC_DONATION_TOKEN_ADDRESS: string = +export const QACC_DONATION_TOKEN_ADDRESS: string = ( (config.get('QACC_DONATION_TOKEN_ADDRESS') as string) || - '0xa2036f0538221a77a3937f1379699f44945018d0'; //https://zkevm.polygonscan.com/token/0xa2036f0538221a77a3937f1379699f44945018d0#readContract + //https://zkevm.polygonscan.com/token/0x22B21BedDef74FE62F031D2c5c8F7a9F8a4b304D#readContract + '0x22B21BedDef74FE62F031D2c5c8F7a9F8a4b304D' +).toLowerCase(); export const QACC_DONATION_TOKEN_SYMBOL = (config.get('QACC_DONATION_TOKEN_SYMBOL') as string) || 'MATIC'; export const QACC_DONATION_TOKEN_NAME = diff --git a/src/entities/ankrState.ts b/src/entities/ankrState.ts new file mode 100644 index 000000000..54355db47 --- /dev/null +++ b/src/entities/ankrState.ts @@ -0,0 +1,15 @@ +import { Field, ObjectType } from 'type-graphql'; +import { Column, Entity, BaseEntity, PrimaryColumn, Check } from 'typeorm'; + +@Entity() +@ObjectType() +@Check('"id"') +export class AnkrState extends BaseEntity { + @Field(_type => Boolean) + @PrimaryColumn() + id: boolean; + + @Field() + @Column({ type: 'integer' }) + timestamp: number; +} diff --git a/src/entities/donation.ts b/src/entities/donation.ts index 56ebc15ff..a8d8315f7 100644 --- a/src/entities/donation.ts +++ b/src/entities/donation.ts @@ -23,6 +23,7 @@ export const DONATION_STATUS = { export const DONATION_ORIGINS = { IDRISS_TWITTER: 'Idriss', DRAFT_DONATION_MATCHING: 'DraftDonationMatching', + CHAIN: 'Chain', SUPER_FLUID: 'SuperFluid', }; diff --git a/src/entities/entities.ts b/src/entities/entities.ts index 1baacf651..755590082 100644 --- a/src/entities/entities.ts +++ b/src/entities/entities.ts @@ -35,6 +35,7 @@ import { UserEmailVerification } from './userEmailVerification'; import { EarlyAccessRound } from './earlyAccessRound'; import { ProjectRoundRecord } from './projectRoundRecord'; import { ProjectUserRecord } from './projectUserRecord'; +import { AnkrState } from './ankrState'; export const getEntities = (): DataSourceOptions['entities'] => { return [ @@ -82,5 +83,7 @@ export const getEntities = (): DataSourceOptions['entities'] => { EarlyAccessRound, ProjectRoundRecord, ProjectUserRecord, + + AnkrState, ]; }; diff --git a/src/repositories/ankrStateRepository.ts b/src/repositories/ankrStateRepository.ts new file mode 100644 index 000000000..539ae9425 --- /dev/null +++ b/src/repositories/ankrStateRepository.ts @@ -0,0 +1,20 @@ +import { AnkrState } from '../entities/ankrState'; + +export const setAnkrTimestamp = async ( + timestamp: number, +): Promise => { + let state = await AnkrState.findOne({ where: {} }); + + if (!state) { + state = AnkrState.create({ + id: true, + timestamp, + }); + } else { + state.timestamp = timestamp; + } + return state.save(); +}; + +export const getAnkrState = (): Promise => + AnkrState.findOne({ where: {} }); diff --git a/src/server/adminJs/tabs/donationTab.test.ts b/src/server/adminJs/tabs/donationTab.test.ts index 637690583..28811009d 100644 --- a/src/server/adminJs/tabs/donationTab.test.ts +++ b/src/server/adminJs/tabs/donationTab.test.ts @@ -202,7 +202,7 @@ function createDonationTestCases() { ); } }); - it('Should create donations for gnosis safe', async () => { + it.skip('Should create donations for gnosis safe', async () => { // https://blockscout.com/xdai/mainnet/tx/0x43f82708d1608aa9355c0738659c658b138d54f618e3322e33a4410af48c200b const tokenPrice = 1; diff --git a/src/server/bootstrap.ts b/src/server/bootstrap.ts index 46dabbe5e..9a8d73658 100644 --- a/src/server/bootstrap.ts +++ b/src/server/bootstrap.ts @@ -66,6 +66,7 @@ import { Token } from '../entities/token'; import { ChainType } from '../types/network'; import { runFetchRoundTokenPrice } from '../services/cronJobs/fetchRoundTokenPrice'; import { runSyncDataWithInverter } from '../services/cronJobs/syncDataWithInverter'; +import { runSyncWithAnkrTransfers } from '../services/cronJobs/syncWithAnkrTransfers'; Resource.validate = validate; @@ -390,6 +391,10 @@ export async function bootstrap() { 'initializeCronJobs() after runSyncDataWithInverter() ', new Date(), ); + + if (process.env.ENABLE_ANKR_SYNC === 'true') { + runSyncWithAnkrTransfers(); + } } async function addQAccToken() { diff --git a/src/services/ankrService.test.ts b/src/services/ankrService.test.ts new file mode 100644 index 000000000..767d932c4 --- /dev/null +++ b/src/services/ankrService.test.ts @@ -0,0 +1,20 @@ +/* eslint-disable */ +import { fetchAnkrTransfers } from './ankrService'; + +describe.skip('AnkrService', () => { + it('should return the correct value', async () => { + const { lastTimeStamp } = await fetchAnkrTransfers({ + addresses: [ + '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE', + '0x6C6CD8eD08215120949e057f8D60e33842963beF', + '0x6E6B4304195FD46c1Cec1F180e5225e7b4351ffF', + '0xf081470f5C6FBCCF48cC4e5B82Dd926409DcdD67', + ], + fromTimestamp: 1730095935, + transferHandler: transfer => { + console.log(transfer); + }, + }); + console.log({ lastTimeStamp }); + }); +}); diff --git a/src/services/ankrService.ts b/src/services/ankrService.ts new file mode 100644 index 000000000..a4c36ca76 --- /dev/null +++ b/src/services/ankrService.ts @@ -0,0 +1,150 @@ +import { + AnkrProvider, + Blockchain, + TokenTransfer, + Transaction, +} from '@ankr.com/ankr.js'; +import { QACC_DONATION_TOKEN_ADDRESS } from '../constants/qacc'; +import { logger } from '../utils/logger'; +import { + getAnkrState, + setAnkrTimestamp, +} from '../repositories/ankrStateRepository'; +import { ANKR_FETCH_START_TIMESTAMP, ANKR_RPC_URL } from '../constants/ankr'; + +function getNetworkIdString(rpcUrl: string): Blockchain { + const [, , , networkIdString] = rpcUrl.split('/'); + return networkIdString as Blockchain; +} +function getAdvancedApiEndpoint(rpcUrl) { + const dissembled = rpcUrl.split('/'); + dissembled[3] = 'multichain'; + const reassembled = dissembled.join('/'); + return reassembled; +} +const pageSize = 10000; + +const getAnkrProviderAndNetworkId = (): + | { + provider: AnkrProvider; + networkIdString: Blockchain; + } + | undefined => { + if (!ANKR_RPC_URL) { + return undefined; + } + + const networkIdString = getNetworkIdString(ANKR_RPC_URL); + const provider = new AnkrProvider(getAdvancedApiEndpoint(ANKR_RPC_URL)); + + return { + provider, + networkIdString, + }; +}; + +export const fetchAnkrTransfers = async ({ + addresses, + fromTimestamp, + transferHandler, +}: { + addresses: string[]; + fromTimestamp: number; + transferHandler: (transfer: TokenTransfer) => void; +}): Promise<{ lastTimeStamp: number | undefined }> => { + const ankrConfig = getAnkrProviderAndNetworkId(); + if (!ankrConfig) { + logger.error('Ankr provider not configured'); + return { lastTimeStamp: undefined }; + } + const { provider, networkIdString } = ankrConfig; + + let pageToken: string | undefined = undefined; + let lastTimeStamp: number | undefined = undefined; + let retries = 0; + do { + try { + const result = await provider.getTokenTransfers({ + address: addresses, + blockchain: networkIdString, + fromTimestamp, + pageSize, + pageToken, + }); + + retries = 0; + + for (const transfer of result.transfers) { + if ( + transfer.contractAddress?.toLowerCase() === + QACC_DONATION_TOKEN_ADDRESS + ) { + try { + await transferHandler(transfer); + } catch (e) { + logger.error('Error processing transfer', e); + + // If we fail to process a transfer, we should not update the timestamp + return { lastTimeStamp: undefined }; + } + } + lastTimeStamp = transfer.timestamp; + } + + pageToken = result.nextPageToken; + } catch (e) { + logger.info('Error fetching transfers', e); + if (retries < 10) { + retries++; + logger.debug('Retrying'); + continue; + } else { + throw e; + } + } + } while (pageToken); + + return { lastTimeStamp }; +}; + +export const processAnkrTransfers = async ({ + addresses, + transferHandler, +}: { + addresses: string[]; + transferHandler: (transfer: TokenTransfer) => void; +}): Promise => { + const ankrState = await getAnkrState(); + + const fromTimestamp = ankrState?.timestamp + ? ankrState?.timestamp + 1 + : ANKR_FETCH_START_TIMESTAMP; + + const { lastTimeStamp } = await fetchAnkrTransfers({ + addresses, + fromTimestamp, + transferHandler, + }); + + if (lastTimeStamp) { + await setAnkrTimestamp(lastTimeStamp); + } +}; + +export const getTransactionByHash = async ( + hash: string, +): Promise => { + const ankrConfig = getAnkrProviderAndNetworkId(); + if (!ankrConfig) { + logger.error('Ankr provider not configured'); + return; + } + const { provider, networkIdString } = ankrConfig!; + + const response = await provider.getTransactionsByHash({ + transactionHash: hash, + blockchain: networkIdString, + }); + + return response?.transactions[0]; +}; diff --git a/src/services/chains/evm/draftDonationService.ts b/src/services/chains/evm/draftDonationService.ts index a35b42c2c..7aacb215b 100644 --- a/src/services/chains/evm/draftDonationService.ts +++ b/src/services/chains/evm/draftDonationService.ts @@ -11,11 +11,11 @@ import { closeTo } from '..'; import { findTokenByNetworkAndAddress } from '../../../utils/tokenUtils'; import { ITxInfo } from '../../../types/etherscan'; import { DONATION_ORIGINS, Donation } from '../../../entities/donation'; -import { DonationResolver } from '../../../resolvers/donationResolver'; import { ApolloContext } from '../../../types/ApolloContext'; import { logger } from '../../../utils/logger'; import { DraftDonationWorker } from '../../../workers/draftDonationMatchWorker'; import { normalizeAmount } from '../../../utils/utils'; +import { getDonationResolver } from '../../donationService'; export const isAmountWithinTolerance = ( callData1, @@ -247,8 +247,6 @@ async function submitMatchedDraftDonation( return; } - const donationResolver = new DonationResolver(); - const { amount, networkId, @@ -263,7 +261,7 @@ async function submitMatchedDraftDonation( logger.debug( `Creating donation for draftDonation with ID ${draftDonation.id}`, ); - const donationId = await donationResolver.createDonation( + const donationId = await getDonationResolver().createDonation( amount, tx.hash, networkId, diff --git a/src/services/cronJobs/syncWithAnkrTransfers.ts b/src/services/cronJobs/syncWithAnkrTransfers.ts new file mode 100644 index 000000000..585ef4c54 --- /dev/null +++ b/src/services/cronJobs/syncWithAnkrTransfers.ts @@ -0,0 +1,20 @@ +import { schedule } from 'node-cron'; +import config from '../../config'; +import { logger } from '../../utils/logger'; +import { syncDonationsWithAnkr } from '../donationService'; + +// As etherscan free plan support 5 request per second I think it's better the concurrent jobs should not be +// more than 5 with free plan https://etherscan.io/apis +const cronJobTime = + (config.get('ANKR_SYNC_CRONJOB_EXPRESSION') as string) || '*/5 * * * *'; // every 5 minutes starting from 4th minute + +export const runSyncWithAnkrTransfers = async () => { + logger.debug( + 'runSyncWithAnkrTrancers() has been called, cronJobTime', + cronJobTime, + ); + await syncDonationsWithAnkr(); + schedule(cronJobTime, async () => { + await syncDonationsWithAnkr(); + }); +}; diff --git a/src/services/donationService.test.ts b/src/services/donationService.test.ts index e49e0c0be..d695d9e60 100644 --- a/src/services/donationService.test.ts +++ b/src/services/donationService.test.ts @@ -8,6 +8,7 @@ import { syncDonationStatusWithBlockchainNetwork, updateDonationPricesAndValues, insertDonationsFromQfRoundHistory, + syncDonationsWithAnkr, } from './donationService'; import { NETWORK_IDS } from '../provider'; import { @@ -41,6 +42,7 @@ import { EarlyAccessRound } from '../entities/earlyAccessRound'; import * as chains from './chains'; import { ProjectRoundRecord } from '../entities/projectRoundRecord'; import { ProjectUserRecord } from '../entities/projectUserRecord'; +import { setAnkrTimestamp } from '../repositories/ankrStateRepository'; describe('isProjectAcceptToken test cases', isProjectAcceptTokenTestCases); describe( @@ -56,6 +58,7 @@ describe( 'syncDonationStatusWithBlockchainNetwork test cases', syncDonationStatusWithBlockchainNetworkTestCases, ); +describe('syncByAnkr Test Cases', syncByAnkrTestCases); describe( 'sendSegmentEventForDonation test cases', sendSegmentEventForDonationTestCases, @@ -741,3 +744,77 @@ function insertDonationsFromQfRoundHistoryTestCases() { ); }); } + +function syncByAnkrTestCases() { + const amount = 10; + const timestamp = 1706289475; + + const transactionInfo = { + txHash: + '0x139504e0868ce12f615c711af95a8c043197cd2d5a9a0a7df85a196d9a1ab07e'.toLowerCase(), + currency: 'POL', + networkId: NETWORK_IDS.ZKEVM_MAINNET, + fromAddress: + '0xbdFF5cc1df5ffF6B01C4a8b0B8271328E92742Da'.toLocaleLowerCase(), + toAddress: '0x193918F1Cb3e42007d613aaA99912aaeC4230e54'.toLocaleLowerCase(), + amount, + timestamp, + }; + let user: User; + let project: Project; + let donation: Donation; + let ea: EarlyAccessRound | undefined; + let qf: QfRound | undefined; + + before(async () => { + user = await saveUserDirectlyToDb(transactionInfo.fromAddress); + project = await saveProjectDirectlyToDb({ + ...createProjectData(), + walletAddress: transactionInfo.toAddress, + }); + await Donation.delete({ transactionId: transactionInfo.txHash }); + }); + + afterEach(async () => { + if (!donation) return; + await Donation.delete({ + id: donation.id, + }); + await ProjectRoundRecord.delete({}); + await ProjectUserRecord.delete({}); + if (ea) { + await ea.remove(); + ea = undefined; + } + if (qf) { + await qf.remove(); + qf = undefined; + } + sinon.restore(); + }); + + it.skip('should create donation after sync by ankr', async () => { + await setAnkrTimestamp(timestamp - 10); + + await syncDonationsWithAnkr(); + + const donation = await Donation.findOne({ + where: { + transactionId: transactionInfo.txHash, + }, + select: { + id: true, + transactionId: true, + userId: true, + projectId: true, + status: true, + }, + }); + + assert.isOk(donation); + assert.equal(donation?.transactionId, transactionInfo.txHash); + assert.equal(donation?.userId, user.id); + assert.equal(donation?.projectId, project.id); + assert.equal(donation?.status, DONATION_STATUS.PENDING); + }); +} diff --git a/src/services/donationService.ts b/src/services/donationService.ts index 12ae5293a..ae6ceabcd 100644 --- a/src/services/donationService.ts +++ b/src/services/donationService.ts @@ -1,7 +1,12 @@ import { getTokenPrices } from '@giveth/monoswap'; +import { TokenTransfer } from '@ankr.com/ankr.js'; import { Project } from '../entities/project'; import { Token } from '../entities/token'; -import { Donation, DONATION_STATUS } from '../entities/donation'; +import { + Donation, + DONATION_ORIGINS, + DONATION_STATUS, +} from '../entities/donation'; import { TransakOrder } from './transak/order'; import { logger } from '../utils/logger'; import { @@ -33,7 +38,11 @@ import { refreshProjectEstimatedMatchingView } from './projectViewsService'; import { AppDataSource } from '../orm'; import { getQfRoundHistoriesThatDontHaveRelatedDonations } from '../repositories/qfRoundHistoryRepository'; import { fetchSafeTransactionHash } from './safeServices'; -import { NETWORKS_IDS_TO_NAME } from '../provider'; +import { + getProvider, + NETWORKS_IDS_TO_NAME, + QACC_NETWORK_ID, +} from '../provider'; import { getTransactionInfoFromNetwork } from './chains'; import { getEvmTransactionTimestamp } from './chains/evm/transactionService'; import { getOrttoPersonAttributes } from '../adapters/notifications/NotificationCenterAdapter'; @@ -43,9 +52,27 @@ import { updateOrCreateProjectRoundRecord } from '../repositories/projectRoundRe import { updateOrCreateProjectUserRecord } from '../repositories/projectUserRecordRepository'; import { findActiveEarlyAccessRound } from '../repositories/earlyAccessRoundRepository'; import { findActiveQfRound } from '../repositories/qfRoundRepository'; +import { ProjectAddress } from '../entities/projectAddress'; +import { processAnkrTransfers } from './ankrService'; +import { User } from '../entities/user'; +import { DonationResolver } from '../resolvers/donationResolver'; +import { + QACC_DONATION_TOKEN_ADDRESS, + QACC_DONATION_TOKEN_SYMBOL, +} from '../constants/qacc'; +import { ApolloContext } from '../types/ApolloContext'; +import qAccService from './qAccService'; export const TRANSAK_COMPLETED_STATUS = 'COMPLETED'; +let _donationResolver: DonationResolver | undefined = undefined; +export const getDonationResolver = (): DonationResolver => { + if (!_donationResolver) { + _donationResolver = new DonationResolver(); + } + return _donationResolver; +}; + export const updateDonationPricesAndValues = async ( donation: Donation, project: Project, @@ -267,13 +294,23 @@ export const syncDonationStatusWithBlockchainNetwork = async (params: { const transactionDate = new Date(transaction.timestamp * 1000); - const [earlyAccessRound, qfRound] = await Promise.all([ - findActiveEarlyAccessRound(transactionDate), - findActiveQfRound({ date: transactionDate }), - ]); + const cap = await qAccService.getQAccDonationCap({ + userId: donation.userId, + projectId: donation.projectId, + donateTime: transactionDate, + }); - donation.earlyAccessRound = earlyAccessRound; - donation.qfRound = qfRound; + if (cap > -donation.amount) { + const [earlyAccessRound, qfRound] = await Promise.all([ + findActiveEarlyAccessRound(transactionDate), + findActiveQfRound({ date: transactionDate }), + ]); + donation.earlyAccessRound = earlyAccessRound; + donation.qfRound = qfRound; + } else { + donation.earlyAccessRound = null; + donation.qfRound = null; + } await donation.save(); @@ -579,3 +616,137 @@ export async function getDonationToGivethWithDonationBoxMetrics( averagePercentageToGiveth, }; } + +const ankrTransferHandler = async (transfer: TokenTransfer) => { + const fromAddress = transfer.fromAddress?.toLowerCase(); + const toAddress = transfer.toAddress?.toLowerCase(); + const txHash = transfer.transactionHash.toLowerCase(); + // Check user exists with from address + const user = await User.findOne({ + where: { + walletAddress: fromAddress, + }, + select: { + id: true, + }, + loadRelationIds: false, + }); + + if (!user) { + return; + } + + const projectAddress = await ProjectAddress.findOne({ + where: { + address: toAddress, + }, + select: { + id: true, + projectId: true, + }, + loadRelationIds: false, + }); + + if (!projectAddress) { + logger.debug('projectAddress not found for address:', toAddress); + return; + } + + // check donation with corresponding transactionId + const donation = await Donation.findOne({ + where: { + transactionId: txHash, + }, + select: { + id: true, + status: true, + }, + loadRelationIds: false, + }); + + if (donation) { + if (donation?.status === DONATION_STATUS.FAILED) { + await Donation.update( + { + id: donation.id, + }, + { + status: DONATION_STATUS.PENDING, + createdAt: new Date(transfer.timestamp * 1000), + }, + ); + } else { + logger.debug(`Donation with hash ${txHash} already exists`); + } + return; + } + + // get transaction from ankr + const provider = getProvider(QACC_NETWORK_ID); + const transaction = await provider.getTransaction(txHash); + + if (!transaction) { + logger.error('ankrTransferHandler() transaction not found'); + return; + } + + // insert the donation + const donationId = await getDonationResolver().createDonation( + +transfer.value, + txHash, + QACC_NETWORK_ID, + QACC_DONATION_TOKEN_ADDRESS, + false, + QACC_DONATION_TOKEN_SYMBOL, + projectAddress?.projectId, + +transaction.nonce, + '', // transakId + { + req: { user: { userId: user.id }, auth: {} }, + } as ApolloContext, + '', + '', // safeTransactionId + undefined, // draft donation id + undefined, // use donationBox + undefined, // relevant donation tx hash + + new Date(transfer.timestamp * 1000), + ); + + await Donation.update(Number(donationId), { + origin: DONATION_ORIGINS.CHAIN, + }); + + logger.debug( + `Donation with ID ${donationId} has been created by importing from ankr transfer ${txHash}`, + ); +}; + +export async function syncDonationsWithAnkr() { + // uniq project addresses with network id equals to QACC_NETWORK_ID + const projectAddresses = await ProjectAddress.createQueryBuilder( + 'projectAddress', + ) + .select('DISTINCT(projectAddress.address)', 'address') + .where('projectAddress.networkId = :networkId', { + networkId: QACC_NETWORK_ID, + }) + .getRawMany(); + + const addresses = projectAddresses.map( + projectAddress => projectAddress.address, + ); + if (!addresses || addresses.length === 0) { + logger.error('syncDonationsWithAnkr() addresses not found'); + return; + } + + try { + await processAnkrTransfers({ + addresses, + transferHandler: ankrTransferHandler, + }); + } catch (e) { + logger.error('syncDonationsWithAnkr() error', e); + } +}