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-4191 | Implement liquidity purchase #50

Merged
merged 3 commits into from
Oct 14, 2024
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
130 changes: 130 additions & 0 deletions frontend/src/api/mutations/ProvideLiquidity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { useActiveWalletType, useCosmWasmSigningClient } from "graz"
import { useMutation, useQueryClient } from "@tanstack/react-query"
import type { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate"

import { useCurrentAccount } from "@config/chain"
import { useNotifications } from "@config/notifications"
import { querierAwaitCacheAnd, querierBroadcastAndWait } from "@api/querier"
import { MARKET_KEYS, type MarketId } from "@api/queries/Market"
import { POSITIONS_KEYS } from "@api/queries/Positions"
import { BALANCES_KEYS } from "@api/queries/Balances"
import { trackProvideLiquidity } from "@utils/analytics"
import type { Coins } from "@utils/coins"
import { AppError, errorsMiddleware } from "@utils/errors"

interface ProvideLiquidityRequest {
provide: {
id: number
}
}

interface ProvideLiquidityArgs {
coinsAmount: Coins
}

const putProvideLiquidity = (
address: string,
signer: SigningCosmWasmClient,
marketId: MarketId,
args: ProvideLiquidityArgs,
) => {
const provideMsg: ProvideLiquidityRequest = {
provide: {
id: Number(marketId),
},
}

return querierBroadcastAndWait(address, signer, [
{
payload: provideMsg,
funds: [
{
denom: args.coinsAmount.denom,
amount: args.coinsAmount.units.toFixed(0),
},
],
},
])
}

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

const useProvideLiquidity = (marketId: MarketId) => {
const account = useCurrentAccount()
const walletName = useActiveWalletType().walletType
const signer = useCosmWasmSigningClient()
const queryClient = useQueryClient()
const notifications = useNotifications()

const mutation = useMutation({
mutationKey: PROVIDE_LIQUIDITY_KEYS.market(account.bech32Address, marketId),
mutationFn: (args: ProvideLiquidityArgs) => {
if (signer.data) {
return errorsMiddleware(
"provide",
putProvideLiquidity(
account.bech32Address,
signer.data,
marketId,
args,
),
)
} else {
return Promise.reject()
}
},
onSuccess: (_, args) => {
notifications.notifySuccess(
`Successfully provided ${args.coinsAmount.toFormat(true)} of liquidity.`,
)

trackProvideLiquidity({
marketId: marketId,
coins: args.coinsAmount,
walletName: walletName,
})

return querierAwaitCacheAnd(
() =>
queryClient.invalidateQueries({
queryKey: MARKET_KEYS.market(marketId),
}),
() =>
queryClient.invalidateQueries({
queryKey: POSITIONS_KEYS.market(account.bech32Address, marketId),
}),
() =>
queryClient.invalidateQueries({
queryKey: BALANCES_KEYS.address(account.bech32Address),
}),
)
},
onError: (err, args) => {
notifications.notifyError(
AppError.withCause(
`Failed to provide ${args.coinsAmount.toFormat(true)} of liquidity.`,
err,
),
)

trackProvideLiquidity(
{
marketId: marketId,
coins: args.coinsAmount,
walletName: walletName,
},
err,
)
},
})

return mutation
}

export { useProvideLiquidity }
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { useForm } from "react-hook-form"
import { useQuery } from "@tanstack/react-query"

import type { Market } from "@api/queries/Market"
import { coinPricesQuery } from "@api/queries/Prices"
import { useProvideLiquidity } from "@api/mutations/ProvideLiquidity"
import { Coins, USD } from "@utils/coins"

interface ProvideLiquidityFormValues {
liquidityAmount: {
value: string
toggled: boolean
}
}

const useProvideLiquidityForm = (market: Market) => {
const form = useForm<ProvideLiquidityFormValues>({
defaultValues: {
liquidityAmount: {
value: "",
toggled: false,
},
},
})

const denom = market.denom
const provideLiquidity = useProvideLiquidity(market.id)
const prices = useQuery(coinPricesQuery)

const onSubmit = (formValues: ProvideLiquidityFormValues) => {
const isToggled = formValues.liquidityAmount.toggled
const liquidityAmount = formValues.liquidityAmount.value
const price = prices.data?.get(denom)

if (liquidityAmount && price) {
const coinsAmount = isToggled
? new USD(liquidityAmount).toCoins(denom, price)
: Coins.fromValue(denom, liquidityAmount)

return provideLiquidity
.mutateAsync({ coinsAmount: coinsAmount })
.then(() => {
form.reset()
})
} else {
return Promise.reject()
}
}

const canSubmit =
form.formState.isValid &&
!form.formState.isSubmitting &&
!!prices.data?.has(market.denom)

return { form, canSubmit, onSubmit }
}

export { useProvideLiquidityForm }
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Button, Stack } from "@mui/joy"
import { FormProvider } from "react-hook-form"
import { useQuery } from "@tanstack/react-query"

import { useCurrentAccount } from "@config/chain"
import type { Market } from "@api/queries/Market"
import { balancesQuery } from "@api/queries/Balances"
import { coinPricesQuery } from "@api/queries/Prices"
import { useProvideLiquidityForm } from "./form"
import { CoinsAmountField } from "../CoinsAmountField"

const MarketProvideLiquidityForm = (props: { market: Market }) => {
const { market } = props
const account = useCurrentAccount()
const balances = useQuery(balancesQuery(account.bech32Address))
const price = useQuery(coinPricesQuery).data?.get(market.denom)

const { form, canSubmit, onSubmit } = useProvideLiquidityForm(market)

return (
<FormProvider {...form}>
<Stack
component="form"
onSubmit={form.handleSubmit(onSubmit)}
direction="column"
rowGap={1.5}
>
<CoinsAmountField
name="liquidityAmount"
denom={market.denom}
balance={balances.data?.get(market.denom)}
price={price}
/>

<Button
aria-label="Provide liquidity"
type="submit"
size="lg"
disabled={!canSubmit}
fullWidth
>
{form.formState.isSubmitting
? "Providing liquidity..."
: "Provide liquidity"}
</Button>
</Stack>
</FormProvider>
)
}

export { MarketProvideLiquidityForm }
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import { MarketClaimForm } from "./Claim"
import { MarketBuyForm } from "./Buy"
import { MarketSellForm } from "./Sell"
import { MarketProvideLiquidityForm } from "./ProvideLiquidity"

const MarketBetting = (props: StyleProps) => {
return (
Expand Down Expand Up @@ -46,7 +47,7 @@ const MarketBettingContent = (props: { market: Market }) => {

const MarketBettingForm = (props: { market: Market; status: MarketStatus }) => {
const { market, status } = props
const [action, setAction] = useState<"buy" | "sell">("buy")
const [action, setAction] = useState<"buy" | "sell" | "liquidity">("buy")

return (
<>
Expand All @@ -68,6 +69,7 @@ const MarketBettingForm = (props: { market: Market; status: MarketStatus }) => {
>
Buy
</Button>

<Button
color="neutral"
variant="plain"
Expand All @@ -86,11 +88,30 @@ const MarketBettingForm = (props: { market: Market; status: MarketStatus }) => {
>
Sell
</Button>

<Button
color="neutral"
variant="plain"
size="lg"
sx={{
px: 2,
py: 1,
width: "max-content",
borderRadius: 0,
borderBottom: action === "sell" ? "2px solid white" : undefined,
}}
onClick={() => {
setAction("liquidity")
}}
>
Liquidity
</Button>
</Stack>

{match(action)
.with("buy", () => <MarketBuyForm market={market} />)
.with("sell", () => <MarketSellForm market={market} />)
.with("liquidity", () => <MarketProvideLiquidityForm market={market} />)
.exhaustive()}
</>
)
Expand Down
35 changes: 33 additions & 2 deletions frontend/src/utils/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import type { MarketId, OutcomeId } from "@api/queries/Market"
import type { Coins } from "./coins"
import type { Shares } from "./shares"

type EventName = "place_bet" | "cancel_bet" | "claim_earnings"
type EventName =
| "place_bet"
| "cancel_bet"
| "provide_liquidity"
| "claim_earnings"

const trackSuccess = (eventName: EventName, params: Gtag.CustomParams) => {
gtag("event", eventName, {
Expand Down Expand Up @@ -76,6 +80,28 @@ const trackCancelBet = (params: TrackCancelBetParams, failure?: Error) => {
)
}

interface TrackBuyLiquidityParams {
marketId: MarketId
coins: Coins
walletName: string
}

const trackProvideLiquidity = (
params: TrackBuyLiquidityParams,
failure?: Error,
) => {
trackEvent(
"provide_liquidity",
{
market_id: params.marketId,
tokens_amount: params.coins.units.toFixed(0),
denom: params.coins.denom,
wallet: params.walletName,
},
failure,
)
}

interface TrackClaimEarningsParams {
marketId: MarketId
walletName: string
Expand All @@ -95,4 +121,9 @@ const trackClaimEarnings = (
)
}

export { trackPlaceBet, trackCancelBet, trackClaimEarnings }
export {
trackPlaceBet,
trackCancelBet,
trackProvideLiquidity,
trackClaimEarnings,
}
14 changes: 13 additions & 1 deletion frontend/src/utils/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class AppError extends Error {
}
}

type UserAction = "connect" | "buy" | "sell" | "claim"
type UserAction = "connect" | "buy" | "sell" | "provide" | "claim"

/**
* @returns user-friendly errors (if possible), based on the action that is being performed.
Expand Down Expand Up @@ -100,6 +100,18 @@ const errorForAction = (err: any, actionType?: UserAction): AppError | any => {
},
() => AppError.withCause("You don't have enough gas funds.", err),
)
.with(
{
message: P.string.regex(
"The transaction will use all your funds, not leaving any funds available for paying gas fees",
),
},
() =>
AppError.withCause(
"This transaction will use all your funds, not leaving enough for gas. Try using a different token for paying gas fees.",
err,
),
)
.otherwise(() => err)
}

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/utils/shares.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ const getPurchaseResult = (

// Step 2: take off the liquidity, add to pool, prepare to use the remainder
const liquidity = buyAmountWithoutFees
.times(BigNumber(LIQUDITY_PORTION))
.times(LIQUDITY_PORTION)
.integerValue(BigNumber.ROUND_DOWN)
const buyAmount = buyAmountWithoutFees.minus(liquidity)
const { pool, returned } = addToPool(poolAfterFees, liquidity)
Expand Down
Loading