Skip to content

Commit

Permalink
Merge pull request #33 from Levana-Protocol/perp-4099/shares-precision
Browse files Browse the repository at this point in the history
PERP-4099 | Shares precision depends on collateral coin
  • Loading branch information
lvn-rusty-dragon authored Sep 20, 2024
2 parents aaf4f51 + 82d1a10 commit 8c78c4c
Show file tree
Hide file tree
Showing 19 changed files with 400 additions and 168 deletions.
2 changes: 1 addition & 1 deletion frontend/src/api/mutations/CancelBet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const putCancelBet = (
withdraw: {
id: Number(marketId),
outcome: Number(args.outcomeId),
tokens: args.sharesAmount.value.toFixed(),
tokens: args.sharesAmount.units.toFixed(),
},
}

Expand Down
50 changes: 11 additions & 39 deletions frontend/src/api/queries/Market.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}`,
Expand All @@ -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<Market> => {
await sleep(3000)
return await fetchQuerier("/v1/predict/market", marketFromResponse, {
const fetchMarket = (marketId: MarketId): Promise<Market> => {
return fetchQuerier("/v1/predict/market", marketFromResponse, {
network: NETWORK_ID,
contract: CONTRACT_ADDRESS,
market_id: marketId,
Expand Down
36 changes: 22 additions & 14 deletions frontend/src/api/queries/Positions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
Expand All @@ -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<Positions> => {
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 = {
Expand All @@ -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 }
19 changes: 19 additions & 0 deletions frontend/src/assets/icons/QuestionMark.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { SvgIcon, type SvgIconProps } from "@mui/joy"

const QuestionMarkIcon = (props: SvgIconProps) => {
const Svg = () => (
<g>
<path d="M4.00084 7.77706C4.67894 7.77706 5.34182 7.57598 5.90565 7.19925C6.46947 6.82251 6.90892 6.28704 7.16842 5.66055C7.42792 5.03406 7.49582 4.34469 7.36353 3.67961C7.23124 3.01453 6.9047 2.40362 6.4252 1.92413C5.94571 1.44463 5.3348 1.11809 4.66972 0.985801C4.00464 0.853509 3.31527 0.921406 2.68878 1.18091C2.06229 1.44041 1.52682 1.87986 1.15008 2.44368C0.773348 3.00751 0.572266 3.67039 0.572266 4.34849L2.26055 4.34849C2.26055 4.0043 2.36261 3.66783 2.55384 3.38164C2.74506 3.09545 3.01686 2.87239 3.33486 2.74067C3.65285 2.60896 4.00277 2.57449 4.34035 2.64164C4.67793 2.70879 4.98802 2.87454 5.23141 3.11792C5.47479 3.36131 5.64054 3.6714 5.70769 4.00898C5.77484 4.34656 5.74037 4.69648 5.60865 5.01447C5.47694 5.33247 5.25388 5.60427 4.96769 5.79549C4.6815 5.98672 4.34503 6.08878 4.00084 6.08878L4.00084 7.77706Z" />
<rect x="2.85742" y="6.08984" width="2.09738" height="2.01025" />
<circle cx="3.97803" cy="10.1606" r="1.2085" />
</g>
)

return (
<SvgIcon {...props} viewBox="0 0 8 12">
<Svg />
</SvgIcon>
)
}

export { QuestionMarkIcon }
1 change: 1 addition & 0 deletions frontend/src/components/common/ConnectionModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const CONNECTION_MODAL_KEY = "connection_modal"
const presentConnectionModal = () => {
present(CONNECTION_MODAL_KEY, <ConnectionModal />)
}

const dismissConnectionModal = () => {
dismiss(CONNECTION_MODAL_KEY)
}
Expand Down
89 changes: 89 additions & 0 deletions frontend/src/components/lib/MoreInfoButton/index.tsx
Original file line number Diff line number Diff line change
@@ -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<IconButtonProps, "children" | "onClick"> {
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 (
<IconButton
color="neutral"
variant="soft"
aria-label={`More info about ${infoTitle}`}
{...iconButtonProps}
sx={mergeSx(
{
"--IconButton-size": "1rem",
paddingInline: 0,
},
iconButtonProps.sx,
)}
onClick={() => {
presentMoreInfoModal({ title: infoTitle, content: infoContent })
}}
>
<QuestionMarkIcon />
</IconButton>
)
}

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, <MoreInfoModal {...props} />)
}

const dismissMoreInfoModal = () => {
dismiss(MORE_INFO_MODAL_KEY)
}

const MoreInfoModal = (props: MoreInfoModalProps) => {
const { title, content } = props

return (
<ModalDialog size="lg" sx={{ pt: { sm: 6 } }}>
<ModalClose color="neutral" variant="outlined" aria-label="Close" />
{title && <DialogTitle>{title}</DialogTitle>}
<DialogContent sx={{ textAlign: "center" }}>
<Typography level="body-md" textColor="text.primary">
{content}
</Typography>
</DialogContent>
</ModalDialog>
)
}

export {
MoreInfoButton,
type MoreInfoButtonProps,
presentMoreInfoModal,
dismissMoreInfoModal,
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,21 @@ 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()

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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ const Earnings = (props: { market: Market }) => {
return (
<LoadableComponent
useDeps={() =>
useSuspenseQuery(positionsQuery(account.bech32Address, market.id)).data
useSuspenseQuery(positionsQuery(account.bech32Address, market)).data
}
renderContent={(positions) => (
<EarningsContent market={market} positions={positions} />
Expand Down Expand Up @@ -79,7 +79,7 @@ const EarningsContent = (props: { market: Market; positions: Positions }) => {
<Typography level="body-md">
You have claimed your earnings for this market.
</Typography>
) : earnings?.value.gt(0) ? (
) : earnings?.units.gt(0) ? (
<Box>
<Typography
level="title-lg"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,12 @@ const CoinsAmountField = (props: CoinsAmountFieldProps) => {
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())) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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])

Expand Down Expand Up @@ -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"
}
}
Expand Down
Loading

0 comments on commit 8c78c4c

Please sign in to comment.