Skip to content

Commit

Permalink
Adding deposit logic to markets page (#700)
Browse files Browse the repository at this point in the history
  • Loading branch information
IanWoodard authored Nov 4, 2023
1 parent 916dc65 commit 729b91b
Show file tree
Hide file tree
Showing 3 changed files with 208 additions and 29 deletions.
7 changes: 6 additions & 1 deletion earn/src/components/lend/SupplyTable.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { useMemo, useState } from 'react';

import { SendTransactionResult } from '@wagmi/core';
import Pagination from 'shared/lib/components/common/Pagination';
import TokenIcon from 'shared/lib/components/common/TokenIcon';
import { Text, Display } from 'shared/lib/components/common/Typography';
import { Kitty } from 'shared/lib/data/Kitty';
import { Token } from 'shared/lib/data/Token';
import { formatTokenAmount } from 'shared/lib/util/Numbers';
import styled from 'styled-components';
Expand Down Expand Up @@ -38,6 +40,7 @@ const HoverableRow = styled.tr`

export type SupplyTableRow = {
asset: Token;
kitty: Kitty;
collateralAssets: Token[];
apy: number;
supplyBalance: number;
Expand All @@ -47,10 +50,11 @@ export type SupplyTableRow = {

export type SupplyTableProps = {
rows: SupplyTableRow[];
setPendingTxn: (pendingTxn: SendTransactionResult | null) => void;
};

export default function SupplyTable(props: SupplyTableProps) {
const { rows } = props;
const { rows, setPendingTxn } = props;
const [currentPage, setCurrentPage] = useState(1);
const [selectedRow, setSelectedRow] = useState<SupplyTableRow | null>(null);
const pages: SupplyTableRow[][] = useMemo(() => {
Expand Down Expand Up @@ -157,6 +161,7 @@ export default function SupplyTable(props: SupplyTableProps) {
setIsOpen={() => {
setSelectedRow(null);
}}
setPendingTxn={setPendingTxn}
/>
)}
</>
Expand Down
186 changes: 160 additions & 26 deletions earn/src/components/lend/modal/SupplyModal.tsx
Original file line number Diff line number Diff line change
@@ -1,52 +1,191 @@
import { useContext, useEffect, useState } from 'react';
import { useContext, useEffect, useMemo, useState } from 'react';

import { FilledGradientButton } from 'shared/lib/components/common/Buttons';
import { SendTransactionResult } from '@wagmi/core';
import { BigNumber } from 'ethers';
import { routerAbi } from 'shared/lib/abis/Router';
import { FilledStylizedButton } from 'shared/lib/components/common/Buttons';
import Modal from 'shared/lib/components/common/Modal';
import TokenAmountInput from 'shared/lib/components/common/TokenAmountInput';
import Tooltip from 'shared/lib/components/common/Tooltip';
import { Text } from 'shared/lib/components/common/Typography';
import { ALOE_II_ROUTER_ADDRESS } from 'shared/lib/data/constants/ChainSpecific';
import { GN, GNFormat } from 'shared/lib/data/GoodNumber';
import { Permit2State, usePermit2 } from 'shared/lib/data/hooks/UsePermit2';
import { Kitty } from 'shared/lib/data/Kitty';
import { Token } from 'shared/lib/data/Token';
import { formatNumberInput, roundPercentage } from 'shared/lib/util/Numbers';
import { useAccount, useBalance } from 'wagmi';
import { Address, useAccount, useBalance, useContractWrite, usePrepareContractWrite } from 'wagmi';

import { ChainContext } from '../../../App';
import { TokenIconsWithTooltip } from '../../common/TokenIconsWithTooltip';
import { SupplyTableRow } from '../SupplyTable';

const SECONDARY_COLOR = 'rgba(130, 160, 182, 1)';
const GAS_ESTIMATE_WIGGLE_ROOM = 110; // 10% wiggle room

enum ConfirmButtonState {
INSUFFICIENT_ASSET,
PERMIT_ASSET,
APPROVE_ASSET,
WAITING_FOR_TRANSACTION,
WAITING_FOR_USER,
READY,
LOADING,
INSUFFICIENT_ASSET,
DISABLED,
READY,
}

function getConfirmButton(state: ConfirmButtonState): { text: string; enabled: boolean } {
const permit2StateToButtonStateMap = {
[Permit2State.ASKING_USER_TO_APPROVE]: ConfirmButtonState.WAITING_FOR_USER,
[Permit2State.ASKING_USER_TO_SIGN]: ConfirmButtonState.WAITING_FOR_USER,
[Permit2State.DONE]: undefined,
[Permit2State.FETCHING_DATA]: ConfirmButtonState.LOADING,
[Permit2State.READY_TO_APPROVE]: ConfirmButtonState.APPROVE_ASSET,
[Permit2State.READY_TO_SIGN]: ConfirmButtonState.PERMIT_ASSET,
[Permit2State.WAITING_FOR_TRANSACTION]: ConfirmButtonState.WAITING_FOR_TRANSACTION,
};

function getConfirmButton(state: ConfirmButtonState, token: Token): { text: string; enabled: boolean } {
switch (state) {
case ConfirmButtonState.INSUFFICIENT_ASSET:
return {
text: `Insufficient ${token.symbol}`,
enabled: false,
};
case ConfirmButtonState.PERMIT_ASSET:
return {
text: `Permit ${token.symbol}`,
enabled: true,
};
case ConfirmButtonState.APPROVE_ASSET:
return {
text: `Approve ${token.symbol}`,
enabled: true,
};
case ConfirmButtonState.WAITING_FOR_TRANSACTION:
return { text: 'Pending', enabled: false };
case ConfirmButtonState.WAITING_FOR_USER:
return { text: 'Check Wallet', enabled: false };
case ConfirmButtonState.READY:
return { text: 'Confirm', enabled: true };
case ConfirmButtonState.LOADING:
return { text: 'Loading', enabled: false };
case ConfirmButtonState.INSUFFICIENT_ASSET:
return { text: 'Insufficient Asset', enabled: false };
case ConfirmButtonState.DISABLED:
default:
return { text: 'Confirm', enabled: false };
}
}

type DepositButtonProps = {
depositAmount: GN;
depositBalance: GN;
token: Token;
kitty: Kitty;
accountAddress: Address;
setIsOpen: (isOpen: boolean) => void;
setPendingTxn: (pendingTxn: SendTransactionResult | null) => void;
};

function DepositButton(props: DepositButtonProps) {
const { depositAmount, depositBalance, token, kitty, accountAddress, setIsOpen, setPendingTxn } = props;
const { activeChain } = useContext(ChainContext);
const [isPending, setIsPending] = useState(false);

const {
state: permit2State,
action: permit2Action,
result: permit2Result,
} = usePermit2(activeChain, token, accountAddress, ALOE_II_ROUTER_ADDRESS[activeChain.id], depositAmount);

const { config: depsitWithPermit2Config, refetch: refetchDepositWithPermit2 } = usePrepareContractWrite({
address: ALOE_II_ROUTER_ADDRESS[activeChain.id],
abi: routerAbi,
functionName: 'depositWithPermit2',
args: [
kitty.address,
permit2Result.amount.toBigNumber(),
BigNumber.from(permit2Result.nonce ?? '0'),
BigNumber.from(permit2Result.deadline),
permit2Result.signature ?? '0x',
],
chainId: activeChain.id,
enabled: permit2State === Permit2State.DONE,
});
const depositWithPermit2ConfigUpdatedRequest = useMemo(() => {
if (depsitWithPermit2Config.request) {
return {
...depsitWithPermit2Config.request,
gasLimit: depsitWithPermit2Config.request.gasLimit.mul(GAS_ESTIMATE_WIGGLE_ROOM).div(100),
};
}
return undefined;
}, [depsitWithPermit2Config.request]);
const {
write: depositWithPermit2,
isError: contractDidError,
isSuccess: contractDidSucceed,
data: contractData,
} = useContractWrite({
...depsitWithPermit2Config,
request: depositWithPermit2ConfigUpdatedRequest,
});

useEffect(() => {
if (contractDidSucceed && contractData) {
setPendingTxn(contractData);
setIsPending(false);
setIsOpen(false);
} else if (contractDidError) {
setIsPending(false);
}
}, [contractDidSucceed, contractData, contractDidError, setPendingTxn, setIsOpen]);

let confirmButtonState: ConfirmButtonState;
if (isPending) {
confirmButtonState = ConfirmButtonState.WAITING_FOR_TRANSACTION;
} else if (depositAmount.isZero()) {
confirmButtonState = ConfirmButtonState.LOADING;
} else if (depositAmount.gt(depositBalance)) {
confirmButtonState = ConfirmButtonState.INSUFFICIENT_ASSET;
} else {
confirmButtonState = permit2StateToButtonStateMap[permit2State] ?? ConfirmButtonState.READY;
}

const confirmButton = getConfirmButton(confirmButtonState, token);

function handleClickConfirm() {
if (permit2Action) {
permit2Action();
return;
}

if (confirmButtonState === ConfirmButtonState.READY) {
if (!depsitWithPermit2Config.request) {
refetchDepositWithPermit2();
return;
}
setIsPending(true);
depositWithPermit2?.();
}
}

return (
<FilledStylizedButton
size='M'
onClick={() => handleClickConfirm()}
fillWidth={true}
disabled={!confirmButton.enabled}
>
{confirmButton.text}
</FilledStylizedButton>
);
}

export type SupplyModalProps = {
isOpen: boolean;
selectedRow: SupplyTableRow;
setIsOpen: (isOpen: boolean) => void;
setPendingTxn: (pendingTxn: SendTransactionResult | null) => void;
};

export default function SupplyModal(props: SupplyModalProps) {
const { isOpen, selectedRow, setIsOpen } = props;
const { isOpen, selectedRow, setIsOpen, setPendingTxn } = props;
const [amount, setAmount] = useState<string>('');
const { activeChain } = useContext(ChainContext);
const { address: userAddress } = useAccount();
Expand All @@ -73,17 +212,6 @@ export default function SupplyModal(props: SupplyModalProps) {
const supplyAmount = GN.fromDecimalString(amount || '0', selectedRow.asset.decimals);
const apyPercentage = roundPercentage(selectedRow.apy, 2).toFixed(2);

let confirmButtonState: ConfirmButtonState;
if (supplyAmount.gt(userBalance)) {
confirmButtonState = ConfirmButtonState.INSUFFICIENT_ASSET;
} else if (amount === '') {
confirmButtonState = ConfirmButtonState.DISABLED;
} else {
confirmButtonState = ConfirmButtonState.READY;
}

const confirmButton = getConfirmButton(confirmButtonState);

const format = new Intl.ListFormat('en-US', {
style: 'long',
type: 'disjunction',
Expand Down Expand Up @@ -140,9 +268,15 @@ export default function SupplyModal(props: SupplyModalProps) {
{selectedRow.asset.symbol}.
</Text>
</div>
<FilledGradientButton size='M' fillWidth={true} disabled={!confirmButton.enabled}>
{confirmButton.text}
</FilledGradientButton>
<DepositButton
accountAddress={userAddress ?? '0x'}
depositAmount={supplyAmount}
depositBalance={userBalance}
kitty={selectedRow.kitty}
token={selectedRow.asset}
setIsOpen={setIsOpen}
setPendingTxn={setPendingTxn}
/>
</div>
</Modal>
);
Expand Down
44 changes: 42 additions & 2 deletions earn/src/pages/MarketsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useContext, useEffect, useMemo } from 'react';
import { useContext, useEffect, useMemo, useState } from 'react';

import { SendTransactionResult } from '@wagmi/core';
import axios, { AxiosResponse } from 'axios';
import { useNavigate } from 'react-router-dom';
import { borrowerLensAbi } from 'shared/lib/abis/BorrowerLens';
import AppPage from 'shared/lib/components/common/AppPage';
import { Text } from 'shared/lib/components/common/Typography';
Expand All @@ -11,6 +13,7 @@ import { getTokenBySymbol } from 'shared/lib/data/TokenData';
import { useAccount, useContract, useProvider } from 'wagmi';

import { ChainContext } from '../App';
import PendingTxnModal, { PendingTxnModalStatus } from '../components/common/PendingTxnModal';
import BorrowingWidget, { BorrowEntry, CollateralEntry } from '../components/lend/BorrowingWidget';
import SupplyTable, { SupplyTableRow } from '../components/lend/SupplyTable';
import { API_PRICE_RELAY_LATEST_URL } from '../data/constants/Values';
Expand Down Expand Up @@ -52,11 +55,15 @@ export default function MarketsPage() {
);
const [marginAccounts, setMarginAccounts] = useChainDependentState<MarginAccount[] | null>(null, activeChain.id);
const [tokenColors, setTokenColors] = useChainDependentState<Map<string, string>>(new Map(), activeChain.id);
const [pendingTxn, setPendingTxn] = useState<SendTransactionResult | null>(null);
const [isPendingTxnModalOpen, setIsPendingTxnModalOpen] = useState(false);
const [pendingTxnModalStatus, setPendingTxnModalStatus] = useState<PendingTxnModalStatus | null>(null);

// MARK: wagmi hooks
const account = useAccount();
const provider = useProvider({ chainId: activeChain.id });
const userAddress = account.address;
const navigate = useNavigate();

const uniqueTokens = useMemo(() => {
const tokens = new Set<Token>();
Expand All @@ -78,6 +85,20 @@ export default function MarketsPage() {

const availablePools = useAvailablePools();

useEffect(() => {
(async () => {
if (!pendingTxn) return;
setPendingTxnModalStatus(PendingTxnModalStatus.PENDING);
setIsPendingTxnModalOpen(true);
const receipt = await pendingTxn.wait();
if (receipt.status === 1) {
setPendingTxnModalStatus(PendingTxnModalStatus.SUCCESS);
} else {
setPendingTxnModalStatus(PendingTxnModalStatus.FAILURE);
}
})();
}, [pendingTxn, setIsPendingTxnModalOpen, setPendingTxnModalStatus]);

useEffect(() => {
(async () => {
const tokenColorMap: Map<string, string> = new Map();
Expand Down Expand Up @@ -232,6 +253,7 @@ export default function MarketsPage() {
);
rows.push({
asset: pair.token0,
kitty: pair.kitty0,
apy: pair.kitty0Info.apy,
collateralAssets: [pair.token1],
supplyBalance: kitty0Balance?.balance || 0,
Expand All @@ -240,6 +262,7 @@ export default function MarketsPage() {
});
rows.push({
asset: pair.token1,
kitty: pair.kitty1,
apy: pair.kitty1Info.apy,
collateralAssets: [pair.token0],
supplyBalance: kitty1Balance?.balance || 0,
Expand Down Expand Up @@ -304,7 +327,7 @@ export default function MarketsPage() {
<AppPage>
<div className='flex flex-col gap-6 max-w-screen-2xl m-auto'>
<Text size='XL'>Supply</Text>
<SupplyTable rows={supplyRows} />
<SupplyTable rows={supplyRows} setPendingTxn={setPendingTxn} />
<div className='flex flex-col gap-6'>
<BorrowingWidget
marginAccounts={marginAccounts}
Expand All @@ -314,6 +337,23 @@ export default function MarketsPage() {
/>
</div>
</div>
<PendingTxnModal
isOpen={isPendingTxnModalOpen}
txnHash={pendingTxn?.hash}
setIsOpen={(isOpen: boolean) => {
setIsPendingTxnModalOpen(isOpen);
if (!isOpen) {
setPendingTxn(null);
}
}}
onConfirm={() => {
setIsPendingTxnModalOpen(false);
setTimeout(() => {
navigate(0);
}, 100);
}}
status={pendingTxnModalStatus}
/>
</AppPage>
);
}

0 comments on commit 729b91b

Please sign in to comment.