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

feat: add getTokens and getMarkets #146

Merged
merged 3 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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