Skip to content

Commit

Permalink
Merge pull request #226 from 1Hive/token-price-caching
Browse files Browse the repository at this point in the history
[DONT MERGE BEFORE Uniswap and Network] - Token price caching
  • Loading branch information
Corantin committed Apr 14, 2022
2 parents 18cfc43 + bb9531b commit 15d65c5
Show file tree
Hide file tree
Showing 12 changed files with 1,361 additions and 219 deletions.
3 changes: 3 additions & 0 deletions packages/react-app/config/jest/setupTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const { TextDecoder } = require('util');

global.TextDecoder = TextDecoder;
23 changes: 15 additions & 8 deletions packages/react-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,6 @@
"@sentry/react": "^6.3.5",
"@sentry/tracing": "^6.3.5",
"@sentry/types": "^6.17.2",
"@types/jest": "^27.0.2",
"@types/node": "^16.11.0",
"@types/react": "^17.0.30",
"@types/react-dom": "^17.0.9",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"@uniswap/sdk-core": "^3.0.1",
"@uniswap/v2-sdk": "^3.0.1",
"@urql/devtools": "^2.0.2",
Expand Down Expand Up @@ -95,6 +89,12 @@
"@babel/preset-react": "^7.7.4",
"@sentry/webpack-plugin": "^1.15.1",
"@testing-library/react-hooks": "^7.0.2",
"@types/jest": "^27.4.1",
"@types/node": "^16.11.0",
"@types/react": "^17.0.30",
"@types/react-dom": "^17.0.9",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"@types/lodash-es": "^4.17.5",
"@types/react-router-dom": "^5.3.1",
"@types/styled-components": "^5.1.15",
Expand All @@ -104,6 +104,7 @@
"cross-env": "^7.0.2",
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-prettier": "^3.3.1",
"jest": "^27.5.1",
"parcel-bundler": "^1.12.4",
"prettier": "^2.2.1",
"pretty-quick": "^3.1.0",
Expand All @@ -114,7 +115,8 @@
"rimraf": "^2.6.2",
"sass": "^1.39.0",
"sass-loader": "^11.0.1",
"svgo": "^1.3.2"
"svgo": "^1.3.2",
"ts-jest": "^27.1.4"
},
"scripts": {
"start": "cross-env HTTPS=true npm run sync-assets && react-app-rewired start",
Expand Down Expand Up @@ -143,9 +145,14 @@
"styled-components": "^5"
},
"jest": {
"setupFiles": [
"./config/jest/setupTest.js"
],
"verbose": true,
"moduleNameMapper": {
"^lodash-es$": "lodash"
}
},
"preset": "ts-jest",
"testEnvironment": "node"
}
}
3 changes: 1 addition & 2 deletions packages/react-app/src/models/token-amount.model.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { BigNumber } from 'ethers';
import { TokenModel } from './token.model';

export type TokenAmountModel = {
parsedAmount: number;
usdValue?: BigNumber; // Only set when fetching from getBalanceOf
usdValue?: number; // Only set when fetching from getBalanceOf
token: TokenModel;
};
83 changes: 0 additions & 83 deletions packages/react-app/src/queries/quest-entity.query.ts

This file was deleted.

127 changes: 127 additions & 0 deletions packages/react-app/src/queries/quests.query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import request from 'graphql-request';
import gql from 'graphql-tag';
import { ENUM_QUEST_STATE, GQL_MAX_INT_MS } from 'src/constants';
import { FilterModel } from 'src/models/filter.model';
import { getNetwork } from 'src/networks';
import { msToSec } from 'src/utils/date.utils';

const { questsSubgraph } = getNetwork();

const QuestEntityQuery = gql`
query questEntity($ID: String) {
questEntity(id: $ID, subgraphError: allow) {
id
questAddress
questTitle
questDescription
questExpireTimeSec
questDetailsRef
questRewardTokenAddress
creationTimestamp
}
}
`;

const QuestEntitiesQuery = gql`
query questEntities(
$first: Int
$skip: Int
$expireTimeLower: Int
$expireTimeUpper: Int
$address: String
$title: String
$description: String
) {
questEntities(
first: $first
skip: $skip
where: {
questExpireTimeSec_gte: $expireTimeLower
questExpireTimeSec_lte: $expireTimeUpper
questTitle_contains: $title
questDescription_contains: $description
}
orderBy: creationTimestamp
orderDirection: desc
subgraphError: allow
) {
id
questAddress
questTitle
questDescription
questExpireTimeSec
questDetailsRef
questRewardTokenAddress
creationTimestamp
}
}
`;

// TODO : Uncoment when subgraph have support for combining where and full text query
// const QuestSearchQuery = gql`
// query questSearch($first: Int, $skip: Int, $text: String) {
// questSearch(first: $first, skip: $skip, text: $text) {
// id
// questAddress
// questTitle
// questDescription
// questExpireTimeSec
// questDetailsRef
// questRewardTokenAddress
// creationTimestamp
// }
// }
// `;

const QuestRewardTokens = gql`
query questEntities($first: Int) {
questEntities(first: $first, orderBy: creationTimestamp, orderDirection: desc) {
questRewardTokenAddress
}
}
`;

const QuestEntitiesLight = gql`
query questEntities($expireTimeLower: Int) {
questEntities(where: { questExpireTimeSec_gt: $expireTimeLower }) {
id
questRewardTokenAddress
}
}
`;

export const fetchQuestEnity = (questAddress: string) =>
request(questsSubgraph, QuestEntityQuery, {
ID: questAddress.toLowerCase(), // Subgraph address are stored lowercase
}).then((res) => res.questEntity);

export const fetchQuestEntities = (currentIndex: number, count: number, filter: FilterModel) => {
let expireTimeLowerMs = 0;
let expireTimeUpperMs = GQL_MAX_INT_MS;
if (filter.status === ENUM_QUEST_STATE.Active) {
expireTimeLowerMs = Math.max(filter.minExpireTime?.getTime() ?? 0, Date.now());
} else if (filter.status === ENUM_QUEST_STATE.Expired) {
expireTimeLowerMs = Math.min(filter.minExpireTime?.getTime() ?? 0, Date.now());
expireTimeUpperMs = Date.now();
} else {
expireTimeLowerMs = filter.minExpireTime?.getTime() ?? 0;
}
return request(questsSubgraph, QuestEntitiesQuery, {
skip: currentIndex,
first: count,
expireTimeLower: Math.round(expireTimeLowerMs / 1000),
expireTimeUpper: Math.round(expireTimeUpperMs / 1000),
title: filter.title,
description: filter.description,
}).then((res) => res.questEntities);
};

export const fetchQuestRewardTokens = () =>
request(questsSubgraph, QuestRewardTokens, { first: 100 }).then((res) =>
res.questEntities.map((quest: any) => quest.questRewardTokenAddress),
);

export const fetchActiveQuestEntitiesLight = () =>
request(questsSubgraph, QuestEntitiesLight, {
expireTimeLower: msToSec(Date.now()),
});
110 changes: 110 additions & 0 deletions packages/react-app/src/services/__tests__/quest.service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { BigNumber } from 'ethers';
import { getDashboardInfo } from '../quest.service';

const token1 = '0x6e7c3BC98bee14302AA2A98B4c5C86b13eB4b6Cd';
const token2 = '0xa0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48';
const token3 = '0x6B175474E89094C44Da98b954EedeAC495271d0F';

const quests = [
{
questAddress: '0x1',
questTitle: 'Quest 1',
questDescription: 'Quest 1 description',
questExpireTimeSec: new Date().getTime() + 1000,
creationTimestamp: new Date().getTime(),
questRewardTokenAddress: token1,
questDetailsRef: '0x1',
},
{
questAddress: '0x1',
questTitle: 'Quest 2',
questDescription: 'Quest 2 description',
questExpireTimeSec: new Date().getTime() + 1000,
creationTimestamp: new Date().getTime(),
questRewardTokenAddress: token2,
questDetailsRef: '0x1',
},
{
questAddress: '0x1',
questTitle: 'Quest 3',
questDescription: 'Quest 3 description',
questExpireTimeSec: new Date().getTime() + 1000,
creationTimestamp: new Date().getTime(),
questRewardTokenAddress: token3,
questDetailsRef: '0x1',
},
];

const mockFetchQuestEntitiesLight = jest.fn();
jest.mock('../../../src/queries/quests.query', () => ({
fetchQuestEntitiesLight: () => mockFetchQuestEntitiesLight(),
}));

const mockCacheFetchTokenPrice = jest.fn();
jest.mock('../../../src/services/cache.service', () => ({
cacheFetchTokenPrice: () => mockCacheFetchTokenPrice(),
}));

const mockGetERC20Contract = jest.fn();
const mockGetTokenInfo = jest.fn();
jest.mock('../../../src/utils/contract.util', () => ({
getERC20Contract: () => mockGetERC20Contract(),
getTokenInfo: () => mockGetTokenInfo(),
}));

const mockGetObjectFromIpfs = jest.fn();
jest.mock('../ipfs.service', () => ({
getObjectFromIpfs: () => mockGetObjectFromIpfs(),
}));

describe('QuestService', () => {
describe('getDashboardInfo', () => {
beforeEach(() => {
mockFetchQuestEntitiesLight.mockReturnValue(Promise.resolve({ questEntities: quests }));
mockCacheFetchTokenPrice.mockReturnValue(Promise.resolve(BigNumber.from(1)));
/* (token: TokenModel) => {
switch (token.token) {
case token1:
return Promise.resolve(BigNumber.from(1));
case token2:
return Promise.resolve(BigNumber.from(2));
case token3:
return Promise.resolve(BigNumber.from(0));
default:
return Promise.resolve(BigNumber.from(0));
}
}); */
mockGetERC20Contract.mockReturnValue({
balanceOf: () => Promise.resolve(BigNumber.from(1)), // Always 1 to make test easy
symbol: () => Promise.resolve('TEST'),
name: () => Promise.resolve('TestToken'),
decimals: () => Promise.resolve(18),
});
mockGetTokenInfo.mockReturnValue(
Promise.resolve({
symbol: 'TEST',
decimals: 18,
name: 'TestToken',
}),
);
mockGetObjectFromIpfs.mockReturnValue(Promise.resolve('Quest description fetched from ipfs'));
});
it('should return dashboard correct number of quests', async () => {
// Arrange

// Act
const res = await getDashboardInfo();
// Assert
expect(res.questCount === quests.length);
});
it('should return dashboard correct total', async () => {
// Arrange

// Act
const res = await getDashboardInfo();
// Assert
expect(res).toBeTruthy();
expect(res.totalFunds === 4);
});
});
});
Loading

1 comment on commit 15d65c5

@vercel
Copy link

@vercel vercel bot commented on 15d65c5 Apr 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

quests – ./

quests-git-main-1hive.vercel.app
quests-1hive.vercel.app
quests.vercel.app

Please sign in to comment.