diff --git a/frontend/src/api/mutations/CancelBet.ts b/frontend/src/api/mutations/CancelBet.ts index 6797383..9bc65b0 100644 --- a/frontend/src/api/mutations/CancelBet.ts +++ b/frontend/src/api/mutations/CancelBet.ts @@ -34,7 +34,7 @@ const putCancelBet = ( withdraw: { id: Number(marketId), outcome: Number(args.outcomeId), - tokens: args.sharesAmount.value.toFixed(), + tokens: args.sharesAmount.units.toFixed(), }, } diff --git a/frontend/src/api/queries/Market.ts b/frontend/src/api/queries/Market.ts index 68f4182..f0b726c 100644 --- a/frontend/src/api/queries/Market.ts +++ b/frontend/src/api/queries/Market.ts @@ -5,9 +5,9 @@ import lvnLogo from "@assets/brand/logo.png" import { NETWORK_ID } from "@config/chain" import { CONTRACT_ADDRESS } from "@config/environment" import { fetchQuerier } from "@api/querier" -import { Nanoseconds, sleep } from "@utils/time" +import { Nanoseconds } from "@utils/time" import { Coins } from "@utils/coins" -import { Shares } from "@utils/shares" +import { getOddsForOutcome, Shares } from "@utils/shares" interface ResponseMarket { id: number @@ -67,7 +67,7 @@ type OutcomeId = string const marketFromResponse = (response: ResponseMarket): Market => { const outcomes = response.outcomes.map((outcome) => - outcomeFromResponse(response, outcome), + outcomeFromResponse(outcome, response), ) return { id: `${response.id}`, @@ -92,54 +92,26 @@ const marketFromResponse = (response: ResponseMarket): Market => { } const outcomeFromResponse = ( - market: ResponseMarket, response: ResponseMarketOutcome, + market: ResponseMarket, ): MarketOutcome => { - let totalPoolTokens = BigNumber(0) - for (const outcome of market.outcomes) { - totalPoolTokens = totalPoolTokens.plus(outcome.pool_tokens) - } - - // Taken from: https://docs.gnosis.io/conditionaltokens/docs/introduction3/ - // oddsWeightForOutcome = product(numOutcomeTokensInInventoryForOtherOutcome for every otherOutcome) - // oddsForOutcome = oddsWeightForOutcome / sum(oddsWeightForOutcome for every outcome) - - const oddsWeights = [] - let totalOddsWeights = BigNumber(0) - let totalProduct = BigNumber(1) - for (let i = 0; i < market.outcomes.length; ++i) { - totalProduct = totalProduct.times(market.outcomes[i].pool_tokens) - let oddsWeight = BigNumber(1) - for (let j = 0; j < market.outcomes.length; ++j) { - if (i !== j) { - oddsWeight = oddsWeight.times(market.outcomes[j].pool_tokens) - } - } - oddsWeights.push(oddsWeight) - totalOddsWeights = totalOddsWeights.plus(oddsWeight) - } - const oddsForOutcome = totalProduct - .div(response.pool_tokens) - .div(totalOddsWeights) - let oddsWeightForOutcome = BigNumber(1) - for (const outcome of market.outcomes) { - oddsWeightForOutcome = oddsWeightForOutcome.times(outcome.pool_tokens) - } - oddsWeightForOutcome = oddsWeightForOutcome.div(response.pool_tokens) + const oddsForOutcome = getOddsForOutcome( + response.pool_tokens, + market.outcomes.map((outcome) => outcome.pool_tokens), + ) return { id: `${response.id}`, label: response.label, - poolShares: Shares.fromValue(response.pool_tokens), + poolShares: Shares.fromCollateralUnits(market.denom, response.pool_tokens), wallets: response.wallets, price: Coins.fromValue(market.denom, oddsForOutcome), percentage: oddsForOutcome.times(100), } } -const fetchMarket = async (marketId: MarketId): Promise => { - await sleep(3000) - return await fetchQuerier("/v1/predict/market", marketFromResponse, { +const fetchMarket = (marketId: MarketId): Promise => { + return fetchQuerier("/v1/predict/market", marketFromResponse, { network: NETWORK_ID, contract: CONTRACT_ADDRESS, market_id: marketId, diff --git a/frontend/src/api/queries/Positions.ts b/frontend/src/api/queries/Positions.ts index fc1ff5f..d5ff5d5 100644 --- a/frontend/src/api/queries/Positions.ts +++ b/frontend/src/api/queries/Positions.ts @@ -4,7 +4,7 @@ import { NETWORK_ID } from "@config/chain" import { CONTRACT_ADDRESS } from "@config/environment" import { fetchQuerier } from "@api/querier" import { Shares } from "@utils/shares" -import type { MarketId, OutcomeId } from "./Market" +import type { Market, MarketId, OutcomeId } from "./Market" interface PositionsResponse { outcomes: string[] @@ -18,27 +18,35 @@ interface Positions { shares: Shares } -const positionsFromResponse = (response: PositionsResponse): Positions => { +const positionsFromResponse = ( + response: PositionsResponse, + market: Market, +): Positions => { const entries = response.outcomes.map( - (amount, index) => [`${index}`, Shares.fromValue(amount)] as const, + (amount, index) => + [`${index}`, Shares.fromCollateralUnits(market.denom, amount)] as const, ) return { outcomes: new Map(entries), claimed: response.claimed_winnings, - shares: Shares.fromValue(response.shares), + shares: Shares.fromCollateralUnits(market.denom, response.shares), } } const fetchPositions = ( address: string, - marketId: MarketId, + market: Market, ): Promise => { - return fetchQuerier("/v1/predict/positions", positionsFromResponse, { - network: NETWORK_ID, - contract: CONTRACT_ADDRESS, - addr: address, - market_id: marketId, - }) + return fetchQuerier( + "/v1/predict/positions", + (res: PositionsResponse) => positionsFromResponse(res, market), + { + network: NETWORK_ID, + contract: CONTRACT_ADDRESS, + addr: address, + market_id: market.id, + }, + ) } const POSITIONS_KEYS = { @@ -48,10 +56,10 @@ const POSITIONS_KEYS = { [...POSITIONS_KEYS.address(address), marketId] as const, } -const positionsQuery = (address: string, marketId: MarketId) => +const positionsQuery = (address: string, market: Market) => queryOptions({ - queryKey: POSITIONS_KEYS.market(address, marketId), - queryFn: () => fetchPositions(address, marketId), + queryKey: POSITIONS_KEYS.market(address, market.id), + queryFn: () => fetchPositions(address, market), }) export { positionsQuery, POSITIONS_KEYS, type Positions } diff --git a/frontend/src/assets/icons/QuestionMark.tsx b/frontend/src/assets/icons/QuestionMark.tsx new file mode 100644 index 0000000..3d05407 --- /dev/null +++ b/frontend/src/assets/icons/QuestionMark.tsx @@ -0,0 +1,19 @@ +import { SvgIcon, type SvgIconProps } from "@mui/joy" + +const QuestionMarkIcon = (props: SvgIconProps) => { + const Svg = () => ( + + + + + + ) + + return ( + + + + ) +} + +export { QuestionMarkIcon } diff --git a/frontend/src/components/common/ConnectionModal/index.tsx b/frontend/src/components/common/ConnectionModal/index.tsx index 8a84eff..55daad9 100644 --- a/frontend/src/components/common/ConnectionModal/index.tsx +++ b/frontend/src/components/common/ConnectionModal/index.tsx @@ -22,6 +22,7 @@ const CONNECTION_MODAL_KEY = "connection_modal" const presentConnectionModal = () => { present(CONNECTION_MODAL_KEY, ) } + const dismissConnectionModal = () => { dismiss(CONNECTION_MODAL_KEY) } diff --git a/frontend/src/components/lib/MoreInfoButton/index.tsx b/frontend/src/components/lib/MoreInfoButton/index.tsx new file mode 100644 index 0000000..bb8a5df --- /dev/null +++ b/frontend/src/components/lib/MoreInfoButton/index.tsx @@ -0,0 +1,89 @@ +import { + DialogContent, + DialogTitle, + IconButton, + type IconButtonProps, + ModalClose, + ModalDialog, + Typography, +} from "@mui/joy" + +import { QuestionMarkIcon } from "@assets/icons/QuestionMark" +import { mergeSx } from "@utils/styles" +import { dismiss, present } from "@state/modals" + +interface MoreInfoButtonProps + extends Omit { + infoTitle?: string + infoContent: string +} + +/** + * An info button that displays an explanation dialog with the given text when clicked. + * + * @param infoTitle The title for the dialog displayed on click + * @param infoContent The description for the dialog displayed on click + * @returns + */ +const MoreInfoButton = (props: MoreInfoButtonProps) => { + const { infoTitle, infoContent, ...iconButtonProps } = props + + return ( + { + presentMoreInfoModal({ title: infoTitle, content: infoContent }) + }} + > + + + ) +} + +interface MoreInfoModalProps { + title?: string + content: string +} + +const MORE_INFO_MODAL_KEY = "more_info_modal" as const + +const presentMoreInfoModal = (props: MoreInfoModalProps) => { + present(MORE_INFO_MODAL_KEY, ) +} + +const dismissMoreInfoModal = () => { + dismiss(MORE_INFO_MODAL_KEY) +} + +const MoreInfoModal = (props: MoreInfoModalProps) => { + const { title, content } = props + + return ( + + + {title && {title}} + + + {content} + + + + ) +} + +export { + MoreInfoButton, + type MoreInfoButtonProps, + presentMoreInfoModal, + dismissMoreInfoModal, +} diff --git a/frontend/src/features/MarketDetail/components/MarketBetting/Claim/form.ts b/frontend/src/features/MarketDetail/components/MarketBetting/Claim/form.ts index 4204c33..dd59f00 100644 --- a/frontend/src/features/MarketDetail/components/MarketBetting/Claim/form.ts +++ b/frontend/src/features/MarketDetail/components/MarketBetting/Claim/form.ts @@ -5,6 +5,7 @@ import { useCurrentAccount } from "@config/chain" import type { Market } from "@api/queries/Market" import { positionsQuery } from "@api/queries/Positions" import { useClaimEarnings } from "@api/mutations/ClaimEarnings" +import { getShares } from "@utils/shares" const useMarketClaimForm = (market: Market) => { const form = useForm() @@ -12,13 +13,13 @@ const useMarketClaimForm = (market: Market) => { const claimEarnings = useClaimEarnings(market.id) const account = useCurrentAccount() - const positions = useQuery(positionsQuery(account.bech32Address, market.id)) + const positions = useQuery(positionsQuery(account.bech32Address, market)) const hasEarnings = market.winnerOutcome !== undefined && !!positions.data && !positions.data.claimed && - !!positions.data.outcomes.get(market.winnerOutcome.id)?.value?.gt(0) + getShares(positions.data, market.winnerOutcome.id).units.gt(0) const onSubmit = () => { return claimEarnings.mutateAsync() diff --git a/frontend/src/features/MarketDetail/components/MarketBetting/Claim/index.tsx b/frontend/src/features/MarketDetail/components/MarketBetting/Claim/index.tsx index eb7a0c8..25473b7 100644 --- a/frontend/src/features/MarketDetail/components/MarketBetting/Claim/index.tsx +++ b/frontend/src/features/MarketDetail/components/MarketBetting/Claim/index.tsx @@ -50,7 +50,7 @@ const Earnings = (props: { market: Market }) => { return ( - useSuspenseQuery(positionsQuery(account.bech32Address, market.id)).data + useSuspenseQuery(positionsQuery(account.bech32Address, market)).data } renderContent={(positions) => ( @@ -79,7 +79,7 @@ const EarningsContent = (props: { market: Market; positions: Positions }) => { You have claimed your earnings for this market. - ) : earnings?.value.gt(0) ? ( + ) : earnings?.units.gt(0) ? ( { rules={{ required: "This field is required", validate: (fieldValue: string) => { - if (fieldValue) { - const value = new BigNumber(fieldValue) + if (fieldValue && price) { + const isToggled = form.getValues()[toggledFieldName] as boolean + const value = isToggled + ? new USD(fieldValue).toCoins(denom, price).getValue() + : Coins.fromValue(denom, fieldValue).getValue() + if (!value.gt(0)) { return `${coinConfig.symbol} amount has to be greater than 0` } else if (balance && value.gt(balance.getValue())) { diff --git a/frontend/src/features/MarketDetail/components/MarketBetting/Sell/form.ts b/frontend/src/features/MarketDetail/components/MarketBetting/Sell/form.ts index 1f30bde..ec15b27 100644 --- a/frontend/src/features/MarketDetail/components/MarketBetting/Sell/form.ts +++ b/frontend/src/features/MarketDetail/components/MarketBetting/Sell/form.ts @@ -28,7 +28,7 @@ const useMarketSellForm = (market: Market) => { const sellOutcome = formValues.sellOutcome if (sellAmount && sellOutcome) { - const sharesAmount = Shares.fromValue(sellAmount) + const sharesAmount = Shares.fromCollateralValue(market.denom, sellAmount) return cancelBet.mutateAsync({ outcomeId: sellOutcome, sharesAmount: sharesAmount, diff --git a/frontend/src/features/MarketDetail/components/MarketBetting/Sell/index.tsx b/frontend/src/features/MarketDetail/components/MarketBetting/Sell/index.tsx index d0eaf69..b080d38 100644 --- a/frontend/src/features/MarketDetail/components/MarketBetting/Sell/index.tsx +++ b/frontend/src/features/MarketDetail/components/MarketBetting/Sell/index.tsx @@ -12,7 +12,7 @@ import { SharesAmountField } from "../SharesAmountField" const MarketSellForm = (props: { market: Market }) => { const { market } = props const account = useCurrentAccount() - const positions = useQuery(positionsQuery(account.bech32Address, market.id)) + const positions = useQuery(positionsQuery(account.bech32Address, market)) const { form, canSubmit, onSubmit, outcome } = useMarketSellForm(market) const sharesBalance = outcome diff --git a/frontend/src/features/MarketDetail/components/MarketBetting/SharesAmountField/index.tsx b/frontend/src/features/MarketDetail/components/MarketBetting/SharesAmountField/index.tsx index 72a88c9..26b17b4 100644 --- a/frontend/src/features/MarketDetail/components/MarketBetting/SharesAmountField/index.tsx +++ b/frontend/src/features/MarketDetail/components/MarketBetting/SharesAmountField/index.tsx @@ -54,7 +54,7 @@ const SharesAmountField = (props: SharesAmountFieldProps) => { if (!balance || !formValue) { return 0 } else { - return getPercentage(BigNumber(formValue), balance.value) + return getPercentage(BigNumber(formValue), balance.getValue()) } }, [formValue, balance]) @@ -106,7 +106,7 @@ const SharesAmountField = (props: SharesAmountFieldProps) => { const value = new BigNumber(fieldValue) if (!value.gt(0)) { return "Shares amount has to be greater than 0" - } else if (balance && value.gt(balance.value)) { + } else if (balance && value.gt(balance.getValue())) { return "You don't have enough shares" } } diff --git a/frontend/src/features/MarketDetail/components/MarketOutcomes/index.tsx b/frontend/src/features/MarketDetail/components/MarketOutcomes/index.tsx index aaefe79..c831576 100644 --- a/frontend/src/features/MarketDetail/components/MarketOutcomes/index.tsx +++ b/frontend/src/features/MarketDetail/components/MarketOutcomes/index.tsx @@ -1,7 +1,8 @@ -import { Box, Stack, Typography } from "@mui/joy" +import { Box, Chip, Stack, Typography } from "@mui/joy" import type { Market } from "@api/queries/Market" import type { StyleProps } from "@utils/styles" +import { pluralize } from "@utils/string" import { LoadableWidget } from "@lib/Loadable/Widget" import { useSuspenseCurrentMarket } from "@features/MarketDetail/utils" @@ -43,28 +44,31 @@ const MarketOutcomesContent = (props: { market: Market }) => { {outcome.percentage.toFixed(1)}% - {outcome.wallets} {outcome.wallets === 1 ? "holder" : "holders"} + {pluralize("holder", outcome.wallets, true)} ))} - + Prize pool size: {market.poolSize.toFormat(true)} - - - {market.totalWallets}{" "} - {market.totalWallets === 1 ? "participant" : "participants"} - + ) @@ -82,16 +86,17 @@ const MarketOutcomesPlaceholder = () => { {outcome} - 0.000 - + 0.0% + + 0 holders + ))} - - Prize pool size: 0.000000 NTRN - + Prize pool size: 0.000000 NTRN ) diff --git a/frontend/src/features/MarketDetail/components/MarketTitle/index.tsx b/frontend/src/features/MarketDetail/components/MarketTitle/index.tsx index 6597098..9d38086 100644 --- a/frontend/src/features/MarketDetail/components/MarketTitle/index.tsx +++ b/frontend/src/features/MarketDetail/components/MarketTitle/index.tsx @@ -5,6 +5,7 @@ import { TickIcon } from "@assets/icons/Tick" import { routes } from "@config/router" import type { Market } from "@api/queries/Market" import type { StyleProps } from "@utils/styles" +import { pluralize } from "@utils/string" import { useCopyToClipboard } from "@utils/hooks" import { LoadableWidget } from "@lib/Loadable/Widget" import { useSuspenseCurrentMarket } from "@features/MarketDetail/utils" @@ -43,7 +44,10 @@ const MarketTitleContent = (props: { market: Market }) => { /> {market.title} - + + + {pluralize("participant", market.totalWallets, true)} + @@ -77,9 +81,10 @@ const MarketTitlePlaceholder = () => { Loading market's title... - - Loading market's status... - + + Loading market's status... + 0 participants + ) diff --git a/frontend/src/features/MarketDetail/components/MyLiquidity/index.tsx b/frontend/src/features/MarketDetail/components/MyLiquidity/index.tsx index bb79083..685c930 100644 --- a/frontend/src/features/MarketDetail/components/MyLiquidity/index.tsx +++ b/frontend/src/features/MarketDetail/components/MyLiquidity/index.tsx @@ -6,9 +6,10 @@ import { useCurrentAccount } from "@config/chain" import type { Market } from "@api/queries/Market" import { type Positions, positionsQuery } from "@api/queries/Positions" import type { StyleProps } from "@utils/styles" +import { getPotentialWinnings } from "@utils/shares" +import { pluralize } from "@utils/string" import { LoadableWidget } from "@lib/Loadable/Widget" import { useSuspenseCurrentMarket } from "@features/MarketDetail/utils" -import { getPotentialWinnings } from "@utils/shares" const LIQUIDITY_POOLS_URL = "https://levana-prediction.zendesk.com/hc/en-us/articles/29284778150555-Liquidity-pools-in-Levana-Predict" @@ -30,7 +31,7 @@ const useDeps = () => { const account = useCurrentAccount() const market = useSuspenseCurrentMarket() const positions = useSuspenseQuery( - positionsQuery(account.bech32Address, market.id), + positionsQuery(account.bech32Address, market), ).data return { market, positions } @@ -41,19 +42,13 @@ const MyLiquidityContent = (props: { positions: Positions }) => { const { market, positions } = props - const poolPortion = positions.shares.value.div(market.lpShares) + const poolPortion = positions.shares.units.div(market.lpShares) return ( <> My liquidity - - You own {poolPortion.times(100).toFixed(3)}% of the liquidity pool. - - - {market.lpWallets} liquidity provider{market.lpWallets !== 1 && "s"} - {market.possibleOutcomes.map((outcome) => ( @@ -73,23 +68,44 @@ const MyLiquidityContent = (props: { - Potential winnings:{" "} {getPotentialWinnings( market, outcome.poolShares.times(poolPortion), ).toFormat(true)} + + potential pool winnings + ))} - - The potential winnings from the pool will change over time as further - prediction activity occurs.{" "} - - + + + + You own{" "} + + {poolPortion.times(100).toFixed(3)}% + {" "} + of the liquidity pool. + + + {pluralize("liquidity provider", market.lpWallets, true)}. + + + The potential winnings from the pool will change over time as further + prediction activity occurs.{" "} + Learn more about liquidity pools. - + ) } @@ -110,10 +126,36 @@ const MyLiquidityPlaceholder = () => { My liquidity - - You own 0.000% of the liquidity pool.{" "} - Learn more about liquidity pools. - + + {[0, 1, 2].map((index) => ( + + + Yes + + + 0.000000 NTRN + + + potential pool winnings + + + ))} + + + + + You own 0.000% of the + liquidity pool. + + 0 liquidity providers. + + The potential winnings from the pool will change over time as further + prediction activity occurs.{" "} + + + Learn more about liquidity pools. + + ) } diff --git a/frontend/src/features/MarketDetail/components/MyPositions/index.tsx b/frontend/src/features/MarketDetail/components/MyPositions/index.tsx index 12f066d..af3a265 100644 --- a/frontend/src/features/MarketDetail/components/MyPositions/index.tsx +++ b/frontend/src/features/MarketDetail/components/MyPositions/index.tsx @@ -26,7 +26,7 @@ const useDeps = () => { const account = useCurrentAccount() const market = useSuspenseCurrentMarket() const positions = useSuspenseQuery( - positionsQuery(account.bech32Address, market.id), + positionsQuery(account.bech32Address, market), ).data return { market, positions } @@ -38,44 +38,63 @@ const MyPositionsContent = (props: { }) => { const { market, positions } = props const outcomesWithPositions = market.possibleOutcomes.filter((outcome) => - positions.outcomes.get(outcome.id)?.value.gt(0), + positions.outcomes.get(outcome.id)?.units.gt(0), ) return ( <> - + My positions - - {outcomesWithPositions.map((outcome) => ( - - - {outcome.label} - - - Potential winnings:{" "} - {getPotentialWinnings( - market, - getShares(positions, outcome.id), - ).toFormat(true)} - - - ))} - + + {outcomesWithPositions.length > 0 ? ( + + {outcomesWithPositions.map((outcome) => ( + + + {outcome.label} + + + {getPotentialWinnings( + market, + getShares(positions, outcome.id), + ).toFormat(true)} + + + potential winnings + + + ))} + + ) : ( + + - + + )} ) } @@ -83,17 +102,20 @@ const MyPositionsContent = (props: { const MyPositionsPlaceholder = () => { return ( <> - + My positions - + {[0, 1, 2].map((index) => ( Yes - - Potential winnings: 0.000000 NTRN + + 0.000000 NTRN + + + potential winnings ))} diff --git a/frontend/src/utils/coins.ts b/frontend/src/utils/coins.ts index 12579ac..2bb9a94 100644 --- a/frontend/src/utils/coins.ts +++ b/frontend/src/utils/coins.ts @@ -245,8 +245,8 @@ const getCoinConfig = (denom: Denom): CoinConfig => { abstract class Asset { public symbol: string public units: BigNumber - protected exponent: number - protected maxDecimalPlaces: number + public exponent: number + public maxDecimalPlaces: number constructor( symbol: string, @@ -264,6 +264,10 @@ abstract class Asset { return this.getValue().decimalPlaces(this.maxDecimalPlaces).toFixed() } + toFullPrecision(withSuffix: boolean): string { + return `${this.getValue().toFormat(this.exponent)}${withSuffix ? ` ${this.symbol}` : ""}` + } + getValue(): BigNumber { return unitsToValue(this.units, this.exponent) } @@ -296,16 +300,12 @@ class Coins extends Asset { return `${formatted}${withSuffix ? ` ${this.symbol}` : ""}` } - toFullPrecision(withSuffix: boolean): string { - return `${this.getValue().toFormat(this.exponent)}${withSuffix ? ` ${this.symbol}` : ""}` - } - - static fromUnits(denom: string, units: BigNumber.Value): Coins { + static fromUnits(denom: Denom, units: BigNumber.Value): Coins { const coinConfig = getCoinConfig(denom) return new Coins(coinConfig.symbol, denom, units, coinConfig.exponent) } - static fromValue(denom: string, value: BigNumber.Value): Coins { + static fromValue(denom: Denom, value: BigNumber.Value): Coins { const coinConfig = getCoinConfig(denom) const units = valueToUnits(value, coinConfig.exponent) return Coins.fromUnits(denom, units) diff --git a/frontend/src/utils/number.ts b/frontend/src/utils/number.ts index 744e9dd..dd83b80 100644 --- a/frontend/src/utils/number.ts +++ b/frontend/src/utils/number.ts @@ -27,7 +27,7 @@ const getPercentage = ( return 0 } else { const bigValue = BigNumber(value) - return Number(bigValue.dividedBy(bigTotal).times(100).toFormat(2)) + return Number(bigValue.dividedBy(bigTotal).times(100).toFixed()) } } diff --git a/frontend/src/utils/shares.ts b/frontend/src/utils/shares.ts index f2eb6cb..f6f72d6 100644 --- a/frontend/src/utils/shares.ts +++ b/frontend/src/utils/shares.ts @@ -2,51 +2,115 @@ import BigNumber from "bignumber.js" import type { Market, OutcomeId } from "@api/queries/Market" import type { Positions } from "@api/queries/Positions" -import { Coins } from "./coins" +import { + Asset, + Coins, + getCoinConfig, + SIGNIFICANT_DIGITS, + type Denom, +} from "./coins" +import { formatToSignificantDigits, unitsToValue, valueToUnits } from "./number" -class Shares { - static precision = 3 - value: BigNumber +class Shares extends Asset { + static symbol = "shares" - protected constructor(value: BigNumber) { - this.value = value + protected constructor(units: BigNumber.Value, exponent: number) { + super(Shares.symbol, units, exponent, exponent) } - static fromValue(value: BigNumber.Value): Shares { - return new Shares(BigNumber(value).decimalPlaces(Shares.precision)) + static fromUnits(units: BigNumber.Value, exponent: number): Shares { + return new Shares(units, exponent) } - toFormat(withSuffix: boolean): string { - return `${this.value.toFormat(Shares.precision)}${withSuffix ? " shares" : ""}` + static fromValue(value: BigNumber.Value, exponent: number): Shares { + const units = valueToUnits(value, exponent) + return Shares.fromUnits(units, exponent) + } + + static fromCollateralUnits( + collateralDenom: Denom, + units: BigNumber.Value, + ): Shares { + const coinConfig = getCoinConfig(collateralDenom) + return Shares.fromUnits(units, coinConfig.exponent) } - toInput(): string { - return this.value.decimalPlaces(Shares.precision).toFixed() + static fromCollateralValue( + collateralDenom: Denom, + units: BigNumber.Value, + ): Shares { + const coinConfig = getCoinConfig(collateralDenom) + return Shares.fromValue(units, coinConfig.exponent) + } + + toFormat(withSuffix: boolean): string { + const value = unitsToValue(this.units, this.exponent) + const formatted = formatToSignificantDigits( + value, + SIGNIFICANT_DIGITS, + this.exponent, + ) + + return `${formatted}${withSuffix ? ` ${this.symbol}` : ""}` } plus(shares: Shares): Shares { - return Shares.fromValue(this.value.plus(shares.value)) + return new Shares(this.units.plus(shares.units), this.exponent) } minus(shares: Shares): Shares { - return Shares.fromValue(this.value.minus(shares.value)) + return new Shares(this.units.minus(shares.units), this.exponent) } times(value: BigNumber.Value): Shares { - return Shares.fromValue(this.value.times(value)) + return new Shares(this.units.times(value), this.exponent) } dividedBy(value: BigNumber.Value): Shares { - return Shares.fromValue(this.value.dividedBy(value)) + return new Shares(this.units.dividedBy(value), this.exponent) } } const getShares = (positions: Positions, outcomeId: OutcomeId): Shares => { - return positions.outcomes.get(outcomeId) ?? Shares.fromValue(0) + const exponent = positions.shares.exponent + return positions.outcomes.get(outcomeId) ?? Shares.fromUnits(0, exponent) } const getPotentialWinnings = (market: Market, positionSize: Shares): Coins => { - return Coins.fromUnits(market.denom, positionSize.value) + return Coins.fromUnits(market.denom, positionSize.units) +} + +const getOddsForOutcome = ( + outcomePoolTokens: BigNumber.Value, + outcomesPoolTokens: BigNumber.Value[], +): BigNumber => { + // Taken from: https://docs.gnosis.io/conditionaltokens/docs/introduction3/ + // oddsWeightForOutcome = product(numOutcomeTokensInInventoryForOtherOutcome for every otherOutcome) + // oddsForOutcome = oddsWeightForOutcome / sum(oddsWeightForOutcome for every outcome) + + const oddsWeights = [] + let totalProduct = BigNumber(1) + for (let i = 0; i < outcomesPoolTokens.length; ++i) { + totalProduct = totalProduct.times(outcomesPoolTokens[i]) + let oddsWeight = BigNumber(1) + for (let j = 0; j < outcomesPoolTokens.length; ++j) { + if (i !== j) { + oddsWeight = oddsWeight.times(outcomesPoolTokens[j]) + } + } + oddsWeights.push(oddsWeight) + } + + const totalOddsWeights = oddsWeights.reduce( + (sum, value) => sum.plus(value), + BigNumber(0), + ) + + const oddsForOutcome = totalProduct + .div(outcomePoolTokens) + .div(totalOddsWeights) + + return oddsForOutcome } -export { Shares, getShares, getPotentialWinnings } +export { Shares, getShares, getPotentialWinnings, getOddsForOutcome }