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

PERP-4037 | Better UX #15

Merged
merged 16 commits into from
Sep 11, 2024
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