Skip to content

Commit

Permalink
Merge pull request #15 from Levana-Protocol/perp-4037/better-ux
Browse files Browse the repository at this point in the history
PERP-4037 | Better UX
  • Loading branch information
lvn-rusty-dragon authored Sep 11, 2024
2 parents 017fcbd + f138d9c commit c25042d
Show file tree
Hide file tree
Showing 38 changed files with 1,331 additions and 181 deletions.
6 changes: 1 addition & 5 deletions contract/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,7 @@ pub struct OutcomeInfo {
pub wallet_count: u32,
}

#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
#[serde(rename_all = "snake_case")]
pub struct PositionsResp {
pub outcomes: Vec<Token>,
}
pub type PositionsResp = ShareInfo;

#[derive(Serialize, Deserialize, JsonSchema, Debug)]
pub struct MigrateMsg {}
4 changes: 2 additions & 2 deletions contract/src/migrate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result<Response> {
.into());
}

if current >= new {
if current > new {
return Err(StdError::generic_err(format!(
"Current contract version is older or equivalent to the new one. Current: {}, New: {}",
"Current contract version is newer than the new one. Current: {}, New: {}",
current, new
))
.into());
Expand Down
7 changes: 2 additions & 5 deletions contract/src/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,6 @@ fn market(deps: Deps, id: MarketId) -> Result<MarketResp> {
fn positions(deps: Deps, id: MarketId, addr: String) -> Result<PositionsResp> {
let addr = deps.api.addr_validate(&addr)?;
let market = StoredMarket::load(deps.storage, id)?;
let outcomes = ShareInfo::load(deps.storage, &market, &addr)?
.unwrap_or_else(|| ShareInfo::new(market.outcomes.len()))
.outcomes;

Ok(PositionsResp { outcomes })
Ok(ShareInfo::load(deps.storage, &market, &addr)?
.unwrap_or_else(|| ShareInfo::new(market.outcomes.len())))
}
5 changes: 4 additions & 1 deletion contract/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,10 @@ impl Predict {
}

fn query_tokens(&self, better: &Addr, outcome: u8) -> StdResult<Token> {
let PositionsResp { outcomes } = self.query(&QueryMsg::Positions {
let PositionsResp {
outcomes,
claimed_winnings: _,
} = self.query(&QueryMsg::Positions {
id: self.id,
addr: better.to_string(),
})?;
Expand Down
2 changes: 1 addition & 1 deletion frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Levana Predict</title>
</head>
Expand Down
83 changes: 83 additions & 0 deletions frontend/src/api/mutations/CancelBet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { useCosmWasmSigningClient } from 'graz'
import BigNumber from 'bignumber.js'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'

import { useCurrentAccount } from '@config/chain'
import { useNotifications } from '@config/notifications'
import { querierAwaitCacheAnd, querierBroadcastAndWait } from '@api/querier'
import { MARKET_KEYS, MarketId, OutcomeId } from '@api/queries/Market'
import { POSITIONS_KEYS } from '@api/queries/Positions'
import { BALANCE_KEYS } from '@api/queries/NtrnBalance'
import { AppError, errorsMiddleware } from '@utils/errors'

interface CancelBetRequest {
withdraw: {
id: number,
outcome: number,
tokens: string,
},
}

interface CancelBetArgs {
outcomeId: OutcomeId,
tokensAmount: BigNumber,
}

const putCancelBet = (address: string, signer: SigningCosmWasmClient, marketId: MarketId, args: CancelBetArgs) => {
const msg: CancelBetRequest = {
withdraw: {
id: Number(marketId),
outcome: Number(args.outcomeId),
tokens: args.tokensAmount.toFixed(),
},
}

return querierBroadcastAndWait(
address,
signer,
{ payload: msg },
)
}

const CANCEL_BET_KEYS = {
all: ["cancel_bet"] as const,
address: (address: string) => [...CANCEL_BET_KEYS.all, address] as const,
market: (address: string, marketId: MarketId) => [...CANCEL_BET_KEYS.address(address), marketId] as const,
}

const useCancelBet = (marketId: MarketId) => {
const account = useCurrentAccount()
const signer = useCosmWasmSigningClient()
const queryClient = useQueryClient()
const notifications = useNotifications()

const mutation = useMutation({
mutationKey: CANCEL_BET_KEYS.market(account.bech32Address, marketId),
mutationFn: (args: CancelBetArgs) => {
if (signer.data) {
return errorsMiddleware("sell", putCancelBet(account.bech32Address, signer.data, marketId, args))
} else {
return Promise.reject()
}
},
onSuccess: (_, args) => {
notifications.notifySuccess(`Successfully cancelled bet of ${args.tokensAmount.toFixed(3)} tokens.`)

return querierAwaitCacheAnd(
() => queryClient.invalidateQueries({ queryKey: MARKET_KEYS.market(marketId)}),
() => queryClient.invalidateQueries({ queryKey: POSITIONS_KEYS.market(account.bech32Address, marketId)}),
() => queryClient.invalidateQueries({ queryKey: BALANCE_KEYS.address(account.bech32Address)}),
)
},
onError: (err, args) => {
notifications.notifyError(
AppError.withCause(`Failed to cancel bet of ${args.tokensAmount.toFixed(3)} tokens.`, err)
)
},
})

return mutation
}

export { useCancelBet }
72 changes: 72 additions & 0 deletions frontend/src/api/mutations/ClaimEarnings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { useCosmWasmSigningClient } from 'graz'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'

import { useCurrentAccount } from '@config/chain'
import { useNotifications } from '@config/notifications'
import { querierAwaitCacheAnd, querierBroadcastAndWait } from '@api/querier'
import { MarketId } from '@api/queries/Market'
import { POSITIONS_KEYS } from '@api/queries/Positions'
import { AppError, errorsMiddleware } from '@utils/errors'
import { BALANCE_KEYS } from '@api/queries/NtrnBalance'

interface ClaimEarningsRequest {
collect: {
id: number,
},
}

const putClaimEarnings = (address: string, signer: SigningCosmWasmClient, marketId: MarketId) => {
const msg: ClaimEarningsRequest = {
collect: {
id: Number(marketId),
},
}

return querierBroadcastAndWait(
address,
signer,
{ payload: msg },
)
}

const CLAIM_EARNINGS_KEYS = {
all: ["claim_earnings"] as const,
address: (address: string) => [...CLAIM_EARNINGS_KEYS.all, address] as const,
market: (address: string, marketId: MarketId) => [...CLAIM_EARNINGS_KEYS.address(address), marketId] as const,
}

const useClaimEarnings = (marketId: MarketId) => {
const account = useCurrentAccount()
const signer = useCosmWasmSigningClient()
const queryClient = useQueryClient()
const notifications = useNotifications()

const mutation = useMutation({
mutationKey: CLAIM_EARNINGS_KEYS.market(account.bech32Address, marketId),
mutationFn: () => {
if (signer.data) {
return errorsMiddleware("claim", putClaimEarnings(account.bech32Address, signer.data, marketId))
} else {
return Promise.reject()
}
},
onSuccess: () => {
notifications.notifySuccess("Successfully claimed earnings.")

return querierAwaitCacheAnd(
() => queryClient.invalidateQueries({ queryKey: POSITIONS_KEYS.market(account.bech32Address, marketId)}),
() => queryClient.invalidateQueries({ queryKey: BALANCE_KEYS.address(account.bech32Address)}),
)
},
onError: (err) => {
notifications.notifyError(
AppError.withCause("Failed to claim earnings.", err)
)
},
})

return mutation
}

export { useClaimEarnings }
7 changes: 4 additions & 3 deletions frontend/src/api/mutations/PlaceBet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import { useCurrentAccount } from '@config/chain'
import { NTRN_DENOM } from '@config/environment'
import { useNotifications } from '@config/notifications'
import { querierAwaitCacheAnd, querierBroadcastAndWait } from '@api/querier'
import { MarketId, OutcomeId } from '@api/queries/Market'
import { MARKET_KEYS, MarketId, OutcomeId } from '@api/queries/Market'
import { POSITIONS_KEYS } from '@api/queries/Positions'
import { BALANCE_KEYS } from '@api/queries/NtrnBalance'
import { NTRN } from '@utils/tokens'
import { AppError, errorsMiddleware } from '@utils/errors'
import { BALANCE_KEYS } from '@api/queries/NtrnBalance'

interface PlaceBetRequest {
deposit: {
Expand Down Expand Up @@ -38,7 +38,7 @@ const putPlaceBet = (address: string, signer: SigningCosmWasmClient, marketId: M
{
payload: msg,
funds: [{ denom: NTRN_DENOM, amount: args.ntrnAmount.units.toFixed(0) }],
}
},
)
}

Expand Down Expand Up @@ -67,6 +67,7 @@ const usePlaceBet = (marketId: MarketId) => {
notifications.notifySuccess(`Successfully bet ${args.ntrnAmount.toFormat(true)}.`)

return querierAwaitCacheAnd(
() => queryClient.invalidateQueries({ queryKey: MARKET_KEYS.market(marketId)}),
() => queryClient.invalidateQueries({ queryKey: POSITIONS_KEYS.market(account.bech32Address, marketId)}),
() => queryClient.invalidateQueries({ queryKey: BALANCE_KEYS.address(account.bech32Address)}),
)
Expand Down
42 changes: 30 additions & 12 deletions frontend/src/api/queries/Market.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { BigNumber } from 'bignumber.js'
import { queryOptions } from '@tanstack/react-query'

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 } from '@utils/time'
import { Nanoseconds, sleep } from '@utils/time'

interface ResponseMarket {
id: number,
Expand All @@ -16,8 +17,9 @@ interface ResponseMarket {
withdrawal_fee: string,
deposit_stop_date: string,
withdrawal_stop_date: string,
winner: string | null,
winner: number | null,
total_wallets: number,
pool_size: string,
}

interface ResponseMarketOutcome {
Expand All @@ -32,55 +34,71 @@ interface Market {
id: MarketId,
title: string,
description: string,
image: string,
possibleOutcomes: MarketOutcome[],
denom: string,
depositFee: BigNumber,
withdrawalFee: BigNumber,
depositStopDate: Nanoseconds,
withdrawalStopDate: Nanoseconds,
winnerOutcome: string | undefined,
winnerOutcome: MarketOutcome | undefined,
totalWallets: number,
poolSize: BigNumber,
}

interface MarketOutcome {
id: OutcomeId,
label: string,
poolTokens: BigNumber,
totalTokens: BigNumber,
wallets: number,
/// This is the amount of collateral you'd have to bet on an outcome
/// to receive 1 collateral of winnings.
price: BigNumber,
/// What percentage of the vote this outcome received
percentage: BigNumber,
}

type MarketId = string
type OutcomeId = string

const marketFromResponse = (response: ResponseMarket): Market => {
const outcomes = response.outcomes.map((outcome) => outcomeFromResponse(response, outcome))
return {
id: `${response.id}`,
title: response.title,
image: lvnLogo, // ToDo: use real image from each market
description: response.description,
possibleOutcomes: response.outcomes.map(outcomeFromResponse),
possibleOutcomes: outcomes,
denom: response.denom,
depositFee: BigNumber(response.deposit_fee),
withdrawalFee: BigNumber(response.withdrawal_fee),
depositStopDate: new Nanoseconds(response.deposit_stop_date),
withdrawalStopDate: new Nanoseconds(response.withdrawal_stop_date),
winnerOutcome: response.winner ?? undefined,
winnerOutcome: response.winner ? outcomes.find(outcome => outcome.id === `${response.winner}`) : undefined,
totalWallets: response.total_wallets,
poolSize: BigNumber(response.pool_size),
}
}

const outcomeFromResponse = (response: ResponseMarketOutcome): MarketOutcome => {
const outcomeFromResponse = (market: ResponseMarket, response: ResponseMarketOutcome): MarketOutcome => {
let totalTokens = BigNumber(0);
for (const outcome of market.outcomes) {
totalTokens = totalTokens.plus(outcome.total_tokens)
}
const outcomeTotalTokens = BigNumber(response.total_tokens);
return {
id: `${response.id}`,
label: response.label,
poolTokens: BigNumber(response.pool_tokens),
totalTokens: BigNumber(response.total_tokens),
totalTokens: outcomeTotalTokens,
wallets: response.wallets,
price: totalTokens.isZero() ? BigNumber(1) : outcomeTotalTokens.div(totalTokens),
percentage: totalTokens.isZero() ? BigNumber(0) : outcomeTotalTokens.times(100).div(totalTokens)
}
}

const fetchMarket = (marketId: MarketId): Promise<Market> => {
return fetchQuerier(
const fetchMarket = async (marketId: MarketId): Promise<Market> => {
await sleep(3000)
return await fetchQuerier(
"/v1/predict/market",
marketFromResponse,
{
Expand All @@ -101,4 +119,4 @@ const marketQuery = (marketId: string) => queryOptions({
queryFn: () => fetchMarket(marketId),
})

export { marketQuery, type Market, type MarketOutcome, type MarketId, type OutcomeId }
export { marketQuery, MARKET_KEYS, type Market, type MarketOutcome, type MarketId, type OutcomeId }
13 changes: 10 additions & 3 deletions frontend/src/api/queries/Positions.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
import BigNumber from 'bignumber.js'
import { queryOptions } from '@tanstack/react-query'

import { NETWORK_ID } from '@config/chain'
import { CONTRACT_ADDRESS } from '@config/environment'
import { fetchQuerier } from '@api/querier'
import { MarketId, OutcomeId } from './Market'
import { queryOptions } from '@tanstack/react-query'

interface PositionsResponse {
outcomes: string[],
claimed_winnings: boolean,
}

type Positions = Map<OutcomeId, BigNumber>
interface Positions {
outcomes: Map<OutcomeId, BigNumber>,
claimed: boolean,
}

const positionsFromResponse = (response: PositionsResponse): Positions => {
const entries = response.outcomes.map((amount, index) => [`${index}`, BigNumber(amount)] as const)
return new Map(entries)
return {
outcomes: new Map(entries),
claimed: response.claimed_winnings,
}
}

const fetchPositions = (address: string, marketId: MarketId): Promise<Positions> => {
Expand Down
Binary file added frontend/src/assets/brand/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit c25042d

Please sign in to comment.