Skip to content

Commit

Permalink
SOV-4410: add liquidity modal (#999)
Browse files Browse the repository at this point in the history
* chore: use pool data from subgraph

* feat: add liquidity dialog

* chore: add changesets

* fix: review comments
  • Loading branch information
creed-victor authored Sep 11, 2024
1 parent b25248c commit e1905c4
Show file tree
Hide file tree
Showing 17 changed files with 734 additions and 140 deletions.
5 changes: 5 additions & 0 deletions .changeset/nice-pots-cover.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'frontend': patch
---

SOV-4410: add liquidity modal
5 changes: 5 additions & 0 deletions .changeset/nice-pots-coverup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sovryn/sdk': patch
---

chore: retrieve pool pairs from subgraph
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { BOB_CHAIN_ID } from '../../../config/chains';

import { NetworkBanner } from '../../2_molecules/NetworkBanner/NetworkBanner';
import { translations } from '../../../locales/i18n';
import { LiquidityBookModal } from './components/AddLiquidityModal/AddLiquidityModal';
import { BalanceRenderer } from './components/BalanceRenderer/BalanceRenderer';
import { LiquidityBookFrame } from './components/LiquidityBookFrame/LiquidityBookFrame';

Expand All @@ -30,6 +31,7 @@ const LiquidityBookPage: FC = () => (
</NetworkBanner>
</div>
</div>
<LiquidityBookModal />
</>
);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Token } from '@sovryn/joe-core';

/** @deprecated */
export type LiquidityBookPool = {
pair: Token[];
liquidity: string[];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React, { useCallback, useEffect, useMemo } from 'react';

import { t } from 'i18next';

import { Dialog, DialogBody, DialogHeader } from '@sovryn/ui';

import { translations } from '../../../../../locales/i18n';
import { eventDriven } from '../../../../../store/rxjs/event-driven';
import { Nullable } from '../../../../../types/global';
import { LiquidityBookPool } from '../../LiquidityBookPage.types';
import { LBModalType } from '../../utils/constants';
import { Content } from './Content';

export const LiquidityBookModal = () => {
const { subscribe } = useMemo(
() => eventDriven<Nullable<LiquidityBookPool>>(LBModalType.deposit),
[],
);
const [isOpen, setIsOpen] = React.useState(false);
const [pool, setPool] = React.useState<Nullable<LiquidityBookPool>>(null);

useEffect(() => {
const sub = subscribe(value => {
setIsOpen(value !== null);
setPool(value);
});

return () => sub.unsubscribe();
}, [subscribe]);

const handleClose = useCallback(() => {
setIsOpen(false);
setPool(null);
}, []);

return (
<Dialog disableFocusTrap isOpen={isOpen}>
<DialogHeader
title={t(translations.liquidityBookDeposit.title)}
onClose={handleClose}
/>
<DialogBody>
{isOpen && pool && <Content pool={pool} onClose={handleClose} />}
</DialogBody>
</Dialog>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
import React, { FC, useCallback, useMemo, useState } from 'react';

import { t } from 'i18next';

import { LiquidityDistribution } from '@sovryn/joe-sdk-v2';
import {
AmountInput,
Button,
ButtonStyle,
ErrorBadge,
ErrorLevel,
FormGroup,
} from '@sovryn/ui';

import { AssetRenderer } from '../../../../2_molecules/AssetRenderer/AssetRenderer';
import { CurrentStatistics } from '../../../../2_molecules/CurrentStatistics/CurrentStatistics';
import { MaxButton } from '../../../../2_molecules/MaxButton/MaxButton';
import { useAmountInput } from '../../../../../hooks/useAmountInput';
import { useAssetBalance } from '../../../../../hooks/useAssetBalance';
import { useCurrentChain } from '../../../../../hooks/useChainStore';
import { translations } from '../../../../../locales/i18n';
import { decimalic } from '../../../../../utils/math';
import { LiquidityBookPool } from '../../LiquidityBookPage.types';
import { useHandleLiquidity } from '../../hooks/useHandleLiquidity';
import { LiquidityPriceForm } from './LiquidityPriceForm';
import { DEFAULT_BIN_RADIUS, MAX_BIN_RANGE } from './constants';

type ContentProps = {
pool: LiquidityBookPool;
onClose: () => void;
};

export const Content: FC<ContentProps> = ({ pool, onClose }) => {
const chainId = useCurrentChain();
const baseSymbol = pool.pair[0].symbol!;
const quoteSymbol = pool.pair[1].symbol!;

const { balance: baseBalance } = useAssetBalance(baseSymbol, chainId);
const { balance: quoteBalance } = useAssetBalance(quoteSymbol, chainId);

const [baseAmount, onBaseAmountChange] = useAmountInput('');
const [quoteAmount, onQuoteAmountChange] = useAmountInput('');

const [shape /*, setShape*/] = useState<LiquidityDistribution>(
LiquidityDistribution.SPOT,
);

const [min, setMin] = useState(pool.activeBinId - DEFAULT_BIN_RADIUS);
const [max, setMax] = useState(pool.activeBinId + DEFAULT_BIN_RADIUS);

const handleMaxBase = useCallback(
() => onBaseAmountChange(baseBalance.toString()),
[baseBalance, onBaseAmountChange],
);

const handleMaxQuote = useCallback(
() => onQuoteAmountChange(quoteBalance.toString()),
[onQuoteAmountChange, quoteBalance],
);

const handleRange = useCallback((min: number, max: number) => {
setMin(min);
setMax(max);
}, []);

const isBaseInvalid = useMemo(
() => decimalic(baseAmount).gt(baseBalance),
[baseAmount, baseBalance],
);
const isQuoteInvalid = useMemo(
() => decimalic(quoteAmount).gt(quoteBalance),
[quoteAmount, quoteBalance],
);

const { handleDeposit } = useHandleLiquidity(pool);

const handleSubmit = useCallback(
() => handleDeposit(baseAmount, quoteAmount, shape, min, max, onClose),
[baseAmount, handleDeposit, max, min, onClose, quoteAmount, shape],
);

const submitDisabled = useMemo(() => {
if (isBaseInvalid || isQuoteInvalid) {
return true;
}
if (max - min > MAX_BIN_RANGE) {
return true;
}
if (decimalic(baseAmount).isZero() || decimalic(quoteAmount).isZero()) {
return true;
}
}, [baseAmount, isBaseInvalid, isQuoteInvalid, max, min, quoteAmount]);

return (
<>
<div className="bg-gray-90 p-4 rounded">
<CurrentStatistics
symbol={baseSymbol}
symbol2={quoteSymbol}
className="flex justify-between"
/>
</div>

<FormGroup
label={
<div className="flex justify-end w-full">
<MaxButton
value={baseBalance}
token={baseSymbol}
onClick={handleMaxBase}
/>
</div>
}
labelElement="div"
className="max-w-none mt-6"
dataAttribute="bob-amm-pool-deposit-asset1"
>
<AmountInput
value={baseAmount}
onChangeText={onBaseAmountChange}
maxAmount={baseBalance.toNumber()}
label={t(translations.common.amount)}
className="max-w-none"
unit={<AssetRenderer asset={baseSymbol} />}
invalid={isBaseInvalid}
placeholder="0"
/>
{isBaseInvalid && (
<ErrorBadge
level={ErrorLevel.Critical}
message={t(translations.common.invalidAmountError)}
dataAttribute="bob-deposit-base-amount-error"
/>
)}
</FormGroup>

<FormGroup
label={
<div className="flex justify-end w-full">
<MaxButton
value={quoteBalance}
token={quoteSymbol}
onClick={handleMaxQuote}
/>
</div>
}
labelElement="div"
className="max-w-none mt-6"
dataAttribute="bob-amm-pool-deposit-asset1"
>
<AmountInput
value={quoteAmount}
onChangeText={onQuoteAmountChange}
maxAmount={quoteBalance.toNumber()}
label={t(translations.common.amount)}
className="max-w-none"
unit={<AssetRenderer asset={quoteSymbol} />}
invalid={isQuoteInvalid}
placeholder="0"
/>
{isQuoteInvalid && (
<ErrorBadge
level={ErrorLevel.Critical}
message={t(translations.common.invalidAmountError)}
dataAttribute="bob-deposit-base-amount-error"
/>
)}
</FormGroup>

{/*<h2 className="my-4">Shape</h2>
<div className="flex flex-row justify-between gap-x-4">
<Button
onClick={() => setShape(LiquidityDistribution.SPOT)}
text="Spot"
style={
shape === LiquidityDistribution.SPOT
? ButtonStyle.primary
: ButtonStyle.secondary
}
/>
<Button
onClick={() => setShape(LiquidityDistribution.CURVE)}
text="Curve"
style={
shape === LiquidityDistribution.CURVE
? ButtonStyle.primary
: ButtonStyle.secondary
}
/>
<Button
onClick={() => setShape(LiquidityDistribution.BID_ASK)}
text="Bid-Ask"
style={
shape === LiquidityDistribution.CURVE
? ButtonStyle.primary
: ButtonStyle.secondary
}
/>
</div>*/}

<LiquidityPriceForm
pool={pool}
min={min}
max={max}
onChange={handleRange}
/>

<Button
onClick={handleSubmit}
text={t(translations.common.buttons.confirm)}
className="w-full mt-4"
style={ButtonStyle.primary}
disabled={submitDisabled}
/>
</>
);
};
Loading

0 comments on commit e1905c4

Please sign in to comment.