import { useCallback, useEffect, useRef, useState } from "react";
import styled, { keyframes } from "styled-components";
import { observer } from "mobx-react-lite";
import uuid4 from "uuid4";
import { SwitchIcon } from "../icons/switch";
import { ArrowRightIcon } from "../icons/arrow-right";
import { formatter } from "../../omni/utils";
import { tokens } from "../../omni/tokens";
import { Recipient } from "../../omni/recipient";
import { OmniWallet } from "../../OmniWallet";
import { BridgeReview } from "../../exchange";
import { HotConnector } from "../../HotConnector";
import { WalletType } from "../../omni/config";
import { Token } from "../../omni/token";
import Popup from "../Popup";
import { PopupButton } from "../styles";
import { openSelectRecipient, openSelectTokenPopup, openSelectSender } from "../router";
import { TokenIcon } from "./TokenCard";
import DepositQR from "./DepositQR";
export interface BridgeProps {
hot: HotConnector;
widget?: boolean;
onClose: () => void;
onProcess: (task: Promise) => void;
setup?: {
autoClose?: boolean; // if true, the popup will close automatically when the transaction is successful
title?: string;
readonlyAmount?: boolean;
readonlyTo?: boolean;
readonlyFrom?: boolean;
type?: "exactIn" | "exactOut";
sender?: OmniWallet;
recipient?: Recipient;
amount?: number;
from?: Token;
to?: Token;
};
}
const FIXED = 6;
export const Bridge = observer(({ hot, widget, setup, onClose, onProcess }: BridgeProps) => {
const [isFiat, setIsFiat] = useState(false);
const [type, setType] = useState<"exactIn" | "exactOut">(setup?.type || "exactIn");
const [value, setValue] = useState(setup?.amount?.toString() ?? "");
const [from, setFrom] = useState(setup?.from || tokens.list.find((t) => t.id === localStorage.getItem("bridge:from")) || tokens.list.find((t) => t.symbol === "NEAR")!);
const [to, setTo] = useState(setup?.to || tokens.list.find((t) => t.id === localStorage.getItem("bridge:to")) || tokens.list.find((t) => t.symbol === "USDT")!);
const [review, setReview] = useState(null);
const [isError, setIsError] = useState(null);
const [isReviewing, setIsReviewing] = useState(false);
const [processing, setProcessing] = useState<{
status: "qr" | "processing" | "success" | "error";
resolve?: (value: BridgeReview) => void;
reject?: (error: Error) => void;
message: string;
review: BridgeReview;
} | null>(null);
const [sender, setSender] = useState(() => {
if (setup?.sender) return setup.sender;
if (from.type === WalletType.OMNI) return hot.priorityWallet;
return hot.wallets.find((w) => w.type === from.type);
});
const [recipient, setRecipient] = useState(() => {
if (setup?.recipient) return setup.recipient;
if (to.type === WalletType.OMNI) return Recipient.fromWallet(hot.priorityWallet!);
return Recipient.fromWallet(hot.wallets.find((w) => w.type === to.type)!);
});
const initialSender = useRef(sender);
const initialRecipient = useRef(recipient);
const valueInTokens = isFiat ? +formatter.fromInput(value) / (type === "exactIn" ? from.usd : to.usd) : +formatter.fromInput(value);
const amountFrom = type === "exactOut" ? from.float(review?.amountIn ?? 0) : valueInTokens;
const amountTo = type === "exactIn" ? to.float(review?.amountOut ?? 0) : valueInTokens;
const showAmountFrom = type === "exactOut" ? +from.float(review?.amountIn ?? 0).toFixed(FIXED) : formatter.fromInput(value);
const showAmountTo = type === "exactIn" ? +to.float(review?.amountOut ?? 0).toFixed(FIXED) : formatter.fromInput(value);
const availableBalance = sender !== "qr" ? +Math.max(0, from.float(hot.balance(sender, from)) - from.reserve).toFixed(FIXED) : 0;
useEffect(() => {
localStorage.setItem("bridge:from", from.id);
localStorage.setItem("bridge:to", to.id);
}, [from, to]);
useEffect(() => {
if (initialSender.current == null) {
if (from.type === WalletType.OMNI) setSender(hot.priorityWallet);
else setSender(hot.wallets.find((w) => w.type === from.type));
}
if (initialRecipient.current == null) {
if (to.type === WalletType.OMNI) setRecipient(Recipient.fromWallet(hot.priorityWallet));
else setRecipient(Recipient.fromWallet(hot.wallets.find((w) => w.type === to.type)));
}
}, [to, from, hot.wallets, hot.priorityWallet]);
const reviewId = useRef(uuid4());
const throwError = (message: string) => {
setIsError(message);
setIsReviewing(false);
};
const reviewSwap = useCallback(() => {
reviewId.current = uuid4();
const currentReviewId = reviewId.current;
setIsReviewing(true);
setReview(null);
setIsError(null);
const refund = sender !== "qr" ? sender : hot.priorityWallet;
if (valueInTokens <= 0) return throwError("Enter amount");
if (!sender) return throwError("Set sender");
if (!recipient) return throwError("Set recipient");
if (!refund) return throwError("Connect any wallet");
const debounceTimer = setTimeout(async () => {
try {
if (currentReviewId !== reviewId.current) return;
const amount = type === "exactIn" ? from.int(valueInTokens) : to.int(valueInTokens);
const review = await hot.exchange.reviewSwap({ sender, refund, amount, recipient, slippage: 0.005, type, from, to });
if (currentReviewId !== reviewId.current) return;
setIsReviewing(false);
setIsError(null);
setReview(review);
} catch (e) {
if (currentReviewId !== reviewId.current) return;
setIsError(typeof e === "string" ? e : e instanceof Error ? e.message : "Failed to review swap");
setIsReviewing(false);
setReview(null);
console.error(e);
}
}, 500);
return () => clearTimeout(debounceTimer);
}, [valueInTokens, type, from, to, sender, hot.exchange, recipient, hot.priorityWallet]);
useEffect(() => {
reviewSwap();
}, [reviewSwap]);
const process = async (review: BridgeReview) => {
try {
setProcessing({ status: "processing", message: "Signing transaction", review });
const result = await hot.exchange.makeSwap(review, { log: (message: string) => setProcessing({ status: "processing", message, review }) });
setProcessing({ status: "success", message: "Transaction signed", review: result });
if (setup?.autoClose) onClose();
return result;
} catch (e) {
setProcessing({ status: "error", message: e?.toString?.() ?? "Unknown error", review });
throw e;
}
};
const cancelReview = () => {
setReview(null);
setIsReviewing(false);
setProcessing(null);
setIsError(null);
reviewSwap();
};
const handleConfirm = async () => {
if (sender != "qr") return onProcess(process(review!));
setProcessing({ status: "qr", message: "Scan QR code to sign transaction", review: review! });
};
const handleMax = () => {
if (sender === "qr") return;
if (isFiat) {
setType("exactIn");
setValue(String(+(availableBalance * from.usd).toFixed(FIXED)));
} else {
setType("exactIn");
setValue(String(availableBalance));
}
};
if (processing?.status === "qr") {
return (
{setup?.title}
: null}>
onProcess(process(processing.review))}
onCancel={cancelReview}
/>
);
}
if (processing?.status === "processing") {
return (
{setup?.title} : null}>
{/* @ts-expect-error: dotlottie-wc is not typed */}
{processing.message}
);
}
if (processing?.status === "success") {
return (
{setup?.title} : null}>
{/* @ts-expect-error: dotlottie-wc is not typed */}
Exchange successful
(cancelReview(), onClose())}>
Continue
);
}
if (processing?.status === "error") {
return (
{setup?.title} : null}>
{/* @ts-expect-error: dotlottie-wc is not typed */}
Exchange failed
{processing.message}
(cancelReview(), onClose())}>Continue
);
}
const button = () => {
if (sender == null) return Set sender;
if (recipient == null) return Set recipient;
if (sender !== "qr" && +from.float(hot.balance(sender, from)).toFixed(FIXED) < +amountFrom.toFixed(FIXED)) return Insufficient balance;
return (
{isReviewing ? "Quoting..." : isError != null ? isError : "Confirm"}
);
};
return (
{setup?.title} : null}>
{from.chain === -4 ? "Withdraw HEX from:" : "Send from:"}
openSelectSender({ hot, type: from.type, onSelect: (wallet) => setSender(wallet) })}>
{formatter.truncateAddress(sender === "qr" ? "QR code" : sender?.address ?? "Connect wallet")}
openSelectTokenPopup({ hot, onSelect: (token, wallet) => (setFrom(token), setSender(wallet)) })}
/>
{isReviewing && type === "exactOut" ? (
) : (
(setType("exactIn"), setValue(e.target.value))}
placeholder="0"
autoFocus
/>
)}
{isFiat && (
{sender !== "qr" && (
Balance: ${from.readable(availableBalance, from.usd)}
sender && hot.fetchToken(from, sender)} />
)}
{sender === "qr" &&
}
{from.usd !== 0 &&
{`${from.readable(amountFrom / from.usd)} ${from.symbol}`}
}
{from.usd !== 0 && (
setIsFiat(!isFiat)}>
USD
)}
{sender !== "qr" &&
MAX}
)}
{!isFiat && (
{sender !== "qr" && (
Balance: {`${from.readable(availableBalance)} ${from.symbol}`}
sender && hot.fetchToken(from, sender)} />
)}
{sender === "qr" &&
}
{from.usd !== 0 &&
${from.readable(amountFrom, from.usd)}
}
{from.usd !== 0 &&
setIsFiat(!isFiat)}>USD}
{sender !== "qr" &&
MAX}
)}
{
setFrom(to);
setTo(from);
setSender(hot.wallets.find((w) => w.address === recipient?.address));
setRecipient(sender === "qr" ? undefined : sender ? Recipient.fromWallet(sender) : undefined);
setType(type === "exactIn" ? "exactOut" : "exactIn");
setValue("");
}}
>
{to.chain !== -4 ? "To:" : "Deposit HEX to:"}
openSelectRecipient({ hot, recipient, type: to.type, onSelect: (recipient) => setRecipient(recipient) })}>
{formatter.truncateAddress(recipient?.address ?? "Connect wallet")}
openSelectTokenPopup({
hot,
onSelect: (token, wallet) => {
setRecipient(wallet ? Recipient.fromWallet(wallet) : undefined);
setTo(token);
},
})
}
/>
{isReviewing && type === "exactIn" ? (
) : (
(setType("exactOut"), setValue(e.target.value))}
placeholder="0"
/>
)}
{isFiat &&
Receive: ${`${to.readable(amountTo ?? 0)} ${to.symbol}`}
}
{!isFiat &&
Receive: ${to.readable(amountTo ?? 0, to.usd)}
}
{button()}
);
});
const TokenPreview = ({ style, token, onSelect }: { style?: React.CSSProperties; token: Token; onSelect: (token: Token) => void }) => {
return (
onSelect(token)}>
{token.symbol}
);
};
const BadgeButton = styled.button`
font-size: 12px;
font-weight: 500;
color: #fff;
background: #282c30;
padding: 4px 8px;
border-radius: 16px;
cursor: pointer;
outline: none;
border: none;
border: 1px solid transparent;
transition: 0.2s border-color;
&:hover {
border-color: #4e4e4e;
}
* {
font-size: 14px;
font-weight: bold;
}
`;
const SelectTokenButton = styled.button`
display: flex;
align-items: center;
gap: 12px;
flex-shrink: 0;
cursor: pointer;
outline: none;
border: none;
position: relative;
background: transparent;
border-radius: 32px;
padding: 8px;
padding-right: 32px;
margin: -8px;
max-width: 160px;
transition: 0.2s background-color;
&:hover {
background: rgba(255, 255, 255, 0.2);
}
p {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
`;
const AvailableBalance = styled.div`
display: flex;
align-items: center;
gap: 4;
overflow: hidden;
max-width: 200px;
text-wrap: nowrap;
p {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
`;
const Card = styled.div`
display: flex;
width: 100%;
flex-direction: column;
gap: 16px;
width: 100%;
text-align: left;
align-items: flex-start;
justify-content: center;
border-radius: 12px;
input {
outline: none;
border: none;
background: none;
color: #fff;
font-size: 32px;
font-weight: bold;
width: 100%;
line-height: 40px;
text-align: left;
align-items: flex-start;
justify-content: center;
background: transparent;
text-align: right;
border: none;
padding: 0;
margin: 0;
}
`;
const SwitchButton = styled.button`
position: absolute;
left: 50%;
top: -18px;
transform: translate(-50%, 0);
background: #232323;
border-radius: 50%;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
cursor: pointer;
border: 2px solid #181818;
box-shadow: 0 2px 8px 0 #18181870;
outline: none;
border: none;
cursor: pointer;
`;
const shine = keyframes`
0% {
background-position: -200px 0;
}
100% {
background-position: calc(200px + 100%) 0;
}
`;
const SkeletonShine = styled.div`
display: inline-block;
width: 100px;
height: 40px;
border-radius: 8px;
background: #2e2e2e;
position: relative;
overflow: hidden;
&:after {
content: "";
display: block;
height: 100%;
width: 100%;
position: absolute;
top: 0;
left: 0;
background: linear-gradient(90deg, rgba(34, 34, 34, 0) 0%, rgba(255, 255, 255, 0.06) 40%, rgba(255, 255, 255, 0.12) 50%, rgba(255, 255, 255, 0.06) 60%, rgba(34, 34, 34, 0) 100%);
background-size: 200px 100%;
animation: ${shine} 1.4s infinite linear;
}
`;
const RefreshButton = ({ onClick }: { onClick: () => void }) => {
return (
);
};