diff --git a/.github/workflows/develop-pipeline.yml b/.github/workflows/develop-pipeline.yml index c377286ae..3df6a977d 100644 --- a/.github/workflows/develop-pipeline.yml +++ b/.github/workflows/develop-pipeline.yml @@ -68,6 +68,7 @@ jobs: SOLANA_TEST_NODE_RPC_URL: ${{ secrets.SOLANA_TEST_NODE_RPC_URL }} SOLANA_DEVNET_NODE_RPC_URL: ${{ secrets.SOLANA_DEVNET_NODE_RPC_URL }} SOLANA_MAINNET_NODE_RPC_URL: ${{ secrets.SOLANA_MAINNET_NODE_RPC_URL }} + MPETH_GRAPHQL_PRICES_URL: ${{ secrets.MPETH_GRAPHQL_PRICES_URL }} publish: needs: test diff --git a/.github/workflows/master-pipeline.yml b/.github/workflows/master-pipeline.yml index 77c44f8d7..3195a1695 100644 --- a/.github/workflows/master-pipeline.yml +++ b/.github/workflows/master-pipeline.yml @@ -68,6 +68,7 @@ jobs: SOLANA_TEST_NODE_RPC_URL: ${{ secrets.SOLANA_TEST_NODE_RPC_URL }} SOLANA_DEVNET_NODE_RPC_URL: ${{ secrets.SOLANA_DEVNET_NODE_RPC_URL }} SOLANA_MAINNET_NODE_RPC_URL: ${{ secrets.SOLANA_MAINNET_NODE_RPC_URL }} + MPETH_GRAPHQL_PRICES_URL: ${{ secrets.MPETH_GRAPHQL_PRICES_URL }} publish: needs: test diff --git a/.github/workflows/run-tests-on-pr.yml.bck b/.github/workflows/run-tests-on-pr.yml.bck index be434d0e4..76fc70f7b 100644 --- a/.github/workflows/run-tests-on-pr.yml.bck +++ b/.github/workflows/run-tests-on-pr.yml.bck @@ -66,3 +66,4 @@ jobs: OPTIMISTIC_SCAN_API_KEY: ${{ secrets.OPTIMISTIC_SCAN_API_KEY }} CELO_SCAN_API_KEY: ${{ secrets.CELO_SCAN_API_KEY }} CELO_ALFAJORES_SCAN_API_KEY: ${{ secrets.CELO_ALFAJORES_SCAN_API_KEY }} + MPETH_GRAPHQL_PRICES_URL: ${{ secrets.MPETH_GRAPHQL_PRICES_URL }} diff --git a/.github/workflows/staging-pipeline.yml b/.github/workflows/staging-pipeline.yml index 96f245e76..97966af4e 100644 --- a/.github/workflows/staging-pipeline.yml +++ b/.github/workflows/staging-pipeline.yml @@ -106,6 +106,7 @@ jobs: SOLANA_TEST_NODE_RPC_URL: ${{ secrets.SOLANA_TEST_NODE_RPC_URL }} SOLANA_DEVNET_NODE_RPC_URL: ${{ secrets.SOLANA_DEVNET_NODE_RPC_URL }} SOLANA_MAINNET_NODE_RPC_URL: ${{ secrets.SOLANA_MAINNET_NODE_RPC_URL }} + MPETH_GRAPHQL_PRICES_URL: ${{ secrets.MPETH_GRAPHQL_PRICES_URL }} publish: needs: test diff --git a/config/example.env b/config/example.env index 892420053..d5607c64a 100644 --- a/config/example.env +++ b/config/example.env @@ -260,3 +260,13 @@ DONATION_SAVE_BACKUP_DATABASE= # Default value is saveBackup DONATION_SAVE_BACKUP_ADAPTER=saveBackup + +ENABLE_UPDATE_RECURRING_DONATION_STREAM=true + +# Default value is 1 +NUMBER_OF_UPDATE_RECURRING_DONATION_CONCURRENT_JOB=1 + +# Default value is 0 0 * * * that means one day at 00:00 +UPDATE_RECURRING_DONATIONS_STREAM=0 0 * * * + +MPETH_GRAPHQL_PRICES_URL= diff --git a/migration/1706820821887-addmpEthToDatabaseTokens.ts b/migration/1706820821887-addmpEthToDatabaseTokens.ts new file mode 100644 index 000000000..a71b6beb5 --- /dev/null +++ b/migration/1706820821887-addmpEthToDatabaseTokens.ts @@ -0,0 +1,77 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; +import { NETWORK_IDS } from '../src/provider'; +import { ChainType } from '../src/types/network'; +import { Token } from '../src/entities/token'; + +const mpEthTokens = [ + { + name: 'mpETH', + symbol: 'mpETH', + address: '0x48afbbd342f64ef8a9ab1c143719b63c2ad81710', + decimals: 18, + isGivbackEligible: true, + networkId: NETWORK_IDS.MAIN_NET, + chainType: ChainType.EVM, + }, + { + name: 'mpETH', + symbol: 'mpETH', + address: '0x819845b60a192167ed1139040b4f8eca31834f27', + mainnetAddress: '0x48afbbd342f64ef8a9ab1c143719b63c2ad81710', + decimals: 18, + isGivbackEligible: true, + networkId: NETWORK_IDS.OPTIMISTIC, + chainType: ChainType.EVM, + }, +]; + +export class addmpEthToDatabaseTokens1706820821887 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.manager.save(Token, mpEthTokens); + + const tokens = await queryRunner.query(` + SELECT * FROM token + WHERE + ("address" = '0x48afbbd342f64ef8a9ab1c143719b63c2ad81710' AND "networkId" = ${NETWORK_IDS.MAIN_NET}) OR + ("address" = '0x819845b60a192167ed1139040b4f8eca31834f27' AND "networkId" = ${NETWORK_IDS.OPTIMISTIC}) + `); + const givethOrganization = ( + await queryRunner.query(`SELECT * FROM organization + WHERE label='giveth'`) + )[0]; + + const traceOrganization = ( + await queryRunner.query(`SELECT * FROM organization + WHERE label='trace'`) + )[0]; + + for (const token of tokens) { + await queryRunner.query(`INSERT INTO organization_tokens_token ("tokenId","organizationId") VALUES + (${token.id}, ${givethOrganization.id}), + (${token.id}, ${traceOrganization.id}) + ;`); + } + } + + public async down(queryRunner: QueryRunner): Promise { + const tokens = await queryRunner.query(` + SELECT * FROM token + WHERE + ("address" = '0x48afbbd342f64ef8a9ab1c143719b63c2ad81710' AND "networkId" = ${NETWORK_IDS.MAIN_NET}) OR + ("address" = '0x819845b60a192167ed1139040b4f8eca31834f27' AND "networkId" = ${NETWORK_IDS.OPTIMISTIC}) + `); + await queryRunner.query( + `DELETE FROM organization_tokens_token WHERE "tokenId" IN (${tokens + .map(token => token.id) + .join(',')})`, + ); + + await queryRunner.query( + `DELETE FROM token WHERE "id" IN (${tokens + .map(token => token.id) + .join(',')})`, + ); + } +} diff --git a/src/services/donationService.test.ts b/src/services/donationService.test.ts index 5f6c7b023..dde8ae31f 100644 --- a/src/services/donationService.test.ts +++ b/src/services/donationService.test.ts @@ -818,6 +818,70 @@ function fillOldStableCoinDonationsPriceTestCases() { expect(donation.valueUsd).to.gt(0); }); + it('should fill price for mpETH donation on the MAINNET network', async () => { + const token = 'mpETH'; + const amount = 2; + let donation = await saveDonationDirectlyToDb( + { + ...createDonationData(), + currency: token, + valueUsd: undefined, + valueEth: undefined, + amount, + }, + SEED_DATA.FIRST_USER.id, + SEED_DATA.FIRST_PROJECT.id, + ); + + const project = (await Project.findOne({ + where: { id: SEED_DATA.FIRST_PROJECT.id }, + })) as Project; + + await updateDonationPricesAndValues( + donation, + project, + null, + token, + CHAIN_ID.MAINNET, + amount, + ); + donation = (await findDonationById(donation.id))!; + expect(donation.valueUsd).to.gt(0); + expect(donation.priceUsd).to.below(donation.valueUsd); + }); + + it('should fill price for mpETH donation on the OPTIMISM network', async () => { + const token = 'mpETH'; + const amount = 2; + let donation = await saveDonationDirectlyToDb( + { + ...createDonationData(), + currency: token, + valueUsd: undefined, + valueEth: undefined, + amount, + }, + SEED_DATA.FIRST_USER.id, + SEED_DATA.FIRST_PROJECT.id, + ); + + const project = (await Project.findOne({ + where: { id: SEED_DATA.FIRST_PROJECT.id }, + })) as Project; + + await updateDonationPricesAndValues( + donation, + project, + null, + token, + CHAIN_ID.OPTIMISM, + amount, + ); + donation = (await findDonationById(donation.id))!; + expect(donation.valueUsd).to.gt(0); + expect(donation.priceUsd).to.below(donation.valueUsd); + }); + it('should fill price for Celo donation on the CELO Alfajores network', async () => { const token = 'CELO'; const amount = 100; diff --git a/src/services/donationService.ts b/src/services/donationService.ts index 491f55e13..c1e2c85a4 100644 --- a/src/services/donationService.ts +++ b/src/services/donationService.ts @@ -44,6 +44,7 @@ import { fetchSafeTransactionHash } from './safeServices'; import { ChainType } from '../types/network'; import { NETWORK_IDS } from '../provider'; import { getTransactionInfoFromNetwork } from './chains'; +import { fetchMpEthPrice } from './mpEthPriceService'; export const TRANSAK_COMPLETED_STATUS = 'COMPLETED'; @@ -65,6 +66,10 @@ export const updateDonationPricesAndValues = async ( if (token?.isStableCoin) { donation.priceUsd = 1; donation.valueUsd = Number(amount); + } else if (currency === 'mpETH') { + const mpEthPriceInUsd = await fetchMpEthPrice(); + donation.priceUsd = toFixNumber(mpEthPriceInUsd, 4); + donation.valueUsd = toFixNumber(donation.amount * mpEthPriceInUsd, 4); } else if (currency === 'GIV') { const { givPriceInUsd } = await fetchGivPrice(); donation.priceUsd = toFixNumber(givPriceInUsd, 4); diff --git a/src/services/mpEthPriceService.test.ts b/src/services/mpEthPriceService.test.ts new file mode 100644 index 000000000..12ed411bf --- /dev/null +++ b/src/services/mpEthPriceService.test.ts @@ -0,0 +1,12 @@ +import { assert, expect } from 'chai'; +import { fetchMpEthPrice } from './mpEthPriceService'; + +describe('fetchMpEthPrice test cases', fetchMpEthPriceTestCases); + +function fetchMpEthPriceTestCases() { + it('should fetch the price from velodrome subgraph for mpeth', async () => { + const mpEthPrice = await fetchMpEthPrice(); + assert.isOk(mpEthPrice); + expect(mpEthPrice).to.gt(0); + }); +} diff --git a/src/services/mpEthPriceService.ts b/src/services/mpEthPriceService.ts new file mode 100644 index 000000000..c3d695cf6 --- /dev/null +++ b/src/services/mpEthPriceService.ts @@ -0,0 +1,37 @@ +import { logger } from '../utils/logger'; +import Axios, { AxiosResponse } from 'axios'; +import axiosRetry from 'axios-retry'; + +const mpEthSubgraphUrl = process.env.MPETH_GRAPHQL_PRICES_URL as string; + +// Maximum timeout of axios. +const axiosTimeout = 20000; + +const query = { + query: ` + { + tokens(where:{id:"0x819845b60a192167ed1139040b4f8eca31834f27"}) { + id + name + symbol + decimals + lastPriceUSD + } + } + `, +}; + +export const fetchMpEthPrice = async () => { + try { + const result = await Axios.post(mpEthSubgraphUrl, query, { + headers: { + 'Content-Type': 'application/json', + }, + timeout: axiosTimeout, + }); + return Number(result.data.data.tokens[0].lastPriceUSD); + } catch (e) { + logger.error('fetching Giv Price fetchGivPrice() err', e); + throw e; + } +}; diff --git a/test/testUtils.ts b/test/testUtils.ts index 83b5a0435..d81e1ddb6 100644 --- a/test/testUtils.ts +++ b/test/testUtils.ts @@ -1265,6 +1265,13 @@ export const SEED_DATA = { decimals: 18, isStableCoin: true, }, + { + name: 'mpETH', + symbol: 'mpETH', + address: '0x48afbbd342f64ef8a9ab1c143719b63c2ad81710', + decimals: 18, + isStableCoin: true, + }, ], ropsten: [ { @@ -1329,6 +1336,13 @@ export const SEED_DATA = { decimals: 18, isStableCoin: true, }, + { + name: 'mpETH', + symbol: 'mpETH', + address: '0x819845b60a192167ed1139040b4f8eca31834f27', + decimals: 18, + isStableCoin: true, + }, ], etc: [ {