Skip to content

Commit

Permalink
fix: token handler (#1041)
Browse files Browse the repository at this point in the history
  • Loading branch information
0xsign authored Nov 15, 2023
1 parent 83a9190 commit bd5e34b
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 100 deletions.
34 changes: 29 additions & 5 deletions scripts/insert-llamafolio-tokens.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,36 @@
import '../environment'

import { client } from '@db/clickhouse'
import type { Token } from '@db/tokens'
import { insertERC20Tokens } from '@db/tokens'
import { coingeckoPlatformToChain, fetchCoingeckoCoins } from '@lib/coingecko'
import { chains } from '@llamafolio/tokens'

import { client } from '../src/db/clickhouse'
import type { Token } from '../src/db/tokens'
import { insertERC20Tokens } from '../src/db/tokens'

async function main() {
const coingeckoCoins = await fetchCoingeckoCoins()

const coingeckoIdByChainByAddress: { [chain: string]: { [address: string]: string } } = {}

for (const coin of coingeckoCoins) {
if (!coin.platforms) {
continue
}

for (const platform in coin.platforms) {
const chain = coingeckoPlatformToChain[platform]
if (!chain) {
continue
}

if (!coingeckoIdByChainByAddress[chain]) {
coingeckoIdByChainByAddress[chain] = {}
}

const address = coin.platforms[platform].toLowerCase()
coingeckoIdByChainByAddress[chain][address] = coin.id
}
}

try {
const tokens: Token[] = []

Expand All @@ -19,7 +43,7 @@ async function main() {
name: token.name,
symbol: token.symbol,
decimals: token.decimals,
coingeckoId: token.coingeckoId || undefined,
coingeckoId: token.coingeckoId || coingeckoIdByChainByAddress[chain]?.[token.address] || undefined,
cmcId: undefined,
stable: token.stable,
})
Expand Down
31 changes: 31 additions & 0 deletions src/db/tokens.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ClickHouseClient } from '@clickhouse/client'
import { type Chain, chainById } from '@lib/chains'
import { shortAddress } from '@lib/fmt'

export interface Token {
address: string
Expand Down Expand Up @@ -43,6 +44,36 @@ export function toTokenStorage(tokens: Token[]) {
return tokensStorable
}

export async function selectToken(client: ClickHouseClient, chainId: number, address: string) {
const queryRes = await client.query({
query: `
SELECT
"type",
"decimals",
"symbol",
"name",
"coingecko_id" AS "coingeckoId",
"stable"
FROM evm_indexer2.tokens
WHERE
"chain" = {chainId: UInt64} AND
"address_short" = {addressShort: String} AND
"address" = {address: String};
`,
query_params: {
chainId,
addressShort: shortAddress(address),
address,
},
})

const res = (await queryRes.json()) as {
data: { type: string; decimals: number; symbol: string; name: string; coingeckoId: string; stable: boolean }[]
}

return res.data[0]
}

export async function selectUndecodedChainAddresses(client: ClickHouseClient, limit?: number, offset?: number) {
const queryRes = await client.query({
query: `
Expand Down
114 changes: 19 additions & 95 deletions src/handlers/getToken.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,28 @@
import { client } from '@db/clickhouse'
import { selectAdaptersContractsToken } from '@db/contracts'
import { selectToken } from '@db/tokens'
import { badRequest, serverError, success } from '@handlers/response'
import type { Balance, BaseContext, BaseContract, Contract, PricedBalance } from '@lib/adapter'
import { call } from '@lib/call'
import { type Chain, chainById } from '@lib/chains'
import { isHex } from '@lib/contract'
import { abi as erc20Abi } from '@lib/erc20'
import { sum } from '@lib/math'
import { multicall } from '@lib/multicall'
import { getPricedBalances } from '@lib/price'
import { parseAddress } from '@lib/fmt'
import type { APIGatewayProxyHandler } from 'aws-lambda'
import { keyBy } from 'lodash'

function formatBaseContract(contract: any) {
return {
chain: contract.chain,
address: contract.address,
name: contract.name,
decimals: contract.decimals,
symbol: contract.symbol,
interface TokenResponse {
data: {
type?: string
decimals?: number
symbol?: string
name?: string
coingeckoId?: string
stable: boolean
}
}

export const handler: APIGatewayProxyHandler = async (event) => {
const chain = event.pathParameters?.chain as Chain
const address = event.pathParameters?.address?.toLowerCase() as `0x${string}`
const address = parseAddress(event.pathParameters?.address || '')
if (!address) {
return badRequest('Invalid address parameter')
}

const chain = event.pathParameters?.chain as Chain
if (!chain) {
return badRequest('Missing chain parameter')
}
Expand All @@ -35,87 +32,14 @@ export const handler: APIGatewayProxyHandler = async (event) => {
return badRequest(`Unsupported chain ${chain}`)
}

if (!address) {
return badRequest('Missing address parameter')
}

if (!isHex(address)) {
return badRequest('Invalid address parameter, expected hex')
}

const ctx: BaseContext = { chain, adapterId: '' }

try {
// `token` key can also be used to retrieve token details, ignore it
const adaptersContracts = (await selectAdaptersContractsToken(client, address, chainId)).filter(
(contract) => !contract.token || contract.token?.toLowerCase() === address,
)

const symbol = adaptersContracts[0]?.symbol
const decimals = adaptersContracts[0]?.decimals
const category = adaptersContracts[0]?.category
const adapterId = adaptersContracts[0]?.adapter_id

const contractsUnderlyings = adaptersContracts[0]?.underlyings

const [_symbol, _decimals, totalSupply, underlyingsBalances] = await Promise.all([
!symbol ? call({ ctx, target: address, abi: erc20Abi.symbol }).catch(() => undefined) : undefined,
!decimals ? call({ ctx, target: address, abi: erc20Abi.decimals }).catch(() => undefined) : undefined,
call({ ctx, target: address, abi: erc20Abi.totalSupply }).catch(() => undefined),
contractsUnderlyings
? multicall({
ctx,
calls: contractsUnderlyings.map(
(underlying) => ({ target: (underlying as BaseContract).address, params: [address] }) as const,
),
abi: erc20Abi.balanceOf,
})
: undefined,
])

const contract: Contract = {
chain,
address,
category,
symbol: symbol || _symbol,
decimals: decimals || _decimals,
adapterId,
}

const underlyings = contractsUnderlyings?.map((underlying, idx) => ({
...formatBaseContract(underlying),
amount: underlyingsBalances?.[idx].output || undefined,
}))
const pricedUnderlyings = await getPricedBalances([contract, ...(underlyings || [])] as Balance[])
const pricedUnderlyingByAddress = keyBy(pricedUnderlyings, 'address')
// return underlyings even if we fail to find their price (more info to display)
const maybePricedUnderlyings = underlyings?.map(
(underlying) => pricedUnderlyingByAddress[underlying.address] || underlying,
)

const contractPrice = (pricedUnderlyingByAddress[contract.address] as PricedBalance)?.price

// value of LP token = total pool value / LP token total supply
const totalPoolValue =
pricedUnderlyings &&
pricedUnderlyings.length > 1 &&
pricedUnderlyings?.every((pricedUnderlying) => (pricedUnderlying as PricedBalance).balanceUSD)
? sum(pricedUnderlyings.map((pricedUnderlying) => (pricedUnderlying as PricedBalance).balanceUSD || 0))
: undefined

const lpTokenPrice =
totalSupply != null && totalSupply > 0n && totalPoolValue && decimals
? totalPoolValue / Number(totalSupply / 10n ** BigInt(decimals))
: undefined
const data = await selectToken(client, chainId, address)

const token = {
...contract,
totalSupply,
price: contractPrice || lpTokenPrice,
underlyings: maybePricedUnderlyings,
const response: TokenResponse = {
data,
}

return success(token, { maxAge: 60 * 60 })
return success(response, { maxAge: 60 * 60 })
} catch (e) {
console.error('Failed to retrieve token', e)
return serverError('Failed to retrieve token')
Expand Down
35 changes: 35 additions & 0 deletions src/lib/coingecko.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { Chain } from '@lib/chains'

export const coingeckoPlatformToChain: { [key: string]: Chain } = {
'arbitrum-one': 'arbitrum',
'arbitrum-nova': 'arbitrum-nova',
avalanche: 'avalanche',
base: 'base',
'binance-smart-chain': 'bsc',
celo: 'celo',
ethereum: 'ethereum',
fantom: 'fantom',
'harmony-shard-0': 'harmony',
linea: 'linea',
moonbeam: 'moonbeam',
'polygon-pos': 'polygon',
'polygon-zkevm': 'polygon-zkevm',
opbnb: 'opbnb',
'optimistic-ethereum': 'optimism',
xdai: 'gnosis',
zksync: 'zksync-era',
}

export interface CoingeckoCoin {
id: string
symbol: string
name: string
// chain-address mapping
platforms: { [key: string]: string }
mcap: number
}

export async function fetchCoingeckoCoins(): Promise<CoingeckoCoin[]> {
const res = await fetch('https://defillama-datasets.llama.fi/tokenlist/cgFull.json')
return res.json()
}
13 changes: 13 additions & 0 deletions src/lib/fmt.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isHex } from '@lib/contract'
import { isNotFalsy } from '@lib/type'
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
Expand All @@ -12,6 +13,10 @@ export function isCharNumeric(char: string) {
return /\d/.test(char)
}

export function shortAddress(address?: string) {
return address?.slice(0, 10)
}

export function slugify(adapter: string) {
const slug = adapter
.split(/[-,.]+/)
Expand Down Expand Up @@ -81,6 +86,14 @@ export function fromAddresses(addresses: `0x${string}`[]) {
.join(',')
}

/**
* Parse address
* @param str
*/
export function parseAddress(str: string) {
return isHex(str) ? (str.toLowerCase() as `0x${string}`) : null
}

/**
* Parse comma separated addresses
* @param str
Expand Down

0 comments on commit bd5e34b

Please sign in to comment.