Skip to content

Commit

Permalink
Merge pull request #496 from beethovenxfi/feat/featured-pools
Browse files Browse the repository at this point in the history
add featured pools
franzns authored Dec 22, 2023
2 parents db31f65 + bb88610 commit 8fadbac
Showing 13 changed files with 149 additions and 116 deletions.
7 changes: 6 additions & 1 deletion modules/content/content-types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { Chain } from '@prisma/client';
import { GqlChain } from '../../schema';

export interface ConfigHomeScreen {
featuredPoolGroups: HomeScreenFeaturedPoolGroup[];
newsItems: HomeScreenNewsItem[];
@@ -10,6 +13,8 @@ export interface HomeScreenFeaturedPoolGroup {
id: string;
items: (HomeScreenFeaturedPoolGroupItemPoolId | HomeScreenFeaturedPoolGroupItemExternalLink)[];
title: string;
primary: boolean;
chain: GqlChain;
}

interface HomeScreenFeaturedPoolGroupItemPoolId {
@@ -39,6 +44,6 @@ export interface HomeScreenNewsItem {
export interface ContentService {
syncTokenContentData(): Promise<void>;
syncPoolContentData(): Promise<void>;
getFeaturedPoolGroups(): Promise<HomeScreenFeaturedPoolGroup[]>;
getFeaturedPoolGroups(chains: Chain[]): Promise<HomeScreenFeaturedPoolGroup[]>;
getNewsItems(): Promise<HomeScreenNewsItem[]>;
}
47 changes: 35 additions & 12 deletions modules/content/github-content.service.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
import { isSameAddress } from '@balancer-labs/sdk';
import { Prisma } from '@prisma/client';
import { Chain, Prisma } from '@prisma/client';
import axios from 'axios';
import { prisma } from '../../prisma/prisma-client';
import { networkContext } from '../network/network-context.service';
import { ContentService, HomeScreenFeaturedPoolGroup, HomeScreenNewsItem } from './content-types';
import { chainIdToChain } from '../network/network-config';

const POOLS_METADATA_URL = 'https://raw.githubusercontent.com/balancer/metadata/main/pools/featured.json';

const TOKEN_LIST_URL = 'https://raw.githubusercontent.com/balancer/tokenlists/main/generated/balancer.tokenlist.json';

interface FeaturedPoolMetadata {
id: string;
imageUrl: string;
primary: boolean;
chainId: number;
}
interface WhitelistedTokenList {
name: string;
timestamp: string;
@@ -27,16 +36,14 @@ export class GithubContentService implements ContentService {
async syncTokenContentData(): Promise<void> {
const { data: githubAllTokenList } = await axios.get<WhitelistedTokenList>(TOKEN_LIST_URL);

const filteredTokenList = githubAllTokenList.tokens.filter(
(token) => {
if (`${token.chainId}` !== networkContext.chainId) {
return false;
}

const requiredKeys = ['chainId', 'address', 'name', 'symbol', 'decimals']
return requiredKeys.every((key) => token?.[key as keyof WhitelistedToken] != null)
const filteredTokenList = githubAllTokenList.tokens.filter((token) => {
if (`${token.chainId}` !== networkContext.chainId) {
return false;
}
);

const requiredKeys = ['chainId', 'address', 'name', 'symbol', 'decimals'];
return requiredKeys.every((key) => token?.[key as keyof WhitelistedToken] != null);
});

for (const githubToken of filteredTokenList) {
const tokenAddress = githubToken.address.toLowerCase();
@@ -158,8 +165,24 @@ export class GithubContentService implements ContentService {
await prisma.prismaTokenType.createMany({ skipDuplicates: true, data: types });
}
async syncPoolContentData(): Promise<void> {}
async getFeaturedPoolGroups(): Promise<HomeScreenFeaturedPoolGroup[]> {
return [];
async getFeaturedPoolGroups(chains: Chain[]): Promise<HomeScreenFeaturedPoolGroup[]> {
const { data } = await axios.get<FeaturedPoolMetadata[]>(POOLS_METADATA_URL);
const pools = data.filter((pool) => chains.includes(chainIdToChain[pool.chainId]));
return pools.map(({ id, imageUrl, primary, chainId }) => ({
id,
_type: 'homeScreenFeaturedPoolGroupPoolId',
title: 'Popular pools',
items: [
{
_key: '',
_type: 'homeScreenFeaturedPoolGroupPoolId',
poolId: id,
},
],
icon: imageUrl,
chain: chainIdToChain[chainId],
primary: Boolean(primary),
})) as HomeScreenFeaturedPoolGroup[];
}
async getNewsItems(): Promise<HomeScreenNewsItem[]> {
return [];
113 changes: 64 additions & 49 deletions modules/content/sanity-content.service.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { isSameAddress } from '@balancer-labs/sdk';
import { Prisma, PrismaPoolCategoryType } from '@prisma/client';
import { Chain, Prisma, PrismaPoolCategoryType } from '@prisma/client';
import { prisma } from '../../prisma/prisma-client';
import { networkContext } from '../network/network-context.service';
import { ConfigHomeScreen, ContentService, HomeScreenFeaturedPoolGroup, HomeScreenNewsItem } from './content-types';
import SanityClient from '@sanity/client';
import { env } from '../../app/env';
import { chainToIdMap } from '../network/network-config';

interface SanityToken {
name: string;
@@ -35,9 +35,15 @@ const SANITY_TOKEN_TYPE_MAP: { [key: string]: string } = {
};

export class SanityContentService implements ContentService {
constructor(
private readonly chain: Chain,
private readonly projectId = '1g2ag2hb',
private readonly dataset = 'production',
) {}

async syncTokenContentData(): Promise<void> {
const sanityTokens = await getSanityClient().fetch<SanityToken[]>(`
*[_type=="${SANITY_TOKEN_TYPE_MAP[networkContext.chainId]}"] {
const sanityTokens = await this.getSanityClient().fetch<SanityToken[]>(`
*[_type=="${SANITY_TOKEN_TYPE_MAP[chainToIdMap[this.chain]]}"] {
name,
address,
symbol,
@@ -77,12 +83,12 @@ export class SanityContentService implements ContentService {

await prisma.prismaToken.upsert({
where: {
address_chain: { address: tokenAddress, chain: networkContext.chain },
address_chain: { address: tokenAddress, chain: this.chain },
},
create: {
name: sanityToken.name,
address: tokenAddress,
chain: networkContext.chain,
chain: this.chain,
symbol: sanityToken.symbol,
decimals: sanityToken.decimals,
logoURI: sanityToken.logoURI,
@@ -110,7 +116,7 @@ export class SanityContentService implements ContentService {
const whiteListedTokens = await prisma.prismaTokenType.findMany({
where: {
type: 'WHITE_LISTED',
chain: networkContext.chain,
chain: this.chain,
},
});

@@ -125,15 +131,15 @@ export class SanityContentService implements ContentService {
await prisma.prismaTokenType.createMany({
data: addToWhitelist.map((token) => ({
id: `${token.address}-white-listed`,
chain: networkContext.chain,
chain: this.chain,
tokenAddress: token.address.toLowerCase(),
type: 'WHITE_LISTED' as const,
})),
skipDuplicates: true,
});

await prisma.prismaTokenType.deleteMany({
where: { id: { in: removeFromWhitelist.map((token) => token.id) }, chain: networkContext.chain },
where: { id: { in: removeFromWhitelist.map((token) => token.id) }, chain: this.chain },
});

await this.syncTokenTypes();
@@ -143,7 +149,7 @@ export class SanityContentService implements ContentService {
const pools = await this.loadPoolData();
const tokens = await prisma.prismaToken.findMany({
include: { types: true },
where: { chain: networkContext.chain },
where: { chain: this.chain },
});
const types: Prisma.PrismaTokenTypeCreateManyInput[] = [];

@@ -154,7 +160,7 @@ export class SanityContentService implements ContentService {
if (pool && !tokenTypes.includes('BPT')) {
types.push({
id: `${token.address}-bpt`,
chain: networkContext.chain,
chain: this.chain,
type: 'BPT',
tokenAddress: token.address,
});
@@ -163,7 +169,7 @@ export class SanityContentService implements ContentService {
if ((pool?.type === 'PHANTOM_STABLE' || pool?.type === 'LINEAR') && !tokenTypes.includes('PHANTOM_BPT')) {
types.push({
id: `${token.address}-phantom-bpt`,
chain: networkContext.chain,
chain: this.chain,
type: 'PHANTOM_BPT',
tokenAddress: token.address,
});
@@ -176,7 +182,7 @@ export class SanityContentService implements ContentService {
if (linearPool && !tokenTypes.includes('LINEAR_WRAPPED_TOKEN')) {
types.push({
id: `${token.address}-linear-wrapped`,
chain: networkContext.chain,
chain: this.chain,
type: 'LINEAR_WRAPPED_TOKEN',
tokenAddress: token.address,
});
@@ -188,7 +194,7 @@ export class SanityContentService implements ContentService {

private async loadPoolData() {
return prisma.prismaPool.findMany({
where: { chain: networkContext.chain },
where: { chain: this.chain },
select: {
address: true,
symbol: true,
@@ -201,7 +207,9 @@ export class SanityContentService implements ContentService {
}

public async syncPoolContentData(): Promise<void> {
const response = await getSanityClient().fetch(`*[_type == "config" && chainId == ${networkContext.chainId}][0]{
const response = await this.getSanityClient().fetch(`*[_type == "config" && chainId == ${
chainToIdMap[this.chain]
}][0]{
incentivizedPools,
blacklistedPools,
}`);
@@ -211,7 +219,7 @@ export class SanityContentService implements ContentService {
blacklistedPools: response?.blacklistedPools ?? [],
};

const categories = await prisma.prismaPoolCategory.findMany({ where: { chain: networkContext.chain } });
const categories = await prisma.prismaPoolCategory.findMany({ where: { chain: this.chain } });
const incentivized = categories.filter((item) => item.category === 'INCENTIVIZED').map((item) => item.poolId);
const blacklisted = categories.filter((item) => item.category === 'BLACK_LISTED').map((item) => item.poolId);

@@ -225,7 +233,7 @@ export class SanityContentService implements ContentService {

// make sure the pools really exist to prevent sanity mistakes from breaking the system
const pools = await prisma.prismaPool.findMany({
where: { id: { in: itemsToAdd }, chain: networkContext.chain },
where: { id: { in: itemsToAdd }, chain: this.chain },
select: { id: true },
});
const poolIds = pools.map((pool) => pool.id);
@@ -235,46 +243,53 @@ export class SanityContentService implements ContentService {
prisma.prismaPoolCategory.createMany({
data: existingItemsToAdd.map((poolId) => ({
id: `${poolId}-${category}`,
chain: networkContext.chain,
chain: this.chain,
category,
poolId,
})),
skipDuplicates: true,
}),
prisma.prismaPoolCategory.deleteMany({
where: { poolId: { in: itemsToRemove }, category, chain: networkContext.chain },
where: { poolId: { in: itemsToRemove }, category, chain: this.chain },
}),
]);
}

public async getFeaturedPoolGroups(): Promise<HomeScreenFeaturedPoolGroup[]> {
const data = await getSanityClient().fetch<ConfigHomeScreen | null>(`
*[_type == "homeScreen" && chainId == ${networkContext.chainId}][0]{
...,
"featuredPoolGroups": featuredPoolGroups[]{
public async getFeaturedPoolGroups(chains: Chain[]): Promise<HomeScreenFeaturedPoolGroup[]> {
const featuredPoolGroups: HomeScreenFeaturedPoolGroup[] = [];
for (const chain of chains) {
const data = await this.getSanityClient().fetch<ConfigHomeScreen | null>(`
*[_type == "homeScreen" && chainId == ${chainToIdMap[chain]}][0]{
...,
"icon": icon.asset->url + "?w=64",
"items": items[]{
"featuredPoolGroups": featuredPoolGroups[]{
...,
"icon": icon.asset->url + "?w=64",
"items": items[]{
...,
"image": image.asset->url + "?w=600"
}
},
"newsItems": newsItems[]{
...,
"image": image.asset->url + "?w=600"
"image": image.asset->url + "?w=800"
}
},
"newsItems": newsItems[]{
...,
"image": image.asset->url + "?w=800"
}
`);
if (data) {
featuredPoolGroups.push(
...data.featuredPoolGroups.map((pool) => ({
...pool,
chain: chain,
})),
);
}
}
`);

if (data?.featuredPoolGroups) {
return data.featuredPoolGroups;
}
throw new Error(`No featured pool groups found for chain id ${networkContext.chainId}`);
return featuredPoolGroups;
}

public async getNewsItems(): Promise<HomeScreenNewsItem[]> {
const data = await getSanityClient().fetch<ConfigHomeScreen | null>(`
*[_type == "homeScreen" && chainId == ${networkContext.chainId}][0]{
const data = await this.getSanityClient().fetch<ConfigHomeScreen | null>(`
*[_type == "homeScreen" && chainId == ${chainToIdMap[this.chain]}][0]{
...,
"featuredPoolGroups": featuredPoolGroups[]{
...,
@@ -294,16 +309,16 @@ export class SanityContentService implements ContentService {
if (data?.newsItems) {
return data.newsItems;
}
throw new Error(`No news items found for chain id ${networkContext.chainId}`);
throw new Error(`No news items found for chain id ${this.chain}`);
}
}

export function getSanityClient() {
return SanityClient({
projectId: networkContext.data.sanity!.projectId,
dataset: networkContext.data.sanity!.dataset,
apiVersion: '2021-12-15',
token: env.SANITY_API_TOKEN,
useCdn: false,
});
private getSanityClient() {
return SanityClient({
projectId: this.projectId,
dataset: this.dataset,
apiVersion: '2021-12-15',
token: env.SANITY_API_TOKEN,
useCdn: false,
});
}
}
19 changes: 4 additions & 15 deletions modules/context/header-chain.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,18 @@
import { getRequestScopeContextValue } from '../context/request-scoped-context';
import { Chain } from '@prisma/client';

const chainIdToChain: { [id: string]: Chain } = {
'1': Chain.MAINNET,
'10': Chain.OPTIMISM,
'100': Chain.GNOSIS,
'137': Chain.POLYGON,
'250': Chain.FANTOM,
'1101': Chain.ZKEVM,
'8453': Chain.BASE,
'42161': Chain.ARBITRUM,
'43114': Chain.AVALANCHE,
}
import { chainIdToChain } from '../network/network-config';

/**
* Setup to transition out from the old header-based chainIDs to the new required chain query filters.
*
*
* @returns The chain of the current request, if any.
*/
export const headerChain = (): Chain | undefined => {
const chainId = getRequestScopeContextValue<string>('chainId');

if (chainId) {
return chainIdToChain[chainId];
}

return undefined;
}
};
4 changes: 0 additions & 4 deletions modules/network/base.ts
Original file line number Diff line number Diff line change
@@ -56,10 +56,6 @@ const baseNetworkData: NetworkData = {
},
rpcUrl: 'https://base.gateway.tenderly.co/7mM7DbBouY1JjnQd9MMDsd',
rpcMaxBlockRange: 500,
sanity: {
projectId: '',
dataset: '',
},
protocolToken: 'bal',
bal: {
address: '0x4158734d47fc9692176b5085e0f52ee0da5d47f1',
6 changes: 1 addition & 5 deletions modules/network/fantom.ts
Original file line number Diff line number Diff line change
@@ -101,10 +101,6 @@ const fantomNetworkData: NetworkData = {
? `https://rpc.ankr.com/fantom`
: `https://rpc.fantom.gateway.fm`,
rpcMaxBlockRange: 1000,
sanity: {
projectId: '1g2ag2hb',
dataset: 'production',
},
protocolToken: 'beets',
beets: {
address: '0xf24bcf4d1e507740041c9cfd2dddb29585adce1e',
@@ -298,7 +294,7 @@ const fantomNetworkData: NetworkData = {

export const fantomNetworkConfig: NetworkConfig = {
data: fantomNetworkData,
contentService: new SanityContentService(),
contentService: new SanityContentService(fantomNetworkData.chain.prismaId),
provider: new ethers.providers.JsonRpcProvider({ url: fantomNetworkData.rpcUrl, timeout: 60000 }),
poolAprServices: [
new IbTokensAprService(
4 changes: 0 additions & 4 deletions modules/network/network-config-types.ts
Original file line number Diff line number Diff line change
@@ -77,10 +77,6 @@ export interface NetworkData {
veBalLocks?: string;
userBalances: string;
};
sanity?: {
projectId: string;
dataset: string;
};
protocolToken: 'beets' | 'bal';
beets?: {
address: string;
12 changes: 12 additions & 0 deletions modules/network/network-config.ts
Original file line number Diff line number Diff line change
@@ -34,6 +34,18 @@ export const AllNetworkConfigsKeyedOnChain: { [chain in Chain]: NetworkConfig }
BASE: baseNetworkConfig,
};

export const chainIdToChain: { [id: string]: Chain } = {
'1': Chain.MAINNET,
'10': Chain.OPTIMISM,
'100': Chain.GNOSIS,
'137': Chain.POLYGON,
'250': Chain.FANTOM,
'1101': Chain.ZKEVM,
'8453': Chain.BASE,
'42161': Chain.ARBITRUM,
'43114': Chain.AVALANCHE,
};

export const BalancerChainIds = ['1', '137', '42161', '100', '1101', '43114', '8453'];
export const BeethovenChainIds = ['250', '10'];

6 changes: 1 addition & 5 deletions modules/network/optimism.ts
Original file line number Diff line number Diff line change
@@ -60,10 +60,6 @@ const optimismNetworkData: NetworkData = {
? `https://optimism-mainnet.infura.io/v3/${env.INFURA_API_KEY}`
: 'https://mainnet.optimism.io',
rpcMaxBlockRange: 2000,
sanity: {
projectId: '1g2ag2hb',
dataset: 'production',
},
protocolToken: 'beets',
beets: {
address: '0xb4bc46bc6cb217b59ea8f4530bae26bf69f677f0',
@@ -257,7 +253,7 @@ const optimismNetworkData: NetworkData = {

export const optimismNetworkConfig: NetworkConfig = {
data: optimismNetworkData,
contentService: new SanityContentService(),
contentService: new SanityContentService(optimismNetworkData.chain.prismaId),
provider: new ethers.providers.JsonRpcProvider({ url: optimismNetworkData.rpcUrl, timeout: 60000 }),
poolAprServices: [
new IbTokensAprService(
15 changes: 13 additions & 2 deletions modules/pool/lib/pool-gql-loader.service.ts
Original file line number Diff line number Diff line change
@@ -42,6 +42,10 @@ import { networkContext } from '../../network/network-context.service';
import { fixedNumber } from '../../view-helpers/fixed-number';
import { parseUnits } from 'ethers/lib/utils';
import { formatFixed } from '@ethersproject/bignumber';
import { BalancerChainIds, BeethovenChainIds, chainIdToChain, chainToIdMap } from '../../network/network-config';
import { GithubContentService } from '../../content/github-content.service';
import SanityClientConstructor from '@sanity/client';
import { SanityContentService } from '../../content/sanity-content.service';

export class PoolGqlLoaderService {
public async getPool(id: string, chain: Chain, userAddress?: string): Promise<GqlPoolUnion> {
@@ -154,8 +158,15 @@ export class PoolGqlLoaderService {
return prisma.prismaPool.count({ where: this.mapQueryArgsToPoolQuery(args).where });
}

public async getFeaturedPoolGroups(): Promise<GqlPoolFeaturedPoolGroup[]> {
const featuredPoolGroups = await networkContext.config.contentService.getFeaturedPoolGroups();
public async getFeaturedPoolGroups(chains: Chain[]): Promise<GqlPoolFeaturedPoolGroup[]> {
const featuredPoolGroups = [];
if (chains.some((chain) => BalancerChainIds.includes(chainToIdMap[chain]))) {
const githubContentService = new GithubContentService();
featuredPoolGroups.push(...(await githubContentService.getFeaturedPoolGroups(chains)));
} else if (chains.some((chain) => BeethovenChainIds.includes(chainToIdMap[chain]))) {
const sanityContentService = new SanityContentService('FANTOM');
featuredPoolGroups.push(...(await sanityContentService.getFeaturedPoolGroups(chains)));
}
const poolIds = featuredPoolGroups
.map((group) =>
group.items
4 changes: 3 additions & 1 deletion modules/pool/pool.gql
Original file line number Diff line number Diff line change
@@ -19,7 +19,7 @@ extend type Query {
poolGetSwaps(first: Int, skip: Int, where: GqlPoolSwapFilter): [GqlPoolSwap!]!
poolGetBatchSwaps(first: Int, skip: Int, where: GqlPoolSwapFilter): [GqlPoolBatchSwap!]!
poolGetJoinExits(first: Int, skip: Int, where: GqlPoolJoinExitFilter): [GqlPoolJoinExit!]!
poolGetFeaturedPoolGroups: [GqlPoolFeaturedPoolGroup!]!
poolGetFeaturedPoolGroups(chains: [GqlChain!]): [GqlPoolFeaturedPoolGroup!]!
poolGetSnapshots(id: String!, chain: GqlChain, range: GqlPoolSnapshotDataRange!): [GqlPoolSnapshot!]!
poolGetAllPoolsSnapshots(chains: [GqlChain!], range: GqlPoolSnapshotDataRange!): [GqlPoolSnapshot!]!
poolGetLinearPools(chains: [GqlChain!]): [GqlPoolLinear!]!
@@ -820,6 +820,8 @@ type GqlPoolFeaturedPoolGroup {
id: ID!
title: String!
icon: String!
primary: Boolean!
chain: GqlChain!
items: [GqlPoolFeaturedPoolGroupItem!]!
}

10 changes: 8 additions & 2 deletions modules/pool/pool.resolvers.ts
Original file line number Diff line number Diff line change
@@ -49,8 +49,14 @@ const balancerResolvers: Resolvers = {
}
return poolService.getPoolJoinExits(args);
},
poolGetFeaturedPoolGroups: async (parent, args, context) => {
return poolService.getFeaturedPoolGroups();
poolGetFeaturedPoolGroups: async (parent, { chains }, context) => {
const currentChain = headerChain();
if (!chains && currentChain) {
chains = [currentChain];
} else if (!chains) {
throw new Error('poolGetFeaturedPoolGroups error: Provide "chains" param');
}
return poolService.getFeaturedPoolGroups(chains);
},
poolGetSnapshots: async (parent, { id, chain, range }, context) => {
const currentChain = headerChain();
18 changes: 2 additions & 16 deletions modules/pool/pool.service.ts
Original file line number Diff line number Diff line change
@@ -36,8 +36,6 @@ import { reliquarySubgraphService } from '../subgraphs/reliquary-subgraph/reliqu
import { ReliquarySnapshotService } from './lib/reliquary-snapshot.service';
import { ContentService } from '../content/content-types';

const FEATURED_POOL_GROUPS_CACHE_KEY = `pool:featuredPoolGroups`;

export class PoolService {
private cache = new Cache<string, any>();
constructor(
@@ -118,20 +116,8 @@ export class PoolService {
return this.poolSwapService.getJoinExits(args);
}

public async getFeaturedPoolGroups(): Promise<GqlPoolFeaturedPoolGroup[]> {
const cached: GqlPoolFeaturedPoolGroup[] = await this.cache.get(
`${FEATURED_POOL_GROUPS_CACHE_KEY}:${this.chainId}`,
);

if (cached) {
return cached;
}

const featuredPoolGroups = await this.poolGqlLoaderService.getFeaturedPoolGroups();

this.cache.put(`${FEATURED_POOL_GROUPS_CACHE_KEY}:${this.chainId}`, featuredPoolGroups, 60 * 5 * 1000);

return featuredPoolGroups;
public async getFeaturedPoolGroups(chains: Chain[]): Promise<GqlPoolFeaturedPoolGroup[]> {
return this.poolGqlLoaderService.getFeaturedPoolGroups(chains);
}

public async getSnapshotsForAllPools(chains: Chain[], range: GqlPoolSnapshotDataRange) {

0 comments on commit 8fadbac

Please sign in to comment.