Skip to content

Commit

Permalink
Merge branch 'main' into feat/vote-list
Browse files Browse the repository at this point in the history
  • Loading branch information
agualis committed Dec 19, 2024
2 parents 6829781 + 235dd39 commit 0212124
Show file tree
Hide file tree
Showing 8 changed files with 301 additions and 3 deletions.
5 changes: 5 additions & 0 deletions packages/lib/modules/pool/__mocks__/getPoolMock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from '@repo/lib/shared/services/api/generated/graphql'
import { nested50WETH_50_3poolId } from '@repo/lib/debug-helpers'
import { Address } from 'viem'
import { PoolExample } from './pool-examples/flat'

function astToQueryString(ast: any): string {
return print(ast)
Expand Down Expand Up @@ -47,3 +48,7 @@ export async function getPoolMock(

return getPoolQuery.pool as GqlPoolElement
}

export function getPoolForTest(poolExample: PoolExample): Promise<GqlPoolElement> {
return getPoolMock(poolExample.poolId, poolExample.poolChain)
}
24 changes: 24 additions & 0 deletions packages/lib/modules/pool/__mocks__/pool-examples/boosted.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { GqlChain } from '@repo/lib/shared/services/api/generated/graphql'
import { PoolExample } from './flat'

export const v3SepoliaNestedBoosted: PoolExample = {
description: 'Edge case: V3 Nested Boosted',
poolId: '0x693cc6a39bbf35464f53d6a5dbf7d6c2fa93741c',
poolChain: GqlChain.Sepolia,
version: 2,
}

export const morphoStakeHouse: PoolExample = {
description: 'Edge case: boosted with custom morpho stuff',
poolId: '0x5dd88b3aa3143173eb26552923922bdf33f50949',
poolChain: GqlChain.Mainnet,
version: 3,
}

export const sDAIBoosted: PoolExample = {
description:
'Edge case: BOOSTED with 2 ERC4626 tokens but one of them (sDAI) has isBufferAllowed == false',
poolId: '0xd1d7fa8871d84d0e77020fc28b7cd5718c446522',
poolChain: GqlChain.Gnosis,
version: 3,
}
44 changes: 44 additions & 0 deletions packages/lib/modules/pool/__mocks__/pool-examples/flat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { GqlChain } from '@repo/lib/shared/services/api/generated/graphql'
import { Address } from 'viem'
import { ProtocolVersion } from '../../pool.types'

export type PoolExample = {
description?: string
// Explicit pool prefix to make tests more readable
poolId: Address
poolAddress?: Address
poolChain: GqlChain
version: ProtocolVersion
}

export const balWeth8020: PoolExample = {
description: 'Weighted OG',
poolId: '0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014',
poolAddress: '0x5c6ee304399dbdb9c8ef030ab642b10820db8f56',
poolChain: GqlChain.Mainnet,
version: 2,
}

export const osETHPhantom: PoolExample = {
description:
'Edge case: Phantom composable stable where the pool itself appears in one of the tokens',
poolId: '0xdacf5fa19b1f720111609043ac67a9818262850c000000000000000000000635',
poolChain: GqlChain.Mainnet,
version: 2,
}

// TODO: use it in actionable tests
// THIS is A WRONG TEST CAUSE IT IS V2, we need V3
export const sDAIWeighted: PoolExample = {
description: 'Edge case: sDAI is ERC4626 but has isBufferAllowed == false',
poolId: '0xbc2acf5e821c5c9f8667a36bb1131dad26ed64f9000200000000000000000063',
poolChain: GqlChain.Gnosis,
version: 2,
}

export const v2SepoliaStableWithERC4626: PoolExample = {
description: 'It has ERC4626 (usdc-aave and dai-aave) tokens but it is V2 so it is not boosted',
poolId: '0x6c3966874f49a2f6a8f2f791f82f65b214e90ccb0000000000000000000001a6',
poolChain: GqlChain.Sepolia,
version: 3,
}
16 changes: 16 additions & 0 deletions packages/lib/modules/pool/__mocks__/pool-examples/nested.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { GqlChain } from '@repo/lib/shared/services/api/generated/graphql'
import { PoolExample } from './flat'

export const staBALv2Nested: PoolExample = {
description: 'V2 Nested supporting nested actions (default)',
poolId: '0x66888e4f35063ad8bb11506a6fde5024fb4f1db0000100000000000000000053',
poolChain: GqlChain.Gnosis,
version: 2,
}

export const auraBal: PoolExample = {
description: 'Edge case: Must use nested 8020 BPT to add (does not support nested actions)',
poolId: '0x3dd0843a028c86e0b760b1a76929d1c5ef93a2dd000200000000000000000249',
poolChain: GqlChain.Mainnet,
version: 2,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ApiToken } from '../../pool.types'

// TODO: move to testing utils
export function tokenSymbols(apiTokens: ApiToken[]): string[] {
return apiTokens.map(token => token.symbol).sort()
}

export function underlyingTokenSymbols(apiTokens: ApiToken[]): string[] {
return apiTokens.map(token => token.underlyingToken?.symbol || '-').sort()
}
5 changes: 4 additions & 1 deletion packages/lib/modules/pool/pool.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,10 @@ export type TokenCore = {
index: number
}

export type ApiToken = Omit<GetTokensQuery['tokens'][0], '__typename'>
export type ApiToken = Omit<GetTokensQuery['tokens'][0], '__typename'> & {
nestedTokens?: ApiToken[]
underlyingToken?: ApiToken
}

export enum PoolListDisplayType {
Name = 'name',
Expand Down
196 changes: 196 additions & 0 deletions packages/lib/modules/pool/pool.utils.integration.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import { GqlNestedPool } from '@repo/lib/shared/services/api/generated/graphql'
import { isSameAddress } from '@repo/lib/shared/utils/addresses'
import { sortBy } from 'lodash'
import { Address } from 'viem'
import { PoolToken } from '../tokens/token.helpers'
import { getPoolForTest } from './__mocks__/getPoolMock'
import {
morphoStakeHouse,
sDAIBoosted,
v3SepoliaNestedBoosted,
} from './__mocks__/pool-examples/boosted'
import {
balWeth8020,
osETHPhantom,
PoolExample,
sDAIWeighted,
v2SepoliaStableWithERC4626,
} from './__mocks__/pool-examples/flat'
import { auraBal, staBALv2Nested } from './__mocks__/pool-examples/nested'
import {
tokenSymbols,
underlyingTokenSymbols,
} from './__mocks__/pool-examples/pool-example-helpers'
import { isV3Pool } from './pool.helpers'
import { ApiToken } from './pool.types'
import { getPoolDisplayTokensWithPossibleNestedPools } from './pool.utils'
import { Pool } from './PoolProvider'

function isPool(pool: any): pool is Pool {
return (pool as Pool).poolTokens !== undefined
}

function isGqlNestedPool(pool: any): pool is GqlNestedPool {
return (pool as GqlNestedPool).tokens !== undefined
}

function getPoolTokens(pool: Pool | GqlNestedPool) {
if (isPool(pool)) {
return pool.poolTokens
}
if (isGqlNestedPool(pool)) {
return pool.tokens
}
throw new Error('Invalid pool type: poolTokens or tokens but be defined')
}

// TODO: extract this pool helpers or utils
function getDisplayTokens(pool: Pool | GqlNestedPool): ApiToken[] {
const tokens = getPoolTokens(pool).map(token => {
if (token.hasNestedPool && token.nestedPool) {
return {
...token,
nestedTokens: getDisplayTokens(token.nestedPool as GqlNestedPool),
} as ApiToken
}
return token as ApiToken
})

return sortBy(excludeNestedBptTokens(tokens as ApiToken[], pool.address), 'symbol')
}

function getHeaderDisplayTokens(pool: Pool): ApiToken[] {
// excludeNestedBptTokens(pool.poolTokens, pool.address) //TODO: do we need this case?? How is Panthom displayed after API fix?
if (isV3Pool(pool) && pool.hasErc4626 && pool.hasAnyAllowedBuffer) {
return pool.poolTokens.map(token =>
token.isErc4626 && token.isBufferAllowed
? ({ ...token, ...token.underlyingToken } as ApiToken)
: (token as ApiToken)
)
}
// Is this correct?
return getDisplayTokens(pool)
}

function excludeNestedBptTokens(tokens: PoolToken[] | ApiToken[], poolAddress: string): ApiToken[] {
return tokens
.filter(token => !isSameAddress(token.address, poolAddress as Address)) // Exclude the BPT pool token itself
.filter(token => token !== undefined) as ApiToken[]
}

// Testing utils that can be kept in the test:
async function getDisplaySymbols(poolExample: PoolExample): Promise<string[]> {
const pool = await getPoolForTest(poolExample)

return tokenSymbols(getDisplayTokens(pool))
}

async function getDisplayTokensFromPoolExample(poolExample: PoolExample): Promise<ApiToken[]> {
const pool = await getPoolForTest(poolExample)
return getDisplayTokens(pool)
}

async function getHeaderDisplaySymbols(poolExample: PoolExample): Promise<string[]> {
const pool = await getPoolForTest(poolExample)

return tokenSymbols(getHeaderDisplayTokens(pool))
}

async function getBoostedUnderlyingTokenSymbols(poolExample: PoolExample): Promise<string[]> {
const displayTokens = await getDisplayTokensFromPoolExample(poolExample)

return underlyingTokenSymbols(displayTokens)
}

async function getNestedTokenSymbols(poolExample: PoolExample): Promise<string[]> {
const displayTokens = await getDisplayTokensFromPoolExample(poolExample)

return displayTokens
.filter(token => token.nestedTokens)
.flatMap(token => token.nestedTokens?.map(t => t.symbol) || [])
}

async function getOldCompositionDisplaySymbols(poolExample: PoolExample): Promise<string[]> {
const pool = await getPoolForTest(poolExample)

return tokenSymbols(getPoolDisplayTokensWithPossibleNestedPools(pool) as ApiToken[])
}

describe('getDisplayTokens for flat pools', () => {
it('BAL WETH 80 20', async () => {
expect(await getDisplaySymbols(balWeth8020)).toEqual(['BAL', 'WETH'])
})

it('osETH Phantom Composable Stable', async () => {
expect(await getDisplaySymbols(osETHPhantom)).toEqual(['WETH', 'osETH'])
})

it('sDAI weighted', async () => {
expect(await getDisplaySymbols(sDAIWeighted)).toEqual(['sDAI', 'wstETH'])
})

it.skip('v2 stable with ERC4626 tokens (V2 so no boosted)', async () => {
expect(await getDisplaySymbols(v2SepoliaStableWithERC4626)).toEqual(['dai-aave', 'usdc-aave'])
})
})

describe('getDisplayTokens for NESTED pools', () => {
it('v2 nested', async () => {
expect(await getDisplaySymbols(staBALv2Nested)).toEqual(['WBTC', 'WETH', 'staBAL3'])

expect(await getHeaderDisplaySymbols(staBALv2Nested)).toEqual(['WBTC', 'WETH', 'staBAL3'])

// TODO: merge this function logic into getDisplayTokens above and getPoolDisplayTokens(pool: Pool | PoolListItem) in pool.utils
expect(await getOldCompositionDisplaySymbols(staBALv2Nested)).toEqual([
'USDC',
'USDT',
'WBTC',
'WETH',
'WXDAI',
'staBAL3',
])

expect(await getNestedTokenSymbols(staBALv2Nested)).toEqual(['USDC', 'USDT', 'WXDAI'])
})

it('aura bal (Nested with supportsNestedActions false)', async () => {
expect(await getDisplaySymbols(auraBal)).toEqual(['B-80BAL-20WETH', 'auraBAL'])
})
})

describe('getDisplayTokens for BOOSTED pools', () => {
it('Morpho boosted', async () => {
expect(await getDisplaySymbols(morphoStakeHouse)).toEqual(['csUSDL', 'steakUSDC'])

// Only case where pool composition and header do not match
expect(await getHeaderDisplaySymbols(morphoStakeHouse)).toEqual(['USDC', 'wUSDL'])

expect(await getBoostedUnderlyingTokenSymbols(morphoStakeHouse)).toEqual(['USDC', 'wUSDL'])
})

it('sDAI boosted', async () => {
expect(await getDisplaySymbols(sDAIBoosted)).toEqual(['sDAI', 'waGnoGNO'])
// Only case where pool composition and header do not match
expect(await getHeaderDisplaySymbols(sDAIBoosted)).toEqual(['GNO', 'sDAI'])
})

// unskip when we have a non-sepolia nested v3
it.skip('v3 nested boosted', async () => {
expect(await getDisplaySymbols(v3SepoliaNestedBoosted)).toEqual(['WETH', 'bb-a-USD'])

expect(await getHeaderDisplaySymbols(v3SepoliaNestedBoosted)).toEqual(['WETH', 'bb-a-USD']) // DO WE WANT THIS in the header?

const lpToken = getDisplayTokens(await getPoolForTest(v3SepoliaNestedBoosted))[1]
expect(tokenSymbols(lpToken.nestedTokens as ApiToken[])).toEqual([
'stataEthUSDC',
'stataEthUSDT',
])

// TODO: merge this function logic into getDisplayTokens above and getPoolDisplayTokens(pool: Pool | PoolListItem) in pool.utils
expect(await getOldCompositionDisplaySymbols(v3SepoliaNestedBoosted)).toEqual([
'WETH',
'stataEthUSDC',
'stataEthUSDT',
])
})
})
4 changes: 2 additions & 2 deletions packages/lib/modules/tokens/token.helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ export function getSpenderForAddLiquidity(pool: Pool): Address {
return vaultAddress
}

function buildApiToken(poolToken: PoolToken) {
function buildApiToken(poolToken: PoolToken): ApiToken {
return {
...poolToken,
// The following fields are (wrongly) optional in the generated PoolToken schema so we have to cast them to satisfy TS
Expand All @@ -163,5 +163,5 @@ function buildApiToken(poolToken: PoolToken) {
chain: poolToken.chain as GqlChain,
priority: poolToken.priority as number,
tradable: poolToken.tradable as boolean,
}
} as ApiToken
}

0 comments on commit 0212124

Please sign in to comment.