342 lines
10 KiB
TypeScript
342 lines
10 KiB
TypeScript
import { Button, Card, Popover, Spin, Typography } from "antd";
|
|
import React, { useEffect, useMemo, useState } from "react";
|
|
import {
|
|
useConnection,
|
|
useConnectionConfig,
|
|
useSlippageConfig,
|
|
} from "../../utils/connection";
|
|
import { useWallet } from "../../context/wallet";
|
|
import { CurrencyInput } from "../currencyInput";
|
|
import {
|
|
LoadingOutlined,
|
|
SwapOutlined,
|
|
QuestionCircleOutlined,
|
|
SettingOutlined,
|
|
} from "@ant-design/icons";
|
|
import {
|
|
swap,
|
|
usePoolForBasket,
|
|
PoolOperation,
|
|
LIQUIDITY_PROVIDER_FEE,
|
|
} from "../../utils/pools";
|
|
import { notify } from "../../utils/notifications";
|
|
import { useCurrencyPairState } from "../../utils/currencyPair";
|
|
import { generateActionLabel, POOL_NOT_AVAILABLE, SWAP_LABEL } from "../labels";
|
|
import "./trade.less";
|
|
import { colorWarning, getTokenName } from "../../utils/utils";
|
|
import { AdressesPopover } from "../pool/address";
|
|
import { PoolInfo } from "../../models";
|
|
import { useEnrichedPools } from "../../context/market";
|
|
import { AppBar } from "../appBar";
|
|
import { Settings } from "../settings";
|
|
import { MigrationModal } from "../migration";
|
|
|
|
const { Text } = Typography;
|
|
|
|
const antIcon = <LoadingOutlined style={{ fontSize: 24 }} spin />;
|
|
|
|
export const TradeEntry = () => {
|
|
const { wallet, connect, connected } = useWallet();
|
|
const connection = useConnection();
|
|
const [pendingTx, setPendingTx] = useState(false);
|
|
const {
|
|
A,
|
|
B,
|
|
setLastTypedAccount,
|
|
setPoolOperation,
|
|
} = useCurrencyPairState();
|
|
const pool = usePoolForBasket([A?.mintAddress, B?.mintAddress]);
|
|
const { slippage } = useSlippageConfig();
|
|
const { tokenMap } = useConnectionConfig();
|
|
|
|
const swapAccounts = () => {
|
|
const tempMint = A.mintAddress;
|
|
const tempAmount = A.amount;
|
|
A.setMint(B.mintAddress);
|
|
A.setAmount(B.amount);
|
|
B.setMint(tempMint);
|
|
B.setAmount(tempAmount);
|
|
// @ts-ignore
|
|
setPoolOperation((op: PoolOperation) => {
|
|
switch (+op) {
|
|
case PoolOperation.SwapGivenInput:
|
|
return PoolOperation.SwapGivenProceeds;
|
|
case PoolOperation.SwapGivenProceeds:
|
|
return PoolOperation.SwapGivenInput;
|
|
case PoolOperation.Add:
|
|
return PoolOperation.SwapGivenInput;
|
|
}
|
|
});
|
|
};
|
|
|
|
const handleSwap = async () => {
|
|
if (A.account && B.mintAddress) {
|
|
try {
|
|
setPendingTx(true);
|
|
|
|
const components = [
|
|
{
|
|
account: A.account,
|
|
mintAddress: A.mintAddress,
|
|
amount: A.convertAmount(),
|
|
},
|
|
{
|
|
mintAddress: B.mintAddress,
|
|
amount: B.convertAmount(),
|
|
},
|
|
];
|
|
|
|
await swap(connection, wallet, components, slippage, pool);
|
|
} catch {
|
|
notify({
|
|
description:
|
|
"Please try again and approve transactions from your wallet",
|
|
message: "Swap trade cancelled.",
|
|
type: "error",
|
|
});
|
|
} finally {
|
|
setPendingTx(false);
|
|
}
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div className="input-card">
|
|
<AdressesPopover pool={pool} />
|
|
<CurrencyInput
|
|
title="Input"
|
|
onInputChange={(val: any) => {
|
|
setPoolOperation(PoolOperation.SwapGivenInput);
|
|
if (A.amount !== val) {
|
|
setLastTypedAccount(A.mintAddress);
|
|
}
|
|
|
|
A.setAmount(val);
|
|
}}
|
|
amount={A.amount}
|
|
mint={A.mintAddress}
|
|
onMintChange={(item) => {
|
|
A.setMint(item);
|
|
}}
|
|
/>
|
|
<Button type="primary" className="swap-button" onClick={swapAccounts}>
|
|
⇅
|
|
</Button>
|
|
<CurrencyInput
|
|
title="To (Estimate)"
|
|
onInputChange={(val: any) => {
|
|
setPoolOperation(PoolOperation.SwapGivenProceeds);
|
|
if (B.amount !== val) {
|
|
setLastTypedAccount(B.mintAddress);
|
|
}
|
|
|
|
B.setAmount(val);
|
|
}}
|
|
amount={B.amount}
|
|
mint={B.mintAddress}
|
|
onMintChange={(item) => {
|
|
B.setMint(item);
|
|
}}
|
|
/>
|
|
</div>
|
|
<Button
|
|
className="trade-button"
|
|
type="primary"
|
|
size="large"
|
|
onClick={connected ? handleSwap : connect}
|
|
style={{ width: "100%" }}
|
|
disabled={true}
|
|
// disabled={
|
|
// connected &&
|
|
// (pendingTx ||
|
|
// !A.account ||
|
|
// !B.mintAddress ||
|
|
// A.account === B.account ||
|
|
// !A.sufficientBalance() ||
|
|
// !pool)
|
|
// }
|
|
>
|
|
{/*{generateActionLabel(*/}
|
|
{/* !pool*/}
|
|
{/* ? POOL_NOT_AVAILABLE(*/}
|
|
{/* getTokenName(tokenMap, A.mintAddress),*/}
|
|
{/* getTokenName(tokenMap, B.mintAddress)*/}
|
|
{/* )*/}
|
|
{/* : SWAP_LABEL,*/}
|
|
{/* connected,*/}
|
|
{/* tokenMap,*/}
|
|
{/* A,*/}
|
|
{/* B,*/}
|
|
{/* true*/}
|
|
{/*)}*/}
|
|
{/*{pendingTx && <Spin indicator={antIcon} className="add-spinner" />}*/}
|
|
Swap is being deprecated
|
|
</Button>
|
|
<TradeInfo pool={pool} />
|
|
</>
|
|
);
|
|
};
|
|
|
|
export const TradeInfo = (props: { pool?: PoolInfo }) => {
|
|
const { A, B } = useCurrencyPairState();
|
|
const { pool } = props;
|
|
const { slippage } = useSlippageConfig();
|
|
const pools = useMemo(() => (pool ? [pool] : []), [pool]);
|
|
const enriched = useEnrichedPools(pools);
|
|
|
|
const [amountOut, setAmountOut] = useState(0);
|
|
const [priceImpact, setPriceImpact] = useState(0);
|
|
const [lpFee, setLpFee] = useState(0);
|
|
const [exchangeRate, setExchangeRate] = useState(0);
|
|
const [priceAccount, setPriceAccount] = useState("");
|
|
|
|
useEffect(() => {
|
|
if (!pool || enriched.length === 0) {
|
|
return;
|
|
}
|
|
if (B.amount) {
|
|
const minAmountOut = parseFloat(B?.amount) * (1 - slippage);
|
|
setAmountOut(minAmountOut);
|
|
}
|
|
const liqA = enriched[0].liquidityA;
|
|
const liqB = enriched[0].liquidityB;
|
|
const supplyRatio = liqA / liqB;
|
|
// We need to make sure the order matched the pool's accounts order
|
|
const enrichedA = A.mintAddress === enriched[0].mints[0] ? A : B;
|
|
const enrichedB = enrichedA.mintAddress === A.mintAddress ? B : A;
|
|
const calculatedRatio =
|
|
parseFloat(enrichedA.amount) / parseFloat(enrichedB.amount);
|
|
// % difference between pool ratio and calculated ratio
|
|
setPriceImpact(Math.abs(100 - (calculatedRatio * 100) / supplyRatio));
|
|
|
|
// 6 decimals without trailing zeros
|
|
const lpFeeStr = (parseFloat(A.amount) * LIQUIDITY_PROVIDER_FEE).toFixed(6);
|
|
setLpFee(parseFloat(lpFeeStr));
|
|
|
|
if (priceAccount === B.mintAddress) {
|
|
setExchangeRate(parseFloat(B.amount) / parseFloat(A.amount));
|
|
} else {
|
|
setExchangeRate(parseFloat(A.amount) / parseFloat(B.amount));
|
|
}
|
|
}, [A, B, slippage, pool, enriched, priceAccount]);
|
|
|
|
const handleSwapPriceInfo = () => {
|
|
if (priceAccount !== B.mintAddress) {
|
|
setPriceAccount(B.mintAddress);
|
|
} else {
|
|
setPriceAccount(A.mintAddress);
|
|
}
|
|
};
|
|
return !!parseFloat(B.amount) ? (
|
|
<div className="pool-card" style={{ width: "initial" }}>
|
|
<div className="pool-card-row">
|
|
<Text className="pool-card-cell">Price</Text>
|
|
<div className="pool-card-cell " title={exchangeRate.toString()}>
|
|
<Button
|
|
shape="circle"
|
|
size="middle"
|
|
type="text"
|
|
icon={<SwapOutlined />}
|
|
onClick={handleSwapPriceInfo}
|
|
>
|
|
{exchangeRate.toFixed(6)}
|
|
{priceAccount === B.mintAddress ? B.name : A.name} per
|
|
{priceAccount === B.mintAddress ? A.name : B.name}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
<div className="pool-card-row">
|
|
<Text className="pool-card-cell">
|
|
<Popover
|
|
trigger="hover"
|
|
content={
|
|
<div style={{ width: 300 }}>
|
|
You transaction will revert if there is a large, unfavorable
|
|
price movement before it is confirmed.
|
|
</div>
|
|
}
|
|
>
|
|
Minimum Received <QuestionCircleOutlined />
|
|
</Popover>
|
|
</Text>
|
|
<div className="pool-card-cell " title={amountOut.toString()}>
|
|
{amountOut.toFixed(6)} {B.name}
|
|
</div>
|
|
</div>
|
|
<div className="pool-card-row">
|
|
<Text className="pool-card-cell">
|
|
<Popover
|
|
trigger="hover"
|
|
content={
|
|
<div style={{ width: 300 }}>
|
|
The difference between the market price and estimated price due
|
|
to trade size.
|
|
</div>
|
|
}
|
|
>
|
|
Price Impact <QuestionCircleOutlined />
|
|
</Popover>
|
|
</Text>
|
|
<div
|
|
className="pool-card-cell "
|
|
title={priceImpact.toString()}
|
|
style={{ color: colorWarning(priceImpact) }}
|
|
>
|
|
{priceImpact < 0.01 ? "< 0.01%" : priceImpact.toFixed(3) + "%"}
|
|
</div>
|
|
</div>
|
|
<div className="pool-card-row">
|
|
<Text className="pool-card-cell">
|
|
<Popover
|
|
trigger="hover"
|
|
content={
|
|
<div style={{ width: 300 }}>
|
|
A portion of each trade ({LIQUIDITY_PROVIDER_FEE * 100}%) goes
|
|
to liquidity providers as a protocol incentive.
|
|
</div>
|
|
}
|
|
>
|
|
Liquidity Provider Fee <QuestionCircleOutlined />
|
|
</Popover>
|
|
</Text>
|
|
<div className="pool-card-cell " title={lpFee.toString()}>
|
|
{lpFee} {A.name}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : null;
|
|
};
|
|
|
|
export const TradeView = () => {
|
|
return (
|
|
<>
|
|
<AppBar
|
|
right={
|
|
<Popover
|
|
placement="topRight"
|
|
title="Settings"
|
|
content={<Settings />}
|
|
trigger="click"
|
|
>
|
|
<Button
|
|
shape="circle"
|
|
size="large"
|
|
type="text"
|
|
icon={<SettingOutlined />}
|
|
/>
|
|
</Popover>
|
|
}
|
|
/>
|
|
<Card
|
|
className="exchange-card"
|
|
headStyle={{ padding: 0 }}
|
|
bodyStyle={{ position: "relative" }}
|
|
>
|
|
<TradeEntry />
|
|
</Card>
|
|
<MigrationModal />
|
|
</>
|
|
);
|
|
};
|