Skip to content
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"release": "semantic-release"
},
"dependencies": {
"bignumber.js": "^9.1.1",
"got": "^11.8.6",
"viem": "^0.3.6"
},
Expand Down
18 changes: 9 additions & 9 deletions scripts/getPositions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Helper script to call the plugins and get the positions
/* eslint-disable no-console */
import yargs from 'yargs'
import BigNumber from 'bignumber.js'
import { Token } from '../src/plugin'
import { getPositions } from '../src/getPositions'

Expand Down Expand Up @@ -34,9 +35,9 @@ const argv = yargs(process.argv.slice(2))

function breakdownToken(token: Token): string {
if (token.type === 'base-token') {
const priceUsd = Number(token.priceUsd)
const balance = Number(token.balance) / 10 ** token.decimals
const balanceUsd = Number(balance) * priceUsd
const priceUsd = new BigNumber(token.priceUsd)
const balance = new BigNumber(token.balance)
const balanceUsd = balance.times(priceUsd)
return `${balance.toFixed(2)} ${token.symbol} ($${balanceUsd.toFixed(
2,
)}) @ $${priceUsd?.toFixed(2)}`
Expand All @@ -53,17 +54,16 @@ void (async () => {
console.table(
positions.map((position) => {
if (position.type === 'app-token') {
const balanceDecimal =
Number(position.balance) / 10 ** position.decimals
const balance = new BigNumber(position.balance)
return {
appId: position.appId,
type: position.type,
address: position.address,
network: position.network,
label: position.label,
priceUsd: Number(position.priceUsd).toFixed(2),
balance: balanceDecimal.toFixed(2),
balanceUsd: (balanceDecimal * position.priceUsd).toFixed(2),
priceUsd: new BigNumber(position.priceUsd).toFixed(2),
balance: balance.toFixed(2),
balanceUsd: balance.times(position.priceUsd).toFixed(2),
breakdown: position.tokens.map(breakdownToken).join(', '),
}
} else {
Expand All @@ -73,7 +73,7 @@ void (async () => {
address: position.address,
network: position.network,
label: position.label,
balanceUsd: Number(position.balanceUsd).toFixed(2),
balanceUsd: new BigNumber(position.balanceUsd).toFixed(2),
breakdown: position.tokens.map(breakdownToken).join(', '),
}
}
Expand Down
4 changes: 3 additions & 1 deletion src/apps/halofi/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import BigNumber from 'bignumber.js'
import { DecimalNumber } from '../../numbers'
import {
AppPlugin,
ContractPositionDefinition,
Expand Down Expand Up @@ -146,7 +148,7 @@ const plugin: AppPlugin = {
playerGame.paidAmount,
// Some of these are claimable rewards
...rewards.map((reward) => reward.balance),
]
].map((value) => new BigNumber(value) as DecimalNumber)
},
}
return position
Expand Down
12 changes: 6 additions & 6 deletions src/apps/locked-celo/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import {
} from 'viem'
import { celo } from 'viem/chains'
import { LockedGoldAbi } from './abis/locked-gold'
import { toDecimalNumber } from '../../numbers'

const CELO_ADDRESS = '0x471ece3750da237f93b8e339c536989b8978a438'
const LOCKED_GOLD_ADDRESS = '0x6cc083aed9e3ebe302a6336dbc7c921c9f03349e'
const ONE_WEI = 1e18
const CELO_DECIMALS = 18

interface PendingWithdrawal {
time: bigint
Expand Down Expand Up @@ -98,11 +99,10 @@ const plugin: AppPlugin = {
label: 'Locked CELO',
balances: async () => {
return [
(
Number(
totalLockedCelo + totalCeloUnlocking + totalCeloWithdrawable,
) / ONE_WEI
).toString(),
toDecimalNumber(
totalLockedCelo + totalCeloUnlocking + totalCeloWithdrawable,
CELO_DECIMALS,
),
]
},
}
Expand Down
23 changes: 13 additions & 10 deletions src/apps/ubeswap/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import {
TokenDefinition,
} from '../../plugin'
import got from 'got'
import BigNumber from 'bignumber.js'
import { uniswapV2PairAbi } from './abis/uniswap-v2-pair'
import { FarmInfoEventAbi } from './abis/farm-registry'
import { Address, createPublicClient, http } from 'viem'
import { celo } from 'viem/chains'
import { erc20Abi } from '../../abis/erc-20'
import { DecimalNumber, toDecimalNumber } from '../../numbers'

const FARM_REGISTRY = '0xa2bf67e12EeEDA23C7cA1e5a34ae2441a17789Ec'
const FARM_CREATION_BLOCK = 9840049n
Expand Down Expand Up @@ -86,13 +88,11 @@ async function getPoolPositionDefinition(
const token0 = tokensByAddress[token0Address.toLowerCase()]
const token1 = tokensByAddress[token1Address.toLowerCase()]
const reserves = [
Number(reserve0) / 10 ** token0.decimals,
Number(reserve1) / 10 ** token1.decimals,
toDecimalNumber(reserve0, token0.decimals),
toDecimalNumber(reserve1, token1.decimals),
]
const pricePerShare = reserves.map(
// TODO: use BigNumber
(r) => r / (Number(totalSupply) / 10 ** poolToken.decimals),
)
const supply = toDecimalNumber(totalSupply, poolToken.decimals)
const pricePerShare = reserves.map((r) => r.div(supply) as DecimalNumber)
return pricePerShare
},
}
Expand Down Expand Up @@ -195,7 +195,9 @@ async function getFarmPositionDefinitions(
},
balances: async ({ resolvedTokens }) => {
const poolToken = resolvedTokens[farm.lpAddress.toLowerCase()]
const share = Number(farm.balance) / Number(farm.totalSupply)
const share = new BigNumber(farm.balance.toString()).div(
farm.totalSupply.toString(),
)

const poolContract = {
address: farm.lpAddress,
Expand All @@ -212,10 +214,11 @@ async function getFarmPositionDefinitions(
allowFailure: false,
})

const balance =
(share * Number(poolBalance)) / 10 ** poolToken.decimals
const balance = share.times(
toDecimalNumber(poolBalance, poolToken.decimals),
) as DecimalNumber

return [balance.toString()]
return [balance]
},
}

Expand Down
62 changes: 34 additions & 28 deletions src/getPositions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { promises as fs } from 'fs'
import path from 'path'
import got from 'got'
import BigNumber from 'bignumber.js'
import ubeswapPlugin from './apps/ubeswap/plugin'
import {
Address,
Expand All @@ -24,6 +25,11 @@ import {
PricePerShareContext,
Token,
} from './plugin'
import {
DecimalNumber,
toDecimalNumber,
toSerializedDecimalNumber,
} from './numbers'

interface RawTokenInfo {
address: string
Expand Down Expand Up @@ -106,7 +112,7 @@ async function getBaseTokensInfo(): Promise<TokensInfo> {
symbol: tokenInfo.symbol,
decimals: tokenInfo.decimals,
imageUrl: tokenInfo.imageUrl,
priceUsd: tokenInfo.usdPrice ? Number(tokenInfo.usdPrice) : 0,
priceUsd: toSerializedDecimalNumber(tokenInfo.usdPrice ?? 0),
}
}
return tokensInfo
Expand Down Expand Up @@ -137,21 +143,18 @@ async function getERC20TokenInfo(address: Address): Promise<TokenInfo> {
symbol: symbol,
decimals: decimals,
imageUrl: '',
priceUsd: 0, // Should we use undefined?
priceUsd: toSerializedDecimalNumber(0), // Should we use undefined?
}
}

function tokenWithUnderlyingBalance<T extends Token>(
token: Omit<T, 'balance'>,
decimals: number,
balance: string,
pricePerShare: number,
balance: DecimalNumber,
pricePerShare: DecimalNumber,
): T {
const underlyingBalance = (
(Number(balance) / 10 ** decimals) *
10 ** token.decimals *
pricePerShare
).toFixed(0)
const underlyingBalance = new BigNumber(balance).times(
pricePerShare,
) as DecimalNumber

const appToken =
token.type === 'app-token'
Expand All @@ -164,13 +167,14 @@ function tokenWithUnderlyingBalance<T extends Token>(
tokens: appToken.tokens.map((underlyingToken, i) => {
return tokenWithUnderlyingBalance(
underlyingToken,
token.decimals,
underlyingBalance,
appToken.pricePerShare[i],
new BigNumber(appToken.pricePerShare[i]) as DecimalNumber,
)
}),
}),
balance: underlyingBalance,
balance: toSerializedDecimalNumber(
underlyingBalance.toFixed(token.decimals, BigNumber.ROUND_DOWN),
),
} as T
}

Expand All @@ -191,7 +195,7 @@ async function resolveAppTokenPosition(
tokensByAddress: TokensInfo,
resolvedTokens: Record<string, Omit<Token, 'balance'>>,
): Promise<AppTokenPosition> {
let pricePerShare: number[]
let pricePerShare: DecimalNumber[]
if (typeof positionDefinition.pricePerShare === 'function') {
pricePerShare = await positionDefinition.pricePerShare({
tokensByAddress,
Expand All @@ -200,11 +204,11 @@ async function resolveAppTokenPosition(
pricePerShare = positionDefinition.pricePerShare
}

let priceUsd = 0
let priceUsd = new BigNumber(0)
for (let i = 0; i < positionDefinition.tokens.length; i++) {
const token = positionDefinition.tokens[i]
const tokenInfo = tokensByAddress[token.address]!
priceUsd += Number(tokenInfo.priceUsd) * pricePerShare[i]
priceUsd = priceUsd.plus(tokenInfo.priceUsd).times(pricePerShare[i])
}

const positionTokenInfo = tokensByAddress[positionDefinition.address]!
Expand Down Expand Up @@ -239,15 +243,18 @@ async function resolveAppTokenPosition(
tokens: positionDefinition.tokens.map((token, i) =>
tokenWithUnderlyingBalance(
resolvedTokens[token.address],
positionTokenInfo.decimals,
balance.toString(),
toDecimalNumber(balance, positionTokenInfo.decimals),
pricePerShare[i],
),
),
pricePerShare,
priceUsd,
balance: balance.toString(),
supply: totalSupply.toString(),
pricePerShare: pricePerShare.map(toSerializedDecimalNumber),
priceUsd: toSerializedDecimalNumber(priceUsd),
balance: toSerializedDecimalNumber(
toDecimalNumber(balance, positionTokenInfo.decimals),
),
supply: toSerializedDecimalNumber(
toDecimalNumber(totalSupply, positionTokenInfo.decimals),
),
}

return position
Expand All @@ -259,7 +266,7 @@ async function resolveContractPosition(
_tokensByAddress: TokensInfo,
resolvedTokens: Record<string, Omit<Token, 'balance'>>,
): Promise<ContractPosition> {
let balances: string[]
let balances: DecimalNumber[]
if (typeof positionDefinition.balances === 'function') {
balances = await positionDefinition.balances({
resolvedTokens,
Expand All @@ -271,17 +278,16 @@ async function resolveContractPosition(
const tokens = positionDefinition.tokens.map((token, i) =>
tokenWithUnderlyingBalance(
resolvedTokens[token.address],
0, // This balance is already in the correct decimals
balances[i],
1,
new BigNumber(1) as DecimalNumber,
),
)

let balanceUsd = 0
let balanceUsd = new BigNumber(0)
for (let i = 0; i < positionDefinition.tokens.length; i++) {
const token = positionDefinition.tokens[i]
const tokenInfo = resolvedTokens[token.address]
balanceUsd += Number(balances[i]) * tokenInfo.priceUsd
balanceUsd = balanceUsd.plus(balances[i]).times(tokenInfo.priceUsd)
}

const position: ContractPosition = {
Expand All @@ -291,7 +297,7 @@ async function resolveContractPosition(
appId: positionDefinition.appId,
label: getLabel(positionDefinition, resolvedTokens),
tokens: tokens,
balanceUsd: balanceUsd.toString(),
balanceUsd: toSerializedDecimalNumber(balanceUsd),
}

return position
Expand Down
31 changes: 31 additions & 0 deletions src/numbers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import BigNumber from 'bignumber.js'

// Branded types for numbers to avoid confusion between different number types serialized as strings
// This means numbers (bigint,number,BigNumber) will need to be explicitly converted to match the needed type declared

// Decimal number serialized as a string
export type SerializedDecimalNumber = string & {
__serializedDecimalNumberBrand: never
}

// Decimal number stored as a BigNumber
export type DecimalNumber = BigNumber & { __decimalNumberBrand: never }

export function toSerializedDecimalNumber(
value: BigNumber.Value,
): SerializedDecimalNumber {
return (
new BigNumber(value)
// Use a maximum of 20 decimals, to avoid very long serialized numbers
.decimalPlaces(20)
.toString() as SerializedDecimalNumber
)
}

// Convert bigint balances from ERC20 contracts to decimal numbers
export function toDecimalNumber(
Copy link
Contributor

Choose a reason for hiding this comment

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

I was kinda confused by the "cast" implementation because it's behavior is so different from the "shiftedBy" one. I think it would be slightly less confusing to drop the cast one and just us as directly.

value: bigint,
decimals: number,
): DecimalNumber {
return new BigNumber(value.toString()).shiftedBy(-decimals) as DecimalNumber
}
Loading