Skip to content

Commit

Permalink
feat: add getTokens and getMarkets (#146)
Browse files Browse the repository at this point in the history
* feat: add getTokens and getMarkets

* chore: update tests from biem anvil to prool

* chore: format

---------

Co-authored-by: maxencerb <[email protected]>
  • Loading branch information
maxencerb and maxencerb authored Nov 25, 2024
1 parent 215faac commit c35ba08
Show file tree
Hide file tree
Showing 14 changed files with 478 additions and 17 deletions.
5 changes: 5 additions & 0 deletions .changeset/spotty-islands-admire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@mangrovedao/mgv": patch
---

Added getTokens and getOpenMarkets
Binary file modified bun.lockb
Binary file not shown.
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,12 @@
"devDependencies": {
"@biomejs/biome": "1.8.3",
"@types/bun": "^1.1.8",
"@viem/anvil": "^0.0.10",
"@vitest/coverage-v8": "^2.0.5",
"globby": "^14.0.2",
"rimraf": "^6.0.1",
"simple-git-hooks": "^2.11.1",
"viem": "^2.21.2",
"vitest": "^2.0.5"
"vitest": "^2.0.5",
"prool": "^0.0.16"
},
"peerDependencies": {
"typescript": "^5.5.4"
Expand Down
16 changes: 16 additions & 0 deletions src/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,19 @@ export type {
GetKandelStateResult,
KandelStatus,
} from './kandel/view.js'

export { getTokens, GetTokenInfoError } from './tokens.js'

export type {
GetTokensParams,
GetTokensResult,
} from './tokens.js'

export { getRawOpenMarkets, getOpenMarkets } from './reader.js'

export type {
GetOpenMarketArgs,
GetOpenMarketRawArgs,
GetOpenMarketRawResult,
GetOpenMarketResult,
} from './reader.js'
66 changes: 66 additions & 0 deletions src/actions/reader.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { describe, expect, inject, it } from 'vitest'
import { getClient } from '~test/src/client.js'
import { getOpenMarkets } from './reader.js'

const client = getClient()
const params = inject('mangrove')
const { WETH, USDC, DAI } = inject('tokens')
const { wethDAI, wethUSDC } = inject('markets')

describe('getOpenMarkets', () => {
it('should return the open markets', async () => {
const markets = await getOpenMarkets(client, params, {
cashnesses: {
WETH: 1,
USDC: 1000,
DAI: 1000,
},
})

expect(markets[0]?.base.address.toLowerCase()).toEqual(
wethUSDC.base.address.toLowerCase(),
)
expect(markets[0]?.quote.address.toLowerCase()).toEqual(
wethUSDC.quote.address.toLowerCase(),
)
expect(markets[1]?.base.address.toLowerCase()).toEqual(
wethDAI.base.address.toLowerCase(),
)
expect(markets[1]?.quote.address.toLowerCase()).toEqual(
wethDAI.quote.address.toLowerCase(),
)

expect(markets[0]?.base.symbol).toEqual(WETH.symbol)
expect(markets[0]?.quote.symbol).toEqual(USDC.symbol)
expect(markets[1]?.base.symbol).toEqual(WETH.symbol)
expect(markets[1]?.quote.symbol).toEqual(DAI.symbol)

expect(markets[0]?.base.decimals).toEqual(WETH.decimals)
expect(markets[0]?.quote.decimals).toEqual(USDC.decimals)
expect(markets[1]?.base.decimals).toEqual(WETH.decimals)
expect(markets[1]?.quote.decimals).toEqual(DAI.decimals)
})

it('should return the open markets with inverted cashnesses', async () => {
const markets = await getOpenMarkets(client, params, {
cashnesses: {
WETH: 100000,
USDC: 1000,
DAI: 1000,
},
})

expect(markets[0]?.base.address.toLowerCase()).toEqual(
wethUSDC.quote.address.toLowerCase(),
)
expect(markets[0]?.quote.address.toLowerCase()).toEqual(
wethUSDC.base.address.toLowerCase(),
)
expect(markets[1]?.base.address.toLowerCase()).toEqual(
wethDAI.quote.address.toLowerCase(),
)
expect(markets[1]?.quote.address.toLowerCase()).toEqual(
wethDAI.base.address.toLowerCase(),
)
})
})
80 changes: 80 additions & 0 deletions src/actions/reader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import type { Address, Client, ContractFunctionParameters } from 'viem'
import { readContract } from 'viem/actions'
import { getOpenMarketsParams, type mgvReaderABI } from '../builder/reader.js'
import type {
BuiltArgs,
MangroveActionsDefaultParams,
MarketParams,
} from '../types/index.js'
import { getAction } from '../utils/getAction.js'
import { type GetTokensParams, getTokens } from './tokens.js'

export type GetOpenMarketRawArgs = Omit<
ContractFunctionParameters<typeof mgvReaderABI, 'view', 'openMarkets'>,
BuiltArgs
>

export type GetOpenMarketRawResult = {
tkn0: Address
tkn1: Address
tickSpacing: bigint
}[]

export async function getRawOpenMarkets(
client: Client,
params: MangroveActionsDefaultParams,
args?: GetOpenMarketRawArgs,
): Promise<GetOpenMarketRawResult> {
const result = await getAction(
client,
readContract,
'readContract',
)({
...args,
address: params.mgvReader,
...getOpenMarketsParams,
})

return result[0] as GetOpenMarketRawResult
}

export type GetOpenMarketArgs = Omit<GetTokensParams, 'tokens'> &
GetOpenMarketRawArgs & {
// symbol -> cashness
cashnesses: Record<string, number>
}

export type GetOpenMarketResult = MarketParams[]

export async function getOpenMarkets(
client: Client,
params: MangroveActionsDefaultParams,
args: GetOpenMarketArgs,
): Promise<GetOpenMarketResult> {
const raw = await getRawOpenMarkets(client, params, args)
const tokens = await getTokens(client, {
...args,
tokens: raw.flatMap((market) => [market.tkn0, market.tkn1]),
})

return raw.map((market): MarketParams => {
// we don't use isAddressEqual because both are supposedly checksummed from viem
const tkn0 = tokens.find((token) => token.address === market.tkn0)
const tkn1 = tokens.find((token) => token.address === market.tkn1)

if (!tkn0 || !tkn1) {
throw new Error(
'Token not found, this is a bug, please report at https://github.com/mangrovedao/mgv/issues',
)
}

const tkn0Cashness = args.cashnesses[tkn0.symbol] ?? 0
const tkn1Cashness = args.cashnesses[tkn1.symbol] ?? 0

return {
base: tkn0Cashness > tkn1Cashness ? tkn1 : tkn0,
quote: tkn0Cashness > tkn1Cashness ? tkn0 : tkn1,
tickSpacing: market.tickSpacing,
}
})
}
86 changes: 86 additions & 0 deletions src/actions/tokens.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { ContractFunctionExecutionError, zeroAddress } from 'viem'
import { describe, expect, inject, it } from 'vitest'
import { getClient } from '~test/src/client.js'
import { GetTokenInfoError, getTokens } from './tokens.js'

const { WETH, USDC, DAI } = inject('tokens')
const client = getClient()

describe('tokens', () => {
it('should get tokens', async () => {
const tokens = await getTokens(client, { tokens: [WETH.address] as const })
expect(tokens).toEqual([WETH])

const foundWETH = tokens[0]

expect(foundWETH.mgvTestToken).toBe(false)
expect(foundWETH.address).toBe(WETH.address)
expect(foundWETH.symbol).toBe(WETH.symbol)
expect(foundWETH.decimals).toBe(WETH.decimals)
})

it('should get multiple tokens', async () => {
const tokens = await getTokens(client, {
tokens: [WETH.address, USDC.address, DAI.address] as const,
})
expect(tokens).toEqual([WETH, USDC, DAI])

const foundWETH = tokens[0]
expect(foundWETH.mgvTestToken).toBe(false)
expect(foundWETH.address).toBe(WETH.address)
expect(foundWETH.symbol).toBe(WETH.symbol)
expect(foundWETH.decimals).toBe(WETH.decimals)

const foundUSDC = tokens[1]
expect(foundUSDC.mgvTestToken).toBe(false)
expect(foundUSDC.address).toBe(USDC.address)
expect(foundUSDC.symbol).toBe(USDC.symbol)
expect(foundUSDC.decimals).toBe(USDC.decimals)

const foundDAI = tokens[2]
expect(foundDAI.mgvTestToken).toBe(false)
expect(foundDAI.address).toBe(DAI.address)
expect(foundDAI.symbol).toBe(DAI.symbol)
expect(foundDAI.decimals).toBe(DAI.decimals)
})

it('should get test tokens', async () => {
const tokens = await getTokens(client, {
tokens: [WETH.address] as const,
testTokens: [WETH.address],
})

const foundWETH = tokens[0]
expect(foundWETH.mgvTestToken).toBe(true)
expect(foundWETH.address).toBe(WETH.address)
expect(foundWETH.symbol).toBe(WETH.symbol)
expect(foundWETH.decimals).toBe(WETH.decimals)
})

it('should have display decimals', async () => {
const tokens = await getTokens(client, {
tokens: [WETH.address, USDC.address, DAI.address] as const,
displayDecimals: { [WETH.symbol]: 1000 },
priceDisplayDecimals: { [WETH.symbol]: 1000 },
})

const foundWETH = tokens[0]
expect(foundWETH.displayDecimals).toBe(1000)
expect(foundWETH.priceDisplayDecimals).toBe(1000)
})

it('should fail on unknown token', async () => {
try {
await getTokens(client, { tokens: [zeroAddress] as const })
} catch (error) {
expect(error).toBeInstanceOf(GetTokenInfoError)
const typedError = error as GetTokenInfoError
expect(typedError.shortMessage).toBe(
`No decimals found for token ${zeroAddress}`,
)
expect(typedError.cause).toBeInstanceOf(ContractFunctionExecutionError)
const typedCause = typedError.cause as ContractFunctionExecutionError
expect(typedCause.contractAddress).toBe(zeroAddress)
}
})
})
111 changes: 111 additions & 0 deletions src/actions/tokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import {
type Address,
BaseError,
type Client,
type MulticallParameters,
parseAbi,
} from 'viem'
import { multicall } from 'viem/actions'
import { type Token, buildToken } from '../addresses/index.js'
import { getAction } from '../utils/getAction.js'

export type GetTokensParams<T extends readonly Address[] = readonly Address[]> =
{
tokens: T
displayDecimals?: Record<string, number>
priceDisplayDecimals?: Record<string, number>
testTokens?: T[number][]
} & Omit<MulticallParameters, 'contracts' | 'allowFailure'>

export type GetTokensResult<T extends readonly Address[] = readonly Address[]> =
{
[K in keyof T]: Token<T[K]>
}

const tokenABI = parseAbi([
'function decimals() external view returns (uint8)',
'function symbol() external view returns (string)',
])

export class GetTokenInfoError extends BaseError {
constructor(
tokenAddress: Address,
param: 'decimals' | 'symbol',
cause: Error,
) {
super(`No ${param} found for token ${tokenAddress}`, { cause })
}
}

export async function getTokens<
T extends readonly Address[] = readonly Address[],
>(
client: Client,
{
tokens,
displayDecimals = {},
priceDisplayDecimals = {},
testTokens = [],
}: GetTokensParams<T>,
): Promise<GetTokensResult<T>> {
const tokenInfos = await getAction(
client,
multicall,
'multicall',
)({
contracts: tokens.flatMap(
(token) =>
[
{
address: token,
abi: tokenABI,
functionName: 'decimals',
},
{
address: token,
abi: tokenABI,
functionName: 'symbol',
},
] as const,
),
})

return tokens.map((token: T[number], i) => {
const decimalsResult = tokenInfos[i * 2]
const symbolResult = tokenInfos[i * 2 + 1]

if (!decimalsResult || !symbolResult)
throw new Error(
'Error while getting token infos, This is a bug, please report at https://github.com/mangrovedao/mgv/issues',
)

if (decimalsResult.status === 'failure')
throw new GetTokenInfoError(token, 'decimals', decimalsResult.error)
if (symbolResult.status === 'failure')
throw new GetTokenInfoError(token, 'symbol', symbolResult.error)

const decimals = decimalsResult.result
const symbol = symbolResult.result

if (typeof symbol !== 'string')
throw new Error(
'Error while getting token infos, This is a bug, please report at https://github.com/mangrovedao/mgv/issues',
)
if (typeof decimals !== 'number')
throw new Error(
'Error while getting token infos, This is a bug, please report at https://github.com/mangrovedao/mgv/issues',
)

const display = displayDecimals[symbol]
const priceDisplay = priceDisplayDecimals[symbol]

return buildToken({
address: token,
symbol,
decimals,
displayDecimals: display,
priceDisplayDecimals: priceDisplay,
mgvTestToken: testTokens.includes(token),
})
}) as GetTokensResult<T>
}
4 changes: 4 additions & 0 deletions src/builder/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,7 @@ export {
deployRouterParams,
bindParams,
} from './smart-router.js'

// reader

export { getOpenMarketsParams, mgvReaderABI } from './reader.js'
Loading

0 comments on commit c35ba08

Please sign in to comment.