diff --git a/packages/api/package.json b/packages/api/package.json index 5341c847..44eedca9 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -42,6 +42,7 @@ "jest": "^27.0.1", "migrate-mongo": "^8.2.3", "mongodb": "^3.6.9", + "node-cache": "^5.1.2", "web3": "^1.3.6", "web3-utils": "^1.3.6" } diff --git a/packages/api/src/fetchSvgs.ts b/packages/api/src/fetchSvgs.ts new file mode 100644 index 00000000..41095e44 --- /dev/null +++ b/packages/api/src/fetchSvgs.ts @@ -0,0 +1,38 @@ +import axios from 'axios' + +const DEFAULT_SVG = '' + +export async function fetchSvgs ( + networksToFech: Array +): Promise<{ [key: string]: string }> { + const BASE_URL = + 'https://raw.githubusercontent.com/witnet/data-feeds-explorer/main/packages/ui/assets/svg/' + + const logosUrls = networksToFech.map((networkToFech: string) => { + return `${BASE_URL}${networkToFech}.svg` + }) + + // Fech all logos from github + const promises = logosUrls.map(url => axios.get(url)) + return new Promise(resolve => { + Promise.allSettled(promises).then(results => { + const svgs = results.map((result, index) => { + if (result.status === 'fulfilled') { + return result.value.data + } else { + console.log(`Error fetching logo from: ${logosUrls[index]}`) + return DEFAULT_SVG + } + }) + + const svgByName = networksToFech.reduce((acc, val, index) => { + return { + ...acc, + [val]: svgs[index] + } + }, {}) + + resolve(svgByName) + }) + }) +} diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 5ca60a3a..caa7eddf 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -10,6 +10,7 @@ import { Web3Middleware } from './web3Middleware/index' import { normalizeNetworkConfig } from './utils/index' import axios from 'axios' import { readDataFeeds } from './readDataFeeds' +import { SvgCache } from './svgCache' async function getDataFeedsRouterConfig (): Promise< RouterDataFeedsConfig | null @@ -28,21 +29,39 @@ async function getDataFeedsRouterConfig (): Promise< } async function main () { + const svgCache = new SvgCache() const mongoManager = new MongoManager() const db = await mongoManager.start() const dataFeedsRouterConfig: RouterDataFeedsConfig = await getDataFeedsRouterConfig() const dataFeeds = await readDataFeeds(dataFeedsRouterConfig) - const networksConfig: Array = normalizeNetworkConfig( - dataFeedsRouterConfig + const networksConfigPartial: Array> = normalizeNetworkConfig(dataFeedsRouterConfig) + + const logosToFech = networksConfigPartial.map( + (networksConfig: NetworksConfig) => { + return networksConfig.chain.toLowerCase() + } + ) + + // const networksLogos: Array = await fetchSvgs(logosToFech) + const networksLogos: { [key: string]: string } = await svgCache.getMany( + logosToFech ) + const networksConfig = networksConfigPartial.map((networksConfig, index) => ({ + ...networksConfig, + logo: networksLogos[logosToFech[index]] + })) + const repositories: Repositories = { feedRepository: new FeedRepository(dataFeeds), resultRequestRepository: new ResultRequestRepository(db, dataFeeds) } const config = { dataFeedsConfig: dataFeeds, - networksConfig: networksConfig + networksConfig } const web3Middleware = new Web3Middleware( @@ -51,7 +70,7 @@ async function main () { ) web3Middleware.listen() - const server = await createServer(repositories, config) + const server = await createServer(repositories, svgCache, config) server .listen({ host: '0.0.0.0', port: process.env.SERVER_PORT }) diff --git a/packages/api/src/loaders/index.ts b/packages/api/src/loaders/index.ts index 98ed3b1d..a10548af 100644 --- a/packages/api/src/loaders/index.ts +++ b/packages/api/src/loaders/index.ts @@ -1,11 +1,14 @@ import DataLoader from 'dataloader' +import { SvgCache } from '../svgCache' import { Repositories, ResultRequestDbObjectNormalized } from '../types' export class Loaders { repositories: Repositories + svgCache: SvgCache - constructor (repositories: Repositories) { + constructor (repositories: Repositories, svgCache: SvgCache) { this.repositories = repositories + this.svgCache = svgCache } // returns a loader that fetches data using the given function private genericLoader (load: (filter) => T) { @@ -30,6 +33,7 @@ export class Loaders { getLoaders (): { lastResult: DataLoader requests: DataLoader + logos: DataLoader } { return { lastResult: this.genericLoader>( @@ -72,6 +76,9 @@ export class Loaders { filter.feedFullName, timestamp ) + }), + logos: new DataLoader(async (logos: Array) => { + return Object.values(await this.svgCache.getMany(logos)) }) } } diff --git a/packages/api/src/repository/Feed.ts b/packages/api/src/repository/Feed.ts index f7cdc7ee..a0b226b2 100644 --- a/packages/api/src/repository/Feed.ts +++ b/packages/api/src/repository/Feed.ts @@ -44,8 +44,8 @@ export class FeedRepository { } return { - feeds, - total: feeds.length + feeds: feeds || [], + total: feeds ? feeds.length : 0 } } diff --git a/packages/api/src/resolvers.ts b/packages/api/src/resolvers.ts index 2ecbe18f..f2a5f2d3 100644 --- a/packages/api/src/resolvers.ts +++ b/packages/api/src/resolvers.ts @@ -1,11 +1,12 @@ import { Context } from './types' + const resolvers = { Query: { feeds: async (_parent, args, { feedRepository }: Context) => { return await feedRepository.getFeedsByNetwork(args.network) }, - networks: (_parent, _args, { config }): Context => { + networks: (_parent, _args, { config }: Context) => { return config.networksConfig }, @@ -21,7 +22,15 @@ const resolvers = { return await feedRepository.get(args.feedFullName) } }, + NetworksConfig: { + logo: async (parent, _args, { loaders }: Context) => { + return await loaders.logos.load(parent.chain.toLowerCase()) + } + }, Feed: { + logo: async (parent, _args, { loaders }: Context) => { + return await loaders.logos.load(parent.name.split('/')[0]) + }, requests: async (parent, args, { loaders }: Context) => { return await loaders.requests.load({ feedFullName: parent.feedFullName, diff --git a/packages/api/src/server.ts b/packages/api/src/server.ts index bcb11c41..0ccdb7a0 100644 --- a/packages/api/src/server.ts +++ b/packages/api/src/server.ts @@ -9,9 +9,11 @@ import { Repositories, NetworksConfig } from './types' +import { SvgCache } from './svgCache' export async function createServer ( repositories: Repositories, + svgCache: SvgCache, config: { dataFeedsConfig: Array networksConfig: Array @@ -29,7 +31,7 @@ export async function createServer ( {} ) - const loaders = new Loaders(repositories) + const loaders = new Loaders(repositories, svgCache) return { ...repositories, config: { diff --git a/packages/api/src/svgCache.ts b/packages/api/src/svgCache.ts new file mode 100644 index 00000000..251c3aba --- /dev/null +++ b/packages/api/src/svgCache.ts @@ -0,0 +1,43 @@ +import NodeCache from 'node-cache' +import { fetchSvgs } from './fetchSvgs' + +export class SvgCache { + cache: NodeCache + + constructor () { + this.cache = new NodeCache({ stdTTL: 1, checkperiod: 1 }) + } + + async get (svgName: string): Promise { + const svg = this.cache.get(svgName) + + if (svg) { + return svg as string + } else { + const fetchedSvg: { [key: string]: string } = await fetchSvgs([svgName]) + + const updatedSvg = fetchedSvg[svgName] + this.cache.set(svgName, updatedSvg) + + return fetchedSvg[svgName] + } + } + + async getMany (svgNames: Array): Promise<{ [key: string]: string }> { + const missingSvgs = svgNames.filter(name => !this.cache.get(name)) + + const fetchedSvg = await fetchSvgs(missingSvgs) + + // set missing svgs + Object.entries(fetchSvgs).forEach(([key, value]) => { + this.cache.set(key, value) + }) + + return svgNames.reduce((acc, name) => { + return { + ...acc, + [name]: this.cache.get(name) || fetchedSvg[name] + } + }, {}) + } +} diff --git a/packages/api/src/typeDefs.ts b/packages/api/src/typeDefs.ts index 7c5ff2c2..26cb6cb9 100644 --- a/packages/api/src/typeDefs.ts +++ b/packages/api/src/typeDefs.ts @@ -20,6 +20,7 @@ const typeDefs = gql` heartbeat: String! finality: String! requests(timestamp: Int!): [ResultRequest]! + logo: String! } type Total { @@ -31,10 +32,11 @@ const typeDefs = gql` total: Int! } - type NetworksConfig @entity { - chain: String - label: String - key: String + type NetworksConfig { + chain: String! + label: String! + key: String! + logo: String! } type ResultRequest @entity { diff --git a/packages/api/src/types.ts b/packages/api/src/types.ts index bcf58655..c028d356 100644 --- a/packages/api/src/types.ts +++ b/packages/api/src/types.ts @@ -18,7 +18,7 @@ export type Context = { resultRequestRepository: ResultRequestRepository config: { feedsConfig: ConfigByFullName - networksConfig: NetworksConfig + networksConfig: Array } loaders: { lastResult: DataLoader @@ -33,6 +33,7 @@ export type Context = { timestamp: number } > + logos: DataLoader } } @@ -85,9 +86,10 @@ export type FeedInfoGeneric = { } export type NetworksConfig = { - chain: String - label: String - key: String + chain: string + label: string + key: string + logo: string } export type FeedInfo = FeedInfoGeneric> diff --git a/packages/api/src/utils/index.ts b/packages/api/src/utils/index.ts index 8a4ba066..af3e70e6 100644 --- a/packages/api/src/utils/index.ts +++ b/packages/api/src/utils/index.ts @@ -58,10 +58,9 @@ function getNetworksListByChain (config: RouterDataFeedsConfig) { } // normalize config to fit network schema - export function normalizeNetworkConfig ( config: RouterDataFeedsConfig -): Array { +): Array> { // Get a list of networks where every element of the array contains another array with networks that belong to a chain. const networks = getNetworksListByChain(config) @@ -74,6 +73,7 @@ export function normalizeNetworkConfig ( }) return networks }, []) + return networkConfig } diff --git a/packages/api/test/feeds.spec.ts b/packages/api/test/feeds.spec.ts index 00231a65..022325df 100644 --- a/packages/api/test/feeds.spec.ts +++ b/packages/api/test/feeds.spec.ts @@ -12,7 +12,10 @@ import { readDataFeeds } from '../src/readDataFeeds' import dataFeedsRouterConfig from '../../api/src/dataFeedsRouter.json' const dataFeeds = readDataFeeds(dataFeedsRouterConfig) -const networksConfig = normalizeNetworkConfig(dataFeedsRouterConfig) +const networksConfig = normalizeNetworkConfig(dataFeedsRouterConfig).map(c => ({ + ...c, + logo: '' +})) const state: { mongoManager: MongoManager @@ -29,11 +32,16 @@ describe.skip('feeds', function () { const mongoManager = new MongoManager() const db = await mongoManager.start(process.env.CI ? ciUri : null) + const get = jest.fn(() => '') + const getMany = jest.fn(arr => arr.map(_ => '')) + const svgCache = jest.fn(() => ({ get, getMany })) + const server = await createServer( { feedRepository: new FeedRepository(dataFeeds), resultRequestRepository: new ResultRequestRepository(db, dataFeeds) }, + svgCache as any, { dataFeedsConfig: dataFeeds, networksConfig: networksConfig diff --git a/packages/api/test/loaders/loaders.spec.ts b/packages/api/test/loaders/loaders.spec.ts index 4dceb2ac..41332f17 100644 --- a/packages/api/test/loaders/loaders.spec.ts +++ b/packages/api/test/loaders/loaders.spec.ts @@ -4,11 +4,17 @@ describe('loaders', () => { describe('lastResult', () => { it('lastResult loader should call getLastResult', async () => { const getLastResultMock = jest.fn(() => ({ feedFullName: 'name' })) - const loaders = new Loaders({ - resultRequestRepository: { - getLastResult: getLastResultMock - } - } as any) + const get = jest.fn(() => '') + const getMany = jest.fn(arr => arr.map(_ => '')) + const svgCache = jest.fn(() => ({ get, getMany })) + const loaders = new Loaders( + { + resultRequestRepository: { + getLastResult: getLastResultMock + } + } as any, + svgCache as any + ) await loaders.getLoaders().lastResult.load('feedName') @@ -17,11 +23,17 @@ describe('loaders', () => { it('lastResult loader should call getLastResult the same amount of times than filters provided', async () => { const getLastResultMock = jest.fn(() => ({ feedFullName: 'name' })) - const loaders = new Loaders({ - resultRequestRepository: { - getLastResult: getLastResultMock - } - } as any) + const get = jest.fn(() => '') + const getMany = jest.fn(arr => arr.map(_ => '')) + const svgCache = jest.fn(() => ({ get, getMany })) + const loaders = new Loaders( + { + resultRequestRepository: { + getLastResult: getLastResultMock + } + } as any, + svgCache as any + ) await loaders.getLoaders().lastResult.load('feedName1') await loaders.getLoaders().lastResult.load('feedName2') @@ -32,11 +44,17 @@ describe('loaders', () => { it('lastResult loader should return the result of calling getLastResult', async () => { const getLastResultMock = jest.fn(() => ({ feedFullName: 'name' })) - const loaders = new Loaders({ - resultRequestRepository: { - getLastResult: getLastResultMock - } - } as any) + const get = jest.fn(() => '') + const getMany = jest.fn(arr => arr.map(_ => '')) + const svgCache = jest.fn(() => ({ get, getMany })) + const loaders = new Loaders( + { + resultRequestRepository: { + getLastResult: getLastResultMock + } + } as any, + svgCache as any + ) const result = await loaders.getLoaders().lastResult.load('feedName') @@ -47,12 +65,18 @@ describe('loaders', () => { describe('getRequests', () => { it('should call getFeedRequests', async () => { const getFeedRequestsMock = jest.fn(() => ({ feedFullName: 'name' })) + const get = jest.fn(() => '') + const getMany = jest.fn(arr => arr.map(_ => '')) + const svgCache = jest.fn(() => ({ get, getMany })) const timestamp = Math.floor(Date.now() / 1000) - 10000 - const loaders = new Loaders({ - resultRequestRepository: { - getFeedRequests: getFeedRequestsMock - } - } as any) + const loaders = new Loaders( + { + resultRequestRepository: { + getFeedRequests: getFeedRequestsMock + } + } as any, + svgCache as any + ) await loaders .getLoaders() @@ -63,13 +87,19 @@ describe('loaders', () => { it('should call getFeedRequests the same amount of times than filters provided', async () => { const getFeedRequestsMock = jest.fn(() => ({ feedFullName: 'name' })) + const get = jest.fn(() => '') + const getMany = jest.fn(arr => arr.map(_ => '')) + const svgCache = jest.fn(() => ({ get, getMany })) const timestamp1 = Math.floor(Date.now() / 1000) - 10000 const timestamp2 = Math.floor(Date.now() / 1000) - 20000 - const loaders = new Loaders({ - resultRequestRepository: { - getFeedRequests: getFeedRequestsMock - } - } as any) + const loaders = new Loaders( + { + resultRequestRepository: { + getFeedRequests: getFeedRequestsMock + } + } as any, + svgCache as any + ) await loaders.getLoaders().requests.load({ feedFullName: 'feedName1', @@ -94,12 +124,18 @@ describe('loaders', () => { it('should return the result of calling getFeedRequests', async () => { const getFeedRequestsMock = jest.fn(() => ({ feedFullName: 'name' })) + const get = jest.fn(() => '') + const getMany = jest.fn(arr => arr.map(_ => '')) + const svgCache = jest.fn(() => ({ get, getMany })) const timestamp = Math.floor(Date.now() / 1000) - 10000 - const loaders = new Loaders({ - resultRequestRepository: { - getFeedRequests: getFeedRequestsMock - } - } as any) + const loaders = new Loaders( + { + resultRequestRepository: { + getFeedRequests: getFeedRequestsMock + } + } as any, + svgCache as any + ) const result = await loaders .getLoaders() diff --git a/packages/api/tsconfig.json b/packages/api/tsconfig.json index 3a5a9f94..25efbd41 100644 --- a/packages/api/tsconfig.json +++ b/packages/api/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "lib": ["ES2019", "DOM"], + "lib": ["ES2020", "DOM"], "target": "es5", "module": "commonjs", "esModuleInterop": true, diff --git a/packages/ui/apollo/queries/feed.gql b/packages/ui/apollo/queries/feed.gql index 8b32431a..42591c08 100644 --- a/packages/ui/apollo/queries/feed.gql +++ b/packages/ui/apollo/queries/feed.gql @@ -23,5 +23,6 @@ query feed($feedFullName: String!, $timestamp: Int!) { } blockExplorer color + logo } } \ No newline at end of file diff --git a/packages/ui/apollo/queries/feeds.gql b/packages/ui/apollo/queries/feeds.gql index 77503991..f5e513e4 100644 --- a/packages/ui/apollo/queries/feeds.gql +++ b/packages/ui/apollo/queries/feeds.gql @@ -13,6 +13,7 @@ query feeds ($network: String!) { color heartbeat finality + logo } total } diff --git a/packages/ui/apollo/queries/networks.gql b/packages/ui/apollo/queries/networks.gql index 98c8fe75..52a88243 100644 --- a/packages/ui/apollo/queries/networks.gql +++ b/packages/ui/apollo/queries/networks.gql @@ -3,5 +3,6 @@ query networks { label, key, chain, + logo } } \ No newline at end of file diff --git a/packages/ui/components/ChainCard.vue b/packages/ui/components/ChainCard.vue index 12f1b7ce..447cfe0e 100644 --- a/packages/ui/components/ChainCard.vue +++ b/packages/ui/components/ChainCard.vue @@ -2,7 +2,7 @@
- +

{{ name }}

{{ count }} {{ $t('feeds') }}

@@ -22,7 +22,7 @@ export default { type: String, required: true, }, - img: { + svg: { type: String, required: true, }, diff --git a/packages/ui/components/DataFeedDetails.vue b/packages/ui/components/DataFeedDetails.vue index bbd46275..5d9573b0 100644 --- a/packages/ui/components/DataFeedDetails.vue +++ b/packages/ui/components/DataFeedDetails.vue @@ -129,6 +129,7 @@ export default { `{address}`, this.feed.proxyAddress ), + logo: this.feed.logo, } } else { return null diff --git a/packages/ui/components/DataFeeds.vue b/packages/ui/components/DataFeeds.vue index 8fd9f7b2..3a2366cd 100644 --- a/packages/ui/components/DataFeeds.vue +++ b/packages/ui/components/DataFeeds.vue @@ -7,7 +7,7 @@ :name="feed.name" :decimals="feed.decimals" :time-to-update="feed.timeToUpdate" - :img="feed.img" + :svg="feed.svg" :value="feed.value" :last-result-timestamp="feed.lastResultTimestamp" :label="feed.label" @@ -23,6 +23,7 @@ import feeds from '@/apollo/queries/feeds.gql' import { formatSvgName } from '../utils/formatSvgName' export default { + name: 'DataFeeds', apollo: { feeds: { prefetch: true, @@ -35,7 +36,6 @@ export default { pollInterval: 60000, }, }, - name: 'DataFeeds', props: { network: { type: Object, @@ -84,6 +84,7 @@ export default { chain: feed.chain, color: feed.color, blockExplorer: feed.blockExplorer, + svg: feed.logo, } }) .sort((feed1, feed2) => feed1.name.localeCompare(feed2.name)) diff --git a/packages/ui/components/FeedCard.vue b/packages/ui/components/FeedCard.vue index 856ce806..68db875e 100644 --- a/packages/ui/components/FeedCard.vue +++ b/packages/ui/components/FeedCard.vue @@ -2,7 +2,7 @@
- +

{{ name.toUpperCase() }}

import networks from '@/apollo/queries/networks.gql' import feeds from '@/apollo/queries/feeds.gql' -import { formatSvgChainName } from '@/utils/formatSvgChainName' -import { generateNavOptions } from '../utils/generateNavOptions' import { generateSelectOptions } from '../utils/generateSelectOptions' export default { @@ -41,9 +39,6 @@ export default { }, }, computed: { - navBarOptions() { - return generateNavOptions(Object.values(this.options)) - }, chainsFeeds() { return this.feeds ? this.feeds : [] }, @@ -55,7 +50,8 @@ export default { } }, supportedChains() { - return this.navBarOptions.map((chain) => { + return Object.values(this.options).map((network) => { + const chain = network[0].chain return { name: chain, count: @@ -67,7 +63,7 @@ export default { network: chain.toLowerCase(), }, }, - img: formatSvgChainName(chain), + svg: network[0].logo, } }) }, diff --git a/packages/ui/components/Main.vue b/packages/ui/components/Main.vue index 12caa6f8..ea6feb64 100644 --- a/packages/ui/components/Main.vue +++ b/packages/ui/components/Main.vue @@ -7,10 +7,8 @@
-

{{ $t('main.network_subtitle') }} diff --git a/packages/ui/components/SupportedChains.vue b/packages/ui/components/SupportedChains.vue index 8f426474..92c59d88 100644 --- a/packages/ui/components/SupportedChains.vue +++ b/packages/ui/components/SupportedChains.vue @@ -5,7 +5,7 @@ :key="chain.label" :name="chain.name" :details-path="chain.detailsPath" - :img="chain.img" + :svg="chain.svg" :count="chain.count" />

diff --git a/packages/ui/components/SvgIcon.vue b/packages/ui/components/SvgIcon.vue index 9a4f2e77..f8a4a612 100644 --- a/packages/ui/components/SvgIcon.vue +++ b/packages/ui/components/SvgIcon.vue @@ -1,13 +1,16 @@ diff --git a/packages/ui/components/cards/ChainCard.vue b/packages/ui/components/cards/ChainCard.vue index 0b6cc6f4..5140a084 100644 --- a/packages/ui/components/cards/ChainCard.vue +++ b/packages/ui/components/cards/ChainCard.vue @@ -2,7 +2,7 @@
- +

{{ name }}

{{ count }} {{ $t('feeds') }}

@@ -12,7 +12,7 @@