diff --git a/.github/workflows/develop-pipeline.yml b/.github/workflows/develop-pipeline.yml index ef4848203..417acd9aa 100644 --- a/.github/workflows/develop-pipeline.yml +++ b/.github/workflows/develop-pipeline.yml @@ -63,6 +63,8 @@ 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 }} + ARBITRUM_SCAN_API_KEY: ${{ secrets.ARBITRUM_SCAN_API_KEY }} + ARBITRUM_SEPOLIA_SCAN_API_KEY: ${{ secrets.ARBITRUM_SEPOLIA_SCAN_API_KEY }} MORDOR_ETC_TESTNET: ${{ secrets.MORDOR_ETC_TESTNET }} ETC_NODE_HTTP_URL: ${{ secrets.ETC_NODE_HTTP_URL }} SOLANA_TEST_NODE_RPC_URL: ${{ secrets.SOLANA_TEST_NODE_RPC_URL }} diff --git a/.github/workflows/master-pipeline.yml b/.github/workflows/master-pipeline.yml index a5e94222b..08f6fda9a 100644 --- a/.github/workflows/master-pipeline.yml +++ b/.github/workflows/master-pipeline.yml @@ -100,6 +100,8 @@ 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 }} + ARBITRUM_SCAN_API_KEY: ${{ secrets.ARBITRUM_SCAN_API_KEY }} + ARBITRUM_SEPOLIA_SCAN_API_KEY: ${{ secrets.ARBITRUM_SEPOLIA_SCAN_API_KEY }} MORDOR_ETC_TESTNET: ${{ secrets.MORDOR_ETC_TESTNET }} ETC_NODE_HTTP_URL: ${{ secrets.ETC_NODE_HTTP_URL }} DROP_DATABASE: ${{ secrets.DROP_DATABASE_DURING_TEST_PROD }} diff --git a/.github/workflows/run-tests-on-pr.yml.bck b/.github/workflows/run-tests-on-pr.yml.bck index 76fc70f7b..d3072bd8e 100644 --- a/.github/workflows/run-tests-on-pr.yml.bck +++ b/.github/workflows/run-tests-on-pr.yml.bck @@ -66,4 +66,6 @@ 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 }} + ARBITRUM_SCAN_API_KEY: ${{ secrets.ARBITRUM_SCAN_API_KEY }} + ARBITRUM_SEPOLIA_SCAN_API_KEY: ${{ secrets.ARBITRUM_SEPOLIA_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 14faf85a4..08ec827e4 100644 --- a/.github/workflows/staging-pipeline.yml +++ b/.github/workflows/staging-pipeline.yml @@ -100,6 +100,8 @@ 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 }} + ARBITRUM_SCAN_API_KEY: ${{ secrets.ARBITRUM_SCAN_API_KEY }} + ARBITRUM_SEPOLIA_SCAN_API_KEY: ${{ secrets.ARBITRUM_SEPOLIA_SCAN_API_KEY }} MORDOR_ETC_TESTNET: ${{ secrets.MORDOR_ETC_TESTNET }} ETC_NODE_HTTP_URL: ${{ secrets.ETC_NODE_HTTP_URL }} DROP_DATABASE: ${{ secrets.DROP_DATABASE_DURING_TEST_STAGING }} diff --git a/config/example.env b/config/example.env index efb76d9c0..6b509ac38 100644 --- a/config/example.env +++ b/config/example.env @@ -36,6 +36,10 @@ CELO_SCAN_API_URL=https://api.celoscan.io/api CELO_SCAN_API_KEY=0000000000000000000000000000000000 CELO_ALFAJORES_SCAN_API_URL=https://api-alfajores.celoscan.io/api CELO_ALFAJORES_SCAN_API_KEY=0000000000000000000000000000000000 +ARBITRUM_SCAN_API_URL=https://api.arbiscan.io/api +ARBITRUM_SCAN_API_KEY=0000000000000000000000000000000000 +ARBITRUM_SEPOLIA_SCAN_API_URL=https://api-sepolia.arbiscan.io/api +ARBITRUM_SEPOLIA_SCAN_API_KEY=0000000000000000000000000000000000 GNOSISSCAN_API_URL=https://api.gnosisscan.io/api ETHERSCAN_API_KEY=0000000000000000000000000000000000 GNOSISSCAN_API_KEY=0000000000000000000000000000000000 @@ -223,6 +227,11 @@ MORDOR_ETC_TESTNET_SCAN_API_URL=https://etc-mordor.blockscout.com/api/v1 # https://chainlist.org/chain/63 MORDOR_ETC_TESTNET_NODE_HTTP_URL=https://rpc.mordor.etccooperative.org +# ARBITRUM MAINNET +ARBITRUM_MAINNET_NODE_HTTP_URL=https://arb1.arbitrum.io/rpc + +# ARBITRUM SEPOLIA +ARBITRUM_SEPOLIA_NODE_HTTP_URL=https://sepolia-rollup.arbitrum.io/rpc # This is the address behind donation.eth MATCHING_FUND_DONATIONS_FROM_ADDRESS=0x6e8873085530406995170Da467010565968C7C62 diff --git a/config/test.env b/config/test.env index c137594e2..d9bdbc6d1 100644 --- a/config/test.env +++ b/config/test.env @@ -44,6 +44,10 @@ POLYGON_SCAN_API_URL=https://api.polygonscan.com/api OPTIMISTIC_SCAN_API_URL=https://api-optimistic.etherscan.io/api CELO_SCAN_API_URL=https://api.celoscan.io/api CELO_ALFAJORES_SCAN_API_URL=https://api-alfajores.celoscan.io/api +ARBITRUM_SCAN_API_URL=https://api.arbiscan.io/api +ARBITRUM_SCAN_API_KEY=0000000000000000000000000000000000 +ARBITRUM_SEPOLIA_SCAN_API_URL=https://api-sepolia.arbiscan.io/api +ARBITRUM_SEPOLIA_SCAN_API_KEY=0000000000000000000000000000000000 GNOSISSCAN_API_URL=https://api.gnosisscan.io/api ETHERSCAN_API_KEY=00000000000000000000000000000000 GNOSISSCAN_API_KEY=0000000000000000000000000000000000 diff --git a/migration/1708279692128-addArbitrumTokens.ts b/migration/1708279692128-addArbitrumTokens.ts new file mode 100644 index 000000000..0026a882d --- /dev/null +++ b/migration/1708279692128-addArbitrumTokens.ts @@ -0,0 +1,53 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; +import { Token } from '../src/entities/token'; +import seedTokens from './data/seedTokens'; +import config from '../src/config'; +import { NETWORK_IDS } from '../src/provider'; + +export class AddArbitrumTokens1708279692128 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + const environment = config.get('ENVIRONMENT') as string; + + const networkId = + environment === 'production' + ? NETWORK_IDS.ARBITRUM_MAINNET + : NETWORK_IDS.ARBITRUM_SEPOLIA; + + await queryRunner.manager.save( + Token, + seedTokens + .filter(token => token.networkId === networkId) + .map(token => { + const t = { + ...token, + }; + t.address = t.address?.toLowerCase(); + delete t.chainType; + return t; + }), + ); + const tokens = await queryRunner.query(` + SELECT * FROM token + WHERE "networkId" = ${networkId} + `); + 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) { + // Add all Polygon tokens to Giveth organization + 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 {} +} diff --git a/migration/data/seedTokens.ts b/migration/data/seedTokens.ts index 7d38c8397..3b5a620d8 100644 --- a/migration/data/seedTokens.ts +++ b/migration/data/seedTokens.ts @@ -13,6 +13,7 @@ interface ITokenData { networkId: number; chainType?: ChainType; coingeckoId?: string; + isStableCoin?: boolean; } const seedTokens: ITokenData[] = [ // Mainnet tokens @@ -1376,6 +1377,238 @@ const seedTokens: ITokenData[] = [ chainType: ChainType.SOLANA, coingeckoId: COINGECKO_TOKEN_IDS.SOLANA, }, + + // ARBITRUM Sepolia + { + name: 'Arbitrum Sepolia native token', + symbol: 'ETH', + address: '0x0000000000000000000000000000000000000000', + decimals: 18, + networkId: NETWORK_IDS.ARBITRUM_SEPOLIA, + coingeckoId: 'ethereum', + }, + { + name: 'Chromatic test Eth', + symbol: 'cETH', + address: '0x93252009E644138b906aE1a28792229E577239B9', + decimals: 18, + networkId: NETWORK_IDS.ARBITRUM_SEPOLIA, + coingeckoId: 'weth', + }, + { + name: 'Arbitrum ETH', + symbol: 'ETH', + address: '0x0000000000000000000000000000000000000000', + decimals: 18, + networkId: NETWORK_IDS.ARBITRUM_MAINNET, + coingeckoId: 'ethereum', + }, + { + name: 'usdt', + symbol: 'USDT', + address: '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9', + decimals: 6, + networkId: NETWORK_IDS.ARBITRUM_MAINNET, + coingeckoId: 'tether', + // isStableCoin: true, + }, + { + name: 'USDC', + symbol: 'USDC', + address: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', + decimals: 6, + networkId: NETWORK_IDS.ARBITRUM_MAINNET, + coingeckoId: 'usd-coin', + // isStableCoin: true, + }, + { + name: 'Chainlink', + symbol: 'LINK', + address: '0xf97f4df75117a78c1A5a0DBb814Af92458539FB4', + decimals: 18, + networkId: NETWORK_IDS.ARBITRUM_MAINNET, + coingeckoId: 'chainlink', + }, + { + name: 'Dai', + symbol: 'DAI', + address: '0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1', + decimals: 18, + networkId: NETWORK_IDS.ARBITRUM_MAINNET, + coingeckoId: 'dai', + // isStableCoin: true, + }, + { + name: 'Uniswap', + symbol: 'UNI', + address: '0xFa7F8980b0f1E64A2062791cc3b0871572f1F7f0', + decimals: 18, + networkId: NETWORK_IDS.ARBITRUM_MAINNET, + coingeckoId: 'uniswap', + }, + { + name: 'Lido', + symbol: 'LDO', + address: '0x13Ad51ed4F1B7e9Dc168d8a00cB3f4dDD85EfA60', + decimals: 18, + networkId: NETWORK_IDS.ARBITRUM_MAINNET, + coingeckoId: 'lido-dao', + }, + { + name: 'Arbitrum', + symbol: 'ARB', + address: '0x912CE59144191C1204E64559FE8253a0e49E6548', + decimals: 18, + networkId: NETWORK_IDS.ARBITRUM_MAINNET, + coingeckoId: 'arbitrum', + }, + { + name: 'Brigded ISDC', + symbol: 'USDC', + address: '0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8', + decimals: 6, + networkId: NETWORK_IDS.ARBITRUM_MAINNET, + coingeckoId: 'usd-coin', + // isStableCoin: true, + }, + { + name: 'TrueUSD', + symbol: 'TUSD', + address: '0x4D15a3A2286D883AF0AA1B3f21367843FAc63E07', + decimals: 18, + networkId: NETWORK_IDS.ARBITRUM_MAINNET, + coingeckoId: 'true-usd', + // isStableCoin: true, + }, + { + name: 'The Graph', + symbol: 'GRT', + address: '0x9623063377AD1B27544C965cCd7342f7EA7e88C7', + decimals: 18, + networkId: NETWORK_IDS.ARBITRUM_MAINNET, + coingeckoId: 'the-graph', + }, + { + name: 'Frax Share', + symbol: 'FXS', + address: '0x9d2f299715d94d8a7e6f5eaa8e654e8c74a988a7', + decimals: 18, + networkId: NETWORK_IDS.ARBITRUM_MAINNET, + coingeckoId: 'frax-share', + }, + { + name: 'USDD', + symbol: 'USDD', + address: '0x680447595e8b7b3Aa1B43beB9f6098C79ac2Ab3f', + decimals: 18, + networkId: NETWORK_IDS.ARBITRUM_MAINNET, + coingeckoId: 'usdd', + // isStableCoin: true, + }, + { + name: 'WOO', + symbol: 'WOO', + address: '0xcafcd85d8ca7ad1e1c6f82f651fa15e33aefd07b', + decimals: 18, + networkId: NETWORK_IDS.ARBITRUM_MAINNET, + coingeckoId: 'woo-network', + }, + { + name: 'Gnosis', + symbol: 'GNO', + address: '0xa0b862f60edef4452f25b4160f177db44deb6cf1', + decimals: 18, + networkId: NETWORK_IDS.ARBITRUM_MAINNET, + coingeckoId: 'gnosis', + }, + { + name: 'Curve DAO Token', + symbol: 'CRV', + address: '0x11cDb42B0EB46D95f990BeDD4695A6e3fA034978', + decimals: 18, + networkId: NETWORK_IDS.ARBITRUM_MAINNET, + coingeckoId: 'curve-dao-token', + }, + { + name: 'Compound', + symbol: 'COMP', + address: '0x354A6dA3fcde098F8389cad84b0182725c6C91dE', + decimals: 18, + networkId: NETWORK_IDS.ARBITRUM_MAINNET, + coingeckoId: 'compound-governance-token', + }, + { + name: 'GMX', + symbol: 'GMX', + address: '0xfc5a1a6eb076a2c7ad06ed22c90d7e710e35ad0a', + decimals: 18, + networkId: NETWORK_IDS.ARBITRUM_MAINNET, + coingeckoId: 'gmx', + }, + { + name: 'Loopring', + symbol: 'LRC', + address: '0x46d0cE7de6247b0A95f67b43B589b4041BaE7fbE', + decimals: 18, + networkId: NETWORK_IDS.ARBITRUM_MAINNET, + coingeckoId: 'loopring', + }, + { + name: 'Treasure', + symbol: 'MAGIC', + address: '0x539bdE0d7Dbd336b79148AA742883198BBF60342', + decimals: 18, + networkId: NETWORK_IDS.ARBITRUM_MAINNET, + coingeckoId: 'treasure', + }, + { + name: 'SushiSwap', + symbol: 'SUSHI', + address: '0xd4d42f0b6def4ce0383636770ef773390d85c61a', + decimals: 18, + networkId: NETWORK_IDS.ARBITRUM_MAINNET, + coingeckoId: 'sushi', + }, + { + name: 'yearn.finance', + symbol: 'YFI', + address: '0x82e3A8F066a6989666b031d916c43672085b1582', + decimals: 18, + networkId: NETWORK_IDS.ARBITRUM_MAINNET, + coingeckoId: 'yearn-finance', + }, + { + name: 'Xai', + symbol: 'XAI', + address: '0x4Cb9a7AE498CEDcBb5EAe9f25736aE7d428C9D66', + decimals: 18, + networkId: NETWORK_IDS.ARBITRUM_MAINNET, + coingeckoId: 'xai', + }, + { + name: 'Livepeer', + symbol: 'LPT', + address: '0x289ba1701C2F088cf0faf8B3705246331cB8A839', + decimals: 18, + networkId: NETWORK_IDS.ARBITRUM_MAINNET, + coingeckoId: 'livepeer', + }, + { + name: 'Wrapped Bitcoin', + symbol: 'WBTC', + address: '0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f', + decimals: 8, + networkId: NETWORK_IDS.ARBITRUM_MAINNET, + coingeckoId: 'wrapped-bitcoin', + }, + { + name: 'Cartesi', + symbol: 'CTSI', + address: '0x319f865b287fCC10b30d8cE6144e8b6D1b476999', + decimals: 18, + networkId: NETWORK_IDS.ARBITRUM_MAINNET, + coingeckoId: 'cartesi', + }, ]; export default seedTokens; diff --git a/src/entities/project.ts b/src/entities/project.ts index 3143a9de4..04808bd99 100644 --- a/src/entities/project.ts +++ b/src/entities/project.ts @@ -89,6 +89,7 @@ export enum FilterField { AcceptFundOnPolygon = 'acceptFundOnPolygon', AcceptFundOnETC = 'acceptFundOnETC', AcceptFundOnCelo = 'acceptFundOnCelo', + AcceptFundOnArbitrum = 'acceptFundOnArbitrum', AcceptFundOnOptimism = 'acceptFundOnOptimism', AcceptFundOnSolana = 'acceptFundOnSolana', GivingBlock = 'fromGivingBlock', diff --git a/src/provider.ts b/src/provider.ts index 8d8f14e14..fdaa15dd7 100644 --- a/src/provider.ts +++ b/src/provider.ts @@ -19,6 +19,9 @@ export const NETWORK_IDS = { ETC: 61, MORDOR_ETC_TESTNET: 63, + ARBITRUM_MAINNET: 42161, + ARBITRUM_SEPOLIA: 421614, + // https://docs.particle.network/developers/other-services/node-service/solana-api SOLANA_MAINNET: 101, SOLANA_TESTNET: 102, @@ -38,6 +41,8 @@ export const NETWORKS_IDS_TO_NAME = { 420: 'OPTIMISM_GOERLI', 61: 'ETC', 63: 'MORDOR_ETC_TESTNET', + 42161: 'ARBITRUM_MAINNET', + 421614: 'ARBITRUM_SEPOLIA', }; const NETWORK_NAMES = { @@ -53,6 +58,8 @@ const NETWORK_NAMES = { CELO_ALFAJORES: 'Celo Alfajores', ETC: 'Ethereum Classic', MORDOR_ETC_TESTNET: 'Ethereum Classic Testnet', + ARBITRUM_MAINNET: 'Arbitrum Mainnet', + ARBITRUM_SEPOLIA: 'Arbitrum Sepolia', }; const NETWORK_NATIVE_TOKENS = { @@ -68,6 +75,8 @@ const NETWORK_NATIVE_TOKENS = { CELO_ALFAJORES: 'CELO', ETC: 'ETC', MORDOR_ETC_TESTNET: 'mETC', + ARBITRUM_MAINNET: 'ETH', + ARBITRUM_SEPOLIA: 'ETH', }; const networkNativeTokensList = [ @@ -131,6 +140,16 @@ const networkNativeTokensList = [ networkId: NETWORK_IDS.MORDOR_ETC_TESTNET, nativeToken: NETWORK_NATIVE_TOKENS.MORDOR_ETC_TESTNET, }, + { + networkName: NETWORK_NAMES.ARBITRUM_MAINNET, + networkId: NETWORK_IDS.ARBITRUM_MAINNET, + nativeToken: NETWORK_NATIVE_TOKENS.ARBITRUM_MAINNET, + }, + { + networkName: NETWORK_NAMES.ARBITRUM_SEPOLIA, + networkId: NETWORK_IDS.ARBITRUM_SEPOLIA, + nativeToken: NETWORK_NATIVE_TOKENS.ARBITRUM_SEPOLIA, + }, ]; export function getNetworkNativeToken(networkId: number): string { @@ -184,6 +203,18 @@ export function getProvider(networkId: number) { url = `https://optimism-goerli.infura.io/v3/${INFURA_ID}`; break; + case NETWORK_IDS.ARBITRUM_MAINNET: + url = + (process.env.ARBITRUM_MAINNET_NODE_HTTP_URL as string) || + `https://arbitrum-mainnet.infura.io/v3/${INFURA_ID}`; + break; + + case NETWORK_IDS.ARBITRUM_SEPOLIA: + url = + (process.env.ARBITRUM_SEPOLIA_NODE_HTTP_URL as string) || + `https://arbitrum-sepolia.infura.io/v3/${INFURA_ID}`; + break; + default: { // Use infura const connectionInfo = ethers.providers.InfuraProvider.getUrl( @@ -255,6 +286,14 @@ export function getBlockExplorerApiUrl(networkId: number): string { case NETWORK_IDS.MORDOR_ETC_TESTNET: // ETC network doesn't need API key return config.get('MORDOR_ETC_TESTNET_SCAN_API_URL') as string; + case NETWORK_IDS.ARBITRUM_MAINNET: + apiUrl = config.get('ARBITRUM_SCAN_API_URL'); + apiKey = config.get('ARBITRUM_SCAN_API_KEY'); + break; + case NETWORK_IDS.ARBITRUM_SEPOLIA: + apiUrl = config.get('ARBITRUM_SEPOLIA_SCAN_API_URL'); + apiKey = config.get('ARBITRUM_SEPOLIA_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 236dd935a..3b5d0a40e 100644 --- a/src/resolvers/donationResolver.test.ts +++ b/src/resolvers/donationResolver.test.ts @@ -2379,7 +2379,7 @@ function createDonationTestCases() { ); assert.equal( saveDonationResponse.data.errors[0].message, - '"transactionNetworkId" must be one of [1, 3, 5, 100, 137, 10, 420, 56, 42220, 44787, 61, 63, 101, 102, 103]', + '"transactionNetworkId" must be one of [1, 3, 5, 100, 137, 10, 420, 56, 42220, 44787, 61, 63, 42161, 421614, 101, 102, 103]', ); }); it('should not throw exception when currency is not valid when currency is USDC.e', async () => { diff --git a/src/resolvers/projectResolver.allProject.test.ts b/src/resolvers/projectResolver.allProject.test.ts new file mode 100644 index 000000000..529b30d6e --- /dev/null +++ b/src/resolvers/projectResolver.allProject.test.ts @@ -0,0 +1,1919 @@ +import { assert } from 'chai'; +import 'mocha'; +import { + createDonationData, + createProjectData, + generateRandomEtheriumAddress, + generateRandomSolanaAddress, + graphqlUrl, + REACTION_SEED_DATA, + saveDonationDirectlyToDb, + saveProjectDirectlyToDb, + saveUserDirectlyToDb, + SEED_DATA, +} from '../../test/testUtils'; +import axios from 'axios'; +import { fetchMultiFilterAllProjectsQuery } from '../../test/graphqlQueries'; +import { Project, ReviewStatus, SortingField } from '../entities/project'; +import { User } from '../entities/user'; +import { NETWORK_IDS } from '../provider'; +import { findProjectRecipientAddressByNetworkId } from '../repositories/projectAddressRepository'; +import { setPowerRound } from '../repositories/powerRoundRepository'; +import { + insertSinglePowerBoosting, + takePowerBoostingSnapshot, +} from '../repositories/powerBoostingRepository'; +import { refreshProjectPowerView } from '../repositories/projectPowerViewRepository'; +import { PowerBalanceSnapshot } from '../entities/powerBalanceSnapshot'; +import { PowerBoostingSnapshot } from '../entities/powerBoostingSnapshot'; +import { ProjectAddress } from '../entities/projectAddress'; +import moment from 'moment'; +import { PowerBoosting } from '../entities/powerBoosting'; +import { AppDataSource } from '../orm'; +// We are using cache so redis needs to be cleared for tests with same filters +import { redis } from '../redis'; +import { Campaign, CampaignType } from '../entities/campaign'; +import { generateRandomString, getHtmlTextSummary } from '../utils/utils'; +import { ArgumentValidationError } from 'type-graphql'; +import { InstantPowerBalance } from '../entities/instantPowerBalance'; +import { saveOrUpdateInstantPowerBalances } from '../repositories/instantBoostingRepository'; +import { updateInstantBoosting } from '../services/instantBoostingServices'; +import { QfRound } from '../entities/qfRound'; +import { calculateEstimatedMatchingWithParams } from '../utils/qfUtils'; +import { + refreshProjectDonationSummaryView, + refreshProjectEstimatedMatchingView, +} from '../services/projectViewsService'; +import { addOrUpdatePowerSnapshotBalances } from '../repositories/powerBalanceSnapshotRepository'; +import { findPowerSnapshots } from '../repositories/powerSnapshotRepository'; +import { ChainType } from '../types/network'; + +const ARGUMENT_VALIDATION_ERROR_MESSAGE = new ArgumentValidationError([ + { property: '' }, +]).message; + +// search and filters +describe('all projects test cases --->', allProjectsTestCases); + +function allProjectsTestCases() { + it('should return projects search by owner', async () => { + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + searchTerm: SEED_DATA.SECOND_USER.name, + }, + }); + + const projects = result.data.data.allProjects.projects; + const secondUserProjects = await Project.find({ + where: { + admin: String(SEED_DATA.SECOND_USER.id), + }, + }); + + assert.equal(projects.length, secondUserProjects.length); + assert.equal(Number(projects[0]?.admin), SEED_DATA.SECOND_USER.id); + assert.isNotEmpty(projects[0].addresses); + projects.forEach(project => { + assert.isNotOk(project.adminUser.email); + assert.isOk(project.adminUser.firstName); + assert.isOk(project.adminUser.walletAddress); + assert.isOk(project.categories[0].mainCategory.title); + assert.equal( + project.descriptionSummary, + getHtmlTextSummary(project.description), + ); + assert.isNull(project.estimatedMatching); + assert.exists(project.sumDonationValueUsd); + assert.exists(project.sumDonationValueUsdForActiveQfRound); + assert.exists(project.countUniqueDonorsForActiveQfRound); + assert.exists(project.countUniqueDonors); + }); + }); + + it('should return projects with correct reaction', async () => { + const limit = 1; + const USER_DATA = SEED_DATA.FIRST_USER; + + // Project has not been liked + let result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + limit, + searchTerm: SEED_DATA.SECOND_PROJECT.title, + connectedWalletUserId: USER_DATA.id, + }, + }); + + let projects = result.data.data.allProjects.projects; + assert.equal(projects.length, limit); + assert.isNull(projects[0]?.reaction); + + // Project has been liked, but connectedWalletUserIs is not filled + result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + limit, + searchTerm: SEED_DATA.FIRST_PROJECT.title, + }, + }); + + projects = result.data.data.allProjects.projects; + assert.equal(projects.length, limit); + assert.isNull(projects[0]?.reaction); + + // Project has been liked + result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + limit, + searchTerm: SEED_DATA.FIRST_PROJECT.title, + connectedWalletUserId: USER_DATA.id, + }, + }); + + projects = result.data.data.allProjects.projects; + assert.equal(projects.length, limit); + assert.equal( + projects[0]?.reaction?.id, + REACTION_SEED_DATA.FIRST_LIKED_PROJECT_REACTION.id, + ); + projects.forEach(project => { + assert.isNotOk(project.adminUser.email); + assert.isOk(project.adminUser.firstName); + assert.isOk(project.adminUser.walletAddress); + }); + }); + + it('should return projects, sort by creationDate, DESC', async () => { + const firstProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + }); + const secondProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + }); + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + sortingBy: SortingField.Newest, + }, + }); + assert.equal( + Number(result.data.data.allProjects.projects[0].id), + secondProject.id, + ); + assert.equal( + Number(result.data.data.allProjects.projects[1].id), + firstProject.id, + ); + }); + + it('should return projects, sort by updatedAt, DESC', async () => { + const firstProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + }); + const secondProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + }); + + firstProject.title = String(new Date().getTime()); + firstProject.updatedAt = moment().add(2, 'days').toDate(); + await firstProject.save(); + + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + sortingBy: SortingField.RecentlyUpdated, + }, + }); + // First project should move to first position + assert.equal( + Number(result.data.data.allProjects.projects[0].id), + firstProject.id, + ); + }); + it('should return projects, sort by creationDate, ASC', async () => { + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + sortingBy: SortingField.Oldest, + }, + }); + const projectsCount = result.data.data.allProjects.projects.length; + const firstProjectIsOlder = + new Date(result.data.data.allProjects.projects[0].creationDate) < + new Date( + result.data.data.allProjects.projects[projectsCount - 1].creationDate, + ); + assert.isTrue(firstProjectIsOlder); + }); + it('should return projects, filter by verified, true', async () => { + // There is two verified projects so I just need to create a project with verified: false and listed:true + await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + verified: false, + qualityScore: 0, + }); + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + filters: ['Verified'], + }, + }); + assert.isNotEmpty(result.data.data.allProjects.projects); + result.data.data.allProjects.projects.forEach(project => + assert.isTrue(project.verified), + ); + }); + it('should return projects, filter by acceptGiv, true', async () => { + await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + qualityScore: 0, + }); + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + filters: ['AcceptGiv'], + }, + }); + assert.isNotEmpty(result.data.data.allProjects.projects); + result.data.data.allProjects.projects.forEach(project => + // currently givingBlocks projects doesnt accept GIV + assert.notExists(project.givingBlocksId), + ); + }); + it('should return projects, filter by boosted by givPower, true', async () => { + await AppDataSource.getDataSource().query( + 'truncate power_snapshot cascade', + ); + await PowerBoosting.clear(); + await PowerBalanceSnapshot.clear(); + await PowerBoostingSnapshot.clear(); + + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + }); + + const roundNumber = project.id * 10; + await insertSinglePowerBoosting({ + user, + project, + percentage: 100, + }); + await takePowerBoostingSnapshot(); + const [powerSnapshots] = await findPowerSnapshots(); + const snapshot = powerSnapshots[0]; + + snapshot.blockNumber = 1; + snapshot.roundNumber = roundNumber; + await snapshot.save(); + + await addOrUpdatePowerSnapshotBalances({ + userId: user.id, + powerSnapshotId: snapshot.id, + balance: 200, + }); + + await setPowerRound(roundNumber); + await refreshProjectPowerView(); + + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + filters: ['BoostedWithGivPower'], + limit: 50, + }, + }); + assert.isNotEmpty(result.data.data.allProjects.projects); + result.data.data.allProjects.projects.forEach(projectQueried => + assert.isOk(projectQueried?.projectPower?.totalPower > 0), + ); + }); + it('should return projects, filter from the givingblocks', async () => { + await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + givingBlocksId: '1234355', + qualityScore: 0, + }); + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + filters: ['GivingBlock'], + }, + }); + assert.isNotEmpty(result.data.data.allProjects.projects); + result.data.data.allProjects.projects.forEach(project => + assert.exists(project.givingBlocksId), + ); + }); + it('should return projects, sort by reactions, DESC', async () => { + await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + totalReactions: 100, + qualityScore: 0, + }); + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + sortingBy: SortingField.MostLiked, + }, + }); + assert.isTrue( + result.data.data.allProjects.projects[0].totalReactions >= 100, + ); + }); + it('should return projects, sort by donations, DESC', async () => { + await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + totalDonations: 100, + qualityScore: 0, + }); + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + sortingBy: SortingField.MostFunded, + }, + }); + assert.isTrue( + result.data.data.allProjects.projects[0].totalDonations >= 100, + ); + }); + it('should return projects, sort by qualityScore, DESC', async () => { + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + totalDonations: 100, + qualityScore: 10000, + }); + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + sortingBy: SortingField.QualityScore, + }, + }); + assert.isTrue( + Number(result.data.data.allProjects.projects[0].id) === project.id, + ); + + // default sort + const result2 = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + }); + assert.isTrue( + Number(result2.data.data.allProjects.projects[0].id) === project.id, + ); + }); + + // it('should return projects, sort by project raised funds in the active QF round DESC', async () => { + // const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + // const project1 = await saveProjectDirectlyToDb({ + // ...createProjectData(), + // title: String(new Date().getTime()), + // slug: String(new Date().getTime()), + // }); + // const project2 = await saveProjectDirectlyToDb({ + // ...createProjectData(), + // title: String(new Date().getTime()), + // slug: String(new Date().getTime()), + // }); + + // const qfRound = await QfRound.create({ + // isActive: true, + // name: 'test filter by qfRoundId', + // minimumPassportScore: 10, + // allocatedFund: 100, + // beginDate: new Date(), + // endDate: moment().add(1, 'day').toDate(), + // }).save(); + // project1.qfRounds = [qfRound]; + // await project1.save(); + // project2.qfRounds = [qfRound]; + // await project2.save(); + + // const donation1 = await saveDonationDirectlyToDb( + // { + // ...createDonationData(), + // status: 'verified', + // qfRoundId: qfRound.id, + // valueUsd: 2, + // }, + // donor.id, + // project1.id, + // ); + + // const donation2 = await saveDonationDirectlyToDb( + // { + // ...createDonationData(), + // status: 'verified', + // qfRoundId: qfRound.id, + // valueUsd: 20, + // }, + // donor.id, + // project2.id, + // ); + + // await refreshProjectEstimatedMatchingView(); + // await refreshProjectDonationSummaryView(); + + // const result = await axios.post(graphqlUrl, { + // query: fetchMultiFilterAllProjectsQuery, + // variables: { + // sortingBy: SortingField.ActiveQfRoundRaisedFunds, + // limit: 10, + // }, + // }); + + // assert.equal(result.data.data.allProjects.projects.length, 2); + // assert.equal(result.data.data.allProjects.projects[0].id, project2.id); + // result.data.data.allProjects.projects.forEach(project => { + // assert.equal(project.qfRounds[0].id, qfRound.id); + // }); + // qfRound.isActive = false; + // await qfRound.save(); + // }); + + it('should return projects, sort by project instant power DESC', async () => { + await PowerBoosting.clear(); + await InstantPowerBalance.clear(); + + const user1 = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + const user2 = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + const project1 = await saveProjectDirectlyToDb(createProjectData()); + const project2 = await saveProjectDirectlyToDb(createProjectData()); + const project3 = await saveProjectDirectlyToDb(createProjectData()); + const project4 = await saveProjectDirectlyToDb({ + ...createProjectData(), + verified: false, + }); // Not boosted -Not verified project + const project5 = await saveProjectDirectlyToDb(createProjectData()); // Not boosted project + + const roundNumber = project3.id * 10; + + await Promise.all( + [ + [user1, project1, 10], + [user1, project2, 20], + [user1, project3, 30], + [user1, project4, 40], + [user2, project1, 20], + [user2, project2, 40], + [user2, project3, 60], + ].map(item => { + const [user, project, percentage] = item as [User, Project, number]; + return insertSinglePowerBoosting({ + user, + project, + percentage, + }); + }), + ); + + await saveOrUpdateInstantPowerBalances([ + { + userId: user1.id, + balance: 10000, + balanceAggregatorUpdatedAt: new Date(1_000_000), + }, + { + userId: user2.id, + balance: 1000, + balanceAggregatorUpdatedAt: new Date(1_000_000), + }, + ]); + + await updateInstantBoosting(); + + let result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + sortingBy: SortingField.InstantBoosting, + limit: 50, + }, + }); + + let projects = result.data.data.allProjects.projects; + + assert.equal(projects[0].id, project3.id); + assert.equal(projects[1].id, project2.id); + assert.equal(projects[2].id, project1.id); + + assert.equal(projects[0].projectInstantPower.powerRank, 1); + assert.equal(projects[1].projectInstantPower.powerRank, 2); + assert.equal(projects[2].projectInstantPower.powerRank, 3); + assert.equal(projects[3].projectInstantPower.powerRank, 4); + + result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + sortingBy: SortingField.InstantBoosting, + }, + }); + + projects = result.data.data.allProjects.projects; + const totalCount = projects.length; + for (let i = 1; i < totalCount - 1; i++) { + assert.isTrue( + projects[i].projectInstantPower.totalPower <= + projects[i - 1].projectInstantPower.totalPower, + ); + assert.isTrue( + projects[i].projectInstantPower.powerRank >= + projects[i - 1].projectInstantPower.powerRank, + ); + + if (projects[i].verified === true) { + // verified project come first + assert.isTrue(projects[i - 1].verified); + } + } + }); + + it('should return projects, sort by project power DESC', async () => { + await AppDataSource.getDataSource().query( + 'truncate power_snapshot cascade', + ); + await PowerBoosting.clear(); + await PowerBalanceSnapshot.clear(); + await PowerBoostingSnapshot.clear(); + + const user1 = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + const user2 = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + const project1 = await saveProjectDirectlyToDb(createProjectData()); + const project2 = await saveProjectDirectlyToDb(createProjectData()); + const project3 = await saveProjectDirectlyToDb(createProjectData()); + const project4 = await saveProjectDirectlyToDb({ + ...createProjectData(), + verified: false, + }); // Not boosted -Not verified project + const project5 = await saveProjectDirectlyToDb(createProjectData()); // Not boosted project + + const roundNumber = project3.id * 10; + + await Promise.all( + [ + [user1, project1, 10], + [user1, project2, 20], + [user1, project3, 30], + [user2, project1, 20], + [user2, project2, 40], + [user2, project3, 60], + ].map(item => { + const [user, project, percentage] = item as [User, Project, number]; + return insertSinglePowerBoosting({ + user, + project, + percentage, + }); + }), + ); + + await takePowerBoostingSnapshot(); + const [powerSnapshots] = await findPowerSnapshots(); + const snapshot = powerSnapshots[0]; + + snapshot.blockNumber = 1; + snapshot.roundNumber = roundNumber; + await snapshot.save(); + + await addOrUpdatePowerSnapshotBalances([ + { + userId: user1.id, + powerSnapshotId: snapshot.id, + balance: 10000, + }, + { + userId: user2.id, + powerSnapshotId: snapshot.id, + balance: 20000, + }, + ]); + + await setPowerRound(roundNumber); + await refreshProjectPowerView(); + + let result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + sortingBy: SortingField.GIVPower, + limit: 50, + }, + }); + + let projects = result.data.data.allProjects.projects; + + assert.equal(projects[0].id, project3.id); + assert.equal(projects[1].id, project2.id); + assert.equal(projects[2].id, project1.id); + + assert.equal(projects[0].projectPower.powerRank, 1); + assert.equal(projects[1].projectPower.powerRank, 2); + assert.equal(projects[2].projectPower.powerRank, 3); + assert.equal(projects[3].projectPower.powerRank, 4); + + result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + sortingBy: SortingField.GIVPower, + }, + }); + + projects = result.data.data.allProjects.projects; + const totalCount = projects.length; + for (let i = 1; i < totalCount - 1; i++) { + assert.isTrue( + projects[i].projectPower.totalPower <= + projects[i - 1].projectPower.totalPower, + ); + assert.isTrue( + projects[i].projectPower.powerRank >= + projects[i - 1].projectPower.powerRank, + ); + + if (projects[i].verified === true) { + // verified project come first + assert.isTrue(projects[i - 1].verified); + } + } + }); + + it('should return projects, filtered by sub category', async () => { + await saveProjectDirectlyToDb({ + ...createProjectData(), + categories: ['food5'], + }); + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + category: 'food5', + }, + }); + assert.isNotEmpty(result.data.data.allProjects.projects); + result.data.data.allProjects.projects.forEach(project => { + assert.isOk( + project.categories.find(category => category.name === 'food5'), + ); + }); + }); + it('should return projects, filtered by main category', async () => { + await saveProjectDirectlyToDb({ + ...createProjectData(), + categories: ['drink2'], + }); + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + mainCategory: 'drink', + }, + }); + assert.isNotEmpty(result.data.data.allProjects.projects); + result.data.data.allProjects.projects.forEach(project => { + assert.isOk( + project.categories.find( + category => category.mainCategory.title === 'drink', + ), + ); + }); + }); + it('should return projects, filtered by main category and sub category at the same time', async () => { + await saveProjectDirectlyToDb({ + ...createProjectData(), + categories: ['drink2'], + }); + await saveProjectDirectlyToDb({ + ...createProjectData(), + categories: ['drink3'], + }); + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + mainCategory: 'drink', + category: 'drink3', + }, + }); + assert.isNotEmpty(result.data.data.allProjects.projects); + result.data.data.allProjects.projects.forEach(project => { + assert.isOk( + project.categories.find( + category => category.mainCategory.title === 'drink', + ), + ); + + // Should not return projects with drink2 category + assert.isOk( + project.categories.find(category => category.name === 'drink3'), + ); + }); + }); + + it('should return projects, filter by accept donation on gnosis, not return when it doesnt have gnosis address', async () => { + const savedProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + }); + const gnosisAddress = (await findProjectRecipientAddressByNetworkId({ + projectId: savedProject.id, + networkId: NETWORK_IDS.XDAI, + })) as ProjectAddress; + gnosisAddress.isRecipient = false; + await gnosisAddress.save(); + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + filters: ['AcceptFundOnGnosis'], + sortingBy: SortingField.Newest, + }, + }); + result.data.data.allProjects.projects.forEach(project => { + assert.isOk( + project.addresses.find( + address => + address.isRecipient === true && + address.networkId === NETWORK_IDS.XDAI && + address.chainType === ChainType.EVM, + ), + ); + }); + assert.isNotOk( + result.data.data.allProjects.projects.find( + project => Number(project.id) === Number(savedProject.id), + ), + ); + }); + + it('should return projects, filter by accept donation on celo', async () => { + const savedProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + networkId: NETWORK_IDS.CELO, + }); + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + filters: ['AcceptFundOnCelo'], + sortingBy: SortingField.Newest, + }, + }); + result.data.data.allProjects.projects.forEach(project => { + assert.isOk( + project.addresses.find( + address => + address.isRecipient === true && + (address.networkId === NETWORK_IDS.CELO || + address.networkId === NETWORK_IDS.CELO_ALFAJORES), + ), + ); + }); + assert.isOk( + result.data.data.allProjects.projects.find( + project => Number(project.id) === Number(savedProject.id), + ), + ); + }); + it('should return projects, filter by accept donation on celo', async () => { + const savedProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + networkId: NETWORK_IDS.CELO, + }); + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + filters: ['AcceptFundOnCelo'], + sortingBy: SortingField.Newest, + }, + }); + result.data.data.allProjects.projects.forEach(project => { + assert.isOk( + project.addresses.find( + address => + address.isRecipient === true && + (address.networkId === NETWORK_IDS.CELO || + address.networkId === NETWORK_IDS.CELO_ALFAJORES) && + address.chainType === ChainType.EVM, + ), + ); + }); + assert.isOk( + result.data.data.allProjects.projects.find( + project => Number(project.id) === Number(savedProject.id), + ), + ); + }); + + it('should return projects, filter by accept donation on celo, not return when it doesnt have celo address', async () => { + const celoProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + networkId: NETWORK_IDS.CELO, + }); + const polygonProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + networkId: NETWORK_IDS.POLYGON, + }); + + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + filters: ['AcceptFundOnCelo'], + sortingBy: SortingField.Newest, + }, + }); + + result.data.data.allProjects.projects.forEach(project => { + assert.isOk( + project.addresses.find( + address => + address.isRecipient === true && + (address.networkId === NETWORK_IDS.CELO || + address.networkId === NETWORK_IDS.CELO_ALFAJORES) && + address.chainType === ChainType.EVM, + ), + ); + }); + assert.isNotOk( + result.data.data.allProjects.projects.find( + project => Number(project.id) === Number(polygonProject.id), + ), + ); + assert.isOk( + result.data.data.allProjects.projects.find( + project => Number(project.id) === Number(celoProject.id), + ), + ); + }); + + it('should return projects, filter by accept donation on arbitrum', async () => { + const savedProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + networkId: NETWORK_IDS.ARBITRUM_MAINNET, + }); + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + filters: ['AcceptFundOnArbitrum'], + sortingBy: SortingField.Newest, + }, + }); + result.data.data.allProjects.projects.forEach(project => { + assert.isOk( + project.addresses.find( + address => + address.isRecipient === true && + (address.networkId === NETWORK_IDS.ARBITRUM_MAINNET || + address.networkId === NETWORK_IDS.ARBITRUM_SEPOLIA), + ), + ); + }); + assert.isOk( + result.data.data.allProjects.projects.find( + project => Number(project.id) === Number(savedProject.id), + ), + ); + }); + it('should return projects, filter by accept donation on arbitrum', async () => { + const savedProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + networkId: NETWORK_IDS.ARBITRUM_MAINNET, + }); + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + filters: ['AcceptFundOnArbitrum'], + sortingBy: SortingField.Newest, + }, + }); + result.data.data.allProjects.projects.forEach(project => { + assert.isOk( + project.addresses.find( + address => + address.isRecipient === true && + (address.networkId === NETWORK_IDS.ARBITRUM_MAINNET || + address.networkId === NETWORK_IDS.ARBITRUM_SEPOLIA) && + address.chainType === ChainType.EVM, + ), + ); + }); + assert.isOk( + result.data.data.allProjects.projects.find( + project => Number(project.id) === Number(savedProject.id), + ), + ); + }); + + it('should return projects, filter by accept donation on arbitrum, not return when it doesnt have arbitrum address', async () => { + const arbitrumProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + networkId: NETWORK_IDS.ARBITRUM_MAINNET, + }); + const polygonProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + networkId: NETWORK_IDS.POLYGON, + }); + + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + filters: ['AcceptFundOnArbitrum'], + sortingBy: SortingField.Newest, + }, + }); + + result.data.data.allProjects.projects.forEach(project => { + assert.isOk( + project.addresses.find( + address => + address.isRecipient === true && + (address.networkId === NETWORK_IDS.ARBITRUM_MAINNET || + address.networkId === NETWORK_IDS.ARBITRUM_SEPOLIA) && + address.chainType === ChainType.EVM, + ), + ); + }); + assert.isNotOk( + result.data.data.allProjects.projects.find( + project => Number(project.id) === Number(polygonProject.id), + ), + ); + assert.isOk( + result.data.data.allProjects.projects.find( + project => Number(project.id) === Number(arbitrumProject.id), + ), + ); + }); + + it('should return projects, filter by accept donation on mainnet', async () => { + const savedProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + networkId: NETWORK_IDS.MAIN_NET, + }); + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + filters: ['AcceptFundOnMainnet'], + sortingBy: SortingField.Newest, + }, + }); + result.data.data.allProjects.projects.forEach(project => { + assert.isOk( + project.addresses.find( + address => + address.isRecipient === true && + (address.networkId === NETWORK_IDS.MAIN_NET || + address.networkId === NETWORK_IDS.GOERLI) && + address.chainType === ChainType.EVM, + ), + ); + }); + assert.isOk( + result.data.data.allProjects.projects.find( + project => Number(project.id) === Number(savedProject.id), + ), + ); + }); + + it('should return projects, filter by accept donation on mainnet, not return when it doesnt have mainnet address', async () => { + const polygonProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + networkId: NETWORK_IDS.POLYGON, + }); + + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + filters: ['AcceptFundOnMainnet'], + sortingBy: SortingField.Newest, + }, + }); + result.data.data.allProjects.projects.forEach(project => { + assert.isOk( + project.addresses.find( + address => + address.isRecipient === true && + address.networkId === NETWORK_IDS.MAIN_NET && + address.chainType === ChainType.EVM, + ), + ); + }); + assert.isNotOk( + result.data.data.allProjects.projects.find( + project => Number(project.id) === Number(polygonProject.id), + ), + ); + }); + + it('should return projects, filter by accept donation on polygon', async () => { + const savedProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + }); + const polygonAddress = (await findProjectRecipientAddressByNetworkId({ + projectId: savedProject.id, + networkId: NETWORK_IDS.POLYGON, + })) as ProjectAddress; + polygonAddress.isRecipient = true; + await polygonAddress.save(); + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + filters: ['AcceptFundOnPolygon'], + sortingBy: SortingField.Newest, + }, + }); + result.data.data.allProjects.projects.forEach(project => { + assert.isOk( + project.addresses.find( + address => + address.isRecipient === true && + address.networkId === NETWORK_IDS.POLYGON && + address.chainType === ChainType.EVM, + ), + ); + }); + assert.isOk( + result.data.data.allProjects.projects.find( + project => Number(project.id) === Number(savedProject.id), + ), + ); + }); + + it('should return projects, filter by accept donation on polygon, not return when it doesnt have polygon address', async () => { + const savedProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + networkId: NETWORK_IDS.OPTIMISTIC, + }); + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + filters: ['AcceptFundOnPolygon'], + sortingBy: SortingField.Newest, + }, + }); + result.data.data.allProjects.projects.forEach(project => { + assert.isOk( + project.addresses.find( + address => + address.isRecipient === true && + address.networkId === NETWORK_IDS.POLYGON && + address.chainType === ChainType.EVM, + ), + ); + }); + assert.isNotOk( + result.data.data.allProjects.projects.find( + project => Number(project.id) === Number(savedProject.id), + ), + ); + }); + + it('should return projects, filter by accept donation on GOERLI', async () => { + const savedProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + networkId: NETWORK_IDS.GOERLI, + }); + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + filters: ['AcceptFundOnMainnet'], + sortingBy: SortingField.Newest, + limit: 50, + }, + }); + result.data.data.allProjects.projects.forEach(project => { + assert.isOk( + project.addresses.find( + address => + (address.isRecipient === true && + address.networkId === NETWORK_IDS.MAIN_NET) || + address.networkId === NETWORK_IDS.GOERLI, + ), + ); + }); + assert.isOk( + result.data.data.allProjects.projects.find( + project => Number(project.id) === Number(savedProject.id), + ), + ); + }); + + it('should return projects, filter by accept donation on ALFAJORES', async () => { + const alfajoresProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + networkId: NETWORK_IDS.CELO_ALFAJORES, + }); + + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + filters: ['AcceptFundOnCelo'], + sortingBy: SortingField.Newest, + }, + }); + result.data.data.allProjects.projects.forEach(project => { + assert.isOk( + project.addresses.find( + address => + (address.isRecipient === true && + // We return both Celo and Alfajores when sending AcceptFundOnCelo filter + address.networkId === NETWORK_IDS.CELO_ALFAJORES) || + address.networkId === NETWORK_IDS.CELO, + ), + ); + }); + assert.isOk( + result.data.data.allProjects.projects.find( + project => Number(project.id) === Number(alfajoresProject.id), + ), + ); + }); + + it('should return projects, filter by accept donation on Arbitrum Sepolia', async () => { + const arbSepoliaProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + networkId: NETWORK_IDS.ARBITRUM_SEPOLIA, + }); + + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + filters: ['AcceptFundOnArbitrum'], + sortingBy: SortingField.Newest, + }, + }); + result.data.data.allProjects.projects.forEach(project => { + assert.isOk( + project.addresses.find( + address => + (address.isRecipient === true && + // We return both Arbitrum Mainnet and Arbitrum Sepolia when sending AcceptFundOnArbitrum filter + address.networkId === NETWORK_IDS.ARBITRUM_SEPOLIA) || + address.networkId === NETWORK_IDS.ARBITRUM_MAINNET, + ), + ); + }); + assert.isOk( + result.data.data.allProjects.projects.find( + project => Number(project.id) === Number(arbSepoliaProject.id), + ), + ); + }); + + it('should return projects, filter by accept donation on optimism', async () => { + const savedProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + networkId: NETWORK_IDS.OPTIMISTIC, + }); + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + filters: ['AcceptFundOnOptimism'], + sortingBy: SortingField.Newest, + }, + }); + result.data.data.allProjects.projects.forEach(project => { + assert.isOk( + project.addresses.find( + address => + address.isRecipient === true && + address.networkId === NETWORK_IDS.OPTIMISTIC && + address.chainType === ChainType.EVM, + ), + ); + }); + assert.isOk( + result.data.data.allProjects.projects.find( + project => Number(project.id) === Number(savedProject.id), + ), + ); + }); + it('should return projects, filter by accept donation on optimism, not return when it doesnt have optimism address', async () => { + const gnosisProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + networkId: NETWORK_IDS.XDAI, + }); + + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + filters: ['AcceptFundOnOptimism'], + sortingBy: SortingField.Newest, + }, + }); + result.data.data.allProjects.projects.forEach(project => { + assert.isOk( + project.addresses.find( + address => + address.isRecipient === true && + (address.networkId === NETWORK_IDS.OPTIMISTIC || + address.networkId === NETWORK_IDS.OPTIMISM_GOERLI) && + address.chainType === ChainType.EVM, + ), + ); + }); + assert.isNotOk( + result.data.data.allProjects.projects.find( + project => Number(project.id) === Number(gnosisProject.id), + ), + ); + }); + it('should return projects, filter by accept donation on gnosis, return all addresses', async () => { + await redis.flushall(); // clear cache from other tests + const savedProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + }); + + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + filters: ['AcceptFundOnGnosis'], + sortingBy: SortingField.Newest, + limit: 50, + }, + }); + result.data.data.allProjects.projects.forEach(item => { + assert.isOk( + item.addresses.find( + address => + address.isRecipient === true && + address.networkId === NETWORK_IDS.XDAI && + address.chainType === ChainType.EVM, + ), + ); + }); + const project = result.data.data.allProjects.projects.find( + item => Number(item.id) === Number(savedProject.id), + ); + + assert.isOk(project); + assert.isOk( + project.addresses.find( + address => + address.isRecipient === true && + address.networkId === NETWORK_IDS.XDAI && + address.chainType === ChainType.EVM, + ), + ); + assert.isOk( + project.addresses.find( + address => + address.isRecipient === true && + address.networkId === NETWORK_IDS.MAIN_NET && + address.chainType === ChainType.EVM, + ), + ); + }); + it('should return projects, filter by accept donation on gnosis, should not return if it has no address', async () => { + const savedProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + }); + await ProjectAddress.query(` + DELETE + from project_address + WHERE "projectId" = ${savedProject.id} + `); + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + filters: ['AcceptFundOnGnosis'], + sortingBy: SortingField.Newest, + }, + }); + result.data.data.allProjects.projects.forEach(project => { + assert.isOk( + project.addresses.find( + address => + address.isRecipient === true && + address.networkId === NETWORK_IDS.XDAI && + address.chainType === ChainType.EVM, + ), + ); + }); + assert.isNotOk( + result.data.data.allProjects.projects.find( + project => Number(project.id) === Number(savedProject.id), + ), + ); + }); + it('should return projects, filter by accept donation on Solana', async () => { + const savedProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + }); + await ProjectAddress.delete({ projectId: savedProject.id }); + const solanaAddress = ProjectAddress.create({ + project: savedProject, + title: 'first address', + address: generateRandomSolanaAddress(), + chainType: ChainType.SOLANA, + networkId: 0, + isRecipient: true, + }); + await solanaAddress.save(); + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + filters: ['AcceptFundOnSolana'], + sortingBy: SortingField.Newest, + }, + }); + result.data.data.allProjects.projects.forEach(project => { + assert.isOk( + project.addresses.find( + address => + address.isRecipient === true && + address.chainType === ChainType.SOLANA, + ), + ); + }); + assert.isOk( + result.data.data.allProjects.projects.find( + project => Number(project.id) === Number(savedProject.id), + ), + ); + }); + it('should return projects, filter by accept fund on two Ethereum networks', async () => { + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + }); + + const mainnetAddress = ProjectAddress.create({ + project, + title: 'first address', + address: generateRandomEtheriumAddress(), + networkId: 1, + isRecipient: true, + }); + await mainnetAddress.save(); + + const solanaAddress = ProjectAddress.create({ + project, + title: 'secnod address', + address: generateRandomSolanaAddress(), + chainType: ChainType.SOLANA, + networkId: 0, + isRecipient: true, + }); + await solanaAddress.save(); + + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + filters: ['AcceptFundOnMainnet', 'AcceptFundOnSolana'], + sortingBy: SortingField.Newest, + }, + }); + const { projects } = result.data.data.allProjects; + + const projectIds = projects.map(_project => _project.id); + assert.include(projectIds, String(project.id)); + }); + it('should return projects, when only accpets donation on Solana or an expected Ethereum network', async () => { + const projectWithMainnet = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + }); + const projectWithSolana = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + }); + + const mainnetAddress = ProjectAddress.create({ + project: projectWithMainnet, + title: 'first address', + address: generateRandomEtheriumAddress(), + networkId: 1, + isRecipient: true, + }); + await mainnetAddress.save(); + + const solanaAddress = ProjectAddress.create({ + project: projectWithSolana, + title: 'secnod address', + address: generateRandomSolanaAddress(), + chainType: ChainType.SOLANA, + networkId: 0, + isRecipient: true, + }); + await solanaAddress.save(); + + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + filters: ['AcceptFundOnMainnet', 'AcceptFundOnSolana'], + sortingBy: SortingField.Newest, + }, + }); + const { projects } = result.data.data.allProjects; + const projectIds = projects.map(project => project.id); + assert.include(projectIds, String(projectWithMainnet.id)); + assert.include(projectIds, String(projectWithSolana.id)); + }); + it('should not return a project when it does not accept donation on Solana', async () => { + // Delete all project addresses + await ProjectAddress.delete({ chainType: ChainType.SOLANA }); + + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + }); + + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + filters: ['AcceptFundOnSolana'], + sortingBy: SortingField.Newest, + }, + }); + const { projects } = result.data.data.allProjects; + assert.lengthOf(projects, 0); + }); + it('should return projects, filter by campaignSlug and limit, skip', async () => { + const project1 = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + }); + const project2 = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + }); + const project3 = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + }); + + const campaign = await Campaign.create({ + isActive: true, + type: CampaignType.ManuallySelected, + slug: generateRandomString(), + title: 'title1', + description: 'description1', + photo: 'https://google.com', + relatedProjectsSlugs: [ + project1.slug as string, + project2.slug as string, + project3.slug as string, + ], + order: 1, + }).save(); + + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + limit: 1, + skip: 1, + campaignSlug: campaign.slug, + }, + }); + + assert.equal(result.data.data.allProjects.projects.length, 1); + assert.equal(result.data.data.allProjects.campaign.title, campaign.title); + assert.isOk( + [project1.slug, project2.slug, project3.slug].includes( + result.data.data.allProjects.projects[0].slug, + ), + ); + + await campaign.remove(); + }); + it('should return projects, filter by qfRoundId', async () => { + const project1 = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + }); + const project2 = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + }); + const project3 = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + }); + + const qfRound = await QfRound.create({ + isActive: true, + name: 'test filter by qfRoundId', + slug: new Date().getTime().toString(), + minimumPassportScore: 10, + allocatedFund: 100, + beginDate: new Date(), + endDate: new Date(), + }).save(); + project1.qfRounds = [qfRound]; + await project1.save(); + project2.qfRounds = [qfRound]; + await project2.save(); + project3.qfRounds = [qfRound]; + await project3.save(); + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + qfRoundId: qfRound.id, + }, + }); + + assert.equal(result.data.data.allProjects.projects.length, 3); + result.data.data.allProjects.projects.forEach(project => { + assert.equal(project.qfRounds[0].id, qfRound.id); + }); + qfRound.isActive = false; + await qfRound.save(); + }); + it('should return projects, filter by qfRoundSlug', async () => { + const project1 = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + }); + const project2 = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + }); + const project3 = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + }); + + const qfRound = await QfRound.create({ + isActive: true, + name: 'test filter by qfRoundId', + slug: new Date().getTime().toString(), + minimumPassportScore: 10, + allocatedFund: 100, + beginDate: new Date(), + endDate: new Date(), + }).save(); + project1.qfRounds = [qfRound]; + await project1.save(); + project2.qfRounds = [qfRound]; + await project2.save(); + project3.qfRounds = [qfRound]; + await project3.save(); + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + qfRoundSlug: qfRound.slug, + }, + }); + + assert.equal(result.data.data.allProjects.projects.length, 3); + result.data.data.allProjects.projects.forEach(project => { + assert.equal(project.qfRounds[0].id, qfRound.id); + }); + qfRound.isActive = false; + await qfRound.save(); + }); + it('should just return verified projects, filter by qfRoundId and verified', async () => { + const project1 = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + }); + const project2 = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + }); + const project3 = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + verified: false, + }); + + const qfRound = await QfRound.create({ + isActive: true, + name: 'test filter by qfRoundId', + slug: new Date().getTime().toString(), + minimumPassportScore: 10, + allocatedFund: 100, + beginDate: new Date(), + endDate: new Date(), + }).save(); + project1.qfRounds = [qfRound]; + await project1.save(); + project2.qfRounds = [qfRound]; + await project2.save(); + project3.qfRounds = [qfRound]; + await project3.save(); + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + qfRoundId: qfRound.id, + filters: ['Verified'], + }, + }); + + assert.equal(result.data.data.allProjects.projects.length, 2); + result.data.data.allProjects.projects.forEach(project => { + assert.equal(project.qfRounds[0].id, qfRound.id); + }); + qfRound.isActive = false; + await qfRound.save(); + }); + it('should return projects, filter by qfRoundId, calculate estimated matching', async () => { + const project1 = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + }); + const project2 = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + }); + + const qfRound = await QfRound.create({ + isActive: true, + name: new Date().toString(), + slug: new Date().getTime().toString(), + minimumPassportScore: 8, + allocatedFund: 1000, + beginDate: new Date(), + endDate: moment().add(10, 'days').toDate(), + }).save(); + project1.qfRounds = [qfRound]; + await project1.save(); + project2.qfRounds = [qfRound]; + await project2.save(); + + const donor1 = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + donor1.passportScore = 13; + await donor1.save(); + + const donor2 = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + donor2.passportScore = 13; + await donor2.save(); + + // We should have result similar to https://wtfisqf.com/?grant=2,2&grant=4&grant=&grant=&match=1000 + await saveDonationDirectlyToDb( + { + ...createDonationData(), + status: 'verified', + qfRoundId: qfRound.id, + valueUsd: 2, + }, + donor1.id, + project1.id, + ); + + await saveDonationDirectlyToDb( + { + ...createDonationData(), + status: 'verified', + qfRoundId: qfRound.id, + valueUsd: 2, + }, + donor2.id, + project1.id, + ); + + await saveDonationDirectlyToDb( + { + ...createDonationData(), + status: 'verified', + qfRoundId: qfRound.id, + valueUsd: 4, + }, + donor1.id, + project2.id, + ); + + await refreshProjectEstimatedMatchingView(); + await refreshProjectDonationSummaryView(); + + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + qfRoundId: qfRound.id, + }, + }); + + assert.equal(result.data.data.allProjects.projects.length, 2); + const firstProject = result.data.data.allProjects.projects.find( + p => Number(p.id) === project1.id, + ); + const secondProject = result.data.data.allProjects.projects.find( + p => Number(p.id) === project2.id, + ); + + const project1EstimatedMatching = + await calculateEstimatedMatchingWithParams({ + matchingPool: firstProject.estimatedMatching.matchingPool, + projectDonationsSqrtRootSum: + firstProject.estimatedMatching.projectDonationsSqrtRootSum, + allProjectsSum: firstProject.estimatedMatching.allProjectsSum, + }); + + const project2EstimatedMatching = + await calculateEstimatedMatchingWithParams({ + matchingPool: secondProject.estimatedMatching.matchingPool, + projectDonationsSqrtRootSum: + secondProject.estimatedMatching.projectDonationsSqrtRootSum, + allProjectsSum: secondProject.estimatedMatching.allProjectsSum, + }); + + assert.equal(Math.floor(project1EstimatedMatching), 666); + assert.equal(Math.floor(project2EstimatedMatching), 333); + qfRound.isActive = false; + await qfRound.save(); + }); + + it('should return projects, filter by ActiveQfRound', async () => { + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + verified: true, + listed: true, + }); + + const qfRound = await QfRound.create({ + isActive: true, + name: 'test', + allocatedFund: 100, + slug: new Date().getTime().toString(), + minimumPassportScore: 10, + beginDate: new Date(), + endDate: new Date(), + }).save(); + project.qfRounds = [qfRound]; + await project.save(); + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + limit: 50, + skip: 0, + filters: ['ActiveQfRound'], + }, + }); + assert.equal(result.data.data.allProjects.projects.length, 1); + qfRound.isActive = false; + await qfRound.save(); + }); + + it('should return projects, filter by ActiveQfRound, and not return non verified projects', async () => { + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + verified: false, + listed: true, + }); + + const qfRound = await QfRound.create({ + isActive: true, + name: 'test', + allocatedFund: 100, + slug: new Date().getTime().toString(), + minimumPassportScore: 10, + beginDate: new Date(), + endDate: new Date(), + }).save(); + project.qfRounds = [qfRound]; + await project.save(); + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + limit: 50, + skip: 0, + filters: ['ActiveQfRound', 'Verified'], + }, + }); + assert.equal(result.data.data.allProjects.projects.length, 0); + qfRound.isActive = false; + await qfRound.save(); + }); + it('should return projects, filter by ActiveQfRound, and not return non listed projects', async () => { + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + verified: true, + listed: false, + reviewStatus: ReviewStatus.NotListed, + }); + + const qfRound = await QfRound.create({ + isActive: true, + name: 'test', + allocatedFund: 100, + slug: new Date().getTime().toString(), + minimumPassportScore: 10, + beginDate: new Date(), + endDate: new Date(), + }).save(); + project.qfRounds = [qfRound]; + await project.save(); + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + limit: 50, + skip: 0, + filters: ['ActiveQfRound', 'Verified'], + }, + }); + assert.equal(result.data.data.allProjects.projects.length, 0); + qfRound.isActive = false; + await qfRound.save(); + }); + + it('should return empty list when qfRound is not active, filter by ActiveQfRound', async () => { + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + verified: true, + listed: true, + }); + + const qfRound = await QfRound.create({ + isActive: false, + name: 'test2', + allocatedFund: 100, + slug: new Date().getTime().toString(), + minimumPassportScore: 10, + beginDate: new Date(), + endDate: new Date(), + }).save(); + project.qfRounds = [qfRound]; + await project.save(); + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + limit: 50, + skip: 0, + filters: ['ActiveQfRound'], + }, + }); + + assert.equal(result.data.data.allProjects.projects.length, 0); + qfRound.isActive = false; + await qfRound.save(); + }); +} diff --git a/src/resolvers/projectResolver.test.ts b/src/resolvers/projectResolver.test.ts index 8d9e005e8..a584caf84 100644 --- a/src/resolvers/projectResolver.test.ts +++ b/src/resolvers/projectResolver.test.ts @@ -1,15 +1,12 @@ import { assert, expect } from 'chai'; import 'mocha'; import { - createDonationData, createProjectData, generateRandomEtheriumAddress, generateRandomSolanaAddress, generateTestAccessToken, graphqlUrl, PROJECT_UPDATE_SEED_DATA, - REACTION_SEED_DATA, - saveDonationDirectlyToDb, saveFeaturedProjectDirectlyToDb, saveProjectDirectlyToDb, saveUserDirectlyToDb, @@ -28,7 +25,6 @@ import { fetchFeaturedProjectUpdate, fetchLatestProjectUpdates, fetchLikedProjectsQuery, - fetchMultiFilterAllProjectsQuery, fetchNewProjectsPerDate, fetchProjectBySlugQuery, fetchProjectUpdatesQuery, @@ -52,7 +48,6 @@ import { ProjectUpdate, ProjStatus, ReviewStatus, - SortingField, } from '../entities/project'; import { Category } from '../entities/category'; import { Reaction } from '../entities/reaction'; @@ -64,7 +59,6 @@ import { NETWORK_IDS } from '../provider'; import { addNewProjectAddress, findAllRelatedAddressByWalletAddress, - findProjectRecipientAddressByNetworkId, removeRecipientAddressOfProject, } from '../repositories/projectAddressRepository'; import { @@ -90,14 +84,13 @@ import { PowerBoosting } from '../entities/powerBoosting'; import { refreshUserProjectPowerView } from '../repositories/userProjectPowerViewRepository'; import { AppDataSource } from '../orm'; // We are using cache so redis needs to be cleared for tests with same filters -import { redis } from '../redis'; import { Campaign, CampaignFilterField, CampaignSortingField, CampaignType, } from '../entities/campaign'; -import { generateRandomString, getHtmlTextSummary } from '../utils/utils'; +import { generateRandomString } from '../utils/utils'; import { FeaturedUpdate } from '../entities/featuredUpdate'; import { PROJECT_DESCRIPTION_MAX_LENGTH, @@ -107,12 +100,6 @@ import { ArgumentValidationError } from 'type-graphql'; import { InstantPowerBalance } from '../entities/instantPowerBalance'; import { saveOrUpdateInstantPowerBalances } from '../repositories/instantBoostingRepository'; import { updateInstantBoosting } from '../services/instantBoostingServices'; -import { QfRound } from '../entities/qfRound'; -import { calculateEstimatedMatchingWithParams } from '../utils/qfUtils'; -import { - refreshProjectDonationSummaryView, - refreshProjectEstimatedMatchingView, -} from '../services/projectViewsService'; import { addOrUpdatePowerSnapshotBalances } from '../repositories/powerBalanceSnapshotRepository'; import { findPowerSnapshots } from '../repositories/powerSnapshotRepository'; import { cacheProjectCampaigns } from '../services/campaignService'; @@ -129,9 +116,6 @@ describe( addRecipientAddressToProjectTestCases, ); -// search and filters -describe('all projects test cases --->', allProjectsTestCases); - describe('projectsByUserId test cases --->', projectsByUserIdTestCases); describe('deactivateProject test cases --->', deactivateProjectTestCases); @@ -189,1901 +173,178 @@ describe('projectsPerDate() test cases --->', projectsPerDateTestCases); function projectsPerDateTestCases() { it('should projects created in a time range', async () => { - const project = await saveProjectDirectlyToDb({ - ...createProjectData(), - creationDate: moment().add(10, 'days').toDate(), - }); - const project2 = await saveProjectDirectlyToDb({ - ...createProjectData(), - creationDate: moment().add(44, 'days').toDate(), - }); - const projectsResponse = await axios.post(graphqlUrl, { - query: fetchNewProjectsPerDate, - variables: { - fromDate: moment().add(9, 'days').toDate().toISOString().split('T')[0], - toDate: moment().add(45, 'days').toDate().toISOString().split('T')[0], - }, - }); - - assert.isOk(projectsResponse); - assert.equal(projectsResponse.data.data.projectsPerDate.total, 2); - const total = - projectsResponse.data.data.projectsPerDate.totalPerMonthAndYear.reduce( - (sum, value) => sum + value.total, - 0, - ); - assert.equal(projectsResponse.data.data.projectsPerDate.total, total); - }); -} - -function getProjectsAcceptTokensTestCases() { - it('should return all tokens for giveth projects', async () => { - const project = await saveProjectDirectlyToDb(createProjectData()); - const allTokens = await Token.find({}); - const result = await axios.post(graphqlUrl, { - query: getProjectsAcceptTokensQuery, - variables: { - projectId: project.id, - }, - }); - assert.isOk(result.data.data.getProjectAcceptTokens); - assert.equal( - result.data.data.getProjectAcceptTokens.length, - allTokens.length, - ); - }); - it('should return all tokens for trace projects', async () => { - const project = await saveProjectDirectlyToDb({ - ...createProjectData(), - organizationLabel: ORGANIZATION_LABELS.TRACE, - }); - const traceOrganization = (await Organization.findOne({ - where: { - label: ORGANIZATION_LABELS.TRACE, - }, - })) as Organization; - - const allTokens = ( - await Token.query(` - SELECT COUNT(*) as "tokenCount" - FROM organization_tokens_token - WHERE "organizationId" = ${traceOrganization.id} - `) - )[0]; - const result = await axios.post(graphqlUrl, { - query: getProjectsAcceptTokensQuery, - variables: { - projectId: project.id, - }, - }); - assert.isOk(result.data.data.getProjectAcceptTokens); - assert.equal( - result.data.data.getProjectAcceptTokens.length, - Number(allTokens.tokenCount), - ); - }); - it('should return just Gnosis tokens when project just have Gnosis recipient address', async () => { - const project = await saveProjectDirectlyToDb({ - ...createProjectData(), - organizationLabel: ORGANIZATION_LABELS.TRACE, - networkId: NETWORK_IDS.XDAI, - }); - - const result = await axios.post(graphqlUrl, { - query: getProjectsAcceptTokensQuery, - variables: { - projectId: project.id, - }, - }); - assert.isNotEmpty(result.data.data.getProjectAcceptTokens); - result.data.data.getProjectAcceptTokens.forEach(token => { - assert.equal(token.networkId, NETWORK_IDS.XDAI); - }); - }); - it('should return just Ropsten tokens when project just have Ropsten recipient address', async () => { - const project = await saveProjectDirectlyToDb({ - ...createProjectData(), - organizationLabel: ORGANIZATION_LABELS.TRACE, - networkId: NETWORK_IDS.ROPSTEN, - }); - - const result = await axios.post(graphqlUrl, { - query: getProjectsAcceptTokensQuery, - variables: { - projectId: project.id, - }, - }); - assert.isNotEmpty(result.data.data.getProjectAcceptTokens); - result.data.data.getProjectAcceptTokens.forEach(token => { - assert.equal(token.networkId, NETWORK_IDS.ROPSTEN); - }); - }); - it('should return just Solana and Ropsten tokens when project just have Solana and Ropsten recipient address', async () => { - const project = await saveProjectDirectlyToDb({ - ...createProjectData(), - organizationLabel: ORGANIZATION_LABELS.TRACE, - networkId: NETWORK_IDS.ROPSTEN, - }); - - await addNewProjectAddress({ - project, - user: project.adminUser, - isRecipient: true, - networkId: NETWORK_IDS.SOLANA_MAINNET, - address: generateRandomSolanaAddress(), - chainType: ChainType.SOLANA, - }); - - const result = await axios.post(graphqlUrl, { - query: getProjectsAcceptTokensQuery, - variables: { - projectId: project.id, - }, - }); - assert.isNotEmpty(result.data.data.getProjectAcceptTokens); - result.data.data.getProjectAcceptTokens.forEach(token => { - expect(token.networkId).to.satisfy( - networkId => - networkId === NETWORK_IDS.SOLANA_MAINNET || - networkId === NETWORK_IDS.ROPSTEN, - ); - }); - }); - it('should no tokens when there is not any recipient address', async () => { - const project = await saveProjectDirectlyToDb({ - ...createProjectData(), - organizationLabel: ORGANIZATION_LABELS.TRACE, - networkId: NETWORK_IDS.ROPSTEN, - }); - await removeRecipientAddressOfProject({ project }); - - const result = await axios.post(graphqlUrl, { - query: getProjectsAcceptTokensQuery, - variables: { - projectId: project.id, - }, - }); - assert.isEmpty(result.data.data.getProjectAcceptTokens); - }); - - it('should just return ETH token for givingBlock projects', async () => { - const project = await saveProjectDirectlyToDb({ - ...createProjectData(), - organizationLabel: ORGANIZATION_LABELS.GIVING_BLOCK, - }); - const result = await axios.post(graphqlUrl, { - query: getProjectsAcceptTokensQuery, - variables: { - projectId: project.id, - }, - }); - assert.isOk(result.data.data.getProjectAcceptTokens); - assert.equal(result.data.data.getProjectAcceptTokens.length, 1); - assert.equal(result.data.data.getProjectAcceptTokens[0].symbol, 'ETH'); - assert.equal(result.data.data.getProjectAcceptTokens[0].networkId, 1); - }); -} - -function allProjectsTestCases() { - it('should return projects search by owner', async () => { - const result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, - variables: { - searchTerm: SEED_DATA.SECOND_USER.name, - }, - }); - - const projects = result.data.data.allProjects.projects; - const secondUserProjects = await Project.find({ - where: { - admin: String(SEED_DATA.SECOND_USER.id), - }, - }); - - assert.equal(projects.length, secondUserProjects.length); - assert.equal(Number(projects[0]?.admin), SEED_DATA.SECOND_USER.id); - assert.isNotEmpty(projects[0].addresses); - projects.forEach(project => { - assert.isNotOk(project.adminUser.email); - assert.isOk(project.adminUser.firstName); - assert.isOk(project.adminUser.walletAddress); - assert.isOk(project.categories[0].mainCategory.title); - assert.equal( - project.descriptionSummary, - getHtmlTextSummary(project.description), - ); - assert.isNull(project.estimatedMatching); - assert.exists(project.sumDonationValueUsd); - assert.exists(project.sumDonationValueUsdForActiveQfRound); - assert.exists(project.countUniqueDonorsForActiveQfRound); - assert.exists(project.countUniqueDonors); - }); - }); - - it('should return projects with correct reaction', async () => { - const limit = 1; - const USER_DATA = SEED_DATA.FIRST_USER; - - // Project has not been liked - let result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, - variables: { - limit, - searchTerm: SEED_DATA.SECOND_PROJECT.title, - connectedWalletUserId: USER_DATA.id, - }, - }); - - let projects = result.data.data.allProjects.projects; - assert.equal(projects.length, limit); - assert.isNull(projects[0]?.reaction); - - // Project has been liked, but connectedWalletUserIs is not filled - result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, - variables: { - limit, - searchTerm: SEED_DATA.FIRST_PROJECT.title, - }, - }); - - projects = result.data.data.allProjects.projects; - assert.equal(projects.length, limit); - assert.isNull(projects[0]?.reaction); - - // Project has been liked - result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, - variables: { - limit, - searchTerm: SEED_DATA.FIRST_PROJECT.title, - connectedWalletUserId: USER_DATA.id, - }, - }); - - projects = result.data.data.allProjects.projects; - assert.equal(projects.length, limit); - assert.equal( - projects[0]?.reaction?.id, - REACTION_SEED_DATA.FIRST_LIKED_PROJECT_REACTION.id, - ); - projects.forEach(project => { - assert.isNotOk(project.adminUser.email); - assert.isOk(project.adminUser.firstName); - assert.isOk(project.adminUser.walletAddress); - }); - }); - - it('should return projects, sort by creationDate, DESC', async () => { - const firstProject = await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - }); - const secondProject = await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - }); - const result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, - variables: { - sortingBy: SortingField.Newest, - }, - }); - assert.equal( - Number(result.data.data.allProjects.projects[0].id), - secondProject.id, - ); - assert.equal( - Number(result.data.data.allProjects.projects[1].id), - firstProject.id, - ); - }); - - it('should return projects, sort by updatedAt, DESC', async () => { - const firstProject = await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - }); - const secondProject = await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - }); - - firstProject.title = String(new Date().getTime()); - firstProject.updatedAt = moment().add(2, 'days').toDate(); - await firstProject.save(); - - const result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, - variables: { - sortingBy: SortingField.RecentlyUpdated, - }, - }); - // First project should move to first position - assert.equal( - Number(result.data.data.allProjects.projects[0].id), - firstProject.id, - ); - }); - it('should return projects, sort by creationDate, ASC', async () => { - const result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, - variables: { - sortingBy: SortingField.Oldest, - }, - }); - const projectsCount = result.data.data.allProjects.projects.length; - const firstProjectIsOlder = - new Date(result.data.data.allProjects.projects[0].creationDate) < - new Date( - result.data.data.allProjects.projects[projectsCount - 1].creationDate, - ); - assert.isTrue(firstProjectIsOlder); - }); - it('should return projects, filter by verified, true', async () => { - // There is two verified projects so I just need to create a project with verified: false and listed:true - await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - verified: false, - qualityScore: 0, - }); - const result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, - variables: { - filters: ['Verified'], - }, - }); - assert.isNotEmpty(result.data.data.allProjects.projects); - result.data.data.allProjects.projects.forEach(project => - assert.isTrue(project.verified), - ); - }); - it('should return projects, filter by acceptGiv, true', async () => { - await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - qualityScore: 0, - }); - const result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, - variables: { - filters: ['AcceptGiv'], - }, - }); - assert.isNotEmpty(result.data.data.allProjects.projects); - result.data.data.allProjects.projects.forEach(project => - // currently givingBlocks projects doesnt accept GIV - assert.notExists(project.givingBlocksId), - ); - }); - it('should return projects, filter by boosted by givPower, true', async () => { - await AppDataSource.getDataSource().query( - 'truncate power_snapshot cascade', - ); - await PowerBoosting.clear(); - await PowerBalanceSnapshot.clear(); - await PowerBoostingSnapshot.clear(); - - const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); - const project = await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - }); - - const roundNumber = project.id * 10; - await insertSinglePowerBoosting({ - user, - project, - percentage: 100, - }); - await takePowerBoostingSnapshot(); - const [powerSnapshots] = await findPowerSnapshots(); - const snapshot = powerSnapshots[0]; - - snapshot.blockNumber = 1; - snapshot.roundNumber = roundNumber; - await snapshot.save(); - - await addOrUpdatePowerSnapshotBalances({ - userId: user.id, - powerSnapshotId: snapshot.id, - balance: 200, - }); - - await setPowerRound(roundNumber); - await refreshProjectPowerView(); - - const result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, - variables: { - filters: ['BoostedWithGivPower'], - limit: 50, - }, - }); - assert.isNotEmpty(result.data.data.allProjects.projects); - result.data.data.allProjects.projects.forEach(projectQueried => - assert.isOk(projectQueried?.projectPower?.totalPower > 0), - ); - }); - it('should return projects, filter from the givingblocks', async () => { - await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - givingBlocksId: '1234355', - qualityScore: 0, - }); - const result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, - variables: { - filters: ['GivingBlock'], - }, - }); - assert.isNotEmpty(result.data.data.allProjects.projects); - result.data.data.allProjects.projects.forEach(project => - assert.exists(project.givingBlocksId), - ); - }); - it('should return projects, sort by reactions, DESC', async () => { - await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - totalReactions: 100, - qualityScore: 0, - }); - const result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, - variables: { - sortingBy: SortingField.MostLiked, - }, - }); - assert.isTrue( - result.data.data.allProjects.projects[0].totalReactions >= 100, - ); - }); - it('should return projects, sort by donations, DESC', async () => { - await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - totalDonations: 100, - qualityScore: 0, - }); - const result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, - variables: { - sortingBy: SortingField.MostFunded, - }, - }); - assert.isTrue( - result.data.data.allProjects.projects[0].totalDonations >= 100, - ); - }); - it('should return projects, sort by qualityScore, DESC', async () => { - const project = await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - totalDonations: 100, - qualityScore: 10000, - }); - const result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, - variables: { - sortingBy: SortingField.QualityScore, - }, - }); - assert.isTrue( - Number(result.data.data.allProjects.projects[0].id) === project.id, - ); - - // default sort - const result2 = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, - }); - assert.isTrue( - Number(result2.data.data.allProjects.projects[0].id) === project.id, - ); - }); - - // it('should return projects, sort by project raised funds in the active QF round DESC', async () => { - // const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); - // const project1 = await saveProjectDirectlyToDb({ - // ...createProjectData(), - // title: String(new Date().getTime()), - // slug: String(new Date().getTime()), - // }); - // const project2 = await saveProjectDirectlyToDb({ - // ...createProjectData(), - // title: String(new Date().getTime()), - // slug: String(new Date().getTime()), - // }); - - // const qfRound = await QfRound.create({ - // isActive: true, - // name: 'test filter by qfRoundId', - // minimumPassportScore: 10, - // allocatedFund: 100, - // beginDate: new Date(), - // endDate: moment().add(1, 'day').toDate(), - // }).save(); - // project1.qfRounds = [qfRound]; - // await project1.save(); - // project2.qfRounds = [qfRound]; - // await project2.save(); - - // const donation1 = await saveDonationDirectlyToDb( - // { - // ...createDonationData(), - // status: 'verified', - // qfRoundId: qfRound.id, - // valueUsd: 2, - // }, - // donor.id, - // project1.id, - // ); - - // const donation2 = await saveDonationDirectlyToDb( - // { - // ...createDonationData(), - // status: 'verified', - // qfRoundId: qfRound.id, - // valueUsd: 20, - // }, - // donor.id, - // project2.id, - // ); - - // await refreshProjectEstimatedMatchingView(); - // await refreshProjectDonationSummaryView(); - - // const result = await axios.post(graphqlUrl, { - // query: fetchMultiFilterAllProjectsQuery, - // variables: { - // sortingBy: SortingField.ActiveQfRoundRaisedFunds, - // limit: 10, - // }, - // }); - - // assert.equal(result.data.data.allProjects.projects.length, 2); - // assert.equal(result.data.data.allProjects.projects[0].id, project2.id); - // result.data.data.allProjects.projects.forEach(project => { - // assert.equal(project.qfRounds[0].id, qfRound.id); - // }); - // qfRound.isActive = false; - // await qfRound.save(); - // }); - - it('should return projects, sort by project instant power DESC', async () => { - await PowerBoosting.clear(); - await InstantPowerBalance.clear(); - - const user1 = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); - const user2 = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); - - const project1 = await saveProjectDirectlyToDb(createProjectData()); - const project2 = await saveProjectDirectlyToDb(createProjectData()); - const project3 = await saveProjectDirectlyToDb(createProjectData()); - const project4 = await saveProjectDirectlyToDb({ - ...createProjectData(), - verified: false, - }); // Not boosted -Not verified project - const project5 = await saveProjectDirectlyToDb(createProjectData()); // Not boosted project - - const roundNumber = project3.id * 10; - - await Promise.all( - [ - [user1, project1, 10], - [user1, project2, 20], - [user1, project3, 30], - [user1, project4, 40], - [user2, project1, 20], - [user2, project2, 40], - [user2, project3, 60], - ].map(item => { - const [user, project, percentage] = item as [User, Project, number]; - return insertSinglePowerBoosting({ - user, - project, - percentage, - }); - }), - ); - - await saveOrUpdateInstantPowerBalances([ - { - userId: user1.id, - balance: 10000, - balanceAggregatorUpdatedAt: new Date(1_000_000), - }, - { - userId: user2.id, - balance: 1000, - balanceAggregatorUpdatedAt: new Date(1_000_000), - }, - ]); - - await updateInstantBoosting(); - - let result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, - variables: { - sortingBy: SortingField.InstantBoosting, - limit: 50, - }, - }); - - let projects = result.data.data.allProjects.projects; - - assert.equal(projects[0].id, project3.id); - assert.equal(projects[1].id, project2.id); - assert.equal(projects[2].id, project1.id); - - assert.equal(projects[0].projectInstantPower.powerRank, 1); - assert.equal(projects[1].projectInstantPower.powerRank, 2); - assert.equal(projects[2].projectInstantPower.powerRank, 3); - assert.equal(projects[3].projectInstantPower.powerRank, 4); - - result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, - variables: { - sortingBy: SortingField.InstantBoosting, - }, - }); - - projects = result.data.data.allProjects.projects; - const totalCount = projects.length; - for (let i = 1; i < totalCount - 1; i++) { - assert.isTrue( - projects[i].projectInstantPower.totalPower <= - projects[i - 1].projectInstantPower.totalPower, - ); - assert.isTrue( - projects[i].projectInstantPower.powerRank >= - projects[i - 1].projectInstantPower.powerRank, - ); - - if (projects[i].verified === true) { - // verified project come first - assert.isTrue(projects[i - 1].verified); - } - } - }); - - it('should return projects, sort by project power DESC', async () => { - await AppDataSource.getDataSource().query( - 'truncate power_snapshot cascade', - ); - await PowerBoosting.clear(); - await PowerBalanceSnapshot.clear(); - await PowerBoostingSnapshot.clear(); - - const user1 = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); - const user2 = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); - - const project1 = await saveProjectDirectlyToDb(createProjectData()); - const project2 = await saveProjectDirectlyToDb(createProjectData()); - const project3 = await saveProjectDirectlyToDb(createProjectData()); - const project4 = await saveProjectDirectlyToDb({ - ...createProjectData(), - verified: false, - }); // Not boosted -Not verified project - const project5 = await saveProjectDirectlyToDb(createProjectData()); // Not boosted project - - const roundNumber = project3.id * 10; - - await Promise.all( - [ - [user1, project1, 10], - [user1, project2, 20], - [user1, project3, 30], - [user2, project1, 20], - [user2, project2, 40], - [user2, project3, 60], - ].map(item => { - const [user, project, percentage] = item as [User, Project, number]; - return insertSinglePowerBoosting({ - user, - project, - percentage, - }); - }), - ); - - await takePowerBoostingSnapshot(); - const [powerSnapshots] = await findPowerSnapshots(); - const snapshot = powerSnapshots[0]; - - snapshot.blockNumber = 1; - snapshot.roundNumber = roundNumber; - await snapshot.save(); - - await addOrUpdatePowerSnapshotBalances([ - { - userId: user1.id, - powerSnapshotId: snapshot.id, - balance: 10000, - }, - { - userId: user2.id, - powerSnapshotId: snapshot.id, - balance: 20000, - }, - ]); - - await setPowerRound(roundNumber); - await refreshProjectPowerView(); - - let result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, - variables: { - sortingBy: SortingField.GIVPower, - limit: 50, - }, - }); - - let projects = result.data.data.allProjects.projects; - - assert.equal(projects[0].id, project3.id); - assert.equal(projects[1].id, project2.id); - assert.equal(projects[2].id, project1.id); - - assert.equal(projects[0].projectPower.powerRank, 1); - assert.equal(projects[1].projectPower.powerRank, 2); - assert.equal(projects[2].projectPower.powerRank, 3); - assert.equal(projects[3].projectPower.powerRank, 4); - - result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, - variables: { - sortingBy: SortingField.GIVPower, - }, - }); - - projects = result.data.data.allProjects.projects; - const totalCount = projects.length; - for (let i = 1; i < totalCount - 1; i++) { - assert.isTrue( - projects[i].projectPower.totalPower <= - projects[i - 1].projectPower.totalPower, - ); - assert.isTrue( - projects[i].projectPower.powerRank >= - projects[i - 1].projectPower.powerRank, - ); - - if (projects[i].verified === true) { - // verified project come first - assert.isTrue(projects[i - 1].verified); - } - } - }); - - it('should return projects, filtered by sub category', async () => { - await saveProjectDirectlyToDb({ - ...createProjectData(), - categories: ['food5'], - }); - const result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, - variables: { - category: 'food5', - }, - }); - assert.isNotEmpty(result.data.data.allProjects.projects); - result.data.data.allProjects.projects.forEach(project => { - assert.isOk( - project.categories.find(category => category.name === 'food5'), - ); - }); - }); - it('should return projects, filtered by main category', async () => { - await saveProjectDirectlyToDb({ - ...createProjectData(), - categories: ['drink2'], - }); - const result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, - variables: { - mainCategory: 'drink', - }, - }); - assert.isNotEmpty(result.data.data.allProjects.projects); - result.data.data.allProjects.projects.forEach(project => { - assert.isOk( - project.categories.find( - category => category.mainCategory.title === 'drink', - ), - ); - }); - }); - it('should return projects, filtered by main category and sub category at the same time', async () => { - await saveProjectDirectlyToDb({ - ...createProjectData(), - categories: ['drink2'], - }); - await saveProjectDirectlyToDb({ - ...createProjectData(), - categories: ['drink3'], - }); - const result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, - variables: { - mainCategory: 'drink', - category: 'drink3', - }, - }); - assert.isNotEmpty(result.data.data.allProjects.projects); - result.data.data.allProjects.projects.forEach(project => { - assert.isOk( - project.categories.find( - category => category.mainCategory.title === 'drink', - ), - ); - - // Should not return projects with drink2 category - assert.isOk( - project.categories.find(category => category.name === 'drink3'), - ); - }); - }); - - it('should return projects, filter by accept donation on gnosis, not return when it doesnt have gnosis address', async () => { - const savedProject = await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - }); - const gnosisAddress = (await findProjectRecipientAddressByNetworkId({ - projectId: savedProject.id, - networkId: NETWORK_IDS.XDAI, - })) as ProjectAddress; - gnosisAddress.isRecipient = false; - await gnosisAddress.save(); - const result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, - variables: { - filters: ['AcceptFundOnGnosis'], - sortingBy: SortingField.Newest, - }, - }); - result.data.data.allProjects.projects.forEach(project => { - assert.isOk( - project.addresses.find( - address => - address.isRecipient === true && - address.networkId === NETWORK_IDS.XDAI && - address.chainType === ChainType.EVM, - ), - ); - }); - assert.isNotOk( - result.data.data.allProjects.projects.find( - project => Number(project.id) === Number(savedProject.id), - ), - ); - }); - - it('should return projects, filter by accept donation on celo', async () => { - const savedProject = await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - networkId: NETWORK_IDS.CELO, - }); - const result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, - variables: { - filters: ['AcceptFundOnCelo'], - sortingBy: SortingField.Newest, - }, - }); - result.data.data.allProjects.projects.forEach(project => { - assert.isOk( - project.addresses.find( - address => - address.isRecipient === true && - (address.networkId === NETWORK_IDS.CELO || - address.networkId === NETWORK_IDS.CELO_ALFAJORES), - ), - ); - }); - assert.isOk( - result.data.data.allProjects.projects.find( - project => Number(project.id) === Number(savedProject.id), - ), - ); - }); - it('should return projects, filter by accept donation on celo', async () => { - const savedProject = await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - networkId: NETWORK_IDS.CELO, - }); - const result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, - variables: { - filters: ['AcceptFundOnCelo'], - sortingBy: SortingField.Newest, - }, - }); - result.data.data.allProjects.projects.forEach(project => { - assert.isOk( - project.addresses.find( - address => - address.isRecipient === true && - (address.networkId === NETWORK_IDS.CELO || - address.networkId === NETWORK_IDS.CELO_ALFAJORES) && - address.chainType === ChainType.EVM, - ), - ); - }); - assert.isOk( - result.data.data.allProjects.projects.find( - project => Number(project.id) === Number(savedProject.id), - ), - ); - }); - - it('should return projects, filter by accept donation on celo, not return when it doesnt have celo address', async () => { - const celoProject = await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - networkId: NETWORK_IDS.CELO, - }); - const polygonProject = await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - networkId: NETWORK_IDS.POLYGON, - }); - - const result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, - variables: { - filters: ['AcceptFundOnCelo'], - sortingBy: SortingField.Newest, - }, - }); - - result.data.data.allProjects.projects.forEach(project => { - assert.isOk( - project.addresses.find( - address => - address.isRecipient === true && - (address.networkId === NETWORK_IDS.CELO || - address.networkId === NETWORK_IDS.CELO_ALFAJORES) && - address.chainType === ChainType.EVM, - ), - ); - }); - assert.isNotOk( - result.data.data.allProjects.projects.find( - project => Number(project.id) === Number(polygonProject.id), - ), - ); - assert.isOk( - result.data.data.allProjects.projects.find( - project => Number(project.id) === Number(celoProject.id), - ), - ); - }); - - it('should return projects, filter by accept donation on mainnet', async () => { - const savedProject = await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - networkId: NETWORK_IDS.MAIN_NET, - }); - const result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, - variables: { - filters: ['AcceptFundOnMainnet'], - sortingBy: SortingField.Newest, - }, - }); - result.data.data.allProjects.projects.forEach(project => { - assert.isOk( - project.addresses.find( - address => - address.isRecipient === true && - (address.networkId === NETWORK_IDS.MAIN_NET || - address.networkId === NETWORK_IDS.GOERLI) && - address.chainType === ChainType.EVM, - ), - ); - }); - assert.isOk( - result.data.data.allProjects.projects.find( - project => Number(project.id) === Number(savedProject.id), - ), - ); - }); - - it('should return projects, filter by accept donation on mainnet, not return when it doesnt have mainnet address', async () => { - const polygonProject = await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - networkId: NETWORK_IDS.POLYGON, - }); - - const result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, - variables: { - filters: ['AcceptFundOnMainnet'], - sortingBy: SortingField.Newest, - }, - }); - result.data.data.allProjects.projects.forEach(project => { - assert.isOk( - project.addresses.find( - address => - address.isRecipient === true && - address.networkId === NETWORK_IDS.MAIN_NET && - address.chainType === ChainType.EVM, - ), - ); - }); - assert.isNotOk( - result.data.data.allProjects.projects.find( - project => Number(project.id) === Number(polygonProject.id), - ), - ); - }); - - it('should return projects, filter by accept donation on polygon', async () => { - const savedProject = await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - }); - const polygonAddress = (await findProjectRecipientAddressByNetworkId({ - projectId: savedProject.id, - networkId: NETWORK_IDS.POLYGON, - })) as ProjectAddress; - polygonAddress.isRecipient = true; - await polygonAddress.save(); - const result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, - variables: { - filters: ['AcceptFundOnPolygon'], - sortingBy: SortingField.Newest, - }, - }); - result.data.data.allProjects.projects.forEach(project => { - assert.isOk( - project.addresses.find( - address => - address.isRecipient === true && - address.networkId === NETWORK_IDS.POLYGON && - address.chainType === ChainType.EVM, - ), - ); - }); - assert.isOk( - result.data.data.allProjects.projects.find( - project => Number(project.id) === Number(savedProject.id), - ), - ); - }); - - it('should return projects, filter by accept donation on polygon, not return when it doesnt have polygon address', async () => { - const savedProject = await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - networkId: NETWORK_IDS.OPTIMISTIC, - }); - const result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, - variables: { - filters: ['AcceptFundOnPolygon'], - sortingBy: SortingField.Newest, - }, - }); - result.data.data.allProjects.projects.forEach(project => { - assert.isOk( - project.addresses.find( - address => - address.isRecipient === true && - address.networkId === NETWORK_IDS.POLYGON && - address.chainType === ChainType.EVM, - ), - ); - }); - assert.isNotOk( - result.data.data.allProjects.projects.find( - project => Number(project.id) === Number(savedProject.id), - ), - ); - }); - - it('should return projects, filter by accept donation on GOERLI', async () => { - const savedProject = await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - networkId: NETWORK_IDS.GOERLI, - }); - const result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, - variables: { - filters: ['AcceptFundOnMainnet'], - sortingBy: SortingField.Newest, - limit: 50, - }, - }); - result.data.data.allProjects.projects.forEach(project => { - assert.isOk( - project.addresses.find( - address => - (address.isRecipient === true && - address.networkId === NETWORK_IDS.MAIN_NET) || - address.networkId === NETWORK_IDS.GOERLI, - ), - ); - }); - assert.isOk( - result.data.data.allProjects.projects.find( - project => Number(project.id) === Number(savedProject.id), - ), - ); - }); - - it('should return projects, filter by accept donation on ALFAJORES', async () => { - const alfajoresProject = await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - networkId: NETWORK_IDS.CELO_ALFAJORES, - }); - - const result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, - variables: { - filters: ['AcceptFundOnCelo'], - sortingBy: SortingField.Newest, - }, - }); - result.data.data.allProjects.projects.forEach(project => { - assert.isOk( - project.addresses.find( - address => - (address.isRecipient === true && - // We return both Celo and Alfajores when sending AcceptFundOnCelo filter - address.networkId === NETWORK_IDS.CELO_ALFAJORES) || - address.networkId === NETWORK_IDS.CELO, - ), - ); - }); - assert.isOk( - result.data.data.allProjects.projects.find( - project => Number(project.id) === Number(alfajoresProject.id), - ), - ); - }); - - it('should return projects, filter by accept donation on optimism', async () => { - const savedProject = await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - networkId: NETWORK_IDS.OPTIMISTIC, - }); - const result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, - variables: { - filters: ['AcceptFundOnOptimism'], - sortingBy: SortingField.Newest, - }, - }); - result.data.data.allProjects.projects.forEach(project => { - assert.isOk( - project.addresses.find( - address => - address.isRecipient === true && - address.networkId === NETWORK_IDS.OPTIMISTIC && - address.chainType === ChainType.EVM, - ), - ); - }); - assert.isOk( - result.data.data.allProjects.projects.find( - project => Number(project.id) === Number(savedProject.id), - ), - ); - }); - it('should return projects, filter by accept donation on optimism, not return when it doesnt have optimism address', async () => { - const gnosisProject = await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - networkId: NETWORK_IDS.XDAI, - }); - - const result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, - variables: { - filters: ['AcceptFundOnOptimism'], - sortingBy: SortingField.Newest, - }, - }); - result.data.data.allProjects.projects.forEach(project => { - assert.isOk( - project.addresses.find( - address => - address.isRecipient === true && - (address.networkId === NETWORK_IDS.OPTIMISTIC || - address.networkId === NETWORK_IDS.OPTIMISM_GOERLI) && - address.chainType === ChainType.EVM, - ), - ); - }); - assert.isNotOk( - result.data.data.allProjects.projects.find( - project => Number(project.id) === Number(gnosisProject.id), - ), - ); - }); - it('should return projects, filter by accept donation on gnosis, return all addresses', async () => { - await redis.flushall(); // clear cache from other tests - const savedProject = await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - }); - - const result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, - variables: { - filters: ['AcceptFundOnGnosis'], - sortingBy: SortingField.Newest, - limit: 50, - }, - }); - result.data.data.allProjects.projects.forEach(item => { - assert.isOk( - item.addresses.find( - address => - address.isRecipient === true && - address.networkId === NETWORK_IDS.XDAI && - address.chainType === ChainType.EVM, - ), - ); - }); - const project = result.data.data.allProjects.projects.find( - item => Number(item.id) === Number(savedProject.id), - ); - - assert.isOk(project); - assert.isOk( - project.addresses.find( - address => - address.isRecipient === true && - address.networkId === NETWORK_IDS.XDAI && - address.chainType === ChainType.EVM, - ), - ); - assert.isOk( - project.addresses.find( - address => - address.isRecipient === true && - address.networkId === NETWORK_IDS.MAIN_NET && - address.chainType === ChainType.EVM, - ), - ); - }); - it('should return projects, filter by accept donation on gnosis, should not return if it has no address', async () => { - const savedProject = await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - }); - await ProjectAddress.query(` - DELETE - from project_address - WHERE "projectId" = ${savedProject.id} - `); - const result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, - variables: { - filters: ['AcceptFundOnGnosis'], - sortingBy: SortingField.Newest, - }, - }); - result.data.data.allProjects.projects.forEach(project => { - assert.isOk( - project.addresses.find( - address => - address.isRecipient === true && - address.networkId === NETWORK_IDS.XDAI && - address.chainType === ChainType.EVM, - ), - ); - }); - assert.isNotOk( - result.data.data.allProjects.projects.find( - project => Number(project.id) === Number(savedProject.id), - ), - ); - }); - it('should return projects, filter by accept donation on Solana', async () => { - const savedProject = await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - }); - await ProjectAddress.delete({ projectId: savedProject.id }); - const solanaAddress = ProjectAddress.create({ - project: savedProject, - title: 'first address', - address: generateRandomSolanaAddress(), - chainType: ChainType.SOLANA, - networkId: 0, - isRecipient: true, - }); - await solanaAddress.save(); - const result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, - variables: { - filters: ['AcceptFundOnSolana'], - sortingBy: SortingField.Newest, - }, - }); - result.data.data.allProjects.projects.forEach(project => { - assert.isOk( - project.addresses.find( - address => - address.isRecipient === true && - address.chainType === ChainType.SOLANA, - ), - ); - }); - assert.isOk( - result.data.data.allProjects.projects.find( - project => Number(project.id) === Number(savedProject.id), - ), - ); - }); - it('should return projects, filter by accept fund on two Ethereum networks', async () => { - const project = await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - }); - - const mainnetAddress = ProjectAddress.create({ - project, - title: 'first address', - address: generateRandomEtheriumAddress(), - networkId: 1, - isRecipient: true, - }); - await mainnetAddress.save(); - - const solanaAddress = ProjectAddress.create({ - project, - title: 'secnod address', - address: generateRandomSolanaAddress(), - chainType: ChainType.SOLANA, - networkId: 0, - isRecipient: true, - }); - await solanaAddress.save(); - - const result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, - variables: { - filters: ['AcceptFundOnMainnet', 'AcceptFundOnSolana'], - sortingBy: SortingField.Newest, - }, - }); - const { projects } = result.data.data.allProjects; - - const projectIds = projects.map(_project => _project.id); - assert.include(projectIds, String(project.id)); - }); - it('should return projects, when only accpets donation on Solana or an expected Ethereum network', async () => { - const projectWithMainnet = await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - }); - const projectWithSolana = await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - }); - - const mainnetAddress = ProjectAddress.create({ - project: projectWithMainnet, - title: 'first address', - address: generateRandomEtheriumAddress(), - networkId: 1, - isRecipient: true, - }); - await mainnetAddress.save(); - - const solanaAddress = ProjectAddress.create({ - project: projectWithSolana, - title: 'secnod address', - address: generateRandomSolanaAddress(), - chainType: ChainType.SOLANA, - networkId: 0, - isRecipient: true, - }); - await solanaAddress.save(); - - const result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, - variables: { - filters: ['AcceptFundOnMainnet', 'AcceptFundOnSolana'], - sortingBy: SortingField.Newest, - }, - }); - const { projects } = result.data.data.allProjects; - const projectIds = projects.map(project => project.id); - assert.include(projectIds, String(projectWithMainnet.id)); - assert.include(projectIds, String(projectWithSolana.id)); - }); - it('should not return a project when it does not accept donation on Solana', async () => { - // Delete all project addresses - await ProjectAddress.delete({ chainType: ChainType.SOLANA }); - - const project = await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - }); - - const result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, - variables: { - filters: ['AcceptFundOnSolana'], - sortingBy: SortingField.Newest, - }, - }); - const { projects } = result.data.data.allProjects; - assert.lengthOf(projects, 0); - }); - it('should return projects, filter by campaignSlug and limit, skip', async () => { - const project1 = await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - }); - const project2 = await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - }); - const project3 = await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - }); - - const campaign = await Campaign.create({ - isActive: true, - type: CampaignType.ManuallySelected, - slug: generateRandomString(), - title: 'title1', - description: 'description1', - photo: 'https://google.com', - relatedProjectsSlugs: [ - project1.slug as string, - project2.slug as string, - project3.slug as string, - ], - order: 1, - }).save(); - - const result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, - variables: { - limit: 1, - skip: 1, - campaignSlug: campaign.slug, - }, - }); - - assert.equal(result.data.data.allProjects.projects.length, 1); - assert.equal(result.data.data.allProjects.campaign.title, campaign.title); - assert.isOk( - [project1.slug, project2.slug, project3.slug].includes( - result.data.data.allProjects.projects[0].slug, - ), - ); - - await campaign.remove(); - }); - it('should return projects, filter by qfRoundId', async () => { - const project1 = await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - }); - const project2 = await saveProjectDirectlyToDb({ + const project = await saveProjectDirectlyToDb({ ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), + creationDate: moment().add(10, 'days').toDate(), }); - const project3 = await saveProjectDirectlyToDb({ + const project2 = await saveProjectDirectlyToDb({ ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), + creationDate: moment().add(44, 'days').toDate(), }); - - const qfRound = await QfRound.create({ - isActive: true, - name: 'test filter by qfRoundId', - slug: new Date().getTime().toString(), - minimumPassportScore: 10, - allocatedFund: 100, - beginDate: new Date(), - endDate: new Date(), - }).save(); - project1.qfRounds = [qfRound]; - await project1.save(); - project2.qfRounds = [qfRound]; - await project2.save(); - project3.qfRounds = [qfRound]; - await project3.save(); - const result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, + const projectsResponse = await axios.post(graphqlUrl, { + query: fetchNewProjectsPerDate, variables: { - qfRoundId: qfRound.id, + fromDate: moment().add(9, 'days').toDate().toISOString().split('T')[0], + toDate: moment().add(45, 'days').toDate().toISOString().split('T')[0], }, }); - assert.equal(result.data.data.allProjects.projects.length, 3); - result.data.data.allProjects.projects.forEach(project => { - assert.equal(project.qfRounds[0].id, qfRound.id); - }); - qfRound.isActive = false; - await qfRound.save(); + assert.isOk(projectsResponse); + assert.equal(projectsResponse.data.data.projectsPerDate.total, 2); + const total = + projectsResponse.data.data.projectsPerDate.totalPerMonthAndYear.reduce( + (sum, value) => sum + value.total, + 0, + ); + assert.equal(projectsResponse.data.data.projectsPerDate.total, total); }); - it('should return projects, filter by qfRoundSlug', async () => { - const project1 = await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - }); - const project2 = await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - }); - const project3 = await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - }); +} - const qfRound = await QfRound.create({ - isActive: true, - name: 'test filter by qfRoundId', - slug: new Date().getTime().toString(), - minimumPassportScore: 10, - allocatedFund: 100, - beginDate: new Date(), - endDate: new Date(), - }).save(); - project1.qfRounds = [qfRound]; - await project1.save(); - project2.qfRounds = [qfRound]; - await project2.save(); - project3.qfRounds = [qfRound]; - await project3.save(); +function getProjectsAcceptTokensTestCases() { + it('should return all tokens for giveth projects', async () => { + const project = await saveProjectDirectlyToDb(createProjectData()); + const allTokens = await Token.find({}); const result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, + query: getProjectsAcceptTokensQuery, variables: { - qfRoundSlug: qfRound.slug, + projectId: project.id, }, }); - - assert.equal(result.data.data.allProjects.projects.length, 3); - result.data.data.allProjects.projects.forEach(project => { - assert.equal(project.qfRounds[0].id, qfRound.id); - }); - qfRound.isActive = false; - await qfRound.save(); + assert.isOk(result.data.data.getProjectAcceptTokens); + assert.equal( + result.data.data.getProjectAcceptTokens.length, + allTokens.length, + ); }); - it('should just return verified projects, filter by qfRoundId and verified', async () => { - const project1 = await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - }); - const project2 = await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - }); - const project3 = await saveProjectDirectlyToDb({ + it('should return all tokens for trace projects', async () => { + const project = await saveProjectDirectlyToDb({ ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - verified: false, + organizationLabel: ORGANIZATION_LABELS.TRACE, }); + const traceOrganization = (await Organization.findOne({ + where: { + label: ORGANIZATION_LABELS.TRACE, + }, + })) as Organization; - const qfRound = await QfRound.create({ - isActive: true, - name: 'test filter by qfRoundId', - slug: new Date().getTime().toString(), - minimumPassportScore: 10, - allocatedFund: 100, - beginDate: new Date(), - endDate: new Date(), - }).save(); - project1.qfRounds = [qfRound]; - await project1.save(); - project2.qfRounds = [qfRound]; - await project2.save(); - project3.qfRounds = [qfRound]; - await project3.save(); + const allTokens = ( + await Token.query(` + SELECT COUNT(*) as "tokenCount" + FROM organization_tokens_token + WHERE "organizationId" = ${traceOrganization.id} + `) + )[0]; const result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, + query: getProjectsAcceptTokensQuery, variables: { - qfRoundId: qfRound.id, - filters: ['Verified'], + projectId: project.id, }, }); - - assert.equal(result.data.data.allProjects.projects.length, 2); - result.data.data.allProjects.projects.forEach(project => { - assert.equal(project.qfRounds[0].id, qfRound.id); - }); - qfRound.isActive = false; - await qfRound.save(); + assert.isOk(result.data.data.getProjectAcceptTokens); + assert.equal( + result.data.data.getProjectAcceptTokens.length, + Number(allTokens.tokenCount), + ); }); - it('should return projects, filter by qfRoundId, calculate estimated matching', async () => { - const project1 = await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - }); - const project2 = await saveProjectDirectlyToDb({ + it('should return just Gnosis tokens when project just have Gnosis recipient address', async () => { + const project = await saveProjectDirectlyToDb({ ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), + organizationLabel: ORGANIZATION_LABELS.TRACE, + networkId: NETWORK_IDS.XDAI, }); - const qfRound = await QfRound.create({ - isActive: true, - name: new Date().toString(), - slug: new Date().getTime().toString(), - minimumPassportScore: 8, - allocatedFund: 1000, - beginDate: new Date(), - endDate: moment().add(10, 'days').toDate(), - }).save(); - project1.qfRounds = [qfRound]; - await project1.save(); - project2.qfRounds = [qfRound]; - await project2.save(); - - const donor1 = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); - donor1.passportScore = 13; - await donor1.save(); - - const donor2 = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); - donor2.passportScore = 13; - await donor2.save(); - - // We should have result similar to https://wtfisqf.com/?grant=2,2&grant=4&grant=&grant=&match=1000 - await saveDonationDirectlyToDb( - { - ...createDonationData(), - status: 'verified', - qfRoundId: qfRound.id, - valueUsd: 2, - }, - donor1.id, - project1.id, - ); - - await saveDonationDirectlyToDb( - { - ...createDonationData(), - status: 'verified', - qfRoundId: qfRound.id, - valueUsd: 2, - }, - donor2.id, - project1.id, - ); - - await saveDonationDirectlyToDb( - { - ...createDonationData(), - status: 'verified', - qfRoundId: qfRound.id, - valueUsd: 4, - }, - donor1.id, - project2.id, - ); - - await refreshProjectEstimatedMatchingView(); - await refreshProjectDonationSummaryView(); - const result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, + query: getProjectsAcceptTokensQuery, variables: { - qfRoundId: qfRound.id, + projectId: project.id, }, }); - - assert.equal(result.data.data.allProjects.projects.length, 2); - const firstProject = result.data.data.allProjects.projects.find( - p => Number(p.id) === project1.id, - ); - const secondProject = result.data.data.allProjects.projects.find( - p => Number(p.id) === project2.id, - ); - - const project1EstimatedMatching = - await calculateEstimatedMatchingWithParams({ - matchingPool: firstProject.estimatedMatching.matchingPool, - projectDonationsSqrtRootSum: - firstProject.estimatedMatching.projectDonationsSqrtRootSum, - allProjectsSum: firstProject.estimatedMatching.allProjectsSum, - }); - - const project2EstimatedMatching = - await calculateEstimatedMatchingWithParams({ - matchingPool: secondProject.estimatedMatching.matchingPool, - projectDonationsSqrtRootSum: - secondProject.estimatedMatching.projectDonationsSqrtRootSum, - allProjectsSum: secondProject.estimatedMatching.allProjectsSum, - }); - - assert.equal(Math.floor(project1EstimatedMatching), 666); - assert.equal(Math.floor(project2EstimatedMatching), 333); - qfRound.isActive = false; - await qfRound.save(); + assert.isNotEmpty(result.data.data.getProjectAcceptTokens); + result.data.data.getProjectAcceptTokens.forEach(token => { + assert.equal(token.networkId, NETWORK_IDS.XDAI); + }); }); - - it('should return projects, filter by ActiveQfRound', async () => { + it('should return just Ropsten tokens when project just have Ropsten recipient address', async () => { const project = await saveProjectDirectlyToDb({ ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - verified: true, - listed: true, + organizationLabel: ORGANIZATION_LABELS.TRACE, + networkId: NETWORK_IDS.ROPSTEN, }); - const qfRound = await QfRound.create({ - isActive: true, - name: 'test', - allocatedFund: 100, - slug: new Date().getTime().toString(), - minimumPassportScore: 10, - beginDate: new Date(), - endDate: new Date(), - }).save(); - project.qfRounds = [qfRound]; - await project.save(); const result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, + query: getProjectsAcceptTokensQuery, variables: { - limit: 50, - skip: 0, - filters: ['ActiveQfRound'], + projectId: project.id, }, }); - assert.equal(result.data.data.allProjects.projects.length, 1); - qfRound.isActive = false; - await qfRound.save(); + assert.isNotEmpty(result.data.data.getProjectAcceptTokens); + result.data.data.getProjectAcceptTokens.forEach(token => { + assert.equal(token.networkId, NETWORK_IDS.ROPSTEN); + }); }); - - it('should return projects, filter by ActiveQfRound, and not return non verified projects', async () => { + it('should return just Solana and Ropsten tokens when project just have Solana and Ropsten recipient address', async () => { const project = await saveProjectDirectlyToDb({ ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - verified: false, - listed: true, + organizationLabel: ORGANIZATION_LABELS.TRACE, + networkId: NETWORK_IDS.ROPSTEN, + }); + + await addNewProjectAddress({ + project, + user: project.adminUser, + isRecipient: true, + networkId: NETWORK_IDS.SOLANA_MAINNET, + address: generateRandomSolanaAddress(), + chainType: ChainType.SOLANA, }); - const qfRound = await QfRound.create({ - isActive: true, - name: 'test', - allocatedFund: 100, - slug: new Date().getTime().toString(), - minimumPassportScore: 10, - beginDate: new Date(), - endDate: new Date(), - }).save(); - project.qfRounds = [qfRound]; - await project.save(); const result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, + query: getProjectsAcceptTokensQuery, variables: { - limit: 50, - skip: 0, - filters: ['ActiveQfRound', 'Verified'], + projectId: project.id, }, }); - assert.equal(result.data.data.allProjects.projects.length, 0); - qfRound.isActive = false; - await qfRound.save(); + assert.isNotEmpty(result.data.data.getProjectAcceptTokens); + result.data.data.getProjectAcceptTokens.forEach(token => { + expect(token.networkId).to.satisfy( + networkId => + networkId === NETWORK_IDS.SOLANA_MAINNET || + networkId === NETWORK_IDS.ROPSTEN, + ); + }); }); - it('should return projects, filter by ActiveQfRound, and not return non listed projects', async () => { + it('should no tokens when there is not any recipient address', async () => { const project = await saveProjectDirectlyToDb({ ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - verified: true, - listed: false, - reviewStatus: ReviewStatus.NotListed, + organizationLabel: ORGANIZATION_LABELS.TRACE, + networkId: NETWORK_IDS.ROPSTEN, }); + await removeRecipientAddressOfProject({ project }); - const qfRound = await QfRound.create({ - isActive: true, - name: 'test', - allocatedFund: 100, - slug: new Date().getTime().toString(), - minimumPassportScore: 10, - beginDate: new Date(), - endDate: new Date(), - }).save(); - project.qfRounds = [qfRound]; - await project.save(); const result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, + query: getProjectsAcceptTokensQuery, variables: { - limit: 50, - skip: 0, - filters: ['ActiveQfRound', 'Verified'], + projectId: project.id, }, }); - assert.equal(result.data.data.allProjects.projects.length, 0); - qfRound.isActive = false; - await qfRound.save(); + assert.isEmpty(result.data.data.getProjectAcceptTokens); }); - it('should return empty list when qfRound is not active, filter by ActiveQfRound', async () => { + it('should just return ETH token for givingBlock projects', async () => { const project = await saveProjectDirectlyToDb({ ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - verified: true, - listed: true, + organizationLabel: ORGANIZATION_LABELS.GIVING_BLOCK, }); - - const qfRound = await QfRound.create({ - isActive: false, - name: 'test2', - allocatedFund: 100, - slug: new Date().getTime().toString(), - minimumPassportScore: 10, - beginDate: new Date(), - endDate: new Date(), - }).save(); - project.qfRounds = [qfRound]; - await project.save(); const result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, + query: getProjectsAcceptTokensQuery, variables: { - limit: 50, - skip: 0, - filters: ['ActiveQfRound'], + projectId: project.id, }, }); - - assert.equal(result.data.data.allProjects.projects.length, 0); - qfRound.isActive = false; - await qfRound.save(); + assert.isOk(result.data.data.getProjectAcceptTokens); + assert.equal(result.data.data.getProjectAcceptTokens.length, 1); + assert.equal(result.data.data.getProjectAcceptTokens[0].symbol, 'ETH'); + assert.equal(result.data.data.getProjectAcceptTokens[0].networkId, 1); }); } @@ -4312,6 +2573,40 @@ function addRecipientAddressToProjectTestCases() { ); }); + it('Should add Arbitrum address successfully', async () => { + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + const accessToken = await generateTestAccessToken(user.id); + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + admin: String(user.id), + }); + const newWalletAddress = generateRandomEtheriumAddress(); + + const response = await axios.post( + graphqlUrl, + { + query: addRecipientAddressToProjectQuery, + variables: { + projectId: project.id, + networkId: NETWORK_IDS.ARBITRUM_MAINNET, + address: newWalletAddress, + }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + // assert.equal(JSON.stringify(response.data, null, 4), 'hi'); + assert.isOk(response.data.data.addRecipientAddressToProject); + assert.isOk( + response.data.data.addRecipientAddressToProject.addresses.find( + projectAddress => projectAddress.address === newWalletAddress, + ), + ); + }); + it('Should update successfully listed (true) should not change', async () => { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const accessToken = await generateTestAccessToken(user.id); diff --git a/src/resolvers/projectResolver.ts b/src/resolvers/projectResolver.ts index 089375f44..f32e5c625 100644 --- a/src/resolvers/projectResolver.ts +++ b/src/resolvers/projectResolver.ts @@ -530,6 +530,12 @@ export class ProjectResolver { // Add this to make sure works on Staging networkIds.push(NETWORK_IDS.CELO_ALFAJORES); return; + + case FilterField.AcceptFundOnArbitrum: + networkIds.push(NETWORK_IDS.ARBITRUM_MAINNET); + networkIds.push(NETWORK_IDS.ARBITRUM_SEPOLIA); + return; + case FilterField.AcceptFundOnPolygon: networkIds.push(NETWORK_IDS.POLYGON); return; diff --git a/src/resolvers/projectVerificationFormResolver.test.ts b/src/resolvers/projectVerificationFormResolver.test.ts index a5836faf3..97d44e53d 100644 --- a/src/resolvers/projectVerificationFormResolver.test.ts +++ b/src/resolvers/projectVerificationFormResolver.test.ts @@ -312,6 +312,16 @@ function updateProjectVerificationFormMutationTestCases() { networkId: NETWORK_IDS.CELO, title: 'test title', }, + { + address: generateRandomEtheriumAddress(), + networkId: NETWORK_IDS.ARBITRUM_MAINNET, + title: 'test title', + }, + { + address: generateRandomEtheriumAddress(), + networkId: NETWORK_IDS.ARBITRUM_SEPOLIA, + title: 'test title', + }, { address: generateRandomEtheriumAddress(), networkId: NETWORK_IDS.ETC, diff --git a/src/server/adminJs/tabs/donationTab.ts b/src/server/adminJs/tabs/donationTab.ts index ab80caa7a..fc7c96533 100644 --- a/src/server/adminJs/tabs/donationTab.ts +++ b/src/server/adminJs/tabs/donationTab.ts @@ -604,6 +604,8 @@ export const donationTab = { { value: NETWORK_IDS.POLYGON, label: 'Polygon' }, { value: NETWORK_IDS.CELO, label: 'Celo' }, { value: NETWORK_IDS.CELO_ALFAJORES, label: 'Alfajores' }, + { value: NETWORK_IDS.ARBITRUM_MAINNET, label: 'Arbitrum' }, + { value: NETWORK_IDS.ARBITRUM_SEPOLIA, label: 'Arbitrum Sepolia' }, ], isVisible: { list: true, diff --git a/src/server/adminJs/tabs/qfRoundTab.ts b/src/server/adminJs/tabs/qfRoundTab.ts index beb26cf9e..d002fb0bd 100644 --- a/src/server/adminJs/tabs/qfRoundTab.ts +++ b/src/server/adminJs/tabs/qfRoundTab.ts @@ -133,6 +133,8 @@ export const qfRoundTab = { value: NETWORK_IDS.CELO_ALFAJORES, label: 'ALFAJORES (Test CELO)', }, + { value: NETWORK_IDS.ARBITRUM_MAINNET, label: 'ARBITRUM MAINNET' }, + { value: NETWORK_IDS.ARBITRUM_SEPOLIA, label: 'ARBITRUM SEPOLIA' }, { value: NETWORK_IDS.XDAI, label: 'XDAI' }, { value: NETWORK_IDS.BSC, label: 'BSC' }, ], diff --git a/src/server/adminJs/tabs/tokenTab.ts b/src/server/adminJs/tabs/tokenTab.ts index a4563e84c..1e6551d0c 100644 --- a/src/server/adminJs/tabs/tokenTab.ts +++ b/src/server/adminJs/tabs/tokenTab.ts @@ -190,6 +190,8 @@ export const generateTokenTab = async () => { value: NETWORK_IDS.CELO_ALFAJORES, label: 'ALFAJORES (Test CELO)', }, + { value: NETWORK_IDS.ARBITRUM_MAINNET, label: 'ARBITRUM MAINNET' }, + { value: NETWORK_IDS.ARBITRUM_SEPOLIA, label: 'ARBITRUM SEPOLIA' }, { value: NETWORK_IDS.XDAI, label: 'XDAI' }, { value: NETWORK_IDS.BSC, label: 'BSC' }, { value: NETWORK_IDS.ETC, label: 'Ethereum Classic' }, diff --git a/src/services/chains/index.test.ts b/src/services/chains/index.test.ts index 0504ce2ef..6f74928d3 100644 --- a/src/services/chains/index.test.ts +++ b/src/services/chains/index.test.ts @@ -610,6 +610,44 @@ function getTransactionDetailTestCases() { assert.equal(transactionInfo.amount, amount); }); + it('should return transaction detail for normal transfer on Arbitrum Mainnet', async () => { + // https://arbiscan.io/tx/0xdaca7d68e784a60a6975fa9937abb6b287d7fe992ff806f8c375cb4c3b2152f3 + + const amount = 0.0038; + const transactionInfo = await getTransactionInfoFromNetwork({ + txHash: + '0xdaca7d68e784a60a6975fa9937abb6b287d7fe992ff806f8c375cb4c3b2152f3', + symbol: 'ETH', + networkId: NETWORK_IDS.ARBITRUM_MAINNET, + fromAddress: '0x015e6fbce5119c32db66e7c544365749bb26cf8b', + toAddress: '0x5c66fef6ea22f37e7c1f7eee49e4e116d3fbfc68', + amount, + timestamp: 1708342629, + }); + assert.isOk(transactionInfo); + assert.equal(transactionInfo.currency, 'ETH'); + assert.equal(transactionInfo.amount, amount); + }); + + it('should return transaction detail for normal transfer on Arbitrum Sepolia', async () => { + // https://sepolia.arbiscan.io/tx/0x25f17541ccb7248d931f2a1e11058a51ffb4db4968ed3e1d4a019ddc2d44802c + + const amount = 0.0069; + const transactionInfo = await getTransactionInfoFromNetwork({ + txHash: + '0x25f17541ccb7248d931f2a1e11058a51ffb4db4968ed3e1d4a019ddc2d44802c', + symbol: 'ETH', + networkId: NETWORK_IDS.ARBITRUM_SEPOLIA, + fromAddress: '0xefc58dbf0e606c327868b55334998aacb27f9ef2', + toAddress: '0xc11c479473cd06618fc75816dd6b56be4ac80efd', + amount, + timestamp: 1708344659, + }); + assert.isOk(transactionInfo); + assert.equal(transactionInfo.currency, 'ETH'); + assert.equal(transactionInfo.amount, amount); + }); + it('should return transaction detail for OP token transfer on optimistic', async () => { // https://optimistic.etherscan.io/tx/0xf11be189d967831bb8a76656882eeeac944a799bd222acbd556f2156fdc02db4 const amount = 0.453549908802477308; diff --git a/src/services/donationService.test.ts b/src/services/donationService.test.ts index dde8ae31f..6a77cd1a5 100644 --- a/src/services/donationService.test.ts +++ b/src/services/donationService.test.ts @@ -230,6 +230,182 @@ function syncDonationStatusWithBlockchainNetworkTestCases() { assert.isTrue(updateDonation.segmentNotified); assert.equal(updateDonation.status, DONATION_STATUS.VERIFIED); }); + it('should verify a Arbitrum donation', async () => { + // https://arbiscan.io/tx/0xdaca7d68e784a60a6975fa9937abb6b287d7fe992ff806f8c375cb4c3b2152f3 + + const amount = 0.0038; + + const transactionInfo = { + txHash: + '0xdaca7d68e784a60a6975fa9937abb6b287d7fe992ff806f8c375cb4c3b2152f3', + currency: 'ETH', + networkId: NETWORK_IDS.ARBITRUM_MAINNET, + fromAddress: '0x015e6fbce5119c32db66e7c544365749bb26cf8b', + toAddress: '0x5c66fef6ea22f37e7c1f7eee49e4e116d3fbfc68', + amount, + timestamp: 1708342629, + }; + 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: 100, + 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.isTrue(updateDonation.segmentNotified); + assert.equal(updateDonation.status, DONATION_STATUS.VERIFIED); + }); + it('should verify a erc20 Arbitrum donation', async () => { + // https://arbiscan.io/tx/0xd7ba5a5d8149432217a161559e357904965620b58e776c4482b8b501e092e495 + + const amount = 999.2; + + const transactionInfo = { + txHash: + '0xd7ba5a5d8149432217a161559e357904965620b58e776c4482b8b501e092e495', + currency: 'USDT', + networkId: NETWORK_IDS.ARBITRUM_MAINNET, + fromAddress: '0x62383739d68dd0f844103db8dfb05a7eded5bbe6', + toAddress: '0x513b8c84fb6e36512b641b67de55a18704118fe7', + amount, + timestamp: 1708343905, + }; + 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: 1000, + 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.isTrue(updateDonation.segmentNotified); + assert.equal(updateDonation.status, DONATION_STATUS.VERIFIED); + }); + it('should verify a Arbitrum Sepolia donation', async () => { + // https://sepolia.arbiscan.io/tx/0x25f17541ccb7248d931f2a1e11058a51ffb4db4968ed3e1d4a019ddc2d44802c + + const amount = 0.0069; + + const transactionInfo = { + txHash: + '0x25f17541ccb7248d931f2a1e11058a51ffb4db4968ed3e1d4a019ddc2d44802c', + currency: 'ETH', + networkId: NETWORK_IDS.ARBITRUM_SEPOLIA, + fromAddress: '0xefc58dbf0e606c327868b55334998aacb27f9ef2', + toAddress: '0xc11c479473cd06618fc75816dd6b56be4ac80efd', + amount, + timestamp: 1708344659, + }; + 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: 100, + 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.isTrue(updateDonation.segmentNotified); + assert.equal(updateDonation.status, DONATION_STATUS.VERIFIED); + }); + it('should verify a erc20 Arbitrum Sepolia donation', async () => { + // https://sepolia.arbiscan.io/tx/0x5bcce1bac54ee92ff28e9913e8a002e6e8efc8e8632fdb8e6ebaa16d8c6fd4cb + + const amount = 100; + + const transactionInfo = { + txHash: + '0x5bcce1bac54ee92ff28e9913e8a002e6e8efc8e8632fdb8e6ebaa16d8c6fd4cb', + currency: 'cETH', + networkId: NETWORK_IDS.ARBITRUM_SEPOLIA, + fromAddress: '0x6a446d9d0d153aa07811de2ac8096b87baad305b', + toAddress: '0xf888186663aae1600282c6fb23b764a61937b913', + amount, + timestamp: 1708344801, + }; + 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: 1000, + 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.isTrue(updateDonation.segmentNotified); + assert.equal(updateDonation.status, DONATION_STATUS.VERIFIED); + }); it('should verify a Optimistic donation', async () => { // https://optimistic.etherscan.io/tx/0xc645bd4ebcb1cb249be4b3e4dad46075c973fd30649a39f27f5328ded15074e7 diff --git a/src/utils/networksConfig.ts b/src/utils/networksConfig.ts index 041201478..52ac38fae 100644 --- a/src/utils/networksConfig.ts +++ b/src/utils/networksConfig.ts @@ -35,6 +35,8 @@ const networksConfig = { '44787': { blockExplorer: 'https://explorer.celo.org/alfajores/', }, + '42161': { blockExplorer: 'https://arbiscan.io/' }, + '421614': { blockExplorer: 'https://sepolia.arbiscan.io/' }, }; export default networksConfig; diff --git a/src/utils/validators/graphqlQueryValidators.ts b/src/utils/validators/graphqlQueryValidators.ts index 358dfaad5..767e9ab9c 100644 --- a/src/utils/validators/graphqlQueryValidators.ts +++ b/src/utils/validators/graphqlQueryValidators.ts @@ -220,6 +220,8 @@ const managingFundsValidator = Joi.object({ NETWORK_IDS.POLYGON, NETWORK_IDS.CELO, NETWORK_IDS.CELO_ALFAJORES, + NETWORK_IDS.ARBITRUM_MAINNET, + NETWORK_IDS.ARBITRUM_SEPOLIA, NETWORK_IDS.OPTIMISTIC, NETWORK_IDS.OPTIMISM_GOERLI, NETWORK_IDS.XDAI, diff --git a/src/utils/validators/projectValidator.test.ts b/src/utils/validators/projectValidator.test.ts index 9fb7afdc8..af859414f 100644 --- a/src/utils/validators/projectValidator.test.ts +++ b/src/utils/validators/projectValidator.test.ts @@ -73,17 +73,25 @@ function isWalletAddressSmartContractTestCases() { assert.isTrue(isSmartContract); }); it('should return true for smart contract address in celo', async () => { - // GIV address https://polygonscan.com/address/0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270 const walletAddress = '0x67316300f17f063085Ca8bCa4bd3f7a5a3C66275'; const isSmartContract = await isWalletAddressSmartContract(walletAddress); assert.isTrue(isSmartContract); }); it('should return true for smart contract address in celo alfajores', async () => { - // GIV address https://polygonscan.com/address/0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270 const walletAddress = '0x17bc3304F94c85618c46d0888aA937148007bD3C'; const isSmartContract = await isWalletAddressSmartContract(walletAddress); assert.isTrue(isSmartContract); }); + it('should return true for smart contract address in arbitrum mainnet', async () => { + const walletAddress = '0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE'; + const isSmartContract = await isWalletAddressSmartContract(walletAddress); + assert.isTrue(isSmartContract); + }); + it('should return true for smart contract address in arbitrum sepolia', async () => { + const walletAddress = '0x6b7860b66c0124e8d8c079b279c126ce58c442a2'; + const isSmartContract = await isWalletAddressSmartContract(walletAddress); + assert.isTrue(isSmartContract); + }); } function validateProjectWalletAddressTestCases() { diff --git a/src/utils/validators/projectValidator.ts b/src/utils/validators/projectValidator.ts index d7e9d6845..a9f57b185 100644 --- a/src/utils/validators/projectValidator.ts +++ b/src/utils/validators/projectValidator.ts @@ -140,6 +140,8 @@ export const isWalletAddressSmartContract = async ( NETWORK_IDS.POLYGON, NETWORK_IDS.CELO, NETWORK_IDS.CELO_ALFAJORES, + NETWORK_IDS.ARBITRUM_MAINNET, + NETWORK_IDS.ARBITRUM_SEPOLIA, ]; const _isSmartContracts = await Promise.all( diff --git a/test/pre-test-scripts.ts b/test/pre-test-scripts.ts index f4a1a3aaf..bc683147d 100644 --- a/test/pre-test-scripts.ts +++ b/test/pre-test-scripts.ts @@ -154,6 +154,34 @@ async function seedTokens() { } await Token.create(tokenData as Token).save(); } + for (const token of SEED_DATA.TOKENS.arbitrum_mainnet) { + const tokenData = { + ...token, + networkId: 42161, + isGivbackEligible: true, + }; + if (token.symbol === 'GIV') { + // TODO I'm not sure whether we support GIV or not + (tokenData as any).order = 1; + } else if (token.symbol === 'ETH') { + (tokenData as any).order = 2; + } + await Token.create(tokenData as Token).save(); + } + for (const token of SEED_DATA.TOKENS.arbitrum_sepolia) { + const tokenData = { + ...token, + networkId: 421614, + isGivbackEligible: true, + }; + if (token.symbol === 'GIV') { + // TODO I'm not sure whether we support GIV or not + (tokenData as any).order = 1; + } else if (token.symbol === 'ETH') { + (tokenData as any).order = 2; + } + await Token.create(tokenData as Token).save(); + } for (const token of SEED_DATA.TOKENS.optimistic) { const tokenData = { ...token, diff --git a/test/testUtils.ts b/test/testUtils.ts index 5de1be497..e4af5992f 100644 --- a/test/testUtils.ts +++ b/test/testUtils.ts @@ -1692,6 +1692,38 @@ export const SEED_DATA = { decimals: 18, }, ], + arbitrum_mainnet: [ + { + name: 'Arbitrum ETH', + symbol: 'ETH', + address: '0x0000000000000000000000000000000000000000', + decimals: 18, + coingeckoId: 'ethereum', + }, + { + name: 'usdt', + symbol: 'USDT', + address: '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9', + decimals: 6, + coingeckoId: 'tether', + }, + ], + arbitrum_sepolia: [ + { + name: 'Arbitrum Sepolia native token', + symbol: 'ETH', + address: '0x0000000000000000000000000000000000000000', + decimals: 18, + coingeckoId: 'ethereum', + }, + { + name: 'Chromatic test Eth', + symbol: 'cETH', + address: '0x93252009E644138b906aE1a28792229E577239B9', + decimals: 18, + coingeckoId: 'weth', + }, + ], }, };