Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/solana tokens support #2

Open
wants to merge 10 commits into
base: develop
Choose a base branch
from
11 changes: 11 additions & 0 deletions .changeset/honest-dingos-unite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@ledgerhq/cryptoassets": minor
"@ledgerhq/types-live": minor
"@ledgerhq/coin-solana": minor
"ledger-live-desktop": minor
"live-mobile": minor
"@ledgerhq/live-common": minor
"@ledgerhq/coin-framework": minor
---

Solana spl tokens support
2 changes: 2 additions & 0 deletions apps/ledger-live-desktop/src/config/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export const supportLinkByTokenType = {
trc20: "https://support.ledger.com/article/360013062159-zd",
asa: "https://support.ledger.com/article/360015896040-zd",
nfts: "https://support.ledger.com/article/4404389453841-zd",
// spl: "Solana spl tokens. TODO: to be defined",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing support article link

};

const errors: Record<string, string> = {
Expand Down Expand Up @@ -153,6 +154,7 @@ export const urls = {
},
solana: {
staking: "https://support.ledger.com/article/4731749170461-zd",
splTokenInfo: "Solana spl tokens link TODO: to be defined",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

recipient_info: "https://support.ledger.com",
ledgerByFigmentTC:
"https://cdn.figment.io/legal/Current%20Ledger_Online%20Staking%20Delgation%20Services%20Agreement.pdf",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ const iconsComponent = {
STAKE: IconDelegate,
UNSTAKE: IconUndelegate,
WITHDRAW_UNSTAKED: IconCoins,
BURN: IconTrash,
};
class ConfirmationCheck extends PureComponent<{
marketColor: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@ import { openModal } from "~/renderer/actions/modals";
import IconCoins from "~/renderer/icons/Coins";
import { SolanaFamily } from "./types";

const AccountHeaderActions: SolanaFamily["accountHeaderManageActions"] = ({ account, source }) => {
const AccountHeaderActions: SolanaFamily["accountHeaderManageActions"] = ({
account,
parentAccount,
source,
}) => {
const { t } = useTranslation();
const dispatch = useDispatch();
const mainAccount = getMainAccount(account);
const mainAccount = getMainAccount(account, parentAccount);
const { solanaResources } = mainAccount;

const onClick = useCallback(() => {
Expand All @@ -34,6 +38,10 @@ const AccountHeaderActions: SolanaFamily["accountHeaderManageActions"] = ({ acco
}
}, [account, dispatch, source, solanaResources, mainAccount]);

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

return [
{
key: "Stake",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,30 @@
import React from "react";
import { Trans } from "react-i18next";
import { SolanaAccount, SolanaTokenAccount } from "@ledgerhq/live-common/families/solana/types";
import { isTokenAccountFrozen } from "@ledgerhq/live-common/families/solana/logic";
import { SubAccount } from "@ledgerhq/types-live";

import Box from "~/renderer/components/Box";
import Alert from "~/renderer/components/Alert";
import AccountSubHeader from "../../components/AccountSubHeader/index";
export default function SolanaAccountSubHeader() {
return <AccountSubHeader family="Solana" team="Solana Labs"></AccountSubHeader>;

type Account = SolanaAccount | SolanaTokenAccount | SubAccount;

type Props = {
account: Account;
};

export default function SolanaAccountSubHeader({ account }: Props) {
return (
<>
{isTokenAccountFrozen(account) && (

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To check if it's the right position we want

<Box mb={10}>
<Alert type="warning">
<Trans i18nKey="solana.token.frozenStateWarning" />
</Alert>
</Box>
)}
<AccountSubHeader family="Solana" team="Solana Labs"></AccountSubHeader>
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React, { useMemo } from "react";
import { getDeviceTransactionConfig } from "@ledgerhq/live-common/transaction/index";
import { SolanaFamily } from "./types";
import Alert from "~/renderer/components/Alert";
import { Trans } from "react-i18next";
import ConfirmTitle from "~/renderer/components/TransactionConfirm/ConfirmTitle";
import LinkWithExternalIcon from "~/renderer/components/LinkWithExternalIcon";
import Box from "~/renderer/components/Box";
import { openURL } from "~/renderer/linking";
import { useLocalizedUrl } from "~/renderer/hooks/useLocalizedUrls";
import { urls } from "~/config/urls";

const Title: TitleComponent = props => {
const { transaction, account, parentAccount, status } = props;
const transferTokenHelpUrl = useLocalizedUrl(urls.solana.splTokenInfo);

const fields = getDeviceTransactionConfig({
account,
parentAccount,
transaction,
status,
});

const typeTransaction: string | undefined = useMemo(() => {
const typeField = fields.find(field => field.label && field.label === "Type");

if (typeField && typeField.type === "text" && typeField.value) {
return typeField.value;
}
}, [fields]);

if (transaction.model.commandDescriptor?.command.kind === "token.transfer") {
return (
<Box
flexDirection="column"
alignItems="center"
gap={4}
mb={4}
justifyContent="center"
className="title-test-transfer"
>
<ConfirmTitle title={undefined} typeTransaction={typeTransaction} {...props} />
<Alert type="warning">
<Trans i18nKey="solana.token.transferWarning">
<LinkWithExternalIcon
color="palette.warning.c60"
onClick={() => openURL(transferTokenHelpUrl)}
/>
</Trans>
</Alert>
</Box>
);
}

return <ConfirmTitle title={undefined} typeTransaction={typeTransaction} {...props} />;
};

type TransactionConfirmFields = SolanaFamily["transactionConfirmFields"];
type TitleComponent = NonNullable<NonNullable<TransactionConfirmFields>["title"]>;

const transactionConfirmFields: TransactionConfirmFields = {
// footer: Footer, // is not shown without manifestId
// fieldComponents,
title: Title,
};

export default transactionConfirmFields;
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import AccountBalanceSummaryFooter from "./AccountBalanceSummaryFooter";
import StakeBanner from "./StakeBanner";
import { SolanaFamily } from "./types";
import operationDetails from "./operationDetails";
import transactionConfirmFields from "./TransactionConfirmFields";

const family: SolanaFamily = {
accountHeaderManageActions,
Expand All @@ -15,6 +16,7 @@ const family: SolanaFamily = {
AccountBalanceSummaryFooter,
StakeBanner,
operationDetails,
transactionConfirmFields,
};

export default family;
16 changes: 15 additions & 1 deletion apps/ledger-live-desktop/static/i18n/en/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,8 @@
"STAKE": "Staked",
"UNSTAKE": "Unstaked",
"WITHDRAW_UNSTAKED": "Withdrawn",
"SENDING": "Sending"
"SENDING": "Sending",
"BURN": "Burned"
},
"edit": {
"title": "Speed up or Cancel",
Expand Down Expand Up @@ -3695,6 +3696,10 @@
}
}
}
},
"token": {
"frozenStateWarning": "Account assets are frozen!",
"transferWarning": "Solana SPL tokens transactions have unique characteristics. To learn more, visit: <0>ledger.com/spl</0>"
}
},
"ethereum": {
Expand Down Expand Up @@ -6067,6 +6072,15 @@
"SolanaAccountNotFunded": {
"title": "Account not funded"
},
"SolanaAssociatedTokenAccountWillBeFunded": {
"title": "Account will be funded"
},
"SolanaTokenAccountFrozen": {
"title": "Token account assets are frozen"
},
"SolanaTokenAccounNotInitialized": {
"title": "Account not initialized"
},
"SolanaMemoIsTooLong": {
"title": "Memo is too long. Max length is {{maxLength}}"
},
Expand Down
28 changes: 26 additions & 2 deletions apps/ledger-live-mobile/src/families/solana/AccountSubHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,32 @@
import React from "react";
import { Trans } from "react-i18next";
import { SubAccount } from "@ledgerhq/types-live";
import { Box, Alert, Text } from "@ledgerhq/native-ui";
import { isTokenAccountFrozen } from "@ledgerhq/live-common/families/solana/logic";
import { SolanaAccount, SolanaTokenAccount } from "@ledgerhq/live-common/families/solana/types";
import AccountSubHeader from "~/components/AccountSubHeader";

function SolanaAccountSubHeader() {
return <AccountSubHeader family="Solana" team="Solana Labs" />;
type Account = SolanaAccount | SolanaTokenAccount | SubAccount;

type Props = {
account: Account;
};

function SolanaAccountSubHeader({ account }: Props) {
return (
<>
{isTokenAccountFrozen(account) && (
<Box mt={6}>
<Alert type="warning">
<Text variant="body">
<Trans i18nKey="solana.token.frozenStateWarning" />
</Text>
</Alert>
</Box>
)}
<AccountSubHeader family="Solana" team="Solana Labs" />
</>
);
}

export default SolanaAccountSubHeader;
4 changes: 2 additions & 2 deletions apps/ledger-live-mobile/src/families/solana/SendRowsFee.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,15 @@ type Props = {
StackNavigatorProps<BaseNavigatorStackParamList>
>;

export default function SolanaFeeRow({ account, status }: Props) {
export default function SolanaFeeRow({ account, parentAccount, status }: Props) {
const { colors } = useTheme();
const extraInfoFees = useCallback(() => {
Linking.openURL(urls.solana.supportPage);
}, []);

const fees = (status as SolanaTransactionStatus).estimatedFees;

const unit = useAccountUnit(account);
const unit = useAccountUnit(account.type === "TokenAccount" ? parentAccount || account : account);
const currency = getAccountCurrency(account);

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import invariant from "invariant";
import React from "react";
import { Linking, View } from "react-native";
import { Trans } from "react-i18next";
import { Link } from "@ledgerhq/native-ui";
import { DeviceTransactionField } from "@ledgerhq/live-common/transaction/index";
import {
SolanaAccount,
SolanaTokenAccount,
Transaction,
TransactionStatus,
} from "@ledgerhq/live-common/families/solana/types";
import Alert from "~/components/Alert";
import { urls } from "~/utils/urls";
import LText from "~/components/LText";

type SolanaFieldComponentProps = {
account: SolanaAccount | SolanaTokenAccount;
parentAccount: SolanaAccount | undefined | null;
transaction: Transaction;
status: TransactionStatus;
field: DeviceTransactionField;
};

const Warning = ({ transaction }: SolanaFieldComponentProps) => {
invariant(transaction.family === "solana", "solana transaction");
if (transaction.model.commandDescriptor?.command.kind === "token.transfer") {
return (
<View>
<Alert type="hint">
<LText>
<Trans i18nKey="solana.token.transferWarning">
<Link onPress={() => Linking.openURL(urls.solana.splTokenInfo)} type="color" />
</Trans>
</LText>
</Alert>
</View>
);
}
return null;
};

export default {
warning: Warning,
fieldComponents: {},
};
10 changes: 10 additions & 0 deletions apps/ledger-live-mobile/src/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,12 @@
"SolanaAccountNotFunded": {
"title": "Account not funded"
},
"SolanaAssociatedTokenAccountWillBeFunded": {
"title": "Account will be funded"
},
"SolanaTokenAccountFrozen": {
"title": "Token account assets are frozen"
},
"SolanaAddressOfEd25519": {
"title": "Address off ed25519 curve"
},
Expand Down Expand Up @@ -5856,6 +5862,10 @@
"description": "You may earn rewards by delegating your SOL assets to a validator."
},
"reserveWarning": "Please ensure you reserve at least {{amount}} SOL in your wallet to cover future network fees to deactivate and withdraw your stake"
},
"token": {
"frozenStateWarning": "Account assets are frozen!",
"transferWarning": "Double-check the transaction details on your Ledger device before signing. Solana SPL tokens transactions have unique characteristics. To learn more, visit: <0>ledger.com/spl</0>"
}
},
"near": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ export function getListHeaderComponents({
<Header key="Header" />,
!!AccountSubHeader && (
<Box bg={colors.background.main} key="AccountSubHeader">
<AccountSubHeader />
<AccountSubHeader account={account} parentAccount={parentAccount} />
</Box>
),
oldestEditableOperation ? (
Expand Down
1 change: 1 addition & 0 deletions apps/ledger-live-mobile/src/utils/urls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ export const urls = {
solana: {
supportPage: "https://support.ledger.com",
stakingPage: "https://support.ledger.com/article/4731749170461-zd",
splTokenInfo: "Solana spl tokens link TODO: to be defined",
},
resources: {
gettingStarted:
Expand Down
1 change: 1 addition & 0 deletions libs/coin-framework/src/operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ export function getOperationAmountNumber(op: Operation): BigNumber {
case "OPT_OUT":
case "SLASH":
case "LOCK":
case "BURN":
return op.value.negated();

case "FREEZE":
Expand Down
Loading