Skip to content

Commit

Permalink
Merge pull request #50 from Levana-Protocol/perp-4191/buy-liquidity
Browse files Browse the repository at this point in the history
PERP-4191 | Implement liquidity purchase
  • Loading branch information
snoyberg authored Oct 14, 2024
2 parents 4780ef6 + 57032b7 commit fb76db7
Show file tree
Hide file tree
Showing 7 changed files with 308 additions and 5 deletions.
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

0 comments on commit fb76db7

Please sign in to comment.