Skip to content

Commit

Permalink
Limit minimum transfer amount of RING from Darwinia to Ethereum (#652)
Browse files Browse the repository at this point in the history
* feat: create getCrossInfo of bridge

* fix: check transfer amount valid in transfer action

* refactor: improve balance input

* feat: support minimum transfer amount verification

* feat: limit minimum transfer amount of RING on Darwinia to Ethereum

* refactor: update limit amount
  • Loading branch information
JayJay1024 authored Mar 8, 2024
1 parent 6c5103f commit 6e4b748
Show file tree
Hide file tree
Showing 6 changed files with 68 additions and 76 deletions.
4 changes: 4 additions & 0 deletions src/bridges/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ export abstract class BaseBridge {
return this.targetChain;
}

getCrossInfo() {
return this.crossInfo;
}

getEstimateTime() {
return this.estimateTime;
}
Expand Down
131 changes: 56 additions & 75 deletions src/components/balance-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,24 @@ import Input from "@/ui/input";
import InputAlert from "@/ui/input-alert";
import { formatBalance, getTokenLogoSrc } from "@/utils";
import Image from "next/image";
import { ChangeEventHandler, MouseEventHandler, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { formatUnits, parseUnits } from "viem";
import { ChangeEventHandler, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { parseUnits } from "viem";

enum ErrorCode {
INSUFFICIENT = 1,
REQUIRE_MORE,
REQUIRE_LESS,
}

interface Props {
placeholder?: string;
balance?: bigint;
max?: bigint;
min?: bigint;
compact?: boolean;
autoFocus?: boolean;
disabled?: boolean;
suffix?: "symbol" | "max";
suffix?: "symbol";
enabledDynamicStyle?: boolean;
value: InputValue<bigint>;
token: Token | undefined;
Expand All @@ -28,6 +35,7 @@ export function BalanceInput({
placeholder,
balance,
max,
min,
compact,
autoFocus,
disabled,
Expand All @@ -41,16 +49,13 @@ export function BalanceInput({
onChange = () => undefined,
onTokenChange = () => undefined,
}: Props) {
const tokenRef = useRef<Token>();
const spanRef = useRef<HTMLSpanElement | null>(null);
const inputRef = useRef<HTMLInputElement | null>(null);
const insufficientRef = useRef(false);
const exceededRef = useRef(false);
const balanceRef = useRef(balance);
const tokenRef = useRef(token);

const [enablingMax, setEnablingMax] = useState(false);
const [dynamicStyle, setDynamicStyle] = useState("text-sm font-medium");

const isExceeded = useMemo(() => (typeof max === "bigint" && max < value.value ? true : false), [max, value]);
const [errorCode, setErrorCode] = useState<ErrorCode>();

const _placeholder = useMemo(() => {
if (token && compact) {
Expand All @@ -71,36 +76,50 @@ export function BalanceInput({

if (input) {
if (token && !Number.isNaN(Number(input))) {
parsed = parseValue(input, token.decimals);
insufficientRef.current = balance !== undefined && balance < parsed.value ? true : false;
exceededRef.current = typeof max === "bigint" && max < parsed.value ? true : false;
valid = !(insufficientRef.current || exceededRef.current);
parsed = parseAmount(input, token.decimals);
if (typeof min === "bigint" && parsed.value < min) {
valid = false;
setErrorCode(ErrorCode.REQUIRE_MORE);
} else if (typeof max === "bigint" && max < parsed.value) {
valid = false;
setErrorCode(ErrorCode.REQUIRE_LESS);
} else if (typeof balance === "bigint" && balance < parsed.value) {
valid = false;
setErrorCode(ErrorCode.INSUFFICIENT);
} else {
setErrorCode(undefined);
}
onChange({ valid, ...parsed });
}
} else {
setErrorCode(undefined);
onChange({ valid, ...parsed });
}

setEnablingMax(false);
},
[token, max, balance, onChange],
[token, max, min, balance, onChange],
);

const handleMax = useCallback<MouseEventHandler<HTMLButtonElement>>(
(e) => {
e.stopPropagation();
if (typeof max === "bigint" && token) {
const decimals = token.decimals;
const parsed = parseValue(formatUnits(max, decimals), decimals);
insufficientRef.current = balance !== undefined && balance < parsed.value ? true : false;
exceededRef.current = max && max < parsed.value ? true : false;
const valid = !(insufficientRef.current || exceededRef.current);
onChange({ valid, ...parsed });
setEnablingMax(true);
useEffect(() => {
if (balance !== balanceRef.current) {
balanceRef.current = balance;
if (typeof balance === "bigint") {
if (balance < value.value) {
setErrorCode(ErrorCode.INSUFFICIENT);
onChange({ ...value, valid: false });
}
} else {
setErrorCode(undefined);
onChange({ ...value, valid: true });
}
},
[max, token, balance, onChange],
);
}
}, [balance, value, onChange]);

useEffect(() => {
if (token?.decimals !== tokenRef.current?.decimals) {
tokenRef.current = token;
onChange({ valid: true, input: "", value: 0n });
}
}, [token, onChange]);

useEffect(() => {
if (enabledDynamicStyle) {
Expand All @@ -123,31 +142,7 @@ export function BalanceInput({
setDynamicStyle("text-[1.125rem] font-medium");
}
}
}, [value, enabledDynamicStyle]);

useEffect(() => {
// Fire onChange to update value
if (token && token.decimals !== tokenRef.current?.decimals) {
const parsed = parseValue(value.input, token.decimals);
insufficientRef.current = balance !== undefined && balance < parsed.value ? true : false;
exceededRef.current = typeof max === "bigint" && max < parsed.value ? true : false;
const valid = !(insufficientRef.current || exceededRef.current);
onChange({ valid, ...parsed });
}
tokenRef.current = token;
}, [value, token, balance, max, onChange]);

useEffect(() => {
// Recalculate the maximum value
if (token && enablingMax && isExceeded) {
const decimals = token.decimals;
const parsed = parseValue(formatUnits(max ?? 0n, decimals), decimals);
insufficientRef.current = balance !== undefined && balance < parsed.value ? true : false;
exceededRef.current = typeof max === "bigint" && max < parsed.value ? true : false;
const valid = !(insufficientRef.current || exceededRef.current);
onChange({ valid, ...parsed });
}
}, [token, max, balance, enablingMax, isExceeded, onChange]);
}, [value.input, enabledDynamicStyle]);

return (
<div
Expand All @@ -171,14 +166,6 @@ export function BalanceInput({
{compact ? (
suffix === "symbol" && token ? (
<span className="text-sm font-medium">{token.symbol}</span>
) : suffix === "max" ? (
<button
className="inline-flex items-center bg-transparent px-2 py-1 transition-[transform,color] hover:scale-105 hover:bg-white/[0.15] active:scale-95 disabled:scale-100 disabled:cursor-not-allowed"
onClick={handleMax}
disabled={max === undefined || token === undefined}
>
<span className="text-sm font-medium">Max</span>
</button>
) : null
) : (
<div className="flex shrink-0 items-center gap-medium self-end">
Expand Down Expand Up @@ -218,21 +205,15 @@ export function BalanceInput({
>
<Image alt="Refresh" width={14} height={14} src="/images/refresh.svg" />
</button>
{typeof max === "bigint" ? (
<button
className="inline-flex items-center rounded-small bg-white/10 px-1 py-[1px] text-xs font-medium text-white/50 transition hover:bg-white/20 hover:text-white active:scale-95"
onClick={handleMax}
>
Max
</button>
) : null}
</div>
) : null}

{value.valid ? null : exceededRef.current ? (
<InputAlert text={`* Max: ${formatBalance(max || 0n, token?.decimals || 0, { precision: 6 })}`} />
) : insufficientRef.current ? (
{errorCode === ErrorCode.INSUFFICIENT ? (
<InputAlert text="* Insufficient" />
) : errorCode === ErrorCode.REQUIRE_LESS ? (
<InputAlert text={`* Max: ${formatBalance(max ?? 0n, token?.decimals ?? 0, { precision: 6 })}`} />
) : errorCode === ErrorCode.REQUIRE_MORE ? (
<InputAlert text={`* Min: ${formatBalance(min ?? 0n, token?.decimals ?? 0, { precision: 6 })}`} />
) : null}

<span className="invisible fixed left-0 top-0 -z-50" ref={spanRef}>
Expand All @@ -242,7 +223,7 @@ export function BalanceInput({
);
}

function parseValue(source: string, decimals: number) {
function parseAmount(source: string, decimals: number) {
let input = "";
let value = 0n;
const [i, d] = source.replace(/,/g, "").split(".").concat("-1"); // The commas must be removed or parseUnits will error
Expand Down
3 changes: 2 additions & 1 deletion src/components/transfer-action.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ export default function TransferAction({ recipient, transferable, transferAmount
bridgeInstance &&
(bridgeInstance.getCategory().startsWith("xtoken") || bridgeFee) &&
transferable &&
transferAmount.value &&
transferAmount.input &&
transferAmount.valid &&
transferAmount.value < transferable &&
isAddress(recipient ?? "")
)
Expand Down
1 change: 1 addition & 0 deletions src/components/transfer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@ export default function Transfer() {
placeholder="0"
enabledDynamicStyle
balance={sourceBalance?.value}
min={bridgeInstance?.getCrossInfo()?.min}
value={transferAmount}
token={sourceToken}
balanceLoading={balanceLoading}
Expand Down
1 change: 1 addition & 0 deletions src/config/chains/darwinia.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export const darwiniaChain: ChainConfig = {
target: { network: "ethereum", symbol: "RING" },
bridge: { category: "helix-sub2ethv2(lock)" },
action: "issue",
min: 1000000000000000000000000n,
},
{
target: { network: "polygon", symbol: "RING" },
Expand Down
4 changes: 4 additions & 0 deletions src/types/cross-chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export type CrossChain =
baseFee?: never;
action?: never;
hidden?: boolean;
min?: bigint; // Minimum transfer amount
}
| {
target: Target;
Expand All @@ -35,6 +36,7 @@ export type CrossChain =
baseFee?: never;
action?: never;
hidden?: boolean;
min?: bigint; // Minimum transfer amount
}
| {
target: Target;
Expand All @@ -44,6 +46,7 @@ export type CrossChain =
baseFee?: never;
action: Action;
hidden?: boolean;
min?: bigint; // Minimum transfer amount
}
| {
target: Target;
Expand All @@ -53,6 +56,7 @@ export type CrossChain =
baseFee: bigint;
action: Action;
hidden?: boolean;
min?: bigint; // Minimum transfer amount
};

export type AvailableBridges = {
Expand Down

0 comments on commit 6e4b748

Please sign in to comment.