Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add featured pools #496

Merged
merged 14 commits into from
Dec 22, 2023
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[];
Expand All @@ -10,6 +13,8 @@ export interface HomeScreenFeaturedPoolGroup {
id: string;
items: (HomeScreenFeaturedPoolGroupItemPoolId | HomeScreenFeaturedPoolGroupItemExternalLink)[];
title: string;
primary: boolean;
chain: GqlChain;
}

interface HomeScreenFeaturedPoolGroupItemPoolId {
Expand Down Expand Up @@ -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;
franzns marked this conversation as resolved.
Show resolved Hide resolved
chainId: number;
}
interface WhitelistedTokenList {
name: string;
timestamp: string;
Expand All @@ -27,16 +36,14 @@ export class GithubContentService implements ContentService {
async syncTokenContentData(): Promise<void> {
Copy link
Contributor

Choose a reason for hiding this comment

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

would it make sense to sync all the tokens at once without splitting per chain? i'm not sure if we aren't overcomplicating it here, since the whole list is fetched anyways.

Copy link
Contributor

Choose a reason for hiding this comment

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

bit lower there is upsert call, it can take chain as token.chainId instead of networkContext

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();
Expand Down Expand Up @@ -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);
Copy link
Contributor

@gmbronco gmbronco Dec 22, 2023

Choose a reason for hiding this comment

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

we have native fetch now, how about we move away from axios?

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 [];
Expand Down
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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 },
Copy link
Contributor

Choose a reason for hiding this comment

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

isn't the chain returned in the response? is so we could use it here instead of setting it up externally

},
create: {
name: sanityToken.name,
address: tokenAddress,
chain: networkContext.chain,
chain: this.chain,
symbol: sanityToken.symbol,
decimals: sanityToken.decimals,
logoURI: sanityToken.logoURI,
Expand Down Expand Up @@ -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,
},
});

Expand All @@ -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();
Expand All @@ -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[] = [];

Expand All @@ -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,
});
Expand All @@ -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,
});
Expand All @@ -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,
});
Expand All @@ -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,
Expand All @@ -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,
}`);
Expand All @@ -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);

Expand All @@ -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);
Expand All @@ -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[]{
...,
Expand All @@ -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
Expand Up @@ -56,10 +56,6 @@ const baseNetworkData: NetworkData = {
},
rpcUrl: 'https://base.gateway.tenderly.co/7mM7DbBouY1JjnQd9MMDsd',
rpcMaxBlockRange: 500,
sanity: {
projectId: '',
dataset: '',
},
protocolToken: 'bal',
bal: {
address: '0x4158734d47fc9692176b5085e0f52ee0da5d47f1',
Expand Down
Loading