Skip to content

Commit

Permalink
[LIVE-10249] Bugfix - Cardano invalid data supplied (#6682)
Browse files Browse the repository at this point in the history
* handle tx with no utxos

* remove debug log

* fix: disable undelegate on amount error

* show rewards with decimals at delegation info - handle balance error at undelegate screen

* fix: add unit in useMemo hook

* fix: Alert component to display error

* remove cancel button

* unit test for tx status with no utxos

* add changeset

* fix: error position update

---------

Co-authored-by: pavanvora <pavanvora.cs@gmail.com>
Co-authored-by: Prashanth <prashanthsoordelu@gmail.com>
Co-authored-by: mehulcs <mehul.becs@gmail.com>
  • Loading branch information
4 people committed Apr 18, 2024
1 parent ede5b43 commit 10df676
Show file tree
Hide file tree
Showing 9 changed files with 142 additions and 47 deletions.
7 changes: 7 additions & 0 deletions .changeset/fresh-pillows-poke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"ledger-live-desktop": minor
"live-mobile": minor
"@ledgerhq/live-common": minor
---

Fixed bug regarding invalid data supplied to ledger and added error message for mobile and desktop
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import ValidatorField from "../fields/ValidatorField";
import ErrorBanner from "~/renderer/components/ErrorBanner";
import AccountFooter from "~/renderer/modals/Send/AccountFooter";
import TranslatedError from "~/renderer/components/TranslatedError";
import Text from "~/renderer/components/Text";
import Alert from "~/renderer/components/Alert";

export default function StepDelegation({
account,
Expand All @@ -29,6 +29,9 @@ export default function StepDelegation({
invariant(cardanoResources, "cardanoResources required");
const delegation = cardanoResources.delegation;

const { errors } = status;
const displayError = errors.amount?.message ? errors.amount : "";

const selectPool = (stakePool: StakePool) => {
setSelectedPool(stakePool);
const bridge: AccountBridge<CardanoTransaction> = getAccountBridge(account);
Expand All @@ -55,48 +58,41 @@ export default function StepDelegation({
onChangeValidator={selectPool}
selectedPoolId={selectedPoolId as string}
/>
{displayError ? (
<Alert type="error">
<TranslatedError error={displayError} field="title" />
</Alert>
) : null}
</Box>
);
}

export function StepDelegationFooter({
transitionTo,
account,
onClose,
status,
bridgePending,
transaction,
onClose,
}: StepProps) {
invariant(account, "account required");
const { errors } = status;
const canNext = !bridgePending && !errors.amount && transaction;
const displayError = errors.amount?.message ? errors.amount : "";

return (
<Box horizontal alignItems="center" flow={2} grow>
{displayError ? (
<Box grow>
<Text fontSize={13} color="alertRed">
<TranslatedError error={displayError} field="title" />
</Text>
</Box>
) : (
<AccountFooter account={account} status={status} />
)}

<Box horizontal>
<Button mr={1} secondary onClick={onClose}>
<Trans i18nKey="common.cancel" />
</Button>
<Button
id="delegate-continue-button"
disabled={!canNext}
primary
onClick={() => transitionTo("summary")}
>
<Trans i18nKey="common.continue" />
</Button>
</Box>
<Box horizontal justifyContent="flex-end" flow={2} grow>
<AccountFooter account={account} status={status} />
<Button mr={1} secondary onClick={onClose}>
<Trans i18nKey="common.cancel" />
</Button>
<Button
id="delegate-continue-button"
disabled={!canNext}
primary
onClick={() => transitionTo("summary")}
>
<Trans i18nKey="common.continue" />
</Button>
</Box>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import FormattedVal from "~/renderer/components/FormattedVal";
import Text from "~/renderer/components/Text";
import CounterValue from "~/renderer/components/CounterValue";
import ErrorBanner from "~/renderer/components/ErrorBanner";
import TranslatedError from "~/renderer/components/TranslatedError";
import { StepProps } from "../types";
import BigNumber from "bignumber.js";
import Alert from "~/renderer/components/Alert";

const FromToWrapper = styled.div``;
const Separator = styled.div`
Expand All @@ -22,12 +24,13 @@ const Separator = styled.div`
export default class StepSummary extends PureComponent<StepProps> {
render() {
const { account, transaction, status, error } = this.props;
const { estimatedFees } = status;
const { estimatedFees, errors } = status;
if (!account) return null;
if (!transaction) return null;
const accountUnit = getAccountUnit(account);
const feesCurrency = getAccountCurrency(account);
const stakeKeyDeposit = account.cardanoResources?.protocolParams.stakeKeyDeposit;
const displayError = errors.amount?.message ? errors.amount : "";
return (
<Box flow={4} mx={40}>
{error && <ErrorBanner error={error} />}
Expand Down Expand Up @@ -90,16 +93,32 @@ export default class StepSummary extends PureComponent<StepProps> {
</Box>
</Box>
</FromToWrapper>
{displayError ? (
<Box grow>
<Alert type="error">
<TranslatedError error={displayError} field="title" />
</Alert>
</Box>
) : null}
</Box>
);
}
}
export function StepSummaryFooter({ transitionTo, status, bridgePending, transaction }: StepProps) {
export function StepSummaryFooter({
transitionTo,
status,
bridgePending,
transaction,
onClose,
}: StepProps) {
const { errors } = status;
const canNext = !bridgePending && !errors.validators && transaction;
const canNext = !errors.amount && !bridgePending && !errors.validators && transaction;
return (
<>
<Box horizontal>
<Box horizontal justifyContent="flex-end" flow={2} grow>
<Button mr={1} secondary onClick={onClose}>
<Trans i18nKey="common.cancel" />
</Button>
<Button
id="undelegate-continue-button"
disabled={!canNext}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import DelegationDrawer from "~/components/DelegationDrawer";
import type { IconProps } from "~/components/DelegationDrawer";
import Circle from "~/components/Circle";
import LText from "~/components/LText";
import { Text } from "@ledgerhq/native-ui";
import Touchable from "~/components/Touchable";
import { rgba } from "../../../colors";
import IlluRewards from "~/icons/images/Rewards";
Expand All @@ -27,6 +28,7 @@ import RedelegateIcon from "~/icons/Redelegate";
import UndelegateIcon from "~/icons/Undelegate";
import DelegationRow from "./Row";
import PoolImage from "../shared/PoolImage";
import CurrencyUnitValue from "~/components/CurrencyUnitValue";

type Props = {
account: CardanoAccount;
Expand Down Expand Up @@ -177,16 +179,16 @@ function Delegations({ account }: Props) {
{
label: t("cardano.delegation.drawer.rewards"),
Component: (
<LText numberOfLines={1} semiBold style={[styles.valueText]}>
{delegation.rewards.toString() ?? ""}
</LText>
<Text variant={"body"} fontWeight={"semiBold"}>
<CurrencyUnitValue value={delegation.rewards} unit={unit} />
</Text>
),
},
]
: []),
]
: [];
}, [delegation, t, account, onOpenExplorer]);
}, [delegation, t, account, onOpenExplorer, unit]);

const actions = useMemo<DelegationDrawerActions>(() => {
return [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useTheme } from "@react-navigation/native";
import { BigNumber } from "bignumber.js";
import invariant from "invariant";
import React, { ReactNode, useCallback } from "react";
import React, { ReactNode, useCallback, useMemo } from "react";
import { Trans, useTranslation } from "react-i18next";
import { SafeAreaView, StyleSheet, View } from "react-native";
import { useSelector } from "react-redux";
Expand All @@ -17,7 +17,7 @@ import type {
CardanoAccount,
CardanoDelegation,
} from "@ledgerhq/live-common/families/cardano/types";
import { Text } from "@ledgerhq/native-ui";
import { Text, Box } from "@ledgerhq/native-ui";
import { AccountLike } from "@ledgerhq/types-live";
import Button from "~/components/Button";
import Circle from "~/components/Circle";
Expand All @@ -30,6 +30,7 @@ import { TrackScreen } from "~/analytics";
import { rgba } from "../../../colors";
import { StackNavigatorProps } from "~/components/RootNavigator/types/helpers";
import { CardanoUndelegationFlowParamList } from "./types";
import TranslatedError from "~/components/TranslatedError";

type Props = StackNavigatorProps<
CardanoUndelegationFlowParamList,
Expand Down Expand Up @@ -63,6 +64,10 @@ export default function UndelegationSummary({ navigation, route }: Props) {
return { account, transaction: tx };
});

const displayError = useMemo(() => {
return status.errors.amount ? status.errors.amount : "";
}, [status]);

const currency = getAccountCurrency(account);
const color = getCurrencyColor(currency);

Expand Down Expand Up @@ -92,13 +97,23 @@ export default function UndelegationSummary({ navigation, route }: Props) {
</View>
</View>
<View style={styles.footer}>
{displayError ? (
<Box>
<Text fontSize={13} color="red">
<TranslatedError error={displayError} field="title" />
</Text>
</Box>
) : (
<></>
)}

<Button
event="SummaryContinue"
type="primary"
title={<Trans i18nKey="common.continue" />}
containerStyle={styles.continueButton}
onPress={onContinue}
disabled={bridgePending || !!bridgeError}
disabled={bridgePending || !!bridgeError || !!displayError}
pending={bridgePending}
/>
</View>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,18 @@ const estimateMaxSpendable = async ({
parentAccount?: Account;
transaction?: Transaction;
}): Promise<BigNumber> => {
const mainAccount = getMainAccount(account, parentAccount);

if ((mainAccount as CardanoAccount).cardanoResources.utxos.length === 0) {
return new BigNumber(0);
}

if (account.type === "TokenAccount") {
return account.balance;
}

const dummyRecipient =
"addr1qyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqv2t5am";
const a = getMainAccount(account, parentAccount);
const t: Transaction = {
...createTransaction(),
...transaction,
Expand All @@ -42,7 +47,7 @@ const estimateMaxSpendable = async ({
};
let typhonTransaction: TyphonTransaction;
try {
typhonTransaction = await buildTransaction(a as CardanoAccount, t);
typhonTransaction = await buildTransaction(mainAccount as CardanoAccount, t);
} catch (error) {
log("cardano-error", "Failed to estimate max spendable: " + String(error));
return new BigNumber(0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,19 @@ const getTransactionStatus = async (
throw new AccountAwaitingSendPendingOperations();
}

if (a.cardanoResources.utxos.length === 0) {
const errors = {
amount: new CardanoNotEnoughFunds(),
};
return Promise.resolve({
errors,
warnings: {},
estimatedFees: new BigNumber(0),
amount: new BigNumber(t.amount),
totalSpent: new BigNumber(t.amount),
});
}

if (t.mode === "send") {
return getSendTransactionStatus(a, t);
} else if (t.mode === "delegate") {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import BigNumber from "bignumber.js";
import getTransactionStatus from "./js-getTransactionStatus";
import { CardanoAccount, Transaction } from "./types";

describe("getTransactionStatus", () => {
it("should return not enough funds error when there are no utxos", async () => {
const initialAccount = {
pendingOperations: [],
cardanoResources: {
utxos: [],
},
} as unknown as CardanoAccount;

const sendTx: Transaction = {
amount: new BigNumber(1000000),
recipient:
"addr1qxqm3nxwzf70ke9jqa2zrtrevjznpv6yykptxnv34perjc8a7zgxmpv5pgk4hhhe0m9kfnlsf5pt7d2ahkxaul2zygrq3nura9",
mode: "send",
family: "cardano",
poolId: undefined,
};
const sendTxRes = await getTransactionStatus(initialAccount, sendTx);
expect(sendTxRes.errors.amount.name).toBe("CardanoNotEnoughFunds");

const delegateTx: Transaction = {
amount: new BigNumber(0),
recipient: "",
mode: "delegate",
family: "cardano",
poolId: "d0f48f07e4e5eb8040a988085f7ea3bd32d71a2e2998d53e9bbc959a",
};
const delegateTxRes = await getTransactionStatus(initialAccount, delegateTx);
expect(delegateTxRes.errors.amount.name).toBe("CardanoNotEnoughFunds");

const undelegateTx: Transaction = {
amount: new BigNumber(0),
recipient: "",
mode: "undelegate",
family: "cardano",
poolId: undefined,
};
const undelegateTxRes = await getTransactionStatus(initialAccount, undelegateTx);
expect(undelegateTxRes.errors.amount.name).toBe("CardanoNotEnoughFunds");
});
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { BigNumber } from "bignumber.js";
import { Observable } from "rxjs";
import { FeeNotLoaded } from "@ledgerhq/errors";
import { log } from "@ledgerhq/logs";

import type {
CardanoAccount,
Expand Down Expand Up @@ -308,12 +307,6 @@ const signOperation: SignOperationFnSignature<Transaction> = ({ account, deviceI
additionalWitnessPaths: [],
};

try {
log("debug", `Signing cardano transaction: ${JSON.stringify(trxOptions)}`);
} catch (e) {
log("debug", `Signing cardano transaction: could not log:`, e);
}

// Sign by device
const appAda = new Ada(transport);
const r = await appAda.signTransaction(trxOptions);
Expand Down

0 comments on commit 10df676

Please sign in to comment.