+
+ Asset
+
+ {token.symbol}{" "}
+
+
+
+
+ From → To
+
+ {getChainInfo(transfer.sourceChainId).name} →{" "}
+ {getChainInfo(transfer.destinationChainId).name}
+
+
+
+ Amount
+ {formatTokenUnits(transfer.amount)}
+
+
+ Current fee %
+ {formatWeiPct(transfer.currentRelayerFeePct)}%
+
+
+ Current fee in {token.symbol}
+
+ {formatTokenUnits(
+ calcPctOfTokenAmount(transfer.currentRelayerFeePct, transfer.amount)
+ )}
+
+
+
+
+ New fee %
+ {hideNewFee ? "-" : appendPercentageSign(feeInput)}
+
+
+ New fee in {token.symbol}
+
+ {hideNewFee
+ ? "-"
+ : formatTokenUnits(
+ calcPctOfTokenAmount(
+ feeInputToBigNumberPct(feeInput || "0"),
+ transfer.amount
+ )
+ )}
+
+
+
+ );
+}
+
+const StatsBox = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ padding: 16px;
+ gap: 12px;
+
+ border: 1px solid #34353b;
+ border-radius: 12px;
+`;
+
+const StatRow = styled.div<{ highlightValue?: boolean }>`
+ width: 100%;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ color: #9daab2;
+ font-weight: 400;
+ font-size: ${16 / 16}rem;
+ line-height: ${20 / 16}rem;
+
+ div:nth-of-type(2) {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ color: ${({ highlightValue }) => (highlightValue ? "#E0F3FF" : "inherit")};
+ }
+
+ img {
+ height: 16px;
+ width: 16px;
+ }
+`;
+
+const Divider = styled.div`
+ display: flex;
+ width: 100%;
+ border-top: 1px solid #34353b;
+`;
diff --git a/src/views/Transactions/components/SpeedUpModal/index.tsx b/src/views/Transactions/components/SpeedUpModal/index.tsx
new file mode 100644
index 000000000..a5a13038a
--- /dev/null
+++ b/src/views/Transactions/components/SpeedUpModal/index.tsx
@@ -0,0 +1 @@
+export { SpeedUpModal } from "./SpeedUpModal";
diff --git a/src/views/Transactions/components/SpeedUpModal/utils.tsx b/src/views/Transactions/components/SpeedUpModal/utils.tsx
new file mode 100644
index 000000000..f577d74a6
--- /dev/null
+++ b/src/views/Transactions/components/SpeedUpModal/utils.tsx
@@ -0,0 +1,54 @@
+import { BigNumber, utils } from "ethers";
+import { toWeiSafe } from "utils";
+
+export function appendPercentageSign(feeInput: string) {
+ return feeInput.replaceAll("%", "") + "%";
+}
+
+export function removePercentageSign(feeInput: string) {
+ return feeInput.replaceAll("%", "");
+}
+
+export function feeInputToBigNumberPct(feeInput: string) {
+ return toWeiSafe(removePercentageSign(feeInput) || "0").div(
+ BigNumber.from(100)
+ );
+}
+
+export function calcPctOfTokenAmount(
+ pctBigNumber: BigNumber,
+ tokenAmount: BigNumber
+) {
+ return pctBigNumber.mul(tokenAmount).div(utils.parseEther("1"));
+}
+
+export function validateFeeInput(
+ input: string,
+ opts: {
+ maxFeePct: number;
+ minFeePct: number;
+ maxDecimals: number;
+ }
+) {
+ const cleanedInput = removePercentageSign(input);
+ const inputNum = Number(cleanedInput);
+ if (isNaN(inputNum)) {
+ throw new Error("Invalid number");
+ }
+
+ const inputPct = inputNum / 100;
+ if (inputPct > opts.maxFeePct || inputPct < opts.minFeePct) {
+ throw new Error(
+ `Fee must be between ${opts.minFeePct * 100}% and ${
+ opts.maxFeePct * 100
+ }%`
+ );
+ }
+
+ if (
+ cleanedInput.includes(".") &&
+ cleanedInput.split(".")[1].length > opts.maxDecimals
+ ) {
+ throw new Error(`Max. ${opts.maxDecimals} decimals allowed`);
+ }
+}
diff --git a/src/views/Transactions/components/TransactionsTable/TransactionsTable.styles.tsx b/src/views/Transactions/components/TransactionsTable/TransactionsTable.styles.tsx
index 65d5b7226..673d5b438 100644
--- a/src/views/Transactions/components/TransactionsTable/TransactionsTable.styles.tsx
+++ b/src/views/Transactions/components/TransactionsTable/TransactionsTable.styles.tsx
@@ -1,4 +1,5 @@
import styled from "@emotion/styled";
+
import { ReactComponent as AcrossPlusIcon } from "assets/across-plus-icon.svg";
import { QUERIES } from "utils";
import {
@@ -26,7 +27,17 @@ export const TableBody = BaseTableBody;
export const TableHeadRow = BaseTableHeadRow;
-export const TableRow = BaseTableRow;
+export const TableRow = styled(BaseTableRow)`
+ :hover {
+ #speed-up-cell {
+ svg {
+ opacity: 1;
+ transition: opacity 0.3s ease-in-out;
+ stroke: #44d2ff;
+ }
+ }
+ }
+`;
export const EmptyRow = BaseEmptyRow;
@@ -115,7 +126,7 @@ export const AccordionRow = styled.div`
border-bottom: 1px solid #2c2f33;
text-indent: 12px;
}
- &:nth-of-type(6) > div {
+ &:nth-of-type(7) > div {
border-bottom: none;
}
`;
@@ -140,3 +151,18 @@ export const StyledPlus = styled(AcrossPlusIcon)`
margin-right: 4px;
margin-left: 8px;
`;
+
+export const SpeedUpHeadCell = styled(HeadCell)`
+ flex: 0.3;
+`;
+
+export const SpeedUpCell = styled(TableCell)`
+ cursor: pointer;
+ flex: 0.3;
+
+ svg {
+ opacity: 0;
+ height: 16px;
+ width: 16px;
+ }
+`;
diff --git a/src/views/Transactions/components/TransactionsTable/TransactionsTable.tsx b/src/views/Transactions/components/TransactionsTable/TransactionsTable.tsx
index 9877913f0..188e7b3e4 100644
--- a/src/views/Transactions/components/TransactionsTable/TransactionsTable.tsx
+++ b/src/views/Transactions/components/TransactionsTable/TransactionsTable.tsx
@@ -3,6 +3,7 @@ import { useState } from "react";
import { HeadRow, DataRow, MobileDataRow } from "./rows";
import { FillTxInfoModal } from "../FillTxInfoModal";
import { FillTxsListModal } from "../FillTxsListModal";
+import { SpeedUpModal } from "../SpeedUpModal";
import { doPartialFillsExist } from "../../utils";
import { TxLink, SupportedTxTuple } from "../../types";
@@ -21,6 +22,7 @@ export type Props = {
title: string;
enablePartialFillInfoIcon?: boolean;
isMobile?: boolean;
+ enableSpeedUps?: boolean;
};
export function TransactionsTable({
@@ -28,10 +30,12 @@ export function TransactionsTable({
title,
enablePartialFillInfoIcon = false,
isMobile = false,
+ enableSpeedUps = false,
}: Props) {
const [fillTxLinks, setFillTxLinks] = useState