Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 29 additions & 25 deletions components/DelegatingWidget/Delegate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ import {
InfoCircledIcon,
} from "@radix-ui/react-icons";
import { formatPercent } from "@utils/numberFormatters";
import { parseAmountToWei } from "@utils/web3";
import {
useBondingManagerAddress,
useLivepeerTokenAddress,
} from "hooks/useContracts";
import { useHandleTransaction } from "hooks/useHandleTransaction";
import { useOrchestratorRewardCutSpike } from "hooks/useOrchestratorRewardCutSpike";
import { useMemo, useState } from "react";
import { parseEther } from "viem";
import { useSimulateContract, useWriteContract } from "wagmi";

import ProgressSteps from "../ProgressSteps";
Expand Down Expand Up @@ -124,8 +124,21 @@ const Delegate = ({
}
);

// null when the amount is absent or not yet a valid number.
const amountWei = useMemo(() => parseAmountToWei(amount), [amount]);
const bondAmountWei = amountWei ?? 0n;
// `tokenBalance` and `transferAllowance` are wei, so compare in wei.
const sufficientBalance = useMemo(
() => bondAmountWei > 0n && BigInt(tokenBalance ?? 0) >= bondAmountWei,
[bondAmountWei, tokenBalance]
);
const sufficientTransferAllowance = useMemo(
() => bondAmountWei > 0n && BigInt(transferAllowance ?? 0) >= bondAmountWei,
[bondAmountWei, transferAllowance]
);

const bondWithHintArgs = {
amount: amount?.toString() ? parseEther(amount) : BigInt(0),
amount: bondAmountWei,
to,
oldDelegateNewPosPrev,
oldDelegateNewPosNext,
Expand All @@ -134,12 +147,17 @@ const Delegate = ({
};

const { data: bondWithHintConfig } = useSimulateContract({
query: { enabled: Boolean(to) },
// A reverted simulation is cached and never re-run, so only simulate once
// it can succeed. A zero amount moves no tokens, so it needs no allowance.
query: {
enabled:
Boolean(to) && (bondAmountWei === 0n || sufficientTransferAllowance),
},
address: bondingManagerAddress,
abi: bondingManager,
functionName: "bondWithHint",
args: [
BigInt(bondWithHintArgs.amount?.toString()),
bondWithHintArgs.amount,
to,
oldDelegateNewPosPrev,
oldDelegateNewPosNext,
Expand All @@ -166,28 +184,12 @@ const Delegate = ({

const [approvalSubmitted, setApprovalSubmitted] = useState<boolean>(false);

const amountIsNonEmpty = useMemo(() => amount, [amount]);
const sufficientBalance = useMemo(
() =>
amountIsNonEmpty &&
parseFloat(amount) > 0 &&
Number(tokenBalance) >= amount,
[amount, amountIsNonEmpty, tokenBalance]
);
const sufficientTransferAllowance = useMemo(
() =>
amountIsNonEmpty &&
Number(transferAllowance) > 0 &&
Number(transferAllowance) >= amount,
[amount, amountIsNonEmpty, transferAllowance]
);

// show approve flow when: no error on inputs, not approved or pending, or approved in current session
const showApproveFlow = useMemo(
() =>
(amountIsNonEmpty && +amount >= 0 && !sufficientTransferAllowance) ||
(amountWei !== null && !sufficientTransferAllowance) ||
(approvalSubmitted && sufficientTransferAllowance),
[amount, amountIsNonEmpty, sufficientTransferAllowance, approvalSubmitted]
[amountWei, sufficientTransferAllowance, approvalSubmitted]
);

const onApprove = async () => {
Expand Down Expand Up @@ -217,15 +219,16 @@ const Delegate = ({
reset();
};

if (!amountIsNonEmpty && !isTransferStake) {
// A stake transfer may bond nothing, but only from an empty field.
if (amountWei === null && (amount || !isTransferStake)) {
return (
<Button size="4" disabled variant="neutral" css={{ width: "100%" }}>
Enter an Amount
</Button>
);
}

if (amountIsNonEmpty && +amount >= 0 && !sufficientBalance) {
if (amountWei !== null && !sufficientBalance) {
return (
<Button size="4" disabled variant="neutral" css={{ width: "100%" }}>
Insufficient Balance
Expand Down Expand Up @@ -260,7 +263,7 @@ const Delegate = ({
</Button>
<Button
size="4"
disabled={!sufficientTransferAllowance}
disabled={!sufficientTransferAllowance || !bondWithHintConfig}
variant="primary"
onClick={onDelegate}
css={{ width: "100%" }}
Expand All @@ -282,6 +285,7 @@ const Delegate = ({
{cutChangeNotice}
<Button
size="4"
disabled={!bondWithHintConfig}
onClick={onDelegate}
variant="primary"
css={{ width: "100%" }}
Expand Down
31 changes: 12 additions & 19 deletions components/DelegatingWidget/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
simulateNewActiveSetOrder,
} from "@lib/utils";
import { Box, Button } from "@livepeer/design-system";
import { parseAmountToWei } from "@utils/web3";
import { AccountQueryResult, OrchestratorsSortedQueryResult } from "apollo";
import {
StakingAction,
Expand All @@ -16,7 +17,6 @@ import {
} from "hooks";
import { CHAIN_INFO } from "lib/chains";
import { useMemo } from "react";
import { parseEther } from "viem";

import Delegate from "./Delegate";
import Footnote from "./Footnote";
Expand Down Expand Up @@ -91,15 +91,12 @@ const Footer = ({
: null,
[delegatorPendingStakeAndFees]
);
// null when the amount is absent or not yet a valid number.
const amountWei = useMemo(() => parseAmountToWei(amount), [amount]);
const sufficientStake = useMemo(() => {
if (!delegator || !amount || stakeWei === null) return false;
try {
const amountWei = parseEther(amount);
return amountWei <= stakeWei;
} catch {
return false;
}
}, [delegator, amount, stakeWei]);
if (!delegator || amountWei === null || stakeWei === null) return false;
return amountWei <= stakeWei;
}, [delegator, amountWei, stakeWei]);
const canUndelegate = useMemo(
() => isMyTranscoder && isDelegated && parseFloat(amount) > 0,
[isMyTranscoder, isDelegated, amount]
Expand All @@ -109,11 +106,11 @@ const Footer = ({
simulateNewActiveSetOrder({
action,
transcoders: JSON.parse(JSON.stringify(transcoders)),
amount: parseEther(amount ? amount.toString() : "0"),
amount: amountWei ?? 0n,
newDelegate: transcoder?.id ?? "",
oldDelegate: delegator?.delegate?.id,
}),
[action, transcoders, amount, transcoder, delegator]
[action, transcoders, amountWei, transcoder, delegator]
);
const { newPosPrev, newPosNext } = useMemo(
() => getHint(delegator?.delegate?.id, newActiveSetOrder),
Expand All @@ -135,15 +132,11 @@ const Footer = ({
);
const willDeactivate = useMemo(() => {
// Wait for stake data to load before determining deactivation
if (!isOwnOrchestrator || stakeWei === null || !amount) return false;
try {
const amountWei = parseEther(amount);
// Deactivates if unbonding all stake (amount >= current stake)
return amountWei > 0n && amountWei >= stakeWei;
} catch {
if (!isOwnOrchestrator || stakeWei === null || amountWei === null)
return false;
}
}, [isOwnOrchestrator, stakeWei, amount]);
// Deactivates if unbonding all stake (amount >= current stake)
return amountWei > 0n && amountWei >= stakeWei;
}, [isOwnOrchestrator, stakeWei, amountWei]);

if (isWrongRouteChain) {
return (
Expand Down
43 changes: 42 additions & 1 deletion utils/web3.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { checkAddressEquality, formatAddress, fromWei, toWei } from "./web3";
import {
checkAddressEquality,
formatAddress,
fromWei,
parseAmountToWei,
toWei,
} from "./web3";

describe("checkAddressEquality", () => {
it("returns false for invalid addresses", () => {
Expand Down Expand Up @@ -43,6 +49,41 @@ describe("toWei", () => {
});
});

describe("parseAmountToWei", () => {
it("converts decimal amounts to wei", () => {
expect(parseAmountToWei("0.1")).toBe(100000000000000000n);
expect(parseAmountToWei("2.5")).toBe(2500000000000000000n);
expect(parseAmountToWei("100000")).toBe(100000000000000000000000n);
});

it("keeps decimals that exceed float precision", () => {
expect(parseAmountToWei("1.000000000000000001")).toBe(1000000000000000001n);
});

it("accepts exponent notation", () => {
expect(parseAmountToWei("1e3")).toBe(1000000000000000000000n);
expect(parseAmountToWei("1E3")).toBe(1000000000000000000000n);
expect(parseAmountToWei("1e-2")).toBe(10000000000000000n);
});

it("returns null for an absent or unparseable amount", () => {
expect(parseAmountToWei("")).toBeNull();
expect(parseAmountToWei(null)).toBeNull();
expect(parseAmountToWei(undefined)).toBeNull();
expect(parseAmountToWei("abc")).toBeNull();
expect(parseAmountToWei("1e")).toBeNull();
});

// Exponents are normalised through `Number`, which only writes decimals
// between 1e-6 and 1e21. Decimal notation has no such limit.
it("rejects exponents outside the range Number writes as a decimal", () => {
expect(parseAmountToWei("1e-6")).toBe(1000000000000n);
expect(parseAmountToWei("1e-7")).toBeNull();
expect(parseAmountToWei("0.0000001")).toBe(100000000000n);
expect(parseAmountToWei("1e21")).toBeNull();
});
});

describe("formatAddress", () => {
it("shortens a normal ethereum address", () => {
const addr = "0x1234567890abcdef1234567890abcdef12345678";
Expand Down
21 changes: 20 additions & 1 deletion utils/web3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,31 @@ export const fromWei = (wei: bigint | string | null | undefined) => {
};

/**
* Convert Ether amount (number) to Wei (bigint)
* Convert Ether amount (number) to Wei (bigint).
* @deprecated Use parseAmountToWei instead.
* @param ether - The value in Ether
* @returns BigInt representation in Wei
*/
export const toWei = (ether: number) => parseEther(ether.toString());

/**
* Parse a user-entered amount into Wei.
*
* `parseEther` only accepts decimals, so exponent notation (e.g. `1e3`) is
* normalised first.
*
* @param amount - The user-entered amount in Ether
* @returns BigInt representation in Wei, or null when it cannot be parsed
*/
export const parseAmountToWei = (amount: string | null | undefined) => {
if (!amount) return null;
try {
return parseEther(/[eE]/.test(amount) ? Number(amount).toString() : amount);
} catch {
return null;
}
};

/**
* Shorten an Ethereum address for display.
* @deprecated Use formatAddress instead
Expand Down
Loading