Skip to content

Commit

Permalink
feat: display solana frozen token accounts
Browse files Browse the repository at this point in the history
  • Loading branch information
mikhd committed Dec 25, 2023
1 parent 9a39bc6 commit e7745ec
Show file tree
Hide file tree
Showing 11 changed files with 256 additions and 18 deletions.
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) && (
<Box mb={10}>
<Alert type="warning">
<Trans i18nKey="solana.token.frozenStateWarning" />
</Alert>
</Box>
)}
<AccountSubHeader family="Solana" team="Solana Labs eeee"></AccountSubHeader>
</>
);
}
9 changes: 9 additions & 0 deletions apps/ledger-live-desktop/static/i18n/en/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -3598,6 +3598,9 @@
}
}
}
},
"token": {
"frozenStateWarning": "Account assets are frozen!"
}
},
"ethereum": {
Expand Down Expand Up @@ -5741,6 +5744,12 @@
"SolanaAssociatedTokenAccountWillBeFunded": {
"title": "Account will be funded"
},
"SolanaTokenAccountFrozen": {
"title": "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;
3 changes: 3 additions & 0 deletions apps/ledger-live-mobile/src/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -5743,6 +5743,9 @@
"started": {
"description": "You may earn rewards by delegating your SOL assets to a validator."
}
},
"token": {
"frozenStateWarning": "Account assets are frozen!"
}
},
"near": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,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
159 changes: 151 additions & 8 deletions libs/ledger-live-common/src/families/solana/bridge.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import BigNumber from "bignumber.js";
import {
SolanaAccount,
SolanaStake,
SolanaTokenAccount,
SolanaTokenAccountRaw,
TokenTransferTransaction,
Transaction,
TransactionModel,
TransactionStatus,
Expand All @@ -18,13 +21,7 @@ import {
} from "@ledgerhq/errors";
import { findTokenByAddressInCurrency } from "@ledgerhq/cryptoassets";
import { TokenCurrency } from "@ledgerhq/types-cryptoassets";
import type {
Account,
AccountRaw,
CurrenciesData,
DatasetTest,
TokenAccountRaw,
} from "@ledgerhq/types-live";
import type { Account, AccountRaw, CurrenciesData, DatasetTest } from "@ledgerhq/types-live";
import {
SolanaAccountNotFunded,
SolanaAddressOffEd25519,
Expand All @@ -33,6 +30,7 @@ import {
SolanaRecipientAssociatedTokenAccountWillBeFunded,
SolanaStakeAccountNotFound,
SolanaStakeAccountRequired,
SolanaTokenAccountFrozen,
SolanaTokenAccountHoldsAnotherToken,
SolanaValidatorRequired,
} from "./errors";
Expand Down Expand Up @@ -174,7 +172,7 @@ function makeAccount(freshAddress: string): AccountRaw {
};
}

function makeSubTokenAccount(): TokenAccountRaw {
function makeSubTokenAccount(): SolanaTokenAccountRaw {
return {
type: "TokenAccountRaw",
id: wSolSubAccId,
Expand Down Expand Up @@ -1118,3 +1116,148 @@ const mockedVoteAccount = {
program: "vote",
space: 3731,
};

describe("solana tokens", () => {
const baseAtaMock = {
parsed: {
info: {
isNative: false,
mint: wSolToken.contractAddress,
owner: testOnChainData.fundedSenderAddress,
state: "initialized",
tokenAmount: {
amount: "10000000",
decimals: wSolToken.units[0].magnitude,
uiAmount: 10.0,
uiAmountString: "10",
},
},
type: "account",
},
program: "spl-token",
space: 165,
};
const frozenAtaMock = {
...baseAtaMock,
parsed: {
...baseAtaMock.parsed,
info: {
...baseAtaMock.parsed.info,
state: "frozen",
},
},
};

const mockedTokenAcc: SolanaTokenAccount = {
type: "TokenAccount",
id: wSolSubAccId,
parentId: mainAccId,
token: wSolToken,
balance: new BigNumber(100),
operations: [],
pendingOperations: [],
spendableBalance: new BigNumber(100),
state: "initialized",
creationDate: new Date(),
operationsCount: 0,
starred: false,
balanceHistoryCache: {
HOUR: { balances: [], latestDate: null },
DAY: { balances: [], latestDate: null },
WEEK: { balances: [], latestDate: null },
},
swapHistory: [],
};
test("token.transfer :: status is error: sender ATA is frozen", async () => {
const txModel: TokenTransferTransaction = {
kind: "token.transfer",
uiState: {
subAccountId: wSolSubAccId,
},
};

const api = {
...baseAPI,
getAccountInfo: () => Promise.resolve({ data: baseAtaMock } as any),
getBalance: () => Promise.resolve(10),
} as ChainAPI;

const tokenAcc: SolanaTokenAccount = {
...mockedTokenAcc,
state: "frozen",
};
const account: SolanaAccount = {
...baseAccount,
freshAddress: testOnChainData.fundedSenderAddress,
subAccounts: [tokenAcc],
solanaResources: { stakes: [] },
};

const tx: Transaction = {
model: txModel,
amount: new BigNumber(10),
recipient: testOnChainData.fundedAddress,
family: "solana",
};

const preparedTx = await prepareTransaction(account, tx, api);
const receivedTxStatus = await getTransactionStatus(account, preparedTx);
const expectedTxStatus: TransactionStatus = {
amount: new BigNumber(10),
estimatedFees: new BigNumber(testOnChainData.fees.lamportsPerSignature),
totalSpent: new BigNumber(10),
errors: {
amount: new SolanaTokenAccountFrozen(),
},
warnings: {},
};

expect(receivedTxStatus).toEqual(expectedTxStatus);
});

test("token.transfer :: status is error: recipient ATA is frozen", async () => {
const txModel: TokenTransferTransaction = {
kind: "token.transfer",
uiState: {
subAccountId: wSolSubAccId,
},
};

const api = {
...baseAPI,
getAccountInfo: () => Promise.resolve({ data: frozenAtaMock } as any),
getBalance: () => Promise.resolve(10),
} as ChainAPI;

const tokenAcc: SolanaTokenAccount = {
...mockedTokenAcc,
};
const account: SolanaAccount = {
...baseAccount,
freshAddress: testOnChainData.fundedSenderAddress,
subAccounts: [tokenAcc],
solanaResources: { stakes: [] },
};

const tx: Transaction = {
model: txModel,
amount: new BigNumber(10),
recipient: testOnChainData.fundedAddress,
family: "solana",
};

const preparedTx = await prepareTransaction(account, tx, api);
const receivedTxStatus = await getTransactionStatus(account, preparedTx);
const expectedTxStatus: TransactionStatus = {
amount: new BigNumber(10),
estimatedFees: new BigNumber(testOnChainData.fees.lamportsPerSignature),
totalSpent: new BigNumber(10),
errors: {
recipient: new SolanaTokenAccountFrozen(),
},
warnings: {},
};

expect(receivedTxStatus).toEqual(expectedTxStatus);
});
});
2 changes: 2 additions & 0 deletions libs/ledger-live-common/src/families/solana/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export const SolanaTokenAccounNotInitialized = createCustomErrorClass(
"SolanaTokenAccounNotInitialized",
);

export const SolanaTokenAccountFrozen = createCustomErrorClass("SolanaTokenAccountFrozen");

export const SolanaAddressOffEd25519 = createCustomErrorClass("SolanaAddressOfEd25519");

export const SolanaTokenRecipientIsSenderATA = createCustomErrorClass(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
} from "./api/chain/web3";
import {
SolanaAccountNotFunded,
SolanaTokenAccountFrozen,
SolanaAddressOffEd25519,
SolanaInvalidValidator,
SolanaMemoIsTooLong,
Expand Down Expand Up @@ -47,6 +48,7 @@ import type {
CommandDescriptor,
SolanaAccount,
SolanaStake,
SolanaTokenAccount,
StakeCreateAccountTransaction,
StakeDelegateTransaction,
StakeSplitTransaction,
Expand Down Expand Up @@ -128,6 +130,10 @@ const deriveTokenTransferCommandDescriptor = async (
throw new Error("subaccount not found");
}

if ((subAccount as SolanaTokenAccount)?.state === "frozen") {
errors.amount = new SolanaTokenAccountFrozen();
}

await validateRecipientCommon(mainAccount, tx, errors, warnings, api);

const memo = model.uiState.memo;
Expand Down Expand Up @@ -239,6 +245,9 @@ async function getTokenRecipient(
if (recipientTokenAccount.mint.toBase58() !== mintAddress) {
return new SolanaTokenAccountHoldsAnotherToken();
}
if (recipientTokenAccount.state === "frozen") {
return new SolanaTokenAccountFrozen();
}
if (recipientTokenAccount.state !== "initialized") {
return new SolanaTokenAccounNotInitialized();
}
Expand Down
Loading

0 comments on commit e7745ec

Please sign in to comment.