diff --git a/config/example.env b/config/example.env index 2f5a9f3cc..ae6a24599 100644 --- a/config/example.env +++ b/config/example.env @@ -181,7 +181,7 @@ DONATION_VERIFICAITON_EXPIRATION_HOURS=24 # Default: unnamed SERVICE_NAME=example OPTIMISM_NODE_HTTP_URL=https://optimism-mainnet.public.blastapi.io/ - +OPTIMISM_GOERLI_NODE_HTTP_URL= ####################################### INSTANT BOOSTING ################################# # OPTIONAL - default: false diff --git a/config/test.env b/config/test.env index 75a9454ea..b56f051ac 100644 --- a/config/test.env +++ b/config/test.env @@ -169,5 +169,7 @@ DONATION_VERIFICAITON_EXPIRATION_HOURS=24 # We need it for monoswap POLYGON_MAINNET_NODE_HTTP_URL=https://polygon-rpc.com OPTIMISM_NODE_HTTP_URL=https://optimism-mainnet.public.blastapi.io +OPTIMISM_GOERLI_NODE_HTTP_URL=https://optimism-goerli.public.blastapi.io + GITCOIN_ADAPTER=mock \ No newline at end of file diff --git a/migration/1687383705794-AddOptimismGoerliTokens.ts b/migration/1687383705794-AddOptimismGoerliTokens.ts new file mode 100644 index 000000000..846f5171b --- /dev/null +++ b/migration/1687383705794-AddOptimismGoerliTokens.ts @@ -0,0 +1,70 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; +import { Token } from '../src/entities/token'; +import seedTokens from './data/seedTokens'; +import { NETWORK_IDS } from '../src/provider'; +import config from '../src/config'; + +export class AddOptimismGoerliTokens1687383705794 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + const environment = config.get('ENVIRONMENT') as string; + if (environment === 'production') { + // We dont add optimism-goerli tokens in production ENV + return; + } + + await queryRunner.manager.save( + Token, + seedTokens + .filter(token => token.networkId === NETWORK_IDS.OPTIMISM_GOERLI) + .map(t => { + t.address = t.address?.toLowerCase(); + return t; + }), + ); + const tokens = await queryRunner.query(` + SELECT * FROM token + WHERE "networkId" = ${NETWORK_IDS.OPTIMISM_GOERLI} + `); + 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 environment = config.get('ENVIRONMENT') as string; + if (environment === 'production') { + // We dont add optimism-goerli tokens in production ENV + return; + } + + const tokens = await queryRunner.query(` + SELECT * FROM token + WHERE "networkId" = ${NETWORK_IDS.OPTIMISM_GOERLI} + `); + await queryRunner.query( + `DELETE FROM organization_tokens_token WHERE "tokenId" IN (${tokens + .map(token => token.id) + .join(',')})`, + ); + await queryRunner.query( + ` + DELETE from token + WHERE "networkId" = ${NETWORK_IDS.OPTIMISM_GOERLI} + `, + ); + } +} diff --git a/migration/data/seedTokens.ts b/migration/data/seedTokens.ts index 2c7d58197..4ff1dc521 100644 --- a/migration/data/seedTokens.ts +++ b/migration/data/seedTokens.ts @@ -1045,6 +1045,36 @@ const seedTokens: ITokenData[] = [ networkId: NETWORK_IDS.POLYGON, }, + // OPTIMISM Goerli tokens + { + name: 'OPTIMISM Goerli native token', + symbol: 'ETH', + address: '0x0000000000000000000000000000000000000000', + decimals: 18, + networkId: NETWORK_IDS.OPTIMISM_GOERLI, + }, + { + name: 'OPTIMISM Goerli OP token', + symbol: 'OP', + address: '0x4200000000000000000000000000000000000042', + decimals: 18, + networkId: NETWORK_IDS.OPTIMISM_GOERLI, + }, + { + name: 'Wrapped Ether', + symbol: 'WETH', + address: '0x4200000000000000000000000000000000000006', + decimals: 18, + networkId: NETWORK_IDS.OPTIMISM_GOERLI, + }, + { + name: 'Dai', + symbol: 'DAI', + address: '0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1', + decimals: 18, + networkId: NETWORK_IDS.OPTIMISM_GOERLI, + }, + // OPTIMISTIC tokens { name: 'OPTIMISTIC native token', diff --git a/src/provider.ts b/src/provider.ts index d6fc9e621..9018b00d8 100644 --- a/src/provider.ts +++ b/src/provider.ts @@ -12,6 +12,7 @@ export const NETWORK_IDS = { XDAI: 100, POLYGON: 137, OPTIMISTIC: 10, + OPTIMISM_GOERLI: 420, BSC: 56, CELO: 42220, CELO_ALFAJORES: 44787, @@ -27,6 +28,7 @@ export const NETWORKS_IDS_TO_NAME = { 42220: 'CELO', 44787: 'CELO_ALFAJORES', 10: 'OPTIMISTIC', + 420: 'OPTIMISM_GOERLI', }; const NETWORK_NAMES = { @@ -37,6 +39,7 @@ const NETWORK_NAMES = { GOERLI: 'goerli', POLYGON: 'polygon-mainnet', OPTIMISTIC: 'optimistic-mainnet', + OPTIMISM_GOERLI: 'optimism-goerli-testnet', CELO: 'Celo', CELO_ALFAJORES: 'Celo Alfajores', }; @@ -49,6 +52,7 @@ const NETWORK_NATIVE_TOKENS = { GOERLI: 'ETH', POLYGON: 'MATIC', OPTIMISTIC: 'ETH', + OPTIMISM_GOERLI: 'ETH', CELO: 'CELO', CELO_ALFAJORES: 'CELO', }; @@ -89,6 +93,11 @@ const networkNativeTokensList = [ networkId: NETWORK_IDS.OPTIMISTIC, nativeToken: NETWORK_NATIVE_TOKENS.OPTIMISTIC, }, + { + networkName: NETWORK_NAMES.OPTIMISM_GOERLI, + networkId: NETWORK_IDS.OPTIMISM_GOERLI, + nativeToken: NETWORK_NATIVE_TOKENS.OPTIMISM_GOERLI, + }, { networkName: NETWORK_NAMES.CELO, networkId: NETWORK_IDS.CELO, @@ -142,6 +151,10 @@ export function getProvider(networkId: number) { `https://celo-alfajores.infura.io/v3/${INFURA_ID}`; break; + case NETWORK_IDS.OPTIMISM_GOERLI: + url = `https://optimism-goerli.infura.io/v3/${INFURA_ID}`; + break; + default: { // Use infura const connectionInfo = ethers.providers.InfuraProvider.getUrl( @@ -203,6 +216,10 @@ export function getBlockExplorerApiUrl(networkId: number): string { apiUrl = config.get('OPTIMISTIC_SCAN_API_URL'); apiKey = config.get('OPTIMISTIC_SCAN_API_KEY'); break; + case NETWORK_IDS.OPTIMISM_GOERLI: + apiUrl = config.get('OPTIMISTIC_SCAN_API_URL'); + apiKey = config.get('OPTIMISTIC_SCAN_API_KEY'); + break; default: throw new Error(i18n.__(translationErrorMessagesKeys.INVALID_NETWORK_ID)); } diff --git a/src/resolvers/donationResolver.test.ts b/src/resolvers/donationResolver.test.ts index c962cfe2c..1b97c87b9 100644 --- a/src/resolvers/donationResolver.test.ts +++ b/src/resolvers/donationResolver.test.ts @@ -1731,7 +1731,7 @@ function createDonationTestCases() { ); assert.equal( saveDonationResponse.data.errors[0].message, - '"transactionNetworkId" must be one of [1, 3, 5, 100, 137, 10, 56, 42220, 44787]', + '"transactionNetworkId" must be one of [1, 3, 5, 100, 137, 10, 420, 56, 42220, 44787]', ); }); it('should throw exception when currency is not valid when currency contain characters', async () => { diff --git a/src/resolvers/donationResolver.ts b/src/resolvers/donationResolver.ts index 6f6bb1487..b610e8921 100644 --- a/src/resolvers/donationResolver.ts +++ b/src/resolvers/donationResolver.ts @@ -588,11 +588,21 @@ export class DonationResolver { createDonationQueryValidator, ); - const priceChainId = - transactionNetworkId === NETWORK_IDS.ROPSTEN || - transactionNetworkId === NETWORK_IDS.GOERLI - ? NETWORK_IDS.MAIN_NET - : transactionNetworkId; + let priceChainId: number; + switch (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; + default: + priceChainId = transactionNetworkId; + break; + } const project = await findProjectById(projectId); diff --git a/src/server/adminJs/tabs/tokenTab.ts b/src/server/adminJs/tabs/tokenTab.ts index 90f2050a4..b847e5347 100644 --- a/src/server/adminJs/tabs/tokenTab.ts +++ b/src/server/adminJs/tabs/tokenTab.ts @@ -184,6 +184,7 @@ export const generateTokenTab = async () => { { value: NETWORK_IDS.GOERLI, label: 'GOERLI' }, { value: NETWORK_IDS.POLYGON, label: 'POLYGON' }, { value: NETWORK_IDS.OPTIMISTIC, label: 'OPTIMISTIC' }, + { value: NETWORK_IDS.OPTIMISM_GOERLI, label: 'OPTIMISM GOERLI' }, { value: NETWORK_IDS.CELO, label: 'CELO' }, { value: NETWORK_IDS.CELO_ALFAJORES, diff --git a/src/services/donationService.test.ts b/src/services/donationService.test.ts index d314a82dc..ad9f81a3d 100644 --- a/src/services/donationService.test.ts +++ b/src/services/donationService.test.ts @@ -241,6 +241,50 @@ function syncDonationStatusWithBlockchainNetworkTestCases() { assert.isTrue(updateDonation.segmentNotified); }); + it('should verify a Optimism Goerli donation', async () => { + // https://goerli-optimism.etherscan.io/tx/0x95acfc3a5d1adbc9a4584d6bf92e9dfde48087fe54c2b750b067be718215ffc3 + const amount = 0.011; + + const transactionInfo = { + txHash: + '0x95acfc3a5d1adbc9a4584d6bf92e9dfde48087fe54c2b750b067be718215ffc3', + currency: 'ETH', + networkId: NETWORK_IDS.OPTIMISM_GOERLI, + fromAddress: '0x317bbc1927be411cd05615d2ffdf8d320c6c4052', + toAddress: '0x00d18ca9782be1caef611017c2fbc1a39779a57c', + amount, + timestamp: 1679484540, + }; + const user = await saveUserDirectlyToDb(transactionInfo.fromAddress); + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + walletAddress: transactionInfo.toAddress, + }); + const donation = await saveDonationDirectlyToDb( + { + amount: transactionInfo.amount, + transactionNetworkId: transactionInfo.networkId, + transactionId: transactionInfo.txHash, + currency: transactionInfo.currency, + fromWalletAddress: transactionInfo.fromAddress, + toWalletAddress: transactionInfo.toAddress, + valueUsd: 20.73, + anonymous: false, + createdAt: new Date(transactionInfo.timestamp), + status: DONATION_STATUS.PENDING, + }, + user.id, + project.id, + ); + const updateDonation = await syncDonationStatusWithBlockchainNetwork({ + donationId: donation.id, + }); + assert.isOk(updateDonation); + assert.equal(updateDonation.id, donation.id); + assert.equal(updateDonation.status, DONATION_STATUS.VERIFIED); + assert.isTrue(updateDonation.segmentNotified); + }); + it('should verify a mainnet donation', async () => { // https://etherscan.io/tx/0x37765af1a7924fb6ee22c83668e55719c9ecb1b79928bd4b208c42dfff44da3a const transactionInfo = { diff --git a/src/services/transactionService.test.ts b/src/services/transactionService.test.ts index d83337553..bcdf1f72f 100644 --- a/src/services/transactionService.test.ts +++ b/src/services/transactionService.test.ts @@ -468,6 +468,25 @@ function getTransactionDetailTestCases() { assert.equal(transactionInfo.amount, amount); }); + it('should return transaction detail for normal transfer on optimism-goerli', async () => { + // https://goerli-optimism.etherscan.io/tx/0x95acfc3a5d1adbc9a4584d6bf92e9dfde48087fe54c2b750b067be718215ffc3 + + const amount = 0.011; + const transactionInfo = await getTransactionInfoFromNetwork({ + txHash: + '0x95acfc3a5d1adbc9a4584d6bf92e9dfde48087fe54c2b750b067be718215ffc3', + symbol: 'ETH', + networkId: NETWORK_IDS.OPTIMISM_GOERLI, + fromAddress: '0x317bbc1927be411cd05615d2ffdf8d320c6c4052', + toAddress: '0x00d18ca9782be1caef611017c2fbc1a39779a57c', + amount, + timestamp: 167740007, + }); + assert.isOk(transactionInfo); + assert.equal(transactionInfo.currency, 'ETH'); + assert.equal(transactionInfo.amount, amount); + }); + it('should return transaction detail for normal transfer on CELO', async () => { // https://celoscan.io/tx/0xa2a282cf6a7dec8b166aa52ac3d00fcd15a370d414615e29a168cfbb592e3637 diff --git a/src/utils/validators/graphqlQueryValidators.ts b/src/utils/validators/graphqlQueryValidators.ts index e5f602f8c..982ababa8 100644 --- a/src/utils/validators/graphqlQueryValidators.ts +++ b/src/utils/validators/graphqlQueryValidators.ts @@ -163,6 +163,7 @@ const managingFundsValidator = Joi.object({ NETWORK_IDS.CELO, NETWORK_IDS.CELO_ALFAJORES, NETWORK_IDS.OPTIMISTIC, + NETWORK_IDS.OPTIMISM_GOERLI, NETWORK_IDS.XDAI, ), }), diff --git a/test/pre-test-scripts.ts b/test/pre-test-scripts.ts index 164322903..2385d13d6 100644 --- a/test/pre-test-scripts.ts +++ b/test/pre-test-scripts.ts @@ -156,6 +156,17 @@ async function seedTokens() { } await Token.create(tokenData as Token).save(); } + for (const token of SEED_DATA.TOKENS.optimism_goerli) { + const tokenData = { + ...token, + networkId: NETWORK_IDS.OPTIMISM_GOERLI, + isGivbackEligible: true, + }; + if (token.symbol === 'OP') { + (tokenData as any).order = 2; + } + await Token.create(tokenData as Token).save(); + } } async function seedOrganizations() { diff --git a/test/testUtils.ts b/test/testUtils.ts index 198dba439..82ac9bb8c 100644 --- a/test/testUtils.ts +++ b/test/testUtils.ts @@ -1275,6 +1275,20 @@ export const SEED_DATA = { decimals: 18, }, ], + optimism_goerli: [ + { + name: 'OPTIMISM native token', + symbol: 'ETH', + address: '0x0000000000000000000000000000000000000000', + decimals: 18, + }, + { + name: 'OPTIMISM OP token', + symbol: 'OP', + address: '0x4200000000000000000000000000000000000042', + decimals: 18, + }, + ], goerli: [ { name: 'Ethereum native token',