From 7cb3bc49e7d2e8a28079bae2951afc79833bfefa Mon Sep 17 00:00:00 2001 From: Mikhail Date: Mon, 20 May 2024 12:37:24 +0300 Subject: [PATCH 01/35] feat: add serialization of TokenAccount extra fields [coin-framework] --- .../src/serialization/account.ts | 20 ++++++++++++++++++ .../src/account/serialization.ts | 2 ++ .../packages/types-live/src/bridge.ts | 21 ++++++++++++++++++- 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/libs/coin-framework/src/serialization/account.ts b/libs/coin-framework/src/serialization/account.ts index f16a0fec1d18..b5f982162154 100644 --- a/libs/coin-framework/src/serialization/account.ts +++ b/libs/coin-framework/src/serialization/account.ts @@ -27,6 +27,7 @@ import { export type FromFamiliyRaw = { assignFromAccountRaw?: AccountBridge["assignFromAccountRaw"]; + assignFromTokenAccountRaw?: AccountBridge["assignFromTokenAccountRaw"]; fromOperationExtraRaw?: AccountBridge["fromOperationExtraRaw"]; }; @@ -129,11 +130,21 @@ export function fromAccountRaw(rawAccount: AccountRaw, fromRaw?: FromFamiliyRaw) fromRaw.assignFromAccountRaw(rawAccount, res); } + if (fromRaw?.assignFromTokenAccountRaw && res.subAccounts) { + res.subAccounts.forEach((subAcc, index) => { + const subAccRaw = subAccountsRaw?.[index]; + if (subAcc.type === "TokenAccount" && subAccRaw?.type === "TokenAccountRaw") { + fromRaw.assignFromTokenAccountRaw?.(subAccRaw, subAcc); + } + }); + } + return res; } export type ToFamiliyRaw = { assignToAccountRaw?: AccountBridge["assignToAccountRaw"]; + assignToTokenAccountRaw?: AccountBridge["assignToTokenAccountRaw"]; toOperationExtraRaw?: AccountBridge["toOperationExtraRaw"]; }; @@ -207,6 +218,15 @@ export function toAccountRaw(account: Account, toFamilyRaw?: ToFamiliyRaw): Acco toFamilyRaw.assignToAccountRaw(account, res); } + if (toFamilyRaw?.assignToTokenAccountRaw && res.subAccounts) { + res.subAccounts.forEach((subAccRaw, index) => { + const subAcc = subAccounts?.[index]; + if (subAccRaw.type === "TokenAccountRaw" && subAcc?.type === "TokenAccount") { + toFamilyRaw.assignToTokenAccountRaw?.(subAcc, subAccRaw); + } + }); + } + if (swapHistory) { res.swapHistory = swapHistory.map(toSwapOperationRaw); } diff --git a/libs/ledger-live-common/src/account/serialization.ts b/libs/ledger-live-common/src/account/serialization.ts index 995b6088933f..197b8502abe0 100644 --- a/libs/ledger-live-common/src/account/serialization.ts +++ b/libs/ledger-live-common/src/account/serialization.ts @@ -68,6 +68,7 @@ export function fromAccountRaw(rawAccount: AccountRaw): Account { return commonFromAccountRaw(rawAccount, { assignFromAccountRaw: bridge.assignFromAccountRaw, + assignFromTokenAccountRaw: bridge.assignFromTokenAccountRaw, fromOperationExtraRaw: bridge.fromOperationExtraRaw, }); } @@ -77,6 +78,7 @@ export function toAccountRaw(account: Account, userData?: AccountUserData): Acco const commonAccountRaw = commonToAccountRaw(account, { assignToAccountRaw: bridge.assignToAccountRaw, + assignToTokenAccountRaw: bridge.assignToTokenAccountRaw, toOperationExtraRaw: bridge.toOperationExtraRaw, }); diff --git a/libs/ledgerjs/packages/types-live/src/bridge.ts b/libs/ledgerjs/packages/types-live/src/bridge.ts index 65089031046c..768de66395ae 100644 --- a/libs/ledgerjs/packages/types-live/src/bridge.ts +++ b/libs/ledgerjs/packages/types-live/src/bridge.ts @@ -6,7 +6,7 @@ import { BigNumber } from "bignumber.js"; import type { Observable } from "rxjs"; import type { CryptoCurrency } from "@ledgerhq/types-cryptoassets"; -import type { AccountLike, Account, AccountRaw } from "./account"; +import type { AccountLike, Account, AccountRaw, TokenAccount, TokenAccountRaw } from "./account"; import type { SignOperationEvent, SignedOperation, @@ -202,6 +202,25 @@ interface SendReceiveAccountBridge< * @param {Account} account - The original account object. */ assignFromAccountRaw?: (accountRaw: R, account: A) => void; + /** + * This function mutates the 'tokenAccountRaw' object in-place to add any extra fields that the coin may need to set. + * It is called during the serialization mechanism + * + * @param {TokenAccount} tokenAccount - The original token account object. + * @param {TokenAccountRaw} tokenAccountRaw - The token account in its serialized form. + */ + assignToTokenAccountRaw?: (tokenAccount: TokenAccount, tokenAccountRaw: TokenAccountRaw) => void; + /** + * This function mutates the 'tokenAccount' object in-place to add any extra fields that the coin may need to set. + * It is called during the deserialization mechanism + * + * @param {TokenAccountRaw} tokenAccountRaw - The token account in its serialized form. + * @param {TokenAccount} tokenAccount - The original token account object. + */ + assignFromTokenAccountRaw?: ( + tokenAccountRaw: TokenAccountRaw, + tokenAccount: TokenAccount, + ) => void; /** * This function mutates the 'account' object to extend it with any extra fields of the coin. * For instance bitcoinResources needs to be created. From 700b5341d8635e6dbf9e5910c5b28d2b7ff73949 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Mon, 20 May 2024 12:39:40 +0300 Subject: [PATCH 02/35] feat: add burn operation type --- .../renderer/components/OperationsList/ConfirmationCheck.tsx | 1 + apps/ledger-live-desktop/static/i18n/en/app.json | 3 ++- libs/coin-framework/src/operation.ts | 1 + libs/ledgerjs/packages/types-live/src/operation.ts | 4 +++- 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/ledger-live-desktop/src/renderer/components/OperationsList/ConfirmationCheck.tsx b/apps/ledger-live-desktop/src/renderer/components/OperationsList/ConfirmationCheck.tsx index f2b78eaba678..7557c63eb77d 100644 --- a/apps/ledger-live-desktop/src/renderer/components/OperationsList/ConfirmationCheck.tsx +++ b/apps/ledger-live-desktop/src/renderer/components/OperationsList/ConfirmationCheck.tsx @@ -122,6 +122,7 @@ const iconsComponent = { UNSTAKE: IconUndelegate, WITHDRAW_UNSTAKED: IconCoins, UNKNOWN: IconCheck, + BURN: IconTrash, }; class ConfirmationCheck extends PureComponent<{ marketColor: string; diff --git a/apps/ledger-live-desktop/static/i18n/en/app.json b/apps/ledger-live-desktop/static/i18n/en/app.json index 015411bae401..d39dbaf62518 100644 --- a/apps/ledger-live-desktop/static/i18n/en/app.json +++ b/apps/ledger-live-desktop/static/i18n/en/app.json @@ -200,7 +200,8 @@ "STAKE": "Staked", "UNSTAKE": "Unstaked", "WITHDRAW_UNSTAKED": "Withdrawn", - "SENDING": "Sending" + "SENDING": "Sending", + "BURN": "Burned" }, "edit": { "title": "Speed up or Cancel", diff --git a/libs/coin-framework/src/operation.ts b/libs/coin-framework/src/operation.ts index ca27a99a5b01..889cff983ff0 100644 --- a/libs/coin-framework/src/operation.ts +++ b/libs/coin-framework/src/operation.ts @@ -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": diff --git a/libs/ledgerjs/packages/types-live/src/operation.ts b/libs/ledgerjs/packages/types-live/src/operation.ts index 567a2d741717..9663f1ee6836 100644 --- a/libs/ledgerjs/packages/types-live/src/operation.ts +++ b/libs/ledgerjs/packages/types-live/src/operation.ts @@ -53,7 +53,9 @@ export type OperationType = // NEAR | "STAKE" | "UNSTAKE" - | "WITHDRAW_UNSTAKED"; + | "WITHDRAW_UNSTAKED" + // SOLANA + | "BURN"; export type OperationExtra = unknown; /** From 3abecdd56a516da5baf5df27a3953a1c9021cf52 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Mon, 20 May 2024 12:43:36 +0300 Subject: [PATCH 03/35] feat: enable solana spl tokens import --- .../packages/cryptoassets/src/tokens.ts | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/libs/ledgerjs/packages/cryptoassets/src/tokens.ts b/libs/ledgerjs/packages/cryptoassets/src/tokens.ts index 527e23420e05..cac0d4f98e63 100644 --- a/libs/ledgerjs/packages/cryptoassets/src/tokens.ts +++ b/libs/ledgerjs/packages/cryptoassets/src/tokens.ts @@ -14,6 +14,7 @@ import trc20tokens, { TRC20Token } from "./data/trc20"; import { tokens as mainnetTokens } from "./data/evm/1"; import { tokens as bnbTokens } from "./data/evm/56"; import filecoinTokens from "./data/filecoin-erc20"; +import spltokens, { SPLToken } from "./data/spl"; import { ERC20Token } from "./types"; const emptyArray = []; @@ -54,6 +55,8 @@ addTokens(vechainTokens.map(convertVechainToken)); addTokens(jettonTokens.map(convertJettonToken)); // Filecoin tokens addTokens(filecoinTokens.map(convertERC20)); +// Solana tokens +addTokens(spltokens.map(convertSplTokens)); type TokensListOptions = { withDelisted: boolean; @@ -403,6 +406,39 @@ function convertElrondESDTTokens([ }; } +function convertSplTokens([ + chainId, + name, + symbol, + address, + decimals, + enableCountervalues, +]: SPLToken): TokenCurrency { + const chainIdToCurrencyId = { + 101: "solana", + 102: "solana_testnet", + 103: "solana_devnet", + }; + const currencyId = chainIdToCurrencyId[chainId]; + return { + type: "TokenCurrency", + id: `solana/spl/${address}`, + contractAddress: address, + parentCurrency: getCryptoCurrencyById(currencyId), + name, + tokenType: "spl", + ticker: symbol, + disableCountervalue: !enableCountervalues, + units: [ + { + name, + code: symbol, + magnitude: decimals, + }, + ], + }; +} + function convertCardanoNativeTokens([ parentCurrencyId, policyId, From 510c6d20c3d50d515ca80749834f8cac927f3e74 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Mon, 20 May 2024 12:49:40 +0300 Subject: [PATCH 04/35] feat: finish solana tokens integration into bridge [coin-solana] --- .../coin-solana/src/api/chain/web3.ts | 4 +- .../coin-solana/src/bridge/bridge.ts | 4 + .../src/deviceTransactionConfig.ts | 6 + libs/coin-modules/coin-solana/src/errors.ts | 2 + libs/coin-modules/coin-solana/src/logic.ts | 8 +- .../coin-solana/src/prepareTransaction.ts | 20 ++ .../coin-solana/src/serialization.ts | 31 ++- .../coin-solana/src/synchronization.ts | 220 ++++++++++-------- libs/coin-modules/coin-solana/src/tx-fees.ts | 29 ++- libs/coin-modules/coin-solana/src/types.ts | 5 + libs/coin-modules/coin-solana/src/utils.ts | 1 + 11 files changed, 234 insertions(+), 96 deletions(-) diff --git a/libs/coin-modules/coin-solana/src/api/chain/web3.ts b/libs/coin-modules/coin-solana/src/api/chain/web3.ts index fff6da9b0e03..22928e5ea2c7 100644 --- a/libs/coin-modules/coin-solana/src/api/chain/web3.ts +++ b/libs/coin-modules/coin-solana/src/api/chain/web3.ts @@ -173,6 +173,8 @@ export const buildTokenTransferInstructions = async ( const destinationPubkey = new PublicKey(recipientDescriptor.tokenAccAddress); + const destinationOwnerPubkey = new PublicKey(recipientDescriptor.walletAddress); + const instructions: TransactionInstruction[] = []; const mintPubkey = new PublicKey(mintAddress); @@ -182,7 +184,7 @@ export const buildTokenTransferInstructions = async ( createAssociatedTokenAccountInstruction( ownerPubkey, destinationPubkey, - ownerPubkey, + destinationOwnerPubkey, mintPubkey, ), ); diff --git a/libs/coin-modules/coin-solana/src/bridge/bridge.ts b/libs/coin-modules/coin-solana/src/bridge/bridge.ts index 305011771c86..0b1bea1ae480 100644 --- a/libs/coin-modules/coin-solana/src/bridge/bridge.ts +++ b/libs/coin-modules/coin-solana/src/bridge/bridge.ts @@ -28,6 +28,8 @@ import { assignToAccountRaw, fromOperationExtraRaw, toOperationExtraRaw, + assignFromTokenAccountRaw, + assignToTokenAccountRaw, } from "../serialization"; function makePrepare(getChainAPI: (config: Config) => Promise) { @@ -178,6 +180,8 @@ export function makeBridges({ toOperationExtraRaw, fromOperationExtraRaw, getSerializedAddressParameters, + assignFromTokenAccountRaw, + assignToTokenAccountRaw, }; const currencyBridge: CurrencyBridge = { diff --git a/libs/coin-modules/coin-solana/src/deviceTransactionConfig.ts b/libs/coin-modules/coin-solana/src/deviceTransactionConfig.ts index 89be8eb3759e..965a1997afe0 100644 --- a/libs/coin-modules/coin-solana/src/deviceTransactionConfig.ts +++ b/libs/coin-modules/coin-solana/src/deviceTransactionConfig.ts @@ -97,6 +97,12 @@ function fieldsForTokenTransfer(command: TokenTransferCommand): DeviceTransactio label: "Transfer tokens", }); + fields.push({ + type: "address", + address: command.mintAddress, + label: "Mint", + }); + fields.push({ type: "address", address: command.ownerAssociatedTokenAccountAddress, diff --git a/libs/coin-modules/coin-solana/src/errors.ts b/libs/coin-modules/coin-solana/src/errors.ts index e90be7459bfb..94a9183b003a 100644 --- a/libs/coin-modules/coin-solana/src/errors.ts +++ b/libs/coin-modules/coin-solana/src/errors.ts @@ -16,6 +16,8 @@ export const SolanaTokenAccounNotInitialized = createCustomErrorClass( "SolanaTokenAccounNotInitialized", ); +export const SolanaTokenAccountFrozen = createCustomErrorClass("SolanaTokenAccountFrozen"); + export const SolanaAddressOffEd25519 = createCustomErrorClass("SolanaAddressOfEd25519"); export const SolanaTokenRecipientIsSenderATA = createCustomErrorClass( diff --git a/libs/coin-modules/coin-solana/src/logic.ts b/libs/coin-modules/coin-solana/src/logic.ts index 703f273173ac..fea208d4c098 100644 --- a/libs/coin-modules/coin-solana/src/logic.ts +++ b/libs/coin-modules/coin-solana/src/logic.ts @@ -1,8 +1,8 @@ import { findTokenById } from "@ledgerhq/cryptoassets"; import { PublicKey } from "@solana/web3.js"; -import { TokenAccount } from "@ledgerhq/types-live"; +import { AccountLike, TokenAccount } from "@ledgerhq/types-live"; import { StakeMeta } from "./api/chain/account/stake"; -import { SolanaStake, StakeAction } from "./types"; +import { SolanaStake, SolanaTokenAccount, StakeAction } from "./types"; import { assertUnreachable } from "./utils"; export type Awaited = T extends PromiseLike ? U : T; @@ -121,3 +121,7 @@ export function stakeActivePercent(stake: SolanaStake) { } return (stake.activation.active / amount) * 100; } + +export function isTokenAccountFrozen(account: AccountLike) { + return account.type === "TokenAccount" && (account as SolanaTokenAccount)?.state === "frozen"; +} diff --git a/libs/coin-modules/coin-solana/src/prepareTransaction.ts b/libs/coin-modules/coin-solana/src/prepareTransaction.ts index 994eaa2cc522..669488642712 100644 --- a/libs/coin-modules/coin-solana/src/prepareTransaction.ts +++ b/libs/coin-modules/coin-solana/src/prepareTransaction.ts @@ -17,6 +17,7 @@ import { } from "./api/chain/web3"; import { SolanaAccountNotFunded, + SolanaTokenAccountFrozen, SolanaAddressOffEd25519, SolanaInvalidValidator, SolanaMemoIsTooLong, @@ -44,6 +45,7 @@ import type { CommandDescriptor, SolanaAccount, SolanaStake, + SolanaTokenAccount, StakeCreateAccountTransaction, StakeDelegateTransaction, StakeSplitTransaction, @@ -126,6 +128,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; @@ -232,6 +238,17 @@ async function getTokenRecipient( api, )); + if (!shouldCreateAsAssociatedTokenAccount) { + const associatedTokenAccount = await getMaybeTokenAccount( + recipientAssociatedTokenAccountAddress, + api, + ); + if (associatedTokenAccount instanceof Error) throw recipientTokenAccount; + if (associatedTokenAccount?.state === "frozen") { + return new SolanaTokenAccountFrozen(); + } + } + return { walletAddress: recipientAddress, shouldCreateAsAssociatedTokenAccount, @@ -241,6 +258,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(); } diff --git a/libs/coin-modules/coin-solana/src/serialization.ts b/libs/coin-modules/coin-solana/src/serialization.ts index d280789fb28e..d111910644a9 100644 --- a/libs/coin-modules/coin-solana/src/serialization.ts +++ b/libs/coin-modules/coin-solana/src/serialization.ts @@ -5,8 +5,17 @@ import { SolanaOperationExtraRaw, SolanaResources, SolanaResourcesRaw, + SolanaTokenAccount, + SolanaTokenAccountRaw, } from "./types"; -import { Account, AccountRaw, OperationExtra, OperationExtraRaw } from "@ledgerhq/types-live"; +import { + Account, + AccountRaw, + OperationExtra, + OperationExtraRaw, + TokenAccount, + TokenAccountRaw, +} from "@ledgerhq/types-live"; import { BigNumber } from "bignumber.js"; export function toSolanaResourcesRaw(resources: SolanaResources): SolanaResourcesRaw { @@ -79,3 +88,23 @@ export function toOperationExtraRaw(extra: OperationExtra): OperationExtraRaw { function isExtraValid(extra: OperationExtra | OperationExtraRaw): boolean { return !!extra && typeof extra === "object"; } + +export function assignToTokenAccountRaw( + tokenAccount: TokenAccount, + tokenAccountRaw: TokenAccountRaw, +) { + const solanaTokenAccount = tokenAccount as SolanaTokenAccount; + if (solanaTokenAccount.state) { + (tokenAccountRaw as SolanaTokenAccountRaw).state = solanaTokenAccount.state; + } +} + +export function assignFromTokenAccountRaw( + tokenAccountRaw: TokenAccountRaw, + tokenAccount: TokenAccount, +) { + const stateRaw = (tokenAccountRaw as SolanaTokenAccountRaw).state; + if (stateRaw) { + (tokenAccount as SolanaTokenAccount).state = stateRaw; + } +} diff --git a/libs/coin-modules/coin-solana/src/synchronization.ts b/libs/coin-modules/coin-solana/src/synchronization.ts index 348956645c07..e59f72feaccd 100644 --- a/libs/coin-modules/coin-solana/src/synchronization.ts +++ b/libs/coin-modules/coin-solana/src/synchronization.ts @@ -34,22 +34,12 @@ import { sum, } from "lodash/fp"; import { parseQuiet } from "./api/chain/program"; -import { - InflationReward, - ParsedTransactionMeta, - ParsedMessageAccount, - ParsedTransaction, - StakeActivationData, -} from "@solana/web3.js"; +import { InflationReward, ParsedTransaction, StakeActivationData } from "@solana/web3.js"; import { ChainAPI } from "./api"; -import { - ParsedOnChainTokenAccountWithInfo, - /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ - toTokenAccountWithInfo, -} from "./api/chain/web3"; +import { ParsedOnChainTokenAccountWithInfo, toTokenAccountWithInfo } from "./api/chain/web3"; import { drainSeq } from "./utils"; import { estimateTxFee } from "./tx-fees"; -import { SolanaAccount, SolanaOperationExtra, SolanaStake } from "./types"; +import { SolanaAccount, SolanaOperationExtra, SolanaStake, SolanaTokenAccount } from "./types"; import { Operation, OperationType, TokenAccount } from "@ledgerhq/types-live"; import { DelegateInfo, WithdrawInfo } from "./api/chain/instruction/stake/types"; @@ -113,7 +103,7 @@ export const getAccountShapeWithAPI = async ( const subAcc = subAccByMint.get(mint); - const lastSyncedTxSignature = subAcc?.operations?.[0].hash; + const lastSyncedTxSignature = subAcc?.operations?.[0]?.hash; const txs = await getTransactions( assocTokenAcc.onChainAcc.pubkey.toBase58(), @@ -232,9 +222,7 @@ export const getAccountShapeWithAPI = async ( } const shape: Partial = { - // uncomment when tokens are supported - // subAccounts as undefined makes TokenList disappear in desktop - //subAccounts: nextSubAccs, + subAccounts: nextSubAccs, id: mainAccountId, blockHeight, balance: mainAccBalance.plus(totalStakedBalance), @@ -264,7 +252,7 @@ function newSubAcc({ mainAccountId: string; assocTokenAcc: OnChainTokenAccount; txs: TransactionDescriptor[]; -}): TokenAccount { +}): SolanaTokenAccount { const firstTx = txs[txs.length - 1]; const creationDate = new Date((firstTx.info.blockTime ?? Date.now() / 1000) * 1000); @@ -295,6 +283,7 @@ function newSubAcc({ spendableBalance: balance, swapHistory: [], token: tokenCurrency, + state: assocTokenAcc.info.state, type: "TokenAccount", }; } @@ -307,7 +296,7 @@ function patchedSubAcc({ subAcc: TokenAccount; assocTokenAcc: OnChainTokenAccount; txs: TransactionDescriptor[]; -}): TokenAccount { +}): SolanaTokenAccount { const balance = new BigNumber(assocTokenAcc.info.tokenAmount.amount); const newOps = compact(txs.map(tx => txToTokenAccOperation(tx, assocTokenAcc, subAcc.id))); @@ -319,6 +308,7 @@ function patchedSubAcc({ balance, spendableBalance: balance, operations: totalOps, + state: assocTokenAcc.info.state, }; } @@ -358,26 +348,7 @@ function txToMainAccOperation( balanceDelta, }); - const accum = { - senders: [] as string[], - recipients: [] as string[], - }; - - const { senders, recipients } = - opType === "IN" || opType === "OUT" - ? message.accountKeys.reduce((acc, account, i) => { - const delta = new BigNumber(postBalances[i]).minus(new BigNumber(preBalances[i])); - if (delta.lt(0)) { - const shouldConsiderAsSender = i > 0 || !delta.negated().eq(txFee); - if (shouldConsiderAsSender) { - acc.senders.push(account.pubkey.toBase58()); - } - } else if (delta.gt(0)) { - acc.recipients.push(account.pubkey.toBase58()); - } - return acc; - }, accum) - : accum; + const { senders, recipients } = getMainSendersRecipients(tx, opType, txFee, accountAddress); const txHash = tx.info.signature; const txDate = new Date(tx.info.blockTime * 1000); @@ -467,10 +438,16 @@ function txToTokenAccOperation( return undefined; } + const { message } = tx.parsed.transaction; const assocTokenAccIndex = tx.parsed.transaction.message.accountKeys.findIndex(v => v.pubkey.equals(assocTokenAcc.onChainAcc.pubkey), ); + const accountOwner = assocTokenAcc.info.owner.toBase58(); + const accountOwnerIndex = message.accountKeys.findIndex( + pma => pma.pubkey.toBase58() === accountOwner, + ); + if (assocTokenAccIndex < 0) { return undefined; } @@ -485,14 +462,15 @@ function txToTokenAccOperation( new BigNumber(preTokenBalance?.uiTokenAmount.amount ?? 0), ); + const isFeePayer = accountOwnerIndex === 0; + const txFee = new BigNumber(tx.parsed.meta.fee); + const opType = getTokenAccOperationType({ tx: tx.parsed.transaction, delta }); const txHash = tx.info.signature; + const opFee = isFeePayer ? txFee : new BigNumber(0); - const { senders, recipients } = getTokenSendersRecipients({ - meta: tx.parsed.meta, - accounts: tx.parsed.transaction.message.accountKeys, - }); + const { senders, recipients } = getTokenSendersRecipients(tx); return { id: encodeOperationId(accountId, txHash, opType), @@ -501,7 +479,7 @@ function txToTokenAccOperation( hash: txHash, date: new Date(tx.info.blockTime * 1000), blockHeight: tx.info.slot, - fee: new BigNumber(0), + fee: opFee, recipients, senders, value: delta.abs(), @@ -537,12 +515,63 @@ function getMainAccOperationType({ : "NONE"; } -function getMainAccOperationTypeFromTx(tx: ParsedTransaction): OperationType | undefined { - const { instructions } = tx.message; +function getMainSendersRecipients( + tx: TransactionDescriptor, + opType: OperationType, + txFee: BigNumber, + accountAddress: string, +) { + const initialSendersRecipients = { + senders: [] as string[], + recipients: [] as string[], + }; + if (!tx.parsed.meta) { + return initialSendersRecipients; + } - const parsedIxs = instructions - .map(ix => parseQuiet(ix)) - .filter(({ program }) => program !== "spl-memo" && program !== "unknown"); + const { message } = tx.parsed.transaction; + const { postTokenBalances, preBalances, postBalances } = tx.parsed.meta; + + if (opType === "FEES") { + // SPL transfer to existing ATA. FEES operation is shown for the main account + return getTokenSendersRecipients(tx); + } + + if (opType === "OPT_IN") { + // Associated token account created + const incomingTokens = + postTokenBalances?.filter(tokenBalance => tokenBalance.owner === accountAddress) || []; + + initialSendersRecipients.senders = incomingTokens.map(token => token.mint); + initialSendersRecipients.recipients = incomingTokens.map(token => { + return message.accountKeys[token.accountIndex].pubkey.toBase58(); + }) || [accountAddress]; + + return initialSendersRecipients; + } + + if (opType === "IN" || opType === "OUT") { + const isAccFeePayer = (accIndex: number) => accIndex === 0; + + return message.accountKeys.reduce((acc, account, i) => { + const delta = new BigNumber(postBalances[i]).minus(new BigNumber(preBalances[i])); + if (delta.lt(0)) { + const shouldConsiderAsSender = !isAccFeePayer(i) || !delta.negated().eq(txFee); + if (shouldConsiderAsSender) { + acc.senders.push(account.pubkey.toBase58()); + } + } else if (delta.gt(0)) { + acc.recipients.push(account.pubkey.toBase58()); + } + return acc; + }, initialSendersRecipients); + } + + return initialSendersRecipients; +} + +function getMainAccOperationTypeFromTx(tx: ParsedTransaction): OperationType | undefined { + const parsedIxs = parseTxInstructions(tx); if (parsedIxs.length === 3) { const [first, second, third] = parsedIxs; @@ -568,12 +597,14 @@ function getMainAccOperationTypeFromTx(tx: ParsedTransaction): OperationType | u case "associate": return "OPT_IN"; } - // needed for lint - break; case "spl-token": switch (first.instruction.type) { case "closeAccount": return "OPT_OUT"; + case "freezeAccount": + return "FREEZE"; + case "thawAccount": + return "UNFREEZE"; } break; case "stake": @@ -594,36 +625,34 @@ function getMainAccOperationTypeFromTx(tx: ParsedTransaction): OperationType | u return undefined; } -function getTokenSendersRecipients({ - meta, - accounts, -}: { - meta: ParsedTransactionMeta; - accounts: ParsedMessageAccount[]; -}) { - const { preTokenBalances, postTokenBalances } = meta; - return accounts.reduce( - (accum, account, i) => { - const preTokenBalance = preTokenBalances?.find(b => b.accountIndex === i); - const postTokenBalance = postTokenBalances?.find(b => b.accountIndex === i); - if (preTokenBalance && postTokenBalance) { - const tokenDelta = new BigNumber(postTokenBalance.uiTokenAmount.amount).minus( - new BigNumber(preTokenBalance.uiTokenAmount.amount), - ); - - if (tokenDelta.lt(0)) { - accum.senders.push(account.pubkey.toBase58()); - } else if (tokenDelta.gt(0)) { - accum.recipients.push(account.pubkey.toBase58()); - } +function getTokenSendersRecipients(tx: TransactionDescriptor) { + const initialSendersRecipients = { + senders: [] as string[], + recipients: [] as string[], + }; + + if (!tx.parsed.meta) { + return initialSendersRecipients; + } + + const accounts = tx.parsed.transaction.message.accountKeys; + const { preTokenBalances, postTokenBalances } = tx.parsed.meta; + + return accounts.reduce((accum, account, i) => { + const preTokenBalance = preTokenBalances?.find(b => b.accountIndex === i); + const postTokenBalance = postTokenBalances?.find(b => b.accountIndex === i); + if (preTokenBalance || postTokenBalance) { + const tokenDelta = new BigNumber(postTokenBalance?.uiTokenAmount.amount ?? 0).minus( + new BigNumber(preTokenBalance?.uiTokenAmount.amount ?? 0), + ); + if (tokenDelta.lt(0)) { + accum.senders.push(postTokenBalance?.owner || account.pubkey.toBase58()); + } else if (tokenDelta.gt(0)) { + accum.recipients.push(postTokenBalance?.owner || account.pubkey.toBase58()); } - return accum; - }, - { - senders: [] as string[], - recipients: [] as string[], - }, - ); + } + return accum; + }, initialSendersRecipients); } function getTokenAccOperationType({ @@ -633,17 +662,24 @@ function getTokenAccOperationType({ tx: ParsedTransaction; delta: BigNumber; }): OperationType { - const { instructions } = tx.message; - const [mainIx, ...otherIxs] = instructions - .map(ix => parseQuiet(ix)) - .filter(({ program }) => program !== "spl-memo" && program !== "unknown"); + const parsedIxs = parseTxInstructions(tx); + const [mainIx, ...otherIxs] = parsedIxs; if (mainIx !== undefined && otherIxs.length === 0) { switch (mainIx.program) { case "spl-associated-token-account": switch (mainIx.instruction.type) { case "associate": - return "OPT_IN"; + return "NONE"; // ATA opt-in operation is added to the main account + } + case "spl-token": + switch (mainIx.instruction.type) { + case "freezeAccount": + return "FREEZE"; + case "thawAccount": + return "UNFREEZE"; + case "burn": + return "BURN"; } } } @@ -652,6 +688,12 @@ function getTokenAccOperationType({ return fallbackType; } +function parseTxInstructions(tx: ParsedTransaction) { + return tx.message.instructions + .map(ix => parseQuiet(ix)) + .filter(({ program }) => program !== "spl-memo" && program !== "unknown"); +} + function dropMemoLengthPrefixIfAny(memo: string) { const lengthPrefixMatch = memo.match(/^\[(\d+)\]\s/); if (lengthPrefixMatch) { @@ -678,14 +720,10 @@ async function getAccount( }> { const balanceLamportsWithContext = await api.getBalanceAndContext(address); - const tokenAccounts: ParsedOnChainTokenAccountWithInfo[] = []; - - // no tokens for the first release - /*await api + const tokenAccounts = await api .getParsedTokenAccountsByOwner(address) - .then((res) => res.value) + .then(res => res.value) .then(map(toTokenAccountWithInfo)); - */ const stakeAccountsRaw = [ // ...(await api.getStakeAccountsByStakeAuth(address)), diff --git a/libs/coin-modules/coin-solana/src/tx-fees.ts b/libs/coin-modules/coin-solana/src/tx-fees.ts index 825a613e4385..e4ef0fed53b4 100644 --- a/libs/coin-modules/coin-solana/src/tx-fees.ts +++ b/libs/coin-modules/coin-solana/src/tx-fees.ts @@ -49,9 +49,10 @@ const createDummyTx = (address: string, kind: TransactionModel["kind"]) => { return createDummyStakeUndelegateTx(address); case "stake.withdraw": return createDummyStakeWithdrawTx(address); + case "token.transfer": + return createDummyTokenTransferTx(address); case "stake.split": case "token.createATA": - case "token.transfer": throw new Error(`not implemented for <${kind}>`); default: return assertUnreachable(kind); @@ -158,6 +159,32 @@ const createDummyStakeWithdrawTx = (address: string): Transaction => { }; }; +const createDummyTokenTransferTx = (address: string): Transaction => { + return { + ...createTransaction({} as any), + model: { + kind: "token.transfer", + uiState: {} as any, + commandDescriptor: { + command: { + kind: "token.transfer", + amount: 0, + mintAddress: randomAddresses[0], + mintDecimals: 0, + ownerAddress: address, + ownerAssociatedTokenAccountAddress: randomAddresses[1], + recipientDescriptor: { + walletAddress: randomAddresses[1], + tokenAccAddress: randomAddresses[2], + shouldCreateAsAssociatedTokenAccount: true, + }, + }, + ...commandDescriptorCommons, + }, + }, + }; +}; + const commandDescriptorCommons = { errors: {}, fee: 0, diff --git a/libs/coin-modules/coin-solana/src/types.ts b/libs/coin-modules/coin-solana/src/types.ts index 765a3a3a9a93..310d30d6f486 100644 --- a/libs/coin-modules/coin-solana/src/types.ts +++ b/libs/coin-modules/coin-solana/src/types.ts @@ -3,12 +3,15 @@ import { Account, AccountRaw, Operation, + TokenAccount, + TokenAccountRaw, TransactionCommon, TransactionCommonRaw, TransactionStatusCommon, TransactionStatusCommonRaw, } from "@ledgerhq/types-live"; import { ValidatorsAppValidator } from "./validator-app"; +import { TokenAccountState } from "./api/chain/account/token"; export type TransferCommand = { kind: "transfer"; @@ -262,6 +265,8 @@ export type SolanaAccount = Account & { solanaResources: SolanaResources }; export type SolanaAccountRaw = AccountRaw & { solanaResources: SolanaResourcesRaw; }; +export type SolanaTokenAccount = TokenAccount & { state?: TokenAccountState }; +export type SolanaTokenAccountRaw = TokenAccountRaw & { state?: TokenAccountState }; export type TransactionStatus = TransactionStatusCommon; diff --git a/libs/coin-modules/coin-solana/src/utils.ts b/libs/coin-modules/coin-solana/src/utils.ts index 8506162e58b7..111c2d0a0746 100644 --- a/libs/coin-modules/coin-solana/src/utils.ts +++ b/libs/coin-modules/coin-solana/src/utils.ts @@ -39,6 +39,7 @@ export const LEDGER_VALIDATOR_LIST: ValidatorsAppValidator[] = [ export const LEDGER_VALIDATORS_VOTE_ACCOUNTS = LEDGER_VALIDATOR_LIST.map(v => v.voteAccount); export const SOLANA_DELEGATION_RESERVE = 0.01; +export const SYSTEM_ACCOUNT_RENT_EXEMPT = 890880; export const assertUnreachable = (_: never): never => { throw new Error("unreachable assertion failed"); From ba92b72e4f5f41840381f8308947b76d2a790a9a Mon Sep 17 00:00:00 2001 From: Mikhail Date: Mon, 20 May 2024 12:51:06 +0300 Subject: [PATCH 05/35] chore: update solana bridge and speculos tests --- .../src/bridge.integration.test.ts | 200 ++++++++++++++++-- libs/coin-modules/coin-solana/src/specs.ts | 112 +++++++++- .../coin-solana/src/speculos-deviceActions.ts | 175 ++++++++++++++- .../bridge.integration.test.ts.snap | 37 ++++ 4 files changed, 497 insertions(+), 27 deletions(-) diff --git a/libs/coin-modules/coin-solana/src/bridge.integration.test.ts b/libs/coin-modules/coin-solana/src/bridge.integration.test.ts index 124ae2f7bdfb..048332a678b6 100644 --- a/libs/coin-modules/coin-solana/src/bridge.integration.test.ts +++ b/libs/coin-modules/coin-solana/src/bridge.integration.test.ts @@ -2,6 +2,9 @@ import BigNumber from "bignumber.js"; import { SolanaAccount, SolanaStake, + SolanaTokenAccount, + SolanaTokenAccountRaw, + TokenTransferTransaction, Transaction, TransactionModel, TransactionStatus, @@ -14,34 +17,34 @@ import { NotEnoughBalance, RecipientRequired, } from "@ledgerhq/errors"; +import { findTokenByAddressInCurrency } from "@ledgerhq/cryptoassets"; +import { TokenCurrency } from "@ledgerhq/types-cryptoassets"; +import type { Account, AccountRaw, CurrenciesData, DatasetTest } from "@ledgerhq/types-live"; import { SolanaAccountNotFunded, SolanaAddressOffEd25519, SolanaInvalidValidator, SolanaMemoIsTooLong, - /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ SolanaRecipientAssociatedTokenAccountWillBeFunded, SolanaStakeAccountNotFound, SolanaStakeAccountRequired, - /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ + SolanaTokenAccountFrozen, SolanaTokenAccountHoldsAnotherToken, SolanaValidatorRequired, } from "./errors"; import { encodeAccountIdWithTokenAccountAddress, MAX_MEMO_LENGTH } from "./logic"; import createTransaction from "./createTransaction"; import { compact } from "lodash/fp"; -import { assertUnreachable } from "./utils"; +import { SYSTEM_ACCOUNT_RENT_EXEMPT, assertUnreachable } from "./utils"; import { getEnv } from "@ledgerhq/live-env"; -import { ChainAPI } from "./api"; +import { ChainAPI, LATEST_BLOCKHASH_MOCK } from "./api"; import { SolanaStakeAccountIsNotDelegatable, SolanaStakeAccountValidatorIsUnchangeable, } from "./errors"; import getTransactionStatus from "./getTransactionStatus"; import { prepareTransaction } from "./prepareTransaction"; -import type { Account, CurrenciesData, DatasetTest } from "@ledgerhq/types-live"; -import { encodeAccountId } from "@ledgerhq/coin-framework/account/accountId"; -import { LATEST_BLOCKHASH_MOCK } from "./api/chain"; +import { encodeAccountId } from "@ledgerhq/coin-framework/lib/account/accountId"; // do not change real properties or the test will break const testOnChainData = { @@ -61,7 +64,7 @@ const testOnChainData = { validatorAddress: "9QU2QSxhb24FUX3Tu2FpczXjpK3VYrvRudywSZaM29mF", fees: { stakeAccountRentExempt: 2282880, - systemAccountRentExempt: 890880, + systemAccountRentExempt: SYSTEM_ACCOUNT_RENT_EXEMPT, lamportsPerSignature: 5000, }, // --- maybe outdated or not real, fine for tests --- @@ -77,12 +80,16 @@ const mainAccId = encodeAccountId({ derivationMode: "solanaMain", }); -/* eslint-disable-next-line @typescript-eslint/no-unused-vars */ const wSolSubAccId = encodeAccountIdWithTokenAccountAddress( mainAccId, testOnChainData.wSolSenderAssocTokenAccAddress, ); +const wSolToken = findTokenByAddressInCurrency( + "So11111111111111111111111111111111111111112", + "solana", +) as TokenCurrency; + const fees = (signatureCount: number) => new BigNumber(signatureCount * testOnChainData.fees.lamportsPerSignature); @@ -128,7 +135,7 @@ const solana: CurrenciesData = { }, ...transferTests(), ...stakingTests(), - //...tokenTests() + ...tokenTests(), ], }, ], @@ -141,7 +148,7 @@ export const dataset: DatasetTest = { }, }; -function makeAccount(freshAddress: string) { +function makeAccount(freshAddress: string): AccountRaw { return { id: mainAccId, seedIdentifier: "", @@ -155,6 +162,19 @@ function makeAccount(freshAddress: string) { currencyId: "solana", lastSyncDate: "", balance: "0", + subAccounts: [makeSubTokenAccount()], + }; +} + +function makeSubTokenAccount(): SolanaTokenAccountRaw { + return { + type: "TokenAccountRaw", + id: wSolSubAccId, + parentId: mainAccId, + tokenId: wSolToken.id, + balance: "0", + operations: [], + pendingOperations: [], }; } @@ -168,15 +188,12 @@ type TransactionTestSpec = Exclude< function recipientRequired(): TransactionTestSpec[] { const models: TransactionModel[] = [ - // uncomment when tokens are supported - /* { kind: "token.transfer", uiState: { - subAccountId: "", + subAccountId: wSolSubAccId, }, }, - */ { kind: "transfer", uiState: {}, @@ -429,8 +446,6 @@ function transferTests(): TransactionTestSpec[] { ]; } -// uncomment when tokens are supported -/* eslint-disable-next-line @typescript-eslint/no-unused-vars */ function tokenTests(): TransactionTestSpec[] { return [ { @@ -495,7 +510,7 @@ function tokenTests(): TransactionTestSpec[] { warnings: {}, estimatedFees: fees(1), amount: testOnChainData.wSolSenderAssocTokenAccBalance.dividedBy(2), - totalSpent: zero, + totalSpent: testOnChainData.wSolSenderAssocTokenAccBalance.dividedBy(2), }, }, { @@ -518,7 +533,7 @@ function tokenTests(): TransactionTestSpec[] { warnings: {}, estimatedFees: fees(1), amount: testOnChainData.wSolSenderAssocTokenAccBalance.dividedBy(2), - totalSpent: zero, + totalSpent: testOnChainData.wSolSenderAssocTokenAccBalance.dividedBy(2), }, }, { @@ -537,8 +552,7 @@ function tokenTests(): TransactionTestSpec[] { expectedStatus: { errors: {}, warnings: { - recipient: new SolanaAccountNotFunded(), - recipientAssociatedTokenAccount: new SolanaRecipientAssociatedTokenAccountWillBeFunded(), + recipient: new SolanaRecipientAssociatedTokenAccountWillBeFunded(), }, // this fee is dynamic, skip //estimatedFees: new BigNumber(2044280), @@ -1110,6 +1124,150 @@ const mockedVoteAccount = { 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, + 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: [], unstakeReserve: BigNumber(0) }, + }; + + 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: [], unstakeReserve: BigNumber(0) }, + }; + + 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); + }); +}); + describe("Solana bridge", () => { test.todo( "This is an empty test to make jest command pass. Remove it once there is a real test.", diff --git a/libs/coin-modules/coin-solana/src/specs.ts b/libs/coin-modules/coin-solana/src/specs.ts index d33a8fbdfb61..b21a3bbca124 100644 --- a/libs/coin-modules/coin-solana/src/specs.ts +++ b/libs/coin-modules/coin-solana/src/specs.ts @@ -15,12 +15,16 @@ import { acceptStakeDelegateTransaction, acceptStakeUndelegateTransaction, acceptStakeWithdrawTransaction, + acceptTransferTokensTransaction, + acceptTransferTokensWithATACreationTransaction, acceptTransferTransaction, } from "./speculos-deviceActions"; -import { assertUnreachable } from "./utils"; +import { SYSTEM_ACCOUNT_RENT_EXEMPT, assertUnreachable } from "./utils"; import { getCurrentSolanaPreloadData } from "./preload-data"; import { sample } from "lodash/fp"; import BigNumber from "bignumber.js"; +import { Account, TokenAccount } from "@ledgerhq/types-live"; +import { SolanaRecipientAssociatedTokenAccountWillBeFunded } from "./errors"; const maxAccount = 9; @@ -73,8 +77,16 @@ const solana: AppSpec = { }; }, test: input => { - const { account } = input; - botTest("account balance should be zero", () => + const { accountBeforeTransaction, account, operation } = input; + const extimatedMaxSpendable = BigNumber.max( + accountBeforeTransaction.spendableBalance.minus(SYSTEM_ACCOUNT_RENT_EXEMPT), + 0, + ).toNumber(); + + botTest("operation value should be estimated max spendable", () => + expect(operation.value.toNumber()).toBe(extimatedMaxSpendable), + ); + botTest("account spendableBalance should be zero", () => expect(account.spendableBalance.toNumber()).toBe(0), ); expectCorrectBalanceChange(input); @@ -461,6 +473,66 @@ const solana: AppSpec = { botTest("delegation exists", () => expect(delegationExists).toBe(false)); }, }, + { + name: "Transfer ~50% of spl token with ATA creation", + maxRun: 1, + deviceAction: acceptTransferTokensWithATACreationTransaction, + transaction: ({ account, bridge, siblings, maxSpendable }) => { + invariant(maxSpendable.gt(0), "balance is 0"); + + const senderTokenAcc = findTokenSubAccountWithBalance(account); + invariant(senderTokenAcc, "Sender token account with available balance not found"); + + const token = senderTokenAcc.token; + const siblingWithoutToken = siblings.find(acc => !findTokenSubAccount(acc, token.id)); + invariant(siblingWithoutToken, `Recipient without ${token.ticker} ATA not found`); + + const amount = senderTokenAcc.balance.div(1.9 + 0.2 * Math.random()).integerValue(); + const recipient = siblingWithoutToken.freshAddress; + const transaction = bridge.createTransaction(account); + const subAccountId = senderTokenAcc.id; + + return { + transaction, + updates: [{ subAccountId }, { recipient }, { amount }], + }; + }, + expectStatusWarnings: _ => { + return { + recipient: new SolanaRecipientAssociatedTokenAccountWillBeFunded(), + }; + }, + test: input => { + expectTokenAccountCorrectBalanceChange(input); + }, + }, + { + name: "Transfer ~50% of spl token to existing ATA", + maxRun: 1, + deviceAction: acceptTransferTokensTransaction, + transaction: ({ account, bridge, siblings, maxSpendable }) => { + invariant(maxSpendable.gt(0), "balance is 0"); + + const senderTokenAcc = findTokenSubAccountWithBalance(account); + invariant(senderTokenAcc, "Sender token account with available balance not found"); + + const token = senderTokenAcc.token; + const siblingTokenAccount = siblings.find(acc => findTokenSubAccount(acc, token.id)); + invariant(siblingTokenAccount, `Sibling with ${token.ticker} token ATA not found`); + + const amount = senderTokenAcc.balance.div(1.9 + 0.2 * Math.random()).integerValue(); + const recipient = siblingTokenAccount.freshAddress; + const transaction = bridge.createTransaction(account); + const subAccountId = senderTokenAcc.id; + return { + transaction, + updates: [{ subAccountId }, { recipient }, { amount }], + }; + }, + test: input => { + expectTokenAccountCorrectBalanceChange(input); + }, + }, ], }; @@ -509,6 +581,40 @@ function expectCorrectBalanceChange(input: TransactionTestInput) { ); } +function expectTokenAccountCorrectBalanceChange({ + account, + accountBeforeTransaction, + status, + transaction, +}: TransactionTestInput) { + const tokenAccId = transaction.subAccountId; + if (!tokenAccId) throw new Error("Wrong transaction!"); + const tokenAccAfterTx = account.subAccounts?.find(acc => acc.id === tokenAccId); + const tokenAccBeforeTx = accountBeforeTransaction.subAccounts?.find(acc => acc.id === tokenAccId); + + if (!tokenAccAfterTx || !tokenAccBeforeTx) { + throw new Error("token sub accounts not found!"); + } + + botTest("token balance decreased with operation", () => + expect(tokenAccAfterTx.balance.toString()).toBe( + tokenAccBeforeTx.balance.minus(status.amount).toString(), + ), + ); +} + +function findTokenSubAccount(account: Account, tokenId: string) { + return account.subAccounts?.find( + acc => acc.type === "TokenAccount" && acc.token.id === tokenId, + ) as TokenAccount | undefined; +} + +function findTokenSubAccountWithBalance(account: Account) { + return account.subAccounts?.find(acc => acc.type === "TokenAccount" && acc.balance.gt(0)) as + | TokenAccount + | undefined; +} + export default { solana, }; diff --git a/libs/coin-modules/coin-solana/src/speculos-deviceActions.ts b/libs/coin-modules/coin-solana/src/speculos-deviceActions.ts index 663e7e47ec1c..532536e33d50 100644 --- a/libs/coin-modules/coin-solana/src/speculos-deviceActions.ts +++ b/libs/coin-modules/coin-solana/src/speculos-deviceActions.ts @@ -1,3 +1,4 @@ +import { Account } from "@ledgerhq/types-live"; import type { DeviceAction } from "@ledgerhq/coin-framework/bot/types"; import type { Transaction } from "./types"; import { @@ -7,7 +8,8 @@ import { } from "@ledgerhq/coin-framework/bot/specs"; import { getCryptoCurrencyById } from "@ledgerhq/cryptoassets"; import BigNumber from "bignumber.js"; -import type { CryptoCurrency } from "@ledgerhq/types-cryptoassets"; +import type { CryptoCurrency, TokenCurrency } from "@ledgerhq/types-cryptoassets"; +import { findSubAccountById } from "@ledgerhq/coin-framework/lib/account/helpers"; function getMainCurrency(currency: CryptoCurrency) { if (currency.isTestnetFor !== undefined) { @@ -20,13 +22,25 @@ function ellipsis(str: string) { return `${str.slice(0, 7)}..${str.slice(-7)}`; } -function formatAmount(c: CryptoCurrency, amount: number) { - const currency = getMainCurrency(c); +function formatAmount(c: CryptoCurrency | TokenCurrency, amount: number) { + const currency = c.type === "CryptoCurrency" ? getMainCurrency(c) : c; return formatDeviceAmount(currency, new BigNumber(amount), { postfixCode: true, }); } +function formatTokenAmount(c: TokenCurrency, amount: number) { + return formatAmount(c, amount); +} + +function findTokenAccount(account: Account, id: string) { + const tokenAccount = findSubAccountById(account, id); + if (!tokenAccount || tokenAccount.type !== "TokenAccount") { + throw new Error("expected token account " + id); + } + return tokenAccount; +} + export const acceptTransferTransaction: DeviceAction = deviceActionFlow({ steps: [ { @@ -224,6 +238,161 @@ export const acceptStakeWithdrawTransaction: DeviceAction = de ], }); +export const acceptTransferTokensTransaction: DeviceAction = deviceActionFlow({ + steps: [ + { + title: "Transfer tokens", + button: SpeculosButton.RIGHT, + expectedValue: ({ account, transaction }) => { + const command = transaction.model.commandDescriptor?.command; + if (command?.kind === "token.transfer" && transaction.subAccountId) { + const tokenCurrency = findTokenAccount(account, transaction.subAccountId).token; + return formatTokenAmount(tokenCurrency, command.amount); + } + throwUnexpectedTransaction(); + }, + }, + { + title: "Mint", + button: SpeculosButton.RIGHT, + expectedValue: ({ transaction }) => { + const command = transaction.model.commandDescriptor?.command; + if (command?.kind === "token.transfer") { + return ellipsis(command.mintAddress); + } + throwUnexpectedTransaction(); + }, + }, + { + title: "From", + button: SpeculosButton.RIGHT, + expectedValue: ({ transaction }) => { + const command = transaction.model.commandDescriptor?.command; + if (command?.kind === "token.transfer") { + return ellipsis(command.ownerAssociatedTokenAccountAddress); + } + throwUnexpectedTransaction(); + }, + }, + { + title: "To", + button: SpeculosButton.RIGHT, + expectedValue: ({ transaction }) => { + const command = transaction.model.commandDescriptor?.command; + if (command?.kind === "token.transfer") { + return ellipsis(command.recipientDescriptor.tokenAccAddress); + } + throwUnexpectedTransaction(); + }, + }, + { + title: "Approve", + button: SpeculosButton.BOTH, + final: true, + }, + ], +}); + +export const acceptTransferTokensWithATACreationTransaction: DeviceAction = + deviceActionFlow({ + steps: [ + { + title: "Create token acct", + button: SpeculosButton.RIGHT, + expectedValue: ({ transaction }) => { + const command = transaction.model.commandDescriptor?.command; + if (command?.kind === "token.transfer") { + return ellipsis(command.recipientDescriptor.tokenAccAddress); + } + throwUnexpectedTransaction(); + }, + }, + { + title: "From mint", + button: SpeculosButton.RIGHT, + expectedValue: ({ transaction }) => { + const command = transaction.model.commandDescriptor?.command; + if (command?.kind === "token.transfer") { + return ellipsis(command.mintAddress); + } + throwUnexpectedTransaction(); + }, + }, + { + title: "Owned by", + button: SpeculosButton.RIGHT, + expectedValue: ({ transaction }) => { + const command = transaction.model.commandDescriptor?.command; + if (command?.kind === "token.transfer") { + return ellipsis(command.recipientDescriptor.walletAddress); + } + throwUnexpectedTransaction(); + }, + }, + { + title: "Funded by", + button: SpeculosButton.RIGHT, + expectedValue: ({ transaction }) => { + const command = transaction.model.commandDescriptor?.command; + if (command?.kind === "token.transfer") { + return ellipsis(command.ownerAddress); + } + throwUnexpectedTransaction(); + }, + }, + { + title: "Transfer tokens", + button: SpeculosButton.RIGHT, + expectedValue: ({ account, transaction }) => { + const command = transaction.model.commandDescriptor?.command; + if (command?.kind === "token.transfer" && transaction.subAccountId) { + const tokenCurrency = findTokenAccount(account, transaction.subAccountId).token; + return formatTokenAmount(tokenCurrency, command.amount); + } + throwUnexpectedTransaction(); + }, + }, + { + title: "Mint", + button: SpeculosButton.RIGHT, + expectedValue: ({ transaction }) => { + const command = transaction.model.commandDescriptor?.command; + if (command?.kind === "token.transfer") { + return ellipsis(command.mintAddress); + } + throwUnexpectedTransaction(); + }, + }, + { + title: "From", + button: SpeculosButton.RIGHT, + expectedValue: ({ transaction }) => { + const command = transaction.model.commandDescriptor?.command; + if (command?.kind === "token.transfer") { + return ellipsis(command.ownerAssociatedTokenAccountAddress); + } + throwUnexpectedTransaction(); + }, + }, + { + title: "To", + button: SpeculosButton.RIGHT, + expectedValue: ({ transaction }) => { + const command = transaction.model.commandDescriptor?.command; + if (command?.kind === "token.transfer") { + return ellipsis(command.recipientDescriptor.tokenAccAddress); + } + throwUnexpectedTransaction(); + }, + }, + { + title: "Approve", + button: SpeculosButton.BOTH, + final: true, + }, + ], + }); + function throwUnexpectedTransaction(): never { throw new Error("unexpected or unprepared transaction"); } diff --git a/libs/ledger-live-common/src/families/solana/__snapshots__/bridge.integration.test.ts.snap b/libs/ledger-live-common/src/families/solana/__snapshots__/bridge.integration.test.ts.snap index d87b5fd826f3..c0a8430aad9b 100644 --- a/libs/ledger-live-common/src/families/solana/__snapshots__/bridge.integration.test.ts.snap +++ b/libs/ledger-live-common/src/families/solana/__snapshots__/bridge.integration.test.ts.snap @@ -18,10 +18,24 @@ exports[`solana currency bridge scanAccounts solana seed 1 1`] = ` "unstakeReserve": "0", }, "spendableBalance": "82498960", + "subAccounts": [], "swapHistory": [], "syncHash": undefined, "used": true, }, + { + "approvals": undefined, + "balance": "7960720", + "id": "js:2:solana:AQbkEagmPgmsdAfS4X8V8UyJnXXjVPMvjeD15etqQ3Jh:solanaMain+8RtwWeqdFz4EFuZU3MAadfYMWSdRMamjFrfq6BXkHuNN", + "operationsCount": 1, + "parentId": "js:2:solana:AQbkEagmPgmsdAfS4X8V8UyJnXXjVPMvjeD15etqQ3Jh:solanaMain", + "pendingOperations": [], + "spendableBalance": "7960720", + "state": "initialized", + "swapHistory": [], + "tokenId": "solana/spl/So11111111111111111111111111111111111111112", + "type": "TokenAccountRaw", + }, { "balance": "0", "currencyId": "solana", @@ -38,6 +52,7 @@ exports[`solana currency bridge scanAccounts solana seed 1 1`] = ` "unstakeReserve": "0", }, "spendableBalance": "0", + "subAccounts": [], "swapHistory": [], "syncHash": undefined, "used": false, @@ -102,6 +117,28 @@ exports[`solana currency bridge scanAccounts solana seed 1 2`] = ` "value": "0", }, ], + [ + { + "accountId": "js:2:solana:AQbkEagmPgmsdAfS4X8V8UyJnXXjVPMvjeD15etqQ3Jh:solanaMain+8RtwWeqdFz4EFuZU3MAadfYMWSdRMamjFrfq6BXkHuNN", + "blockHash": "9tPbgLaETEenufCt5SzXMuWijgFJj549W9j5cJLbaogn", + "blockHeight": 108521109, + "contract": undefined, + "extra": {}, + "fee": "5000", + "hasFailed": false, + "hash": "A29zPnK1jPr2tGziTnaAvSnadYR2kLCv9sPywj9FJsaEFjtpwmUonspN3WJgz4u6XWmjtVpoFsDrygEnvW51cgk", + "id": "js:2:solana:AQbkEagmPgmsdAfS4X8V8UyJnXXjVPMvjeD15etqQ3Jh:solanaMain+8RtwWeqdFz4EFuZU3MAadfYMWSdRMamjFrfq6BXkHuNN-A29zPnK1jPr2tGziTnaAvSnadYR2kLCv9sPywj9FJsaEFjtpwmUonspN3WJgz4u6XWmjtVpoFsDrygEnvW51cgk-IN", + "operator": undefined, + "recipients": [ + "8RtwWeqdFz4EFuZU3MAadfYMWSdRMamjFrfq6BXkHuNN", + ], + "senders": [], + "standard": undefined, + "tokenId": undefined, + "type": "IN", + "value": "7960720", + }, + ], [], ] `; From c66f576f06b26ce6ef47b8765a121586f74ec3d5 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Mon, 20 May 2024 12:51:46 +0300 Subject: [PATCH 06/35] feat: display solana tokens in LLD --- .../solana/AccountHeaderManageActions.ts | 12 ++++++-- .../families/solana/AccountSubHeader.tsx | 29 +++++++++++++++++-- .../static/i18n/en/app.json | 12 ++++++++ 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/apps/ledger-live-desktop/src/renderer/families/solana/AccountHeaderManageActions.ts b/apps/ledger-live-desktop/src/renderer/families/solana/AccountHeaderManageActions.ts index 248fa4eb2483..4c67c9f92fe5 100644 --- a/apps/ledger-live-desktop/src/renderer/families/solana/AccountHeaderManageActions.ts +++ b/apps/ledger-live-desktop/src/renderer/families/solana/AccountHeaderManageActions.ts @@ -6,10 +6,14 @@ import IconCoins from "~/renderer/icons/Coins"; import { SolanaFamily } from "./types"; import { useGetStakeLabelLocaleBased } from "~/renderer/hooks/useGetStakeLabelLocaleBased"; -const AccountHeaderActions: SolanaFamily["accountHeaderManageActions"] = ({ account, source }) => { +const AccountHeaderActions: SolanaFamily["accountHeaderManageActions"] = ({ + account, + parentAccount, + source, +}) => { const dispatch = useDispatch(); const label = useGetStakeLabelLocaleBased(); - const mainAccount = getMainAccount(account); + const mainAccount = getMainAccount(account, parentAccount); const { solanaResources } = mainAccount; const onClick = useCallback(() => { @@ -34,6 +38,10 @@ const AccountHeaderActions: SolanaFamily["accountHeaderManageActions"] = ({ acco } }, [account, dispatch, source, solanaResources, mainAccount]); + if (account.type === "TokenAccount") { + return null; + } + return [ { key: "Stake", diff --git a/apps/ledger-live-desktop/src/renderer/families/solana/AccountSubHeader.tsx b/apps/ledger-live-desktop/src/renderer/families/solana/AccountSubHeader.tsx index 2b0fb8935f0a..9fad5942d429 100644 --- a/apps/ledger-live-desktop/src/renderer/families/solana/AccountSubHeader.tsx +++ b/apps/ledger-live-desktop/src/renderer/families/solana/AccountSubHeader.tsx @@ -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 ; + +type Account = SolanaAccount | SolanaTokenAccount | SubAccount; + +type Props = { + account: Account; +}; + +export default function SolanaAccountSubHeader({ account }: Props) { + return ( + <> + {isTokenAccountFrozen(account) && ( + + + + + + )} + + + ); } diff --git a/apps/ledger-live-desktop/static/i18n/en/app.json b/apps/ledger-live-desktop/static/i18n/en/app.json index d39dbaf62518..5c765e9e6d75 100644 --- a/apps/ledger-live-desktop/static/i18n/en/app.json +++ b/apps/ledger-live-desktop/static/i18n/en/app.json @@ -3743,6 +3743,9 @@ } } } + }, + "token": { + "frozenStateWarning": "Account assets are frozen!" } }, "ethereum": { @@ -6205,6 +6208,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}}" }, From d7b34c4232f54b8e979b78e752b8645235a37c51 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Mon, 20 May 2024 12:52:00 +0300 Subject: [PATCH 07/35] feat: display solana tokens in LLM --- .../src/families/solana/AccountSubHeader.tsx | 28 +++++++++++++++++-- .../src/families/solana/SendRowsFee.tsx | 4 +-- .../src/locales/en/common.json | 9 ++++++ .../screens/Account/ListHeaderComponent.tsx | 2 +- 4 files changed, 38 insertions(+), 5 deletions(-) diff --git a/apps/ledger-live-mobile/src/families/solana/AccountSubHeader.tsx b/apps/ledger-live-mobile/src/families/solana/AccountSubHeader.tsx index 54fe755006cc..8cc760b845ff 100644 --- a/apps/ledger-live-mobile/src/families/solana/AccountSubHeader.tsx +++ b/apps/ledger-live-mobile/src/families/solana/AccountSubHeader.tsx @@ -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 ; +type Account = SolanaAccount | SolanaTokenAccount | SubAccount; + +type Props = { + account: Account; +}; + +function SolanaAccountSubHeader({ account }: Props) { + return ( + <> + {isTokenAccountFrozen(account) && ( + + + + + + + + )} + + + ); } export default SolanaAccountSubHeader; diff --git a/apps/ledger-live-mobile/src/families/solana/SendRowsFee.tsx b/apps/ledger-live-mobile/src/families/solana/SendRowsFee.tsx index 9f4c1761c474..64ad6e73fa71 100644 --- a/apps/ledger-live-mobile/src/families/solana/SendRowsFee.tsx +++ b/apps/ledger-live-mobile/src/families/solana/SendRowsFee.tsx @@ -32,7 +32,7 @@ type Props = { StackNavigatorProps >; -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); @@ -40,7 +40,7 @@ export default function SolanaFeeRow({ account, status }: Props) { const fees = (status as SolanaTransactionStatus).estimatedFees; - const unit = useAccountUnit(account); + const unit = useAccountUnit(account.type === "TokenAccount" ? parentAccount || account : account); const currency = getAccountCurrency(account); return ( diff --git a/apps/ledger-live-mobile/src/locales/en/common.json b/apps/ledger-live-mobile/src/locales/en/common.json index 2df369abfc19..36ffbdbeed2e 100644 --- a/apps/ledger-live-mobile/src/locales/en/common.json +++ b/apps/ledger-live-mobile/src/locales/en/common.json @@ -790,6 +790,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" }, @@ -5968,6 +5974,9 @@ "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!" } }, "near": { diff --git a/apps/ledger-live-mobile/src/screens/Account/ListHeaderComponent.tsx b/apps/ledger-live-mobile/src/screens/Account/ListHeaderComponent.tsx index f7af804b39ce..ea4333164cf6 100644 --- a/apps/ledger-live-mobile/src/screens/Account/ListHeaderComponent.tsx +++ b/apps/ledger-live-mobile/src/screens/Account/ListHeaderComponent.tsx @@ -161,7 +161,7 @@ export function getListHeaderComponents({
, !!AccountSubHeader && ( - + ), oldestEditableOperation ? ( From e4a260b58c6e7e33ad53e1c081ceaf72151d185a Mon Sep 17 00:00:00 2001 From: Mikhail Date: Mon, 20 May 2024 12:53:50 +0300 Subject: [PATCH 08/35] chore(LLC): add token account serialization test --- .../src/account/serialization.test.ts | 26 +++++ .../sortByMarketcap.test.ts.snap | 94 +++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 libs/ledger-live-common/src/account/serialization.test.ts diff --git a/libs/ledger-live-common/src/account/serialization.test.ts b/libs/ledger-live-common/src/account/serialization.test.ts new file mode 100644 index 000000000000..0aa979de1482 --- /dev/null +++ b/libs/ledger-live-common/src/account/serialization.test.ts @@ -0,0 +1,26 @@ +import { getCryptoCurrencyById, getTokenById, setSupportedCurrencies } from "../currencies"; +import { genAccount, genTokenAccount } from "@ledgerhq/coin-framework/mocks/account"; +import { toAccountRaw, fromAccountRaw } from "./serialization"; +import { setWalletAPIVersion } from "../wallet-api/version"; +import { WALLET_API_VERSION } from "../wallet-api/constants"; + +setWalletAPIVersion(WALLET_API_VERSION); + +setSupportedCurrencies(["solana"]); +const Solana = getCryptoCurrencyById("solana"); +const USDC = getTokenById("solana/spl/EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); + +describe("serialization", () => { + test("TokenAccount extra fields should be serialized/deserialized", () => { + const acc: any = genAccount("mocked-account-1", { currency: Solana }); + const tokenAcc: any = genTokenAccount(1, acc, USDC); + tokenAcc.state = "initialized"; + acc.subAccounts = [tokenAcc]; + + const accRaw: any = toAccountRaw(acc); + expect(accRaw.subAccounts?.[0]?.state).toBe("initialized"); + + const deserializedAcc: any = fromAccountRaw(accRaw); + expect(deserializedAcc.subAccounts?.[0]?.state).toBe("initialized"); + }); +}); diff --git a/libs/ledger-live-common/src/currencies/__snapshots__/sortByMarketcap.test.ts.snap b/libs/ledger-live-common/src/currencies/__snapshots__/sortByMarketcap.test.ts.snap index 7794bdd69955..492cdb4a1f67 100644 --- a/libs/ledger-live-common/src/currencies/__snapshots__/sortByMarketcap.test.ts.snap +++ b/libs/ledger-live-common/src/currencies/__snapshots__/sortByMarketcap.test.ts.snap @@ -923,6 +923,7 @@ exports[`sortCurrenciesByIds snapshot 1`] = ` "ethereum/erc20/ksm_starter_token", "ethereum/erc20/aditus", "ethereum/erc20/ethopt_io", + "solana/spl/SHDWyBxihqiCj6YekG2GUr7wqKLeLAMK1gHZck9pL6y", "ethereum/erc20/bigboom", "ethereum/erc20/realchain", "ethereum/erc20/ink_protocol", @@ -16235,5 +16236,98 @@ exports[`sortCurrenciesByIds snapshot 1`] = ` "filecoin/erc20/pfil_token", "filecoin/erc20/wrapped_fil", "filecoin/erc20/wrapped_pfil_token", + "solana/spl/2VhjJ9WxaGC3EZFwJG9BDUs9KxKCAjQY4vgd1qxgYWVg", + "solana/spl/35r2jMGKytAJ7FyKfKRHPanT8kpjg3emPy7WG6GANCNB", + "solana/spl/3bRTivrVsitbmCTGtqwp7hxXPsybkjn4XLNtPsHqa3zR", + "solana/spl/3dgCCb15HMQSA4Pn3Tfii5vRk7aRqTH95LJjxzsG2Mug", + "solana/spl/4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R", + "solana/spl/4vMsoUT2BWatFweudnQM1xedRLfJgJ7hswhcpz4xgBTy", + "solana/spl/5MAYDfq5yxtudAhtfyuMBuHZjgAbaS9tbEyEQYAhDS5y", + "solana/spl/5oVNBeEEQvYi1cX3ir8Dx5n1P7pdxydbGF2X4TxVusJm", + "solana/spl/5tB5D6DGJMxxHYmNkfJNG237x6pZGEwTzGpUUh62yQJ7", + "solana/spl/6cVgJUqo4nmvQpbgrDZwyfd6RwWw5bfnCamS3M9N1fd", + "solana/spl/6DNSN2BJsaPFdFFc1zP37kkeNe4Usc1Sqkzr9C9vPWcU", + "solana/spl/6LX8BhMQ4Sy2otmAWj7Y5sKd9YTVVUgfMsBzT6B9W7ct", + "solana/spl/6VNKqgz9hk7zRShTFdg5AnkfKwZUcojzwAkzxSH3bnUm", + "solana/spl/7dHbWXmci3dT8UFYWYZweBLXgycu7Y3iL6trKn1Y7ARj", + "solana/spl/7i5KKsX2weiTkry7jA4ZwSuXGhs5eJBEjY8vVxR4pfRx", + "solana/spl/7kbnvuGBxxj8AG9qp8Scn56muWGaRaFqxg1FsRp3PaFT", + "solana/spl/7Q2afV64in6N6SeZsAAB81TJzwDoD6zpqmHkzi9Dcavn", + "solana/spl/7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU", + "solana/spl/9ET2QCQJdFkeKkuaampNbmicbA8eLYauFCWch9Ddh9p5", + "solana/spl/9mWRABuz2x6koTPCWiCPM49WUbcrNqGTHBV9T9k7y1o7", + "solana/spl/9nEqaUcb16sQ3Tn1psbkWqyhPdLmfHWjKGymREjsAgTE", + "solana/spl/a11bdAAuV8iB2fu7X6AxAvDTo1QZ8FXB3kk5eecdasp", + "solana/spl/A94X2fRy3wydNShU4dRaDyap2UuoeWJGWyATtyp61WZf", + "solana/spl/AFbX8oGjGpmVFywbVouvhQSRmiW2aR1mohfahi4Y2AdB", + "solana/spl/ATLASXmbPQxBUYbxPsV97usA3fPQYEqzQBUHgiFCUsXx", + "solana/spl/AURYydfxJib1ZkTir1Jn1J9ECYUtjb6rKQVmtYaixWPP", + "solana/spl/AZsHEMXd36Bj1EMNXhowJajpUXzrKcK57wW4ZGXVa7yR", + "solana/spl/BgwQjVNMWvt2d8CN51CsbniwRWyZ9H9HfHkEsvikeVuZ", + "solana/spl/BiDB55p4G3n1fGhwKFpxsokBMqgctL4qnZpDH1bVQxMD", + "solana/spl/BKipkearSqAUdNKa1WDstvcMjoPsSKBuNyvKDQDDu9WE", + "solana/spl/BLT1noyNr3GttckEVrtcfC6oyK6yV1DpPgSyXbncMwef", + "solana/spl/bSo13r4TkiE4KumL71LsHTPpL2euBYLFx6h9HP3piy1", + "solana/spl/C98A4nkJXhpVZNAZdHUA95RpTF3T4whtQubL3YobiUX9", + "solana/spl/ChVzxWRmrTeSgwd3Ui3UumcN8KX7VK3WaD4KGeSKpypj", + "solana/spl/CKaKtYvz6dKPyMvYq9Rh3UBrnNqYZAyd7iF4hJtjUvks", + "solana/spl/CRWNYkqdgvhGGae9CKfNka58j6QQkaD5bLhKXvUYqnc1", + "solana/spl/CWBzupvyXN1Cf5rsBEHbzfTFvreLfUaJ77BMNLVJ739y", + "solana/spl/DAtU322C23YpoZyWBm8szk12QyqHa9rUQe1EYXzbm1JE", + "solana/spl/DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263", + "solana/spl/DFL1zNkaGPWm1BqAVqRjCZvHmwTFrEaJtbzJWgseoNJh", + "solana/spl/DkNihsQs1hqEwf9TgKP8FmGv7dmMQ7hnKjS2ZSmMZZBE", + "solana/spl/DUSTawucrTsGU8hcqRdHDCbuYhCPADMLM2VcCb8VnFnQ", + "solana/spl/E5rk3nmgLUuKUiS94gg4bpWwWwyjCMtddsAXkTFLtHEy", + "solana/spl/Ea5SjE2Y6yvCeW5dYTn7PYMuW5ikXkvbGdcmSnXeaLjS", + "solana/spl/EchesyfXePKdLtoiZSL8pBe8Myagyy8ZRqsACNCFGnvp", + "solana/spl/EcQCUYv57C4V6RoPxkVUiDwtX1SP8y8FP5AEToYL8Az", + "solana/spl/EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "solana/spl/Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", + "solana/spl/ETAtLmCmsoiEEKfNrHKJ2kYy3MoABhU6NQvpSfij5tDs", + "solana/spl/F3nefJBcejYbtdREjui1T9DPh5dBgpkKq7u2GAAMXs5B", + "solana/spl/Fm9rHUTF5v3hwMLbStjZXqNBBoZyGriQaFM6sTFz3K8A", + "solana/spl/FR87nWEUxVgerFGhZM8Y4AggKGLnaXswr1Pd8wZ4kZcp", + "solana/spl/GDfnEsia2WLAW5t8yx2X5j2mkfA74i5kwGdDuZHt7XmG", + "solana/spl/GDsVXtyt2CBwieKSYMEsjjZXXvqz2G2VwudD7EvXzoEU", + "solana/spl/GENEtH5amGSi8kHAtQoezp1XEXwZJ8vcuePYnXdKrMYz", + "solana/spl/GFX1ZjR2P15tmrSwow6FjyDYcEkoFb4p4gJCpLBjaxHD", + "solana/spl/GsNzxJfFn6zQdJGeYsupJWzUAm57Ba7335mfhWvFiE9Z", + "solana/spl/HBB111SCo9jkCejsZfz8Ec8nH7T6THF8KEKSnvwT6XK6", + "solana/spl/HHjoYwUp5aU6pnrvN4s2pwEErwXNZKhxKGYjRJMoBjLw", + "solana/spl/HhJpBhRRn4g56VsyLuT8DL5Bv31HkXqsrahTTUCZeZg4", + "solana/spl/hntyVP6YFm1Hg25TN9WGLqM12b8TQmcknKrdu1oxWux", + "solana/spl/HxhWkVpk5NS4Ltg5nij2G671CKXFRKPK8vy271Ub4uEK", + "solana/spl/HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3", + "solana/spl/J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn", + "solana/spl/jtojtomepa8beP8AuQc6eXt5FriJwfFMwQx2v2f9mCL", + "solana/spl/kinXdEcpDQeHPEuQnqmUgtYykqKGVFq6CeVX5iAHJq6", + "solana/spl/MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac", + "solana/spl/MAPS41MDahZ9QdKXhVa4dWB9RuyfV4XqhyAZ8XcYepb", + "solana/spl/MEANeD3XDdUmNMsRGjASkSWdC8prLYsoRJ61pPeHctD", + "solana/spl/METAewgxyPbgwsseH8T16a39CQ5VyVxZi9zXiDPY18m", + "solana/spl/MNDEFzGvMt87ueuHvVU9VcTqsAP5b3fTGPsHuuPA5ey", + "solana/spl/mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So", + "solana/spl/NFTUkR4u7wKxy9QLaX2TGvd9oZSWoMo4jqSJqdMb7Nk", + "solana/spl/orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE", + "solana/spl/poLisWXnNRwC6oBu1vHiuKQzFjGL4XDSu4g9qjz9qVk", + "solana/spl/PoRTjZMPXb9T7dyU7tpLEZRQj7e6ssfAE62j2oQuc6y", + "solana/spl/PRSMNsEPqhGVCH1TtWiJqPjJyh2cKrLostPZTNy1o5x", + "solana/spl/PsyFiqqjiv41G7o5SMRzDJCu4psptThNR2GtfeGHfSq", + "solana/spl/RLBxxFkseAZ4RgJH3Sqn8jXxhmGoz9jWxDNJMh8pL7a", + "solana/spl/rndrizKT3MK1iimdxRdWabcF7Zg7AR5T4nud4EkHBof", + "solana/spl/Saber2gLauYim4Mvftnrasomsv6NvAuncvMEZwcLpD1", + "solana/spl/SCSuPPNUSypLBsV4darsrYNg4ANPgaGhKhsA3GmMyjz", + "solana/spl/SLCLww7nc1PD2gQPQdGayHviVVcpMthnqUz2iWKhNQV", + "solana/spl/SLNDpmoWTVADgEdndyvWzroNL7zSi1dF9PC3xHGtPwp", + "solana/spl/So11111111111111111111111111111111111111112", + "solana/spl/SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt", + "solana/spl/StepAscQoEioFxxWGnh2sLBDFp9d8rvKz2Yp39iDpyT", + "solana/spl/Taki7fi3Zicv7Du1xNAWLaf6mRK7ikdn77HeGzgwvo4", + "solana/spl/TuLipcqtGVXP9XR62wM8WWCm6a9vhLs7T1uoWBk6FDs", + "solana/spl/UXPhBoR3qG4UCiGNJfV7MqhHyFqKN68g45GoYvAeL2M", + "solana/spl/xxxxa1sKNGwFtw2kFn8XauW9xq8hBZ5kVtcSesTT9fW", + "solana/spl/z3dn17yLaGMKffVogeFHQ9zWVcXgqgf3PQnDsNs2g6M", + "solana/spl/zebeczgi5fSEtbpfQKVZKCJ3WgYXxjkMUkNNx7fLKAF", ] `; From 725f327b3c5f1af0508e66677de9bbef38158808 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Mon, 20 May 2024 12:54:08 +0300 Subject: [PATCH 09/35] chore: add changeset --- .changeset/honest-dingos-unite.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .changeset/honest-dingos-unite.md diff --git a/.changeset/honest-dingos-unite.md b/.changeset/honest-dingos-unite.md new file mode 100644 index 000000000000..ee797fa22f83 --- /dev/null +++ b/.changeset/honest-dingos-unite.md @@ -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 From 423374f19ca2458b30f76fb23f257f7786424a1b Mon Sep 17 00:00:00 2001 From: Mikhail Date: Tue, 20 Aug 2024 14:51:45 +0300 Subject: [PATCH 10/35] fix(LLD, LLM): token summary and device screens --- apps/ledger-live-desktop/src/config/urls.ts | 2 + .../solana/TransactionConfirmFields.tsx | 67 +++++++++++++++++++ .../src/renderer/families/solana/index.ts | 2 + .../static/i18n/en/app.json | 3 +- .../solana/TransactionConfirmFields.tsx | 46 +++++++++++++ .../src/locales/en/common.json | 3 +- apps/ledger-live-mobile/src/utils/urls.tsx | 1 + .../src/deviceTransactionConfig.ts | 44 ++---------- 8 files changed, 128 insertions(+), 40 deletions(-) create mode 100644 apps/ledger-live-desktop/src/renderer/families/solana/TransactionConfirmFields.tsx create mode 100644 apps/ledger-live-mobile/src/families/solana/TransactionConfirmFields.tsx diff --git a/apps/ledger-live-desktop/src/config/urls.ts b/apps/ledger-live-desktop/src/config/urls.ts index 62e6aa6607ce..3209f82317ef 100644 --- a/apps/ledger-live-desktop/src/config/urls.ts +++ b/apps/ledger-live-desktop/src/config/urls.ts @@ -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", }; const errors: Record = { @@ -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", recipient_info: "https://support.ledger.com", ledgerByChorusOneTC: "https://chorus.one/tos", ledgerByFigmentTC: diff --git a/apps/ledger-live-desktop/src/renderer/families/solana/TransactionConfirmFields.tsx b/apps/ledger-live-desktop/src/renderer/families/solana/TransactionConfirmFields.tsx new file mode 100644 index 000000000000..877c580773c9 --- /dev/null +++ b/apps/ledger-live-desktop/src/renderer/families/solana/TransactionConfirmFields.tsx @@ -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 ( + + + + + openURL(transferTokenHelpUrl)} + /> + + + + ); + } + + return ; +}; + +type TransactionConfirmFields = SolanaFamily["transactionConfirmFields"]; +type TitleComponent = NonNullable["title"]>; + +const transactionConfirmFields: TransactionConfirmFields = { + // footer: Footer, // is not shown without manifestId + // fieldComponents, + title: Title, +}; + +export default transactionConfirmFields; diff --git a/apps/ledger-live-desktop/src/renderer/families/solana/index.ts b/apps/ledger-live-desktop/src/renderer/families/solana/index.ts index da1314ef035f..3396136c9ec2 100644 --- a/apps/ledger-live-desktop/src/renderer/families/solana/index.ts +++ b/apps/ledger-live-desktop/src/renderer/families/solana/index.ts @@ -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, @@ -15,6 +16,7 @@ const family: SolanaFamily = { AccountBalanceSummaryFooter, StakeBanner, operationDetails, + transactionConfirmFields, }; export default family; diff --git a/apps/ledger-live-desktop/static/i18n/en/app.json b/apps/ledger-live-desktop/static/i18n/en/app.json index 5c765e9e6d75..e278d3a5520a 100644 --- a/apps/ledger-live-desktop/static/i18n/en/app.json +++ b/apps/ledger-live-desktop/static/i18n/en/app.json @@ -3745,7 +3745,8 @@ } }, "token": { - "frozenStateWarning": "Account assets are frozen!" + "frozenStateWarning": "Account assets are frozen!", + "transferWarning": "Solana SPL tokens transactions have unique characteristics. To learn more, visit: <0>ledger.com/spl" } }, "ethereum": { diff --git a/apps/ledger-live-mobile/src/families/solana/TransactionConfirmFields.tsx b/apps/ledger-live-mobile/src/families/solana/TransactionConfirmFields.tsx new file mode 100644 index 000000000000..4bf31f76ebfd --- /dev/null +++ b/apps/ledger-live-mobile/src/families/solana/TransactionConfirmFields.tsx @@ -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 ( + + + + + Linking.openURL(urls.solana.splTokenInfo)} type="color" /> + + + + + ); + } + return null; +}; + +export default { + warning: Warning, + fieldComponents: {}, +}; diff --git a/apps/ledger-live-mobile/src/locales/en/common.json b/apps/ledger-live-mobile/src/locales/en/common.json index 36ffbdbeed2e..8147f6a48d2d 100644 --- a/apps/ledger-live-mobile/src/locales/en/common.json +++ b/apps/ledger-live-mobile/src/locales/en/common.json @@ -5976,7 +5976,8 @@ "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!" + "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" } }, "near": { diff --git a/apps/ledger-live-mobile/src/utils/urls.tsx b/apps/ledger-live-mobile/src/utils/urls.tsx index a3a6777ccbba..c1e947fbeee2 100644 --- a/apps/ledger-live-mobile/src/utils/urls.tsx +++ b/apps/ledger-live-mobile/src/utils/urls.tsx @@ -168,6 +168,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: diff --git a/libs/coin-modules/coin-solana/src/deviceTransactionConfig.ts b/libs/coin-modules/coin-solana/src/deviceTransactionConfig.ts index 965a1997afe0..93500d8d4987 100644 --- a/libs/coin-modules/coin-solana/src/deviceTransactionConfig.ts +++ b/libs/coin-modules/coin-solana/src/deviceTransactionConfig.ts @@ -70,55 +70,23 @@ function fieldsForTransfer(_command: TransferCommand): DeviceTransactionField[] return fields; } -function fieldsForTokenTransfer(command: TokenTransferCommand): DeviceTransactionField[] { +function fieldsForTokenTransfer(_command: TokenTransferCommand): DeviceTransactionField[] { const fields: Array = []; - if (command.recipientDescriptor.shouldCreateAsAssociatedTokenAccount) { - fields.push({ - type: "address", - label: "Create token acct", - address: command.recipientDescriptor.tokenAccAddress, - }); - - fields.push({ - type: "address", - label: "From mint", - address: command.mintAddress, - }); - fields.push({ - type: "address", - label: "Funded by", - address: command.ownerAddress, - }); - } - fields.push({ type: "amount", label: "Transfer tokens", }); fields.push({ - type: "address", - address: command.mintAddress, - label: "Mint", - }); - - fields.push({ - type: "address", - address: command.ownerAssociatedTokenAccountAddress, - label: "From", - }); - - fields.push({ - type: "address", - address: command.ownerAddress, - label: "Owner", + type: "text", + value: "Solana", + label: "Network", }); fields.push({ - type: "address", - address: command.ownerAddress, - label: "Fee payer", + type: "fees", + label: "Max network fees", }); return fields; From 8fcdcec74c58682e50ac27508044ea5aac3c35a9 Mon Sep 17 00:00:00 2001 From: Kant Date: Wed, 16 Oct 2024 17:58:24 +0200 Subject: [PATCH 11/35] fix: non existing param used for SPL Tokens --- libs/ledgerjs/packages/cryptoassets/src/tokens.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/libs/ledgerjs/packages/cryptoassets/src/tokens.ts b/libs/ledgerjs/packages/cryptoassets/src/tokens.ts index cac0d4f98e63..58c9b9176bf3 100644 --- a/libs/ledgerjs/packages/cryptoassets/src/tokens.ts +++ b/libs/ledgerjs/packages/cryptoassets/src/tokens.ts @@ -406,14 +406,7 @@ function convertElrondESDTTokens([ }; } -function convertSplTokens([ - chainId, - name, - symbol, - address, - decimals, - enableCountervalues, -]: SPLToken): TokenCurrency { +function convertSplTokens([chainId, name, symbol, address, decimals]: SPLToken): TokenCurrency { const chainIdToCurrencyId = { 101: "solana", 102: "solana_testnet", @@ -428,7 +421,7 @@ function convertSplTokens([ name, tokenType: "spl", ticker: symbol, - disableCountervalue: !enableCountervalues, + disableCountervalue: false, units: [ { name, From 8629ad133317554d5058816bf516b7b2c7a59078 Mon Sep 17 00:00:00 2001 From: Kant Date: Mon, 4 Nov 2024 16:33:08 +0100 Subject: [PATCH 12/35] fix: wrong chainId for spl tokens Used the import:cal-tokens to update only spl data --- .../sortByMarketcap.test.ts.snap | 99 +++++++++++-------- 1 file changed, 58 insertions(+), 41 deletions(-) diff --git a/libs/ledger-live-common/src/currencies/__snapshots__/sortByMarketcap.test.ts.snap b/libs/ledger-live-common/src/currencies/__snapshots__/sortByMarketcap.test.ts.snap index 492cdb4a1f67..fba53ae3f29c 100644 --- a/libs/ledger-live-common/src/currencies/__snapshots__/sortByMarketcap.test.ts.snap +++ b/libs/ledger-live-common/src/currencies/__snapshots__/sortByMarketcap.test.ts.snap @@ -16236,98 +16236,115 @@ exports[`sortCurrenciesByIds snapshot 1`] = ` "filecoin/erc20/pfil_token", "filecoin/erc20/wrapped_fil", "filecoin/erc20/wrapped_pfil_token", - "solana/spl/2VhjJ9WxaGC3EZFwJG9BDUs9KxKCAjQY4vgd1qxgYWVg", - "solana/spl/35r2jMGKytAJ7FyKfKRHPanT8kpjg3emPy7WG6GANCNB", + "solana/spl/27G8MtK7VtTcCHkpASjSDdkWWYfoqT6ggEuKidVJidD4", + "solana/spl/2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk", + "solana/spl/31k88G5Mq7ptbRDf3AM13HAq6wRQHXHikR8hik7wPygk", "solana/spl/3bRTivrVsitbmCTGtqwp7hxXPsybkjn4XLNtPsHqa3zR", "solana/spl/3dgCCb15HMQSA4Pn3Tfii5vRk7aRqTH95LJjxzsG2Mug", + "solana/spl/3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh", + "solana/spl/3psH1Mj1f7yUfaD5gh6Zj7epE8hhrMkMETgv5TshQA4o", "solana/spl/4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R", + "solana/spl/4LLbsb5ReP3yEtYzmXewyGjcir5uXtKFURtaEUVC2AHs", "solana/spl/4vMsoUT2BWatFweudnQM1xedRLfJgJ7hswhcpz4xgBTy", "solana/spl/5MAYDfq5yxtudAhtfyuMBuHZjgAbaS9tbEyEQYAhDS5y", "solana/spl/5oVNBeEEQvYi1cX3ir8Dx5n1P7pdxydbGF2X4TxVusJm", - "solana/spl/5tB5D6DGJMxxHYmNkfJNG237x6pZGEwTzGpUUh62yQJ7", - "solana/spl/6cVgJUqo4nmvQpbgrDZwyfd6RwWw5bfnCamS3M9N1fd", - "solana/spl/6DNSN2BJsaPFdFFc1zP37kkeNe4Usc1Sqkzr9C9vPWcU", - "solana/spl/6LX8BhMQ4Sy2otmAWj7Y5sKd9YTVVUgfMsBzT6B9W7ct", - "solana/spl/6VNKqgz9hk7zRShTFdg5AnkfKwZUcojzwAkzxSH3bnUm", + "solana/spl/6dKCoWjpj5MFU5gWDEFdpUUeBasBLK3wLEwhUzQPAa1e", + "solana/spl/6gnCPhXtLnUD76HjQuSYPENLSZdG8RvDB1pTLM5aLSJA", + "solana/spl/7atgF8KQo4wJrD5ATGX7t1V2zVvykPJbFfNeVf1icFv1", "solana/spl/7dHbWXmci3dT8UFYWYZweBLXgycu7Y3iL6trKn1Y7ARj", + "solana/spl/7GCihgDB8fe6KNjn2MYtkzZcRjQy3t9GHdC8uHYmW2hr", "solana/spl/7i5KKsX2weiTkry7jA4ZwSuXGhs5eJBEjY8vVxR4pfRx", - "solana/spl/7kbnvuGBxxj8AG9qp8Scn56muWGaRaFqxg1FsRp3PaFT", "solana/spl/7Q2afV64in6N6SeZsAAB81TJzwDoD6zpqmHkzi9Dcavn", + "solana/spl/7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs", "solana/spl/7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU", - "solana/spl/9ET2QCQJdFkeKkuaampNbmicbA8eLYauFCWch9Ddh9p5", - "solana/spl/9mWRABuz2x6koTPCWiCPM49WUbcrNqGTHBV9T9k7y1o7", - "solana/spl/9nEqaUcb16sQ3Tn1psbkWqyhPdLmfHWjKGymREjsAgTE", + "solana/spl/85VBFQZC9TZkfaptBWjvUw7YbZjy52A6mjtPGjstQAmQ", + "solana/spl/947tEoG318GUmyjVYhraNRvWpMX7fpBTDQFBoJvSkSG3", "solana/spl/a11bdAAuV8iB2fu7X6AxAvDTo1QZ8FXB3kk5eecdasp", - "solana/spl/A94X2fRy3wydNShU4dRaDyap2UuoeWJGWyATtyp61WZf", + "solana/spl/A1KLoBrKBde8Ty9qtNQUtq3C2ortoC3u7twggz7sEto6", "solana/spl/AFbX8oGjGpmVFywbVouvhQSRmiW2aR1mohfahi4Y2AdB", + "solana/spl/AMUwxPsqWSd1fbCGzWsrRKDcNoduuWMkdR38qPdit8G8", + "solana/spl/7iT1GRYYhEop2nV1dyCwK2MGyLmPHq47WhPGSwiqcUg5", + "solana/spl/AT79ReYU9XtHUTF5vM6Q4oa9K8w7918Fp5SU7G1MDMQY", "solana/spl/ATLASXmbPQxBUYbxPsV97usA3fPQYEqzQBUHgiFCUsXx", + "solana/spl/ATRLuHph8dxnPny4WSNW7fxkhbeivBrtWbY6BfB4xpLj", "solana/spl/AURYydfxJib1ZkTir1Jn1J9ECYUtjb6rKQVmtYaixWPP", - "solana/spl/AZsHEMXd36Bj1EMNXhowJajpUXzrKcK57wW4ZGXVa7yR", - "solana/spl/BgwQjVNMWvt2d8CN51CsbniwRWyZ9H9HfHkEsvikeVuZ", + "solana/spl/AujTJJ7aMS8LDo3bFzoyXDwT3jBALUbu4VZhzZdTZLmG", + "solana/spl/AHW5N8iqZobTcBepkSJzZ61XtAuSzBDcpxtrLG6KUKPk", "solana/spl/BiDB55p4G3n1fGhwKFpxsokBMqgctL4qnZpDH1bVQxMD", - "solana/spl/BKipkearSqAUdNKa1WDstvcMjoPsSKBuNyvKDQDDu9WE", - "solana/spl/BLT1noyNr3GttckEVrtcfC6oyK6yV1DpPgSyXbncMwef", + "solana/spl/BLZEEuZUBVqFhj8adcCFPJvPVCiCyVmh3hkJMrU8KuJA", "solana/spl/bSo13r4TkiE4KumL71LsHTPpL2euBYLFx6h9HP3piy1", - "solana/spl/C98A4nkJXhpVZNAZdHUA95RpTF3T4whtQubL3YobiUX9", - "solana/spl/ChVzxWRmrTeSgwd3Ui3UumcN8KX7VK3WaD4KGeSKpypj", + "solana/spl/MEW1gQWJ3nEXg2qgERiKu7FAFj79PHvQVREQUzScPP5", "solana/spl/CKaKtYvz6dKPyMvYq9Rh3UBrnNqYZAyd7iF4hJtjUvks", - "solana/spl/CRWNYkqdgvhGGae9CKfNka58j6QQkaD5bLhKXvUYqnc1", - "solana/spl/CWBzupvyXN1Cf5rsBEHbzfTFvreLfUaJ77BMNLVJ739y", - "solana/spl/DAtU322C23YpoZyWBm8szk12QyqHa9rUQe1EYXzbm1JE", + "solana/spl/CvB1ztJvpYQPvdPBePtRzjL4aQidjydtUz61NWgcgQtP", + "solana/spl/4Cnk9EPnW5ixfLZatCPJjDB1PUtcRpVVgTQukm9epump", "solana/spl/DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263", "solana/spl/DFL1zNkaGPWm1BqAVqRjCZvHmwTFrEaJtbzJWgseoNJh", - "solana/spl/DkNihsQs1hqEwf9TgKP8FmGv7dmMQ7hnKjS2ZSmMZZBE", - "solana/spl/DUSTawucrTsGU8hcqRdHDCbuYhCPADMLM2VcCb8VnFnQ", - "solana/spl/E5rk3nmgLUuKUiS94gg4bpWwWwyjCMtddsAXkTFLtHEy", - "solana/spl/Ea5SjE2Y6yvCeW5dYTn7PYMuW5ikXkvbGdcmSnXeaLjS", "solana/spl/EchesyfXePKdLtoiZSL8pBe8Myagyy8ZRqsACNCFGnvp", - "solana/spl/EcQCUYv57C4V6RoPxkVUiDwtX1SP8y8FP5AEToYL8Az", + "solana/spl/EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm", "solana/spl/EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", "solana/spl/Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", "solana/spl/ETAtLmCmsoiEEKfNrHKJ2kYy3MoABhU6NQvpSfij5tDs", - "solana/spl/F3nefJBcejYbtdREjui1T9DPh5dBgpkKq7u2GAAMXs5B", - "solana/spl/Fm9rHUTF5v3hwMLbStjZXqNBBoZyGriQaFM6sTFz3K8A", - "solana/spl/FR87nWEUxVgerFGhZM8Y4AggKGLnaXswr1Pd8wZ4kZcp", + "solana/spl/FANoyuAQZx7AHCnxqsLeWq6te63F6zs6ENkbncCyYUZu", + "solana/spl/FLUXBmPhT3Fd1EDVFdg46YREqHBeNypn1h4EbnTzWERX", + "solana/spl/FoXyMu5xwXre7zEoSvzViRk3nGawHUp9kUh97y2NDhcq", + "solana/spl/FtgGSFADXBtroxq8VCausXRr2of47QBf5AS1NtZCu4GD", + "solana/spl/8wXtPeU6557ETkp9WHFY1n1EcU6NxDvbAggHGsMYiHsB", "solana/spl/GDfnEsia2WLAW5t8yx2X5j2mkfA74i5kwGdDuZHt7XmG", - "solana/spl/GDsVXtyt2CBwieKSYMEsjjZXXvqz2G2VwudD7EvXzoEU", "solana/spl/GENEtH5amGSi8kHAtQoezp1XEXwZJ8vcuePYnXdKrMYz", "solana/spl/GFX1ZjR2P15tmrSwow6FjyDYcEkoFb4p4gJCpLBjaxHD", - "solana/spl/GsNzxJfFn6zQdJGeYsupJWzUAm57Ba7335mfhWvFiE9Z", - "solana/spl/HBB111SCo9jkCejsZfz8Ec8nH7T6THF8KEKSnvwT6XK6", + "solana/spl/GTH3wG3NErjwcf7VGCoXEXkgXSHvYhx5gtATeeM5JAS1", + "solana/spl/H53UGEyBrB9easo9ego8yYk7o4Zq1G5cCtkxD3E3hZav", "solana/spl/HHjoYwUp5aU6pnrvN4s2pwEErwXNZKhxKGYjRJMoBjLw", "solana/spl/HhJpBhRRn4g56VsyLuT8DL5Bv31HkXqsrahTTUCZeZg4", "solana/spl/hntyVP6YFm1Hg25TN9WGLqM12b8TQmcknKrdu1oxWux", "solana/spl/HxhWkVpk5NS4Ltg5nij2G671CKXFRKPK8vy271Ub4uEK", "solana/spl/HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3", + "solana/spl/HzwqbKZw8HxMN6bF2yFZNrht3c2iXXzpKcFu7uBEDKtr", + "solana/spl/BZLbGTNCSFfoth2GYDtwr7e4imWzpR5jqcUuGEwr646K", + "solana/spl/iotEVVZLEywoTn1QdwNPddxPWszn3zFhEot3MfL9fns", "solana/spl/J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn", + "solana/spl/J2LWsSXx4r3pYbJ1fwuX5Nqo7PPxjcGPpUb2zHNadWKa", "solana/spl/jtojtomepa8beP8AuQc6eXt5FriJwfFMwQx2v2f9mCL", + "solana/spl/JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN", + "solana/spl/KMNo3nJsBXfcpJTVhZcXLW7RmTwTt4GVFE7suUBo9sS", "solana/spl/kinXdEcpDQeHPEuQnqmUgtYykqKGVFq6CeVX5iAHJq6", + "solana/spl/LAinEtNLgpmCP9Rvsf5Hn8W6EhNiKLZQti1xfWMLy6X", + "solana/spl/LFNTYraetVioAPnGJht4yNg2aUZFXR776cMeN9VMjXp", + "solana/spl/LSTxxxnJzKDFSLr4dUkPcmCf5VyryEqzPLz5j4bpxFp", "solana/spl/MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac", - "solana/spl/MAPS41MDahZ9QdKXhVa4dWB9RuyfV4XqhyAZ8XcYepb", - "solana/spl/MEANeD3XDdUmNMsRGjASkSWdC8prLYsoRJ61pPeHctD", + "solana/spl/mb1eu7TzEc71KxDpsmsKoucSSuuoGLv1drys1oP2jh6", "solana/spl/METAewgxyPbgwsseH8T16a39CQ5VyVxZi9zXiDPY18m", "solana/spl/MNDEFzGvMt87ueuHvVU9VcTqsAP5b3fTGPsHuuPA5ey", + "solana/spl/ED5nyyWEzpPPiWimP8vYm7sD7TD3LAt3Q3gRTWHzPJBY", "solana/spl/mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So", + "solana/spl/NeonTjSjsuo3rexg9o6vHuMXw62f9V7zvmu8M8Zut44", "solana/spl/NFTUkR4u7wKxy9QLaX2TGvd9oZSWoMo4jqSJqdMb7Nk", + "solana/spl/nosXBVoaCTtYdLvKY6Csb4AC8JCdQKKAaWYtx2ZMoo7", + "solana/spl/NYANpAp9Cr7YarBNrby7Xx4xU6No6JKTBuohNA3yscP", + "solana/spl/octo82drBEdm8CSDaEKBymVn86TBtgmPnDdmE64PTqJ", "solana/spl/orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE", "solana/spl/poLisWXnNRwC6oBu1vHiuKQzFjGL4XDSu4g9qjz9qVk", - "solana/spl/PoRTjZMPXb9T7dyU7tpLEZRQj7e6ssfAE62j2oQuc6y", - "solana/spl/PRSMNsEPqhGVCH1TtWiJqPjJyh2cKrLostPZTNy1o5x", - "solana/spl/PsyFiqqjiv41G7o5SMRzDJCu4psptThNR2GtfeGHfSq", + "solana/spl/WskzsKqEW3ZsmrhPAevfVZb6PuuLzWov9mJWZsfDePC", "solana/spl/RLBxxFkseAZ4RgJH3Sqn8jXxhmGoz9jWxDNJMh8pL7a", "solana/spl/rndrizKT3MK1iimdxRdWabcF7Zg7AR5T4nud4EkHBof", "solana/spl/Saber2gLauYim4Mvftnrasomsv6NvAuncvMEZwcLpD1", "solana/spl/SCSuPPNUSypLBsV4darsrYNg4ANPgaGhKhsA3GmMyjz", - "solana/spl/SLCLww7nc1PD2gQPQdGayHviVVcpMthnqUz2iWKhNQV", + "solana/spl/SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt", + "solana/spl/SHARKSYJjqaNyxVfrpnBN9pjgkhwDhatnMyicWPnr1s", + "solana/spl/7BgBvyjrZX1YKz4oh9mjb8ZScatkkwb8DzFx7LoiVkM3", "solana/spl/SLNDpmoWTVADgEdndyvWzroNL7zSi1dF9PC3xHGtPwp", + "solana/spl/SNSNkV9zfG5ZKWQs6x4hxvBRV6s8SqMfSGCtECDvdMd", "solana/spl/So11111111111111111111111111111111111111112", - "solana/spl/SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt", + "solana/spl/2wme8EVkw8qsfSk2B3QeX4S64ac6wxHPXb3GrdckEkio", "solana/spl/StepAscQoEioFxxWGnh2sLBDFp9d8rvKz2Yp39iDpyT", "solana/spl/Taki7fi3Zicv7Du1xNAWLaf6mRK7ikdn77HeGzgwvo4", - "solana/spl/TuLipcqtGVXP9XR62wM8WWCm6a9vhLs7T1uoWBk6FDs", + "solana/spl/TNSRxcUxoT9xBG3de7PiJyTDYu7kskLqcpddxnEJAS6", + "solana/spl/ukHH6c7mMyiWCf1b9pnWe25TSpkDDt3H5pQZgZ74J82", "solana/spl/UXPhBoR3qG4UCiGNJfV7MqhHyFqKN68g45GoYvAeL2M", + "solana/spl/WENWENvqqNya429ubCdR81ZmD69brwQaaBYY6p3LCpk", "solana/spl/xxxxa1sKNGwFtw2kFn8XauW9xq8hBZ5kVtcSesTT9fW", - "solana/spl/z3dn17yLaGMKffVogeFHQ9zWVcXgqgf3PQnDsNs2g6M", + "solana/spl/yomFPUqz1wJwYSfD5tZJUtS3bNb8xs8mx9XzBv8RL39", "solana/spl/zebeczgi5fSEtbpfQKVZKCJ3WgYXxjkMUkNNx7fLKAF", + "solana/spl/ZEUS1aR7aX8DFFJf5QjWj2ftDDdNTroMNGo8YoQm3Gq", ] `; From 93daf03177cf77f6df0d28105d5413e43469743c Mon Sep 17 00:00:00 2001 From: Kant Date: Wed, 13 Nov 2024 11:32:56 +0100 Subject: [PATCH 13/35] fix: lint issue on missing break --- libs/coin-modules/coin-solana/src/synchronization.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/libs/coin-modules/coin-solana/src/synchronization.ts b/libs/coin-modules/coin-solana/src/synchronization.ts index e59f72feaccd..b0ac0c3618a0 100644 --- a/libs/coin-modules/coin-solana/src/synchronization.ts +++ b/libs/coin-modules/coin-solana/src/synchronization.ts @@ -593,10 +593,10 @@ function getMainAccOperationTypeFromTx(tx: ParsedTransaction): OperationType | u switch (first.program) { case "spl-associated-token-account": - switch (first.instruction.type) { - case "associate": - return "OPT_IN"; + if (first.instruction.type === "associate") { + return "OPT_IN"; } + break; case "spl-token": switch (first.instruction.type) { case "closeAccount": @@ -668,10 +668,10 @@ function getTokenAccOperationType({ if (mainIx !== undefined && otherIxs.length === 0) { switch (mainIx.program) { case "spl-associated-token-account": - switch (mainIx.instruction.type) { - case "associate": - return "NONE"; // ATA opt-in operation is added to the main account + if (mainIx.instruction.type === "associate") { + return "NONE"; // ATA opt-in operation is added to the main account } + break; case "spl-token": switch (mainIx.instruction.type) { case "freezeAccount": From e513d2a1c68dbea5c0bbfff794ec00446f4cf6f9 Mon Sep 17 00:00:00 2001 From: Kant Date: Wed, 13 Nov 2024 12:18:41 +0100 Subject: [PATCH 14/35] chore: update bridge integration test snapshot for solana --- .../solana/__snapshots__/bridge.integration.test.ts.snap | 5 ----- 1 file changed, 5 deletions(-) diff --git a/libs/ledger-live-common/src/families/solana/__snapshots__/bridge.integration.test.ts.snap b/libs/ledger-live-common/src/families/solana/__snapshots__/bridge.integration.test.ts.snap index c0a8430aad9b..f12d8b4810bc 100644 --- a/libs/ledger-live-common/src/families/solana/__snapshots__/bridge.integration.test.ts.snap +++ b/libs/ledger-live-common/src/families/solana/__snapshots__/bridge.integration.test.ts.snap @@ -24,7 +24,6 @@ exports[`solana currency bridge scanAccounts solana seed 1 1`] = ` "used": true, }, { - "approvals": undefined, "balance": "7960720", "id": "js:2:solana:AQbkEagmPgmsdAfS4X8V8UyJnXXjVPMvjeD15etqQ3Jh:solanaMain+8RtwWeqdFz4EFuZU3MAadfYMWSdRMamjFrfq6BXkHuNN", "operationsCount": 1, @@ -122,19 +121,15 @@ exports[`solana currency bridge scanAccounts solana seed 1 2`] = ` "accountId": "js:2:solana:AQbkEagmPgmsdAfS4X8V8UyJnXXjVPMvjeD15etqQ3Jh:solanaMain+8RtwWeqdFz4EFuZU3MAadfYMWSdRMamjFrfq6BXkHuNN", "blockHash": "9tPbgLaETEenufCt5SzXMuWijgFJj549W9j5cJLbaogn", "blockHeight": 108521109, - "contract": undefined, "extra": {}, "fee": "5000", "hasFailed": false, "hash": "A29zPnK1jPr2tGziTnaAvSnadYR2kLCv9sPywj9FJsaEFjtpwmUonspN3WJgz4u6XWmjtVpoFsDrygEnvW51cgk", "id": "js:2:solana:AQbkEagmPgmsdAfS4X8V8UyJnXXjVPMvjeD15etqQ3Jh:solanaMain+8RtwWeqdFz4EFuZU3MAadfYMWSdRMamjFrfq6BXkHuNN-A29zPnK1jPr2tGziTnaAvSnadYR2kLCv9sPywj9FJsaEFjtpwmUonspN3WJgz4u6XWmjtVpoFsDrygEnvW51cgk-IN", - "operator": undefined, "recipients": [ "8RtwWeqdFz4EFuZU3MAadfYMWSdRMamjFrfq6BXkHuNN", ], "senders": [], - "standard": undefined, - "tokenId": undefined, "type": "IN", "value": "7960720", }, From e9f5d755045b0a33fd27fa668534b080484ddde1 Mon Sep 17 00:00:00 2001 From: Kant Date: Wed, 13 Nov 2024 12:44:29 +0100 Subject: [PATCH 15/35] chore: update spl token snapshot --- .../cryptoassets/src/__snapshots__/currencies.test.ts.snap | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/ledgerjs/packages/cryptoassets/src/__snapshots__/currencies.test.ts.snap b/libs/ledgerjs/packages/cryptoassets/src/__snapshots__/currencies.test.ts.snap index f386a43d4b64..cf6adca489ed 100644 --- a/libs/ledgerjs/packages/cryptoassets/src/__snapshots__/currencies.test.ts.snap +++ b/libs/ledgerjs/packages/cryptoassets/src/__snapshots__/currencies.test.ts.snap @@ -8,6 +8,7 @@ exports[`all USDT are countervalue enabled 1`] = ` "elrond/esdt/555344542d663863303863", "ethereum/erc20/usd_tether__erc20_", "polygon/erc20/(pos)_tether_usd", + "solana/spl/Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", "ton/jetton/eqcxe6mutqjkfngfarotkot1lzbdiix1kcixrv7nw2id_sds", "tron/trc20/tr7nhqjekqxgtci8q8zy4pl8otszgjlj6t", ] From 6d2c3e42641652871db4ca0699cea28acb4bad42 Mon Sep 17 00:00:00 2001 From: Kant Date: Mon, 18 Nov 2024 14:37:53 +0100 Subject: [PATCH 16/35] feat: add solana testnet and devnet in supported currencies list --- apps/cli/src/live-common-setup-base.ts | 2 ++ .../src/live-common-set-supported-currencies.ts | 2 ++ apps/ledger-live-mobile/src/live-common-setup.ts | 2 ++ libs/ledgerjs/packages/cryptoassets/src/tokens.ts | 6 +++++- 4 files changed, 11 insertions(+), 1 deletion(-) diff --git a/apps/cli/src/live-common-setup-base.ts b/apps/cli/src/live-common-setup-base.ts index efe239420ef4..0ba0a7638cbc 100644 --- a/apps/cli/src/live-common-setup-base.ts +++ b/apps/cli/src/live-common-setup-base.ts @@ -46,6 +46,8 @@ setSupportedCurrencies([ "hedera", "cardano", "solana", + "solana_testnet", + "solana_devnet", "osmosis", "fantom", "moonbeam", diff --git a/apps/ledger-live-desktop/src/live-common-set-supported-currencies.ts b/apps/ledger-live-desktop/src/live-common-set-supported-currencies.ts index 3b4c4d8d7000..16ba0e4795a8 100644 --- a/apps/ledger-live-desktop/src/live-common-set-supported-currencies.ts +++ b/apps/ledger-live-desktop/src/live-common-set-supported-currencies.ts @@ -20,6 +20,8 @@ setSupportedCurrencies([ "bsc", "polkadot", "solana", + "solana_testnet", + "solana_devnet", "ripple", "litecoin", "polygon", diff --git a/apps/ledger-live-mobile/src/live-common-setup.ts b/apps/ledger-live-mobile/src/live-common-setup.ts index bda0bd4401fe..b5579874dbaf 100644 --- a/apps/ledger-live-mobile/src/live-common-setup.ts +++ b/apps/ledger-live-mobile/src/live-common-setup.ts @@ -53,6 +53,8 @@ setSupportedCurrencies([ "bsc", "polkadot", "solana", + "solana_testnet", + "solana_devnet", "ripple", "litecoin", "polygon", diff --git a/libs/ledgerjs/packages/cryptoassets/src/tokens.ts b/libs/ledgerjs/packages/cryptoassets/src/tokens.ts index 58c9b9176bf3..a1da74195b3e 100644 --- a/libs/ledgerjs/packages/cryptoassets/src/tokens.ts +++ b/libs/ledgerjs/packages/cryptoassets/src/tokens.ts @@ -408,6 +408,10 @@ function convertElrondESDTTokens([ function convertSplTokens([chainId, name, symbol, address, decimals]: SPLToken): TokenCurrency { const chainIdToCurrencyId = { + // Fallback in case CAL is using chainIds for vault + 1: "solana", + 2: "solana_testnet", + 3: "solana_devnet", 101: "solana", 102: "solana_testnet", 103: "solana_devnet", @@ -415,7 +419,7 @@ function convertSplTokens([chainId, name, symbol, address, decimals]: SPLToken): const currencyId = chainIdToCurrencyId[chainId]; return { type: "TokenCurrency", - id: `solana/spl/${address}`, + id: `${currencyId}/spl/${address}`, contractAddress: address, parentCurrency: getCryptoCurrencyById(currencyId), name, From eff3f3775762a5f87205b016e37c163a55d95e6c Mon Sep 17 00:00:00 2001 From: Kant Date: Mon, 18 Nov 2024 15:37:43 +0100 Subject: [PATCH 17/35] fix: spl priority fees and transaction confirmation with blockHeight --- .../coin-solana/src/api/chain/index.ts | 75 ++++++++++++++----- .../coin-solana/src/api/chain/web3.ts | 21 +++--- .../src/bridge.integration.test.ts | 8 +- .../coin-modules/coin-solana/src/broadcast.ts | 8 +- .../coin-solana/src/buildTransaction.ts | 21 ++++-- libs/coin-modules/coin-solana/src/logic.ts | 8 +- .../coin-solana/src/signOperation.ts | 5 +- .../coin-solana/src/synchronization.ts | 7 +- .../coin-solana/src/transaction.ts | 6 +- libs/coin-modules/coin-solana/src/tx-fees.ts | 4 +- 10 files changed, 113 insertions(+), 50 deletions(-) diff --git a/libs/coin-modules/coin-solana/src/api/chain/index.ts b/libs/coin-modules/coin-solana/src/api/chain/index.ts index d07c939b0c98..a685a02d1e5f 100644 --- a/libs/coin-modules/coin-solana/src/api/chain/index.ts +++ b/libs/coin-modules/coin-solana/src/api/chain/index.ts @@ -8,13 +8,16 @@ import { FetchMiddleware, VersionedMessage, PublicKey, - sendAndConfirmRawTransaction, SignaturesForAddressOptions, StakeProgram, TransactionInstruction, ComputeBudgetProgram, VersionedTransaction, TransactionMessage, + SendTransactionError, + BlockhashWithExpiryBlockHeight, + Commitment, + GetLatestBlockhashConfig, } from "@solana/web3.js"; import { makeLRUCache, minutes } from "@ledgerhq/live-network/cache"; import { getEnv } from "@ledgerhq/live-env"; @@ -23,6 +26,7 @@ import { Awaited } from "../../logic"; import { getStakeActivation } from "./stake-activation"; export const LATEST_BLOCKHASH_MOCK = "EEbZs6DmDyDjucyYbo3LwVJU7pQYuVopYcYTSEZXskW3"; +export const LAST_VALID_BLOCK_HEIGHT_MOCK = 280064048; export type Config = { readonly endpoint: string; @@ -31,7 +35,9 @@ export type Config = { export type ChainAPI = Readonly<{ getBalance: (address: string) => Promise; - getLatestBlockhash: () => Promise; + getLatestBlockhash: ( + commitmentOrConfig?: Commitment | GetLatestBlockhashConfig, + ) => Promise; getFeeForMessage: (message: VersionedMessage) => Promise; @@ -66,7 +72,10 @@ export type ChainAPI = Readonly<{ address: string, ) => Promise>["value"]>; - sendRawTransaction: (buffer: Buffer) => ReturnType; + sendRawTransaction: ( + buffer: Buffer, + recentBlockhash?: BlockhashWithExpiryBlockHeight, + ) => ReturnType; findAssocTokenAccAddress: (owner: string, mint: string) => Promise; @@ -105,23 +114,24 @@ export function getChainAPI( fetch(url, options); }; + let _connection: Connection; const connection = () => { - return new Connection(config.endpoint, { - ...(fetchMiddleware ? { fetchMiddleware } : {}), - commitment: "finalized", - confirmTransactionInitialTimeout: getEnv("SOLANA_TX_CONFIRMATION_TIMEOUT") || 0, - }); + if (!_connection) { + _connection = new Connection(config.endpoint, { + ...(fetchMiddleware ? { fetchMiddleware } : {}), + commitment: "finalized", + confirmTransactionInitialTimeout: getEnv("SOLANA_TX_CONFIRMATION_TIMEOUT") || 0, + }); + } + return _connection; }; return { getBalance: (address: string) => connection().getBalance(new PublicKey(address)).catch(remapErrors), - getLatestBlockhash: () => - connection() - .getLatestBlockhash() - .then(r => r.blockhash) - .catch(remapErrors), + getLatestBlockhash: (commitmentOrConfig?: Commitment | GetLatestBlockhashConfig) => + connection().getLatestBlockhash(commitmentOrConfig).catch(remapErrors), getFeeForMessage: (msg: VersionedMessage) => connection() @@ -201,10 +211,39 @@ export function getChainAPI( .then(r => r.value) .catch(remapErrors), - sendRawTransaction: (buffer: Buffer) => { - return sendAndConfirmRawTransaction(connection(), buffer, { - commitment: "confirmed", - }).catch(remapErrors); + sendRawTransaction: (buffer: Buffer, recentBlockhash?: BlockhashWithExpiryBlockHeight) => { + return (async () => { + const conn = connection(); + + const commitment = "confirmed"; + + const signature = await conn.sendRawTransaction(buffer, { + preflightCommitment: commitment, + }); + + if (!recentBlockhash) { + recentBlockhash = await conn.getLatestBlockhash(commitment); + } + const { value: status } = await conn.confirmTransaction( + { + blockhash: recentBlockhash.blockhash, + lastValidBlockHeight: recentBlockhash.lastValidBlockHeight, + signature, + }, + commitment, + ); + if (status.err) { + if (signature != null) { + throw new SendTransactionError({ + action: "send", + signature: signature, + transactionMessage: `Status: (${JSON.stringify(status)})`, + }); + } + throw new Error(`Raw transaction ${signature} failed (${JSON.stringify(status)})`); + } + return signature; + })().catch(remapErrors); }, findAssocTokenAccAddress: (owner: string, mint: string) => { @@ -245,7 +284,7 @@ export function getChainAPI( // RecentBlockhash can by any public key during simulation // since 'replaceRecentBlockhash' is set to 'true' below recentBlockhash: PublicKey.default.toString(), - }).compileToV0Message(), + }).compileToLegacyMessage(), ); const rpcResponse = await connection().simulateTransaction(testTransaction, { replaceRecentBlockhash: true, diff --git a/libs/coin-modules/coin-solana/src/api/chain/web3.ts b/libs/coin-modules/coin-solana/src/api/chain/web3.ts index 22928e5ea2c7..24204516db1f 100644 --- a/libs/coin-modules/coin-solana/src/api/chain/web3.ts +++ b/libs/coin-modules/coin-solana/src/api/chain/web3.ts @@ -211,7 +211,7 @@ export const buildTokenTransferInstructions = async ( ); } - return instructions; + return appendMaybePriorityFeeInstructions(api, instructions, ownerPubkey); }; export async function findAssociatedTokenAccountPubkey( @@ -288,10 +288,12 @@ export async function appendMaybePriorityFeeInstructions( const writableAccs = instructions .map(ix => ix.keys.filter(acc => acc.isWritable).map(acc => acc.pubkey.toBase58())) .flat(); - const priorityFeeIx = await buildMaybePriorityFeeInstruction(api, writableAccs); - if (priorityFeeIx) instructions.unshift(priorityFeeIx); - const computeUnitsIx = await buildComputeUnitInstruction(api, instructions, payer); + const [priorityFeeIx, computeUnitsIx] = await Promise.all([ + buildMaybePriorityFeeInstruction(api, writableAccs), + buildComputeUnitInstruction(api, instructions, payer), + ]); + if (priorityFeeIx) instructions.unshift(priorityFeeIx); if (computeUnitsIx) instructions.unshift(computeUnitsIx); return instructions; } @@ -319,11 +321,10 @@ export async function buildComputeUnitInstruction( : null; } -export function buildCreateAssociatedTokenAccountInstruction({ - mint, - owner, - associatedTokenAccountAddress, -}: TokenCreateATACommand): TransactionInstruction[] { +export function buildCreateAssociatedTokenAccountInstruction( + api: ChainAPI, + { mint, owner, associatedTokenAccountAddress }: TokenCreateATACommand, +): Promise { const ownerPubKey = new PublicKey(owner); const mintPubkey = new PublicKey(mint); const associatedTokenAccPubkey = new PublicKey(associatedTokenAccountAddress); @@ -337,7 +338,7 @@ export function buildCreateAssociatedTokenAccountInstruction({ ), ]; - return instructions; + return appendMaybePriorityFeeInstructions(api, instructions, ownerPubKey); } export async function buildStakeDelegateInstructions( diff --git a/libs/coin-modules/coin-solana/src/bridge.integration.test.ts b/libs/coin-modules/coin-solana/src/bridge.integration.test.ts index 048332a678b6..2eab9609ba2b 100644 --- a/libs/coin-modules/coin-solana/src/bridge.integration.test.ts +++ b/libs/coin-modules/coin-solana/src/bridge.integration.test.ts @@ -37,7 +37,7 @@ import createTransaction from "./createTransaction"; import { compact } from "lodash/fp"; import { SYSTEM_ACCOUNT_RENT_EXEMPT, assertUnreachable } from "./utils"; import { getEnv } from "@ledgerhq/live-env"; -import { ChainAPI, LATEST_BLOCKHASH_MOCK } from "./api"; +import { ChainAPI, LAST_VALID_BLOCK_HEIGHT_MOCK, LATEST_BLOCKHASH_MOCK } from "./api"; import { SolanaStakeAccountIsNotDelegatable, SolanaStakeAccountValidatorIsUnchangeable, @@ -964,7 +964,11 @@ const baseTx = { } as Transaction; const baseAPI = { - getLatestBlockhash: () => Promise.resolve(LATEST_BLOCKHASH_MOCK), + getLatestBlockhash: () => + Promise.resolve({ + blockhash: LATEST_BLOCKHASH_MOCK, + lastValidBlockHeight: LAST_VALID_BLOCK_HEIGHT_MOCK, + }), getFeeForMessage: (_msg: unknown) => Promise.resolve(testOnChainData.fees.lamportsPerSignature), getRecentPrioritizationFees: (_: string[]) => { return Promise.resolve([ diff --git a/libs/coin-modules/coin-solana/src/broadcast.ts b/libs/coin-modules/coin-solana/src/broadcast.ts index 46dcd0633def..2910191748ce 100644 --- a/libs/coin-modules/coin-solana/src/broadcast.ts +++ b/libs/coin-modules/coin-solana/src/broadcast.ts @@ -3,6 +3,7 @@ import { patchOperationWithHash } from "@ledgerhq/coin-framework/operation"; import type { Account, Operation, SignedOperation } from "@ledgerhq/types-live"; import { ChainAPI } from "./api"; import { SolanaTxConfirmationTimeout, SolanaTxSimulationFailedWhilePendingOp } from "./errors"; +import { BlockhashWithExpiryBlockHeight } from "@solana/web3.js"; export const broadcastWithAPI = async ( { @@ -14,10 +15,13 @@ export const broadcastWithAPI = async ( }, api: ChainAPI, ): Promise => { - const { signature, operation } = signedOperation; + const { signature, operation, rawData } = signedOperation; try { - const txSignature = await api.sendRawTransaction(Buffer.from(signature, "hex")); + const txSignature = await api.sendRawTransaction( + Buffer.from(signature, "hex"), + rawData?.recentBlockhash as BlockhashWithExpiryBlockHeight, + ); return patchOperationWithHash(operation, txSignature); } catch (e: any) { // heuristics to make some errors more user friendly diff --git a/libs/coin-modules/coin-solana/src/buildTransaction.ts b/libs/coin-modules/coin-solana/src/buildTransaction.ts index 146b594ca7fc..33fb090081f7 100644 --- a/libs/coin-modules/coin-solana/src/buildTransaction.ts +++ b/libs/coin-modules/coin-solana/src/buildTransaction.ts @@ -15,6 +15,7 @@ import { VersionedTransaction as OnChainTransaction, TransactionInstruction, TransactionMessage, + BlockhashWithExpiryBlockHeight, } from "@solana/web3.js"; import { ChainAPI } from "./api"; @@ -22,16 +23,23 @@ export const buildTransactionWithAPI = async ( address: string, transaction: Transaction, api: ChainAPI, -): Promise OnChainTransaction]> => { - const instructions = await buildInstructions(api, transaction); - - const recentBlockhash = await api.getLatestBlockhash(); +): Promise< + readonly [ + OnChainTransaction, + BlockhashWithExpiryBlockHeight, + (signature: Buffer) => OnChainTransaction, + ] +> => { + const [instructions, recentBlockhash] = await Promise.all([ + buildInstructions(api, transaction), + api.getLatestBlockhash(), + ]); const feePayer = new PublicKey(address); const tm = new TransactionMessage({ payerKey: feePayer, - recentBlockhash, + recentBlockhash: recentBlockhash.blockhash, instructions, }); @@ -39,6 +47,7 @@ export const buildTransactionWithAPI = async ( return [ tx, + recentBlockhash, (signature: Buffer) => { tx.addSignature(new PublicKey(address), signature); return tx; @@ -70,7 +79,7 @@ async function buildInstructionsForCommand( case "token.transfer": return buildTokenTransferInstructions(api, command); case "token.createATA": - return buildCreateAssociatedTokenAccountInstruction(command); + return buildCreateAssociatedTokenAccountInstruction(api, command); case "stake.createAccount": return buildStakeCreateAccountInstructions(api, command); case "stake.delegate": diff --git a/libs/coin-modules/coin-solana/src/logic.ts b/libs/coin-modules/coin-solana/src/logic.ts index fea208d4c098..faba5b53feaa 100644 --- a/libs/coin-modules/coin-solana/src/logic.ts +++ b/libs/coin-modules/coin-solana/src/logic.ts @@ -36,8 +36,8 @@ export function decodeAccountIdWithTokenAccountAddress(accountIdWithTokenAccount }; } -export function toTokenId(mint: string): string { - return `solana/spl/${mint}`; +export function toTokenId(currencyId: string, mint: string): string { + return `${currencyId}/spl/${mint}`; } export function toTokenMint(tokenId: string): string { @@ -48,8 +48,8 @@ export function toSubAccMint(subAcc: TokenAccount): string { return toTokenMint(subAcc.token.id); } -export function tokenIsListedOnLedger(mint: string): boolean { - return findTokenById(toTokenId(mint))?.type === "TokenCurrency"; +export function tokenIsListedOnLedger(currencyId: string, mint: string): boolean { + return findTokenById(toTokenId(currencyId, mint))?.type === "TokenCurrency"; } export function stakeActions(stake: SolanaStake): StakeAction[] { diff --git a/libs/coin-modules/coin-solana/src/signOperation.ts b/libs/coin-modules/coin-solana/src/signOperation.ts index 5d436993281f..619c0a6645d6 100644 --- a/libs/coin-modules/coin-solana/src/signOperation.ts +++ b/libs/coin-modules/coin-solana/src/signOperation.ts @@ -53,7 +53,7 @@ export const buildSignOperation = ({ account, deviceId, transaction }) => new Observable(subscriber => { const main = async () => { - const [tx, signOnChainTransaction] = await buildTransactionWithAPI( + const [tx, recentBlockhash, signOnChainTransaction] = await buildTransactionWithAPI( account.freshAddress, transaction, await api(), @@ -78,6 +78,9 @@ export const buildSignOperation = signedOperation: { operation: buildOptimisticOperation(account, transaction), signature: Buffer.from(signedTx.serialize()).toString("hex"), + rawData: { + recentBlockhash, + }, }, }); }; diff --git a/libs/coin-modules/coin-solana/src/synchronization.ts b/libs/coin-modules/coin-solana/src/synchronization.ts index b0ac0c3618a0..569595b523b1 100644 --- a/libs/coin-modules/coin-solana/src/synchronization.ts +++ b/libs/coin-modules/coin-solana/src/synchronization.ts @@ -87,7 +87,7 @@ export const getAccountShapeWithAPI = async ( const nextSubAccs: TokenAccount[] = []; for (const [mint, accs] of onChainTokenAccsByMint.entries()) { - if (!tokenIsListedOnLedger(mint)) { + if (!tokenIsListedOnLedger(currency.id, mint)) { continue; } @@ -114,6 +114,7 @@ export const getAccountShapeWithAPI = async ( const nextSubAcc = subAcc === undefined ? newSubAcc({ + currencyId: currency.id, mainAccountId, assocTokenAcc, txs, @@ -245,10 +246,12 @@ export const getAccountShapeWithAPI = async ( }; function newSubAcc({ + currencyId, mainAccountId, assocTokenAcc, txs, }: { + currencyId: string; mainAccountId: string; assocTokenAcc: OnChainTokenAccount; txs: TransactionDescriptor[]; @@ -257,7 +260,7 @@ function newSubAcc({ const creationDate = new Date((firstTx.info.blockTime ?? Date.now() / 1000) * 1000); - const tokenId = toTokenId(assocTokenAcc.info.mint.toBase58()); + const tokenId = toTokenId(currencyId, assocTokenAcc.info.mint.toBase58()); const tokenCurrency = getTokenById(tokenId); const accosTokenAccPubkey = assocTokenAcc.onChainAcc.pubkey; diff --git a/libs/coin-modules/coin-solana/src/transaction.ts b/libs/coin-modules/coin-solana/src/transaction.ts index 4475702d81a6..0ed1ef8c3e98 100644 --- a/libs/coin-modules/coin-solana/src/transaction.ts +++ b/libs/coin-modules/coin-solana/src/transaction.ts @@ -72,7 +72,7 @@ function formatCommand(mainAccount: Account, tx: Transaction, command: Command) case "token.transfer": return formatTokenTransfer(mainAccount, tx, command); case "token.createATA": - return formatCreateATA(command); + return formatCreateATA(mainAccount, command); case "stake.createAccount": return formatStakeCreateAccount(mainAccount, tx, command); case "stake.delegate": @@ -147,8 +147,8 @@ function formatTokenTransfer(mainAccount: Account, tx: Transaction, command: Tok return "\n" + str; } -function formatCreateATA(command: TokenCreateATACommand) { - const token = getTokenById(toTokenId(command.mint)); +function formatCreateATA(mainAccount: Account, command: TokenCreateATACommand) { + const token = getTokenById(toTokenId(mainAccount.currency.id, command.mint)); const str = [` OPT IN TOKEN: ${token.ticker}`].filter(Boolean).join("\n"); return "\n" + str; } diff --git a/libs/coin-modules/coin-solana/src/tx-fees.ts b/libs/coin-modules/coin-solana/src/tx-fees.ts index e4ef0fed53b4..f727a77504f4 100644 --- a/libs/coin-modules/coin-solana/src/tx-fees.ts +++ b/libs/coin-modules/coin-solana/src/tx-fees.ts @@ -223,9 +223,9 @@ async function waitNextBlockhash(api: ChainAPI, currentBlockhash: string) { log("info", `sleeping for ${sleepTimeMS} ms, waiting for a new blockhash`); await sleep(sleepTimeMS); const blockhash = await api.getLatestBlockhash(); - if (blockhash !== currentBlockhash) { + if (blockhash.blockhash !== currentBlockhash) { log("info", "got a new blockhash"); - return blockhash; + return blockhash.blockhash; } log("info", "got same blockhash"); } From ffb9da581b3448a1a7d094879d84341ae4356fec Mon Sep 17 00:00:00 2001 From: Kant Date: Mon, 18 Nov 2024 15:51:00 +0100 Subject: [PATCH 18/35] chore: add support links --- apps/ledger-live-desktop/src/config/urls.ts | 4 ++-- apps/ledger-live-mobile/src/utils/urls.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/ledger-live-desktop/src/config/urls.ts b/apps/ledger-live-desktop/src/config/urls.ts index 3209f82317ef..f45d39ba7361 100644 --- a/apps/ledger-live-desktop/src/config/urls.ts +++ b/apps/ledger-live-desktop/src/config/urls.ts @@ -4,7 +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", + spl: "https://support.ledger.com/article/7723954701469-zd", }; const errors: Record = { @@ -154,7 +154,7 @@ export const urls = { }, solana: { staking: "https://support.ledger.com/article/4731749170461-zd", - splTokenInfo: "Solana spl tokens link TODO: to be defined", + splTokenInfo: "https://support.ledger.com/article/7723954701469-zd", recipient_info: "https://support.ledger.com", ledgerByChorusOneTC: "https://chorus.one/tos", ledgerByFigmentTC: diff --git a/apps/ledger-live-mobile/src/utils/urls.tsx b/apps/ledger-live-mobile/src/utils/urls.tsx index c1e947fbeee2..726b39dd6636 100644 --- a/apps/ledger-live-mobile/src/utils/urls.tsx +++ b/apps/ledger-live-mobile/src/utils/urls.tsx @@ -168,7 +168,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", + splTokenInfo: "https://support.ledger.com/article/7723954701469-zd", }, resources: { gettingStarted: From 20787f9afd66e9839e49da0a0be49485072fb917 Mon Sep 17 00:00:00 2001 From: Kant Date: Mon, 18 Nov 2024 17:12:20 +0100 Subject: [PATCH 19/35] test: fix with solana testnet and devnet addition --- .../wallet-api-currencies-darwin.json | 18 ++++++++++++++++++ .../wallet-api-currencies-linux.json | 18 ++++++++++++++++++ .../packages/cryptoassets/src/abandonseed.ts | 2 ++ 3 files changed, 38 insertions(+) diff --git a/apps/ledger-live-desktop/tests/specs/services/wallet-api.spec.ts-snapshots/wallet-api-currencies-darwin.json b/apps/ledger-live-desktop/tests/specs/services/wallet-api.spec.ts-snapshots/wallet-api-currencies-darwin.json index 76ac841b3b4c..afc20d51bbc7 100644 --- a/apps/ledger-live-desktop/tests/specs/services/wallet-api.spec.ts-snapshots/wallet-api-currencies-darwin.json +++ b/apps/ledger-live-desktop/tests/specs/services/wallet-api.spec.ts-snapshots/wallet-api-currencies-darwin.json @@ -143,6 +143,24 @@ "color": "#000", "decimals": 9 }, + { + "type": "CryptoCurrency", + "id": "solana_testnet", + "ticker": "SOL", + "name": "Solana testnet", + "family": "solana", + "color": "#000", + "decimals": 9 + }, + { + "type": "CryptoCurrency", + "id": "solana_devnet", + "ticker": "SOL", + "name": "Solana devnet", + "family": "solana", + "color": "#000", + "decimals": 9 + }, { "type": "CryptoCurrency", "id": "ripple", diff --git a/apps/ledger-live-desktop/tests/specs/services/wallet-api.spec.ts-snapshots/wallet-api-currencies-linux.json b/apps/ledger-live-desktop/tests/specs/services/wallet-api.spec.ts-snapshots/wallet-api-currencies-linux.json index 76ac841b3b4c..afc20d51bbc7 100644 --- a/apps/ledger-live-desktop/tests/specs/services/wallet-api.spec.ts-snapshots/wallet-api-currencies-linux.json +++ b/apps/ledger-live-desktop/tests/specs/services/wallet-api.spec.ts-snapshots/wallet-api-currencies-linux.json @@ -143,6 +143,24 @@ "color": "#000", "decimals": 9 }, + { + "type": "CryptoCurrency", + "id": "solana_testnet", + "ticker": "SOL", + "name": "Solana testnet", + "family": "solana", + "color": "#000", + "decimals": 9 + }, + { + "type": "CryptoCurrency", + "id": "solana_devnet", + "ticker": "SOL", + "name": "Solana devnet", + "family": "solana", + "color": "#000", + "decimals": 9 + }, { "type": "CryptoCurrency", "id": "ripple", diff --git a/libs/ledgerjs/packages/cryptoassets/src/abandonseed.ts b/libs/ledgerjs/packages/cryptoassets/src/abandonseed.ts index c7c987b694bd..ac17615f9ead 100644 --- a/libs/ledgerjs/packages/cryptoassets/src/abandonseed.ts +++ b/libs/ledgerjs/packages/cryptoassets/src/abandonseed.ts @@ -44,6 +44,8 @@ const abandonSeedAddresses: Partial> = { zencash: "zngWJRgpBa45KUeRuCmdMsqti4ohhe9sVwC", bsc: EVM_DEAD_ADDRESS, solana: "GjJyeC1r2RgkuoCWMyPYkCWSGSGLcz266EaAkLA27AhL", + solana_testnet: "GjJyeC1r2RgkuoCWMyPYkCWSGSGLcz266EaAkLA27AhL", + solana_devnet: "GjJyeC1r2RgkuoCWMyPYkCWSGSGLcz266EaAkLA27AhL", polygon: EVM_DEAD_ADDRESS, crypto_org: "cro1r3ywhs4ng96dnm9zkc5y3etl7tps5cvvz26lr4", crypto_org_croeseid: "cro1r3ywhs4ng96dnm9zkc5y3etl7tps5cvvz26lr4", From 727e71368a7d79dfecd5e409b01f8be49328f1a7 Mon Sep 17 00:00:00 2001 From: Kant Date: Wed, 20 Nov 2024 15:00:49 +0100 Subject: [PATCH 20/35] chore: remove comments from review comments --- .../src/renderer/families/solana/TransactionConfirmFields.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/ledger-live-desktop/src/renderer/families/solana/TransactionConfirmFields.tsx b/apps/ledger-live-desktop/src/renderer/families/solana/TransactionConfirmFields.tsx index 877c580773c9..b00c77e1eff9 100644 --- a/apps/ledger-live-desktop/src/renderer/families/solana/TransactionConfirmFields.tsx +++ b/apps/ledger-live-desktop/src/renderer/families/solana/TransactionConfirmFields.tsx @@ -59,8 +59,6 @@ type TransactionConfirmFields = SolanaFamily["transactionConfirmFields"]; type TitleComponent = NonNullable["title"]>; const transactionConfirmFields: TransactionConfirmFields = { - // footer: Footer, // is not shown without manifestId - // fieldComponents, title: Title, }; From 9b5a5e5de7963d9c19c93b73643645e832996c91 Mon Sep 17 00:00:00 2001 From: Kant Date: Wed, 20 Nov 2024 15:01:08 +0100 Subject: [PATCH 21/35] fix: wallet-api spl usage --- libs/coin-modules/coin-solana/src/prepareTransaction.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/libs/coin-modules/coin-solana/src/prepareTransaction.ts b/libs/coin-modules/coin-solana/src/prepareTransaction.ts index 669488642712..dc8687a97842 100644 --- a/libs/coin-modules/coin-solana/src/prepareTransaction.ts +++ b/libs/coin-modules/coin-solana/src/prepareTransaction.ts @@ -110,6 +110,11 @@ const prepareTransaction = async ( model, }; + // Add missing subAccountId for wallet-api sign flow + if (!tx.subAccountId && "subAccountId" in model.uiState) { + patch.subAccountId = model.uiState.subAccountId; + } + return updateTransaction(tx, patch); }; From 025f24909e5de306672b0a5fbba2e98d6e916928 Mon Sep 17 00:00:00 2001 From: Kant Date: Wed, 20 Nov 2024 18:14:44 +0100 Subject: [PATCH 22/35] fix: wallet-api spl token support --- .../coin-evm/src/createTransaction.ts | 14 ++- .../coin-solana/src/prepareTransaction.ts | 5 - .../families/solana/walletApiAdapter.test.ts | 95 +++++++++++++++++++ .../src/families/solana/walletApiAdapter.ts | 26 +++++ .../src/generated/walletApiAdapter.ts | 2 + .../src/wallet-api/ACRE/server.ts | 4 +- .../src/wallet-api/Exchange/server.ts | 4 +- .../src/wallet-api/logic.ts | 4 +- 8 files changed, 141 insertions(+), 13 deletions(-) create mode 100644 libs/ledger-live-common/src/families/solana/walletApiAdapter.test.ts create mode 100644 libs/ledger-live-common/src/families/solana/walletApiAdapter.ts diff --git a/libs/coin-modules/coin-evm/src/createTransaction.ts b/libs/coin-modules/coin-evm/src/createTransaction.ts index f225a42b6cac..7a0f606dfb57 100644 --- a/libs/coin-modules/coin-evm/src/createTransaction.ts +++ b/libs/coin-modules/coin-evm/src/createTransaction.ts @@ -1,5 +1,5 @@ import BigNumber from "bignumber.js"; -import { Account, AccountBridge } from "@ledgerhq/types-live"; +import { AccountBridge, AccountLike } from "@ledgerhq/types-live"; import { Transaction as EvmTransaction } from "./types"; import { DEFAULT_GAS_LIMIT } from "./transaction"; @@ -9,6 +9,16 @@ import { DEFAULT_GAS_LIMIT } from "./transaction"; */ export const DEFAULT_NONCE = -1; +const getChainId = (account: AccountLike): number => { + if (account.type === "Account") { + return account.currency.ethereumLikeInfo?.chainId || 0; + } + if (account.type === "TokenAccount") { + return account.token.parentCurrency.ethereumLikeInfo?.chainId || 0; + } + return 0; +}; + /** * EVM Transaction factory. * By default the transaction is an EIP-1559 transaction. @@ -24,7 +34,7 @@ export const createTransaction: AccountBridge["createTransaction maxPriorityFeePerGas: new BigNumber(0), gasLimit: DEFAULT_GAS_LIMIT, nonce: DEFAULT_NONCE, - chainId: (account as Account).currency?.ethereumLikeInfo?.chainId || 0, + chainId: getChainId(account), feesStrategy: "medium", type: 2, }); diff --git a/libs/coin-modules/coin-solana/src/prepareTransaction.ts b/libs/coin-modules/coin-solana/src/prepareTransaction.ts index dc8687a97842..669488642712 100644 --- a/libs/coin-modules/coin-solana/src/prepareTransaction.ts +++ b/libs/coin-modules/coin-solana/src/prepareTransaction.ts @@ -110,11 +110,6 @@ const prepareTransaction = async ( model, }; - // Add missing subAccountId for wallet-api sign flow - if (!tx.subAccountId && "subAccountId" in model.uiState) { - patch.subAccountId = model.uiState.subAccountId; - } - return updateTransaction(tx, patch); }; diff --git a/libs/ledger-live-common/src/families/solana/walletApiAdapter.test.ts b/libs/ledger-live-common/src/families/solana/walletApiAdapter.test.ts new file mode 100644 index 000000000000..de5c2403c94c --- /dev/null +++ b/libs/ledger-live-common/src/families/solana/walletApiAdapter.test.ts @@ -0,0 +1,95 @@ +import { Account, TokenAccount } from "@ledgerhq/types-live"; +import { SolanaTransaction as WalletAPITransaction } from "@ledgerhq/wallet-api-core"; +import BigNumber from "bignumber.js"; +import { Transaction } from "@ledgerhq/coin-solana/types"; +import sol from "./walletApiAdapter"; + +describe("getWalletAPITransactionSignFlowInfos", () => { + describe("should properly get infos for Solana TX", () => { + it("simple transfer", () => { + const solanaTx: WalletAPITransaction = { + family: "solana", + amount: new BigNumber(100000), + recipient: "0xABCDEFG", + model: { + kind: "transfer", + uiState: {}, + commandDescriptor: { + command: { + kind: "transfer", + amount: 100000, + sender: "0xABCDEF", + recipient: "0xABCDEFG", + }, + fee: 0, + errors: {}, + warnings: {}, + }, + }, + }; + + const expectedLiveTx: Partial = { + ...solanaTx, + }; + + const { canEditFees, hasFeesProvided, liveTx } = sol.getWalletAPITransactionSignFlowInfos({ + walletApiTransaction: solanaTx, + account: {} as Account, + }); + + expect(canEditFees).toBe(true); + + expect(hasFeesProvided).toBe(false); + + expect(liveTx).toEqual(expectedLiveTx); + }); + + it("should add subAccountId for token transfer", () => { + const solanaTx: WalletAPITransaction = { + family: "solana", + amount: new BigNumber(100000), + recipient: "0xABCDEFG", + model: { + kind: "token.transfer", + uiState: { + subAccountId: "", // Automatically replaced by LL + }, + commandDescriptor: { + command: { + kind: "token.transfer", + amount: 100000, + mintAddress: "0xABCDE", + mintDecimals: 6, + ownerAddress: "0xABCDEF", + ownerAssociatedTokenAccountAddress: "0xABCDEF", + recipientDescriptor: { + shouldCreateAsAssociatedTokenAccount: false, + tokenAccAddress: "0xABCDEFG", + walletAddress: "0xABCDEFG", + }, + }, + fee: 0, + errors: {}, + warnings: {}, + }, + }, + }; + + const expectedLiveTx: Partial = { + ...solanaTx, + subAccountId: "subAccountId", + }; + + const { canEditFees, hasFeesProvided, liveTx } = sol.getWalletAPITransactionSignFlowInfos({ + walletApiTransaction: solanaTx, + account: { id: "subAccountId", type: "TokenAccount" } as TokenAccount, + }); + + expect(canEditFees).toBe(true); + + expect(hasFeesProvided).toBe(false); + + expect(liveTx).toEqual(expectedLiveTx); + }); + }); +}); diff --git a/libs/ledger-live-common/src/families/solana/walletApiAdapter.ts b/libs/ledger-live-common/src/families/solana/walletApiAdapter.ts new file mode 100644 index 000000000000..3b5b36ea28cc --- /dev/null +++ b/libs/ledger-live-common/src/families/solana/walletApiAdapter.ts @@ -0,0 +1,26 @@ +import { SolanaTransaction as WalletAPISolanaTransaction } from "@ledgerhq/wallet-api-core"; +import { GetWalletAPITransactionSignFlowInfos } from "../../wallet-api/types"; +import { Transaction } from "@ledgerhq/coin-solana/types"; + +const CAN_EDIT_FEES = false; + +const HAS_FEES_PROVIDED = false; + +const getWalletAPITransactionSignFlowInfos: GetWalletAPITransactionSignFlowInfos< + WalletAPISolanaTransaction, + Transaction +> = ({ walletApiTransaction, account }) => { + const liveTx: Transaction = { ...walletApiTransaction }; + + if (!liveTx.subAccountId && account.type === "TokenAccount") { + liveTx.subAccountId = account.id; + } + + return { + canEditFees: CAN_EDIT_FEES, + liveTx, + hasFeesProvided: HAS_FEES_PROVIDED, + }; +}; + +export default { getWalletAPITransactionSignFlowInfos }; diff --git a/libs/ledger-live-common/src/generated/walletApiAdapter.ts b/libs/ledger-live-common/src/generated/walletApiAdapter.ts index 7ac7d0821085..b126b100ecbb 100644 --- a/libs/ledger-live-common/src/generated/walletApiAdapter.ts +++ b/libs/ledger-live-common/src/generated/walletApiAdapter.ts @@ -1,11 +1,13 @@ import bitcoin from "../families/bitcoin/walletApiAdapter"; import evm from "../families/evm/walletApiAdapter"; import polkadot from "../families/polkadot/walletApiAdapter"; +import solana from "../families/solana/walletApiAdapter"; import xrp from "../families/xrp/walletApiAdapter"; export default { bitcoin, evm, polkadot, + solana, xrp, }; diff --git a/libs/ledger-live-common/src/wallet-api/ACRE/server.ts b/libs/ledger-live-common/src/wallet-api/ACRE/server.ts index 95ed2f2b388a..6afa197339f8 100644 --- a/libs/ledger-live-common/src/wallet-api/ACRE/server.ts +++ b/libs/ledger-live-common/src/wallet-api/ACRE/server.ts @@ -114,7 +114,7 @@ export const handlers = ({ const parentAccount = getParentAccount(account, accounts); const accountFamily = isTokenAccount(account) - ? parentAccount?.currency.family + ? account.token.parentCurrency.family : account.currency.family; const mainAccount = getMainAccount(account, parentAccount); @@ -123,7 +123,7 @@ export const handlers = ({ const { canEditFees, liveTx, hasFeesProvided } = getWalletAPITransactionSignFlowInfos({ walletApiTransaction: transaction, - account: mainAccount, + account, }); if (accountFamily !== liveTx.family) { diff --git a/libs/ledger-live-common/src/wallet-api/Exchange/server.ts b/libs/ledger-live-common/src/wallet-api/Exchange/server.ts index 16865d705120..cebf61396935 100644 --- a/libs/ledger-live-common/src/wallet-api/Exchange/server.ts +++ b/libs/ledger-live-common/src/wallet-api/Exchange/server.ts @@ -233,7 +233,7 @@ export const handlers = ({ const { liveTx } = getWalletAPITransactionSignFlowInfos({ walletApiTransaction: transaction, - account: mainFromAccount, + account: fromAccount, }); if (liveTx.family !== mainFromAccountFamily) { @@ -253,7 +253,7 @@ export const handlers = ({ const subAccountId = fromParentAccount && fromParentAccount.id !== fromAccount.id ? fromAccount.id : undefined; - const bridgeTx = accountBridge.createTransaction(mainFromAccount); + const bridgeTx = accountBridge.createTransaction(fromAccount); /** * We append the `recipient` to the tx created from `createTransaction` * to avoid having userGasLimit reset to null for ETH txs diff --git a/libs/ledger-live-common/src/wallet-api/logic.ts b/libs/ledger-live-common/src/wallet-api/logic.ts index dce1851fc44b..55c4b1bf9269 100644 --- a/libs/ledger-live-common/src/wallet-api/logic.ts +++ b/libs/ledger-live-common/src/wallet-api/logic.ts @@ -435,7 +435,7 @@ export function completeExchangeLogic( const { liveTx } = getWalletAPITransactionSignFlowInfos({ walletApiTransaction: transaction, - account: mainFromAccount, + account: fromAccount, }); if (liveTx.family !== mainFromAccountFamily) { @@ -452,7 +452,7 @@ export function completeExchangeLogic( */ const subAccountId = exchange.fromParentAccount ? fromAccount.id : undefined; - const bridgeTx = accountBridge.createTransaction(mainFromAccount); + const bridgeTx = accountBridge.createTransaction(fromAccount); /** * We append the `recipient` to the tx created from `createTransaction` * to avoid having userGasLimit reset to null for ETH txs From c7712ff9092f0b5bce4757b56001805b6e26e738 Mon Sep 17 00:00:00 2001 From: Kant Date: Wed, 20 Nov 2024 18:46:39 +0100 Subject: [PATCH 23/35] test: fix test expectation --- .../src/families/solana/walletApiAdapter.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/ledger-live-common/src/families/solana/walletApiAdapter.test.ts b/libs/ledger-live-common/src/families/solana/walletApiAdapter.test.ts index de5c2403c94c..f28e6a3c2570 100644 --- a/libs/ledger-live-common/src/families/solana/walletApiAdapter.test.ts +++ b/libs/ledger-live-common/src/families/solana/walletApiAdapter.test.ts @@ -37,7 +37,7 @@ describe("getWalletAPITransactionSignFlowInfos", () => { account: {} as Account, }); - expect(canEditFees).toBe(true); + expect(canEditFees).toBe(false); expect(hasFeesProvided).toBe(false); @@ -85,7 +85,7 @@ describe("getWalletAPITransactionSignFlowInfos", () => { account: { id: "subAccountId", type: "TokenAccount" } as TokenAccount, }); - expect(canEditFees).toBe(true); + expect(canEditFees).toBe(false); expect(hasFeesProvided).toBe(false); From 64c34042f8b1779d7f9c584cc36e1760370c7fea Mon Sep 17 00:00:00 2001 From: Kant Date: Wed, 20 Nov 2024 19:41:47 +0100 Subject: [PATCH 24/35] test: fix wrong findTokenById call and param --- libs/ledger-live-common/src/mock/fixtures/cryptoCurrencies.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/ledger-live-common/src/mock/fixtures/cryptoCurrencies.ts b/libs/ledger-live-common/src/mock/fixtures/cryptoCurrencies.ts index 605bd3288fc2..09ae0d7cde57 100644 --- a/libs/ledger-live-common/src/mock/fixtures/cryptoCurrencies.ts +++ b/libs/ledger-live-common/src/mock/fixtures/cryptoCurrencies.ts @@ -38,7 +38,7 @@ export function createFixtureCryptoCurrency(family: string): CryptoCurrency { } const defaultEthCryptoFamily = cryptocurrenciesById["ethereum"]; -const defaultERC20USDTToken = findTokenById["usd_tether__erc20_"]; +const defaultERC20USDTToken = findTokenById("ethereum/erc20/usd_tether__erc20_")!; export function createFixtureTokenAccount( id = "00", From 45ce8500356e99b0335b32bb864de6eb67d77529 Mon Sep 17 00:00:00 2001 From: Kant Date: Thu, 21 Nov 2024 10:07:55 +0100 Subject: [PATCH 25/35] test: fix missing account type --- .../src/families/evm/walletApiAdapter.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/ledger-live-common/src/families/evm/walletApiAdapter.test.ts b/libs/ledger-live-common/src/families/evm/walletApiAdapter.test.ts index a908cb8e7b12..1c584868cbfc 100644 --- a/libs/ledger-live-common/src/families/evm/walletApiAdapter.test.ts +++ b/libs/ledger-live-common/src/families/evm/walletApiAdapter.test.ts @@ -256,7 +256,7 @@ describe("getWalletAPITransactionSignFlowInfos", () => { const { canEditFees, hasFeesProvided, liveTx } = evm.getWalletAPITransactionSignFlowInfos({ walletApiTransaction: ethPlatformTx, - account: { currency: { ethereumLikeInfo: { chainId: 1 } } } as Account, + account: { type: "Account", currency: { ethereumLikeInfo: { chainId: 1 } } } as Account, }); expect(canEditFees).toBe(true); From e94c6ed473d3933b0f2bf04a394c02dfc28426e2 Mon Sep 17 00:00:00 2001 From: Kant Date: Thu, 28 Nov 2024 16:39:14 +0100 Subject: [PATCH 26/35] feat: add trusted name resolution for signTransaction --- .../coin-solana/src/signOperation.ts | 26 ++++- libs/coin-modules/coin-solana/src/signer.ts | 11 ++- .../ledgerjs/packages/hw-app-solana/README.md | 90 +++++++++++------ .../packages/hw-app-solana/package.json | 6 +- .../packages/hw-app-solana/src/Solana.ts | 97 ++++++++++++++++++- pnpm-lock.yaml | 32 +++--- 6 files changed, 212 insertions(+), 50 deletions(-) diff --git a/libs/coin-modules/coin-solana/src/signOperation.ts b/libs/coin-modules/coin-solana/src/signOperation.ts index 619c0a6645d6..a0789d9cb883 100644 --- a/libs/coin-modules/coin-solana/src/signOperation.ts +++ b/libs/coin-modules/coin-solana/src/signOperation.ts @@ -15,7 +15,7 @@ import type { TransferCommand, } from "./types"; import { buildTransactionWithAPI } from "./buildTransaction"; -import type { SolanaSigner } from "./signer"; +import type { Resolution, SolanaSigner } from "./signer"; import BigNumber from "bignumber.js"; import { encodeOperationId } from "@ledgerhq/coin-framework/operation"; import { assertUnreachable } from "./utils"; @@ -45,6 +45,24 @@ const buildOptimisticOperation = (account: Account, transaction: Transaction): S return optimisticOp; }; +function getResolution(transaction: Transaction): Resolution | undefined { + if (!transaction.subAccountId || !transaction.model.commandDescriptor) return; + + const { command } = transaction.model.commandDescriptor; + switch (command.kind) { + case "token.transfer": { + return { + tokenAddress: command.recipientDescriptor.tokenAccAddress, + }; + } + case "token.createATA": { + return { + tokenAddress: command.associatedTokenAccountAddress, + }; + } + } +} + export const buildSignOperation = ( signerContext: SignerContext, @@ -64,7 +82,11 @@ export const buildSignOperation = }); const { signature } = await signerContext(deviceId, signer => - signer.signTransaction(account.freshAddressPath, Buffer.from(tx.message.serialize())), + signer.signTransaction( + account.freshAddressPath, + Buffer.from(tx.message.serialize()), + getResolution(transaction), + ), ); subscriber.next({ diff --git a/libs/coin-modules/coin-solana/src/signer.ts b/libs/coin-modules/coin-solana/src/signer.ts index b4dfbd316927..440e09675c9b 100644 --- a/libs/coin-modules/coin-solana/src/signer.ts +++ b/libs/coin-modules/coin-solana/src/signer.ts @@ -4,7 +4,16 @@ export type SolanaAddress = { export type SolanaSignature = { signature: Buffer; }; + +export type Resolution = { + tokenAddress?: string; +}; + export interface SolanaSigner { getAddress(path: string, display?: boolean): Promise; - signTransaction(path: string, txBuffer: Buffer): Promise; + signTransaction( + path: string, + txBuffer: Buffer, + resolution?: Resolution, + ): Promise; } diff --git a/libs/ledgerjs/packages/hw-app-solana/README.md b/libs/ledgerjs/packages/hw-app-solana/README.md index 58ad2c349e5b..0b853001ac72 100644 --- a/libs/ledgerjs/packages/hw-app-solana/README.md +++ b/libs/ledgerjs/packages/hw-app-solana/README.md @@ -8,7 +8,7 @@ Ledger Hardware Wallet Solana JavaScript bindings. -*** +--- ## Are you adding Ledger support to your software wallet? @@ -16,10 +16,10 @@ You may be using this package to communicate with the Solana Nano App. For a smooth and quick integration: -* See the developers’ documentation on the [Developer Portal](https://developers.ledger.com/docs/transport/overview/) and -* Go on [Discord](https://developers.ledger.com/discord-pro/) to chat with developer support and the developer community. +- See the developers’ documentation on the [Developer Portal](https://developers.ledger.com/docs/transport/overview/) and +- Go on [Discord](https://developers.ledger.com/discord-pro/) to chat with developer support and the developer community. -*** +--- ## Notes @@ -39,20 +39,22 @@ If ledger returns error `6808` - enable blind signature in settings (not needed #### Table of Contents -* [Solana](#solana) - * [Parameters](#parameters) - * [Examples](#examples) - * [getAddress](#getaddress) - * [Parameters](#parameters-1) - * [Examples](#examples-1) - * [signTransaction](#signtransaction) - * [Parameters](#parameters-2) - * [Examples](#examples-2) - * [signOffchainMessage](#signoffchainmessage) - * [Parameters](#parameters-3) - * [Examples](#examples-3) - * [getAppConfiguration](#getappconfiguration) - * [Examples](#examples-4) +- [Solana](#solana) + - [Parameters](#parameters) + - [Examples](#examples) + - [getAddress](#getaddress) + - [Parameters](#parameters-1) + - [Examples](#examples-1) + - [signTransaction](#signtransaction) + - [Parameters](#parameters-2) + - [Examples](#examples-2) + - [signOffchainMessage](#signoffchainmessage) + - [Parameters](#parameters-3) + - [Examples](#examples-3) + - [getAppConfiguration](#getappconfiguration) + - [Examples](#examples-4) + - [getChallenge](#getchallenge) + - [provideTrustedName](#providetrustedname) ### Solana @@ -60,8 +62,8 @@ Solana API #### Parameters -* `transport` **Transport** a transport for sending commands to a device -* `scrambleKey` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** a scramble key (optional, default `"solana_default_scramble_key"`) +- `transport` **Transport** a transport for sending commands to a device +- `scrambleKey` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** a scramble key (optional, default `"solana_default_scramble_key"`) #### Examples @@ -79,13 +81,13 @@ all derivation-path indexes will be promoted to hardened indexes. ##### Parameters -* `path` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** a BIP32 path -* `display` **[boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** flag to show display (optional, default `false`) +- `path` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** a BIP32 path +- `display` **[boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** flag to show display (optional, default `false`) ##### Examples ```javascript -solana.getAddress("44'/501'/0'").then(r => r.address) +solana.getAddress("44'/501'/0'").then(r => r.address); ``` Returns **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)<{address: [Buffer](https://nodejs.org/api/buffer.html)}>** an object with the address field @@ -96,13 +98,13 @@ Sign a Solana transaction. ##### Parameters -* `path` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** a BIP32 path -* `txBuffer` **[Buffer](https://nodejs.org/api/buffer.html)** serialized transaction +- `path` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** a BIP32 path +- `txBuffer` **[Buffer](https://nodejs.org/api/buffer.html)** serialized transaction ##### Examples ```javascript -solana.signTransaction("44'/501'/0'", txBuffer).then(r => r.signature) +solana.signTransaction("44'/501'/0'", txBuffer).then(r => r.signature); ``` Returns **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)<{signature: [Buffer](https://nodejs.org/api/buffer.html)}>** an object with the signature field @@ -113,13 +115,13 @@ Sign a Solana off-chain message. ##### Parameters -* `path` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** a BIP32 path -* `msgBuffer` **[Buffer](https://nodejs.org/api/buffer.html)** serialized off-chain message +- `path` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** a BIP32 path +- `msgBuffer` **[Buffer](https://nodejs.org/api/buffer.html)** serialized off-chain message ##### Examples ```javascript -solana.signOffchainMessage("44'/501'/0'", msgBuffer).then(r => r.signature) +solana.signOffchainMessage("44'/501'/0'", msgBuffer).then(r => r.signature); ``` Returns **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)<{signature: [Buffer](https://nodejs.org/api/buffer.html)}>** an object with the signature field @@ -131,7 +133,35 @@ Get application configuration. ##### Examples ```javascript -solana.getAppConfiguration().then(r => r.version) +solana.getAppConfiguration().then(r => r.version); ``` Returns **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)\** application config object + +#### getChallenge + +Method returning a 4 bytes TLV challenge as an hex string + +##### Examples + +```javascript +solana.getChallenge().then(challenge => challenge); +``` + +Returns **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)<[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)>** + +#### provideTrustedName + +Provides a trusted name to be displayed during transactions in place of the token address it is associated to. It shall be run just before a transaction involving the associated address that would be displayed on the device. + +##### Parameters + +- `data` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** a stringified buffer of some TLV encoded data to represent the trusted name + +##### Examples + +```javascript +solana.provideTrustedName(data).then(success => success); +``` + +Returns **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)<[boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)>** a boolean diff --git a/libs/ledgerjs/packages/hw-app-solana/package.json b/libs/ledgerjs/packages/hw-app-solana/package.json index ca20b93ed8e9..cf26189c70a9 100644 --- a/libs/ledgerjs/packages/hw-app-solana/package.json +++ b/libs/ledgerjs/packages/hw-app-solana/package.json @@ -29,14 +29,16 @@ "dependencies": { "@ledgerhq/errors": "workspace:^", "@ledgerhq/hw-transport": "workspace:^", - "bip32-path": "^0.4.2" + "@ledgerhq/live-env": "workspace:*", + "axios": "1.7.7", + "bip32-path": "^0.4.2", + "semver": "7.6.3" }, "devDependencies": { "@ledgerhq/hw-transport-mocker": "workspace:^", "@ledgerhq/hw-transport-node-speculos": "workspace:^", "@types/jest": "^29.5.10", "@types/node": "^20.8.10", - "axios": "1.7.7", "documentation": "14.0.2", "jest": "^29.7.0", "rimraf": "^4.4.1", diff --git a/libs/ledgerjs/packages/hw-app-solana/src/Solana.ts b/libs/ledgerjs/packages/hw-app-solana/src/Solana.ts index 3d6f6ef7c4e2..8516255e648d 100644 --- a/libs/ledgerjs/packages/hw-app-solana/src/Solana.ts +++ b/libs/ledgerjs/packages/hw-app-solana/src/Solana.ts @@ -1,12 +1,16 @@ import Transport from "@ledgerhq/hw-transport"; import { StatusCodes } from "@ledgerhq/errors"; +import { getEnv } from "@ledgerhq/live-env"; import BIPPath from "bip32-path"; +import axios from "axios"; +import semver from "semver"; const P1_NON_CONFIRM = 0x00; const P1_CONFIRM = 0x01; +const P2_INIT = 0x00; const P2_EXTEND = 0x01; const P2_MORE = 0x02; @@ -19,12 +23,20 @@ const INS = { GET_ADDR: 0x05, SIGN: 0x06, SIGN_OFFCHAIN: 0x07, + GET_CHALLENGE: 0x20, + PROVIDE_TRUSTED_NAME: 0x21, }; enum EXTRA_STATUS_CODES { BLIND_SIGNATURE_REQUIRED = 0x6808, } +const TRUSTED_NAME_MIN_VERSION = "1.6.0"; + +export type Resolution = { + tokenAddress?: string; +}; + /** * Solana API * @@ -100,9 +112,17 @@ export default class Solana { async signTransaction( path: string, txBuffer: Buffer, + resolution?: Resolution, ): Promise<{ signature: Buffer; }> { + if (resolution) { + const { version } = await this.getAppConfig(); + if (resolution.tokenAddress && semver.gte(version, TRUSTED_NAME_MIN_VERSION)) { + await this.trustedNameResolutionFlow(resolution.tokenAddress); + } + } + const pathBuffer = this.pathToBuffer(path); // Ledger app supports only a single derivation path per call ATM const pathsCountBuffer = Buffer.alloc(1); @@ -157,6 +177,11 @@ export default class Solana { * solana.getAppConfiguration().then(r => r.version) */ async getAppConfiguration(): Promise { + return this.getAppConfig(); + } + + // Created to be able to call it from signTransaction as getAppConfiguration is decorated to avoid calling it while signing + private async getAppConfig(): Promise { const [blindSigningEnabled, pubKeyDisplayMode, major, minor, patch] = await this.sendToDevice( INS.GET_VERSION, P1_NON_CONFIRM, @@ -169,6 +194,74 @@ export default class Solana { }; } + /** + * Method returning a 4 bytes TLV challenge as an hex string + * + * @returns {Promise} + */ + async getChallenge(): Promise { + return this.transport.send(LEDGER_CLA, INS.GET_CHALLENGE, P1_NON_CONFIRM, P2_INIT).then(res => { + const data = res.toString("hex"); + const fourBytesChallenge = data.slice(0, -4); + const statusCode = data.slice(-4); + + if (statusCode !== "9000") { + throw new Error( + `An error happened while generating the challenge. Status code: ${statusCode}`, + ); + } + return `0x${fourBytesChallenge}`; + }); + } + + /** + * Provides a trusted name to be displayed during transactions in place of the token address it is associated to. It shall be run just before a transaction involving the associated address that would be displayed on the device. + * + * @param data a stringified buffer of some TLV encoded data to represent the trusted name + * @returns a boolean + */ + async provideTrustedName(data: string): Promise { + await this.transport.send( + LEDGER_CLA, + INS.PROVIDE_TRUSTED_NAME, + P1_NON_CONFIRM, + P2_INIT, + Buffer.from(data, "hex"), + ); + + return true; + } + + private async trustedNameResolutionFlow(tokenAddress: string) { + const challenge = await this.getChallenge(); + const trustedNameAPDU = await this.signTokenAddressResolution(tokenAddress, challenge); + + if (trustedNameAPDU) { + await this.provideTrustedName(trustedNameAPDU); + } + } + + private async signTokenAddressResolution(tokenAddress: string, challenge: string) { + return axios + .request<{ + contract: string; + descriptorType: string; + descriptorVersion: number; + owner: string; + signedDescriptor: string; + tokenAccount: string; + }>({ + method: "GET", + url: `${getEnv("NFT_ETH_METADATA_SERVICE")}/v2/solana/owner/${tokenAddress}?challenge=${challenge}`, + }) + .then(({ data }) => { + return data.signedDescriptor; + }) + .catch(() => { + return null; + }); + } + private pathToBuffer(originalPath: string) { const path = originalPath .split("/") @@ -191,13 +284,13 @@ export default class Solana { private async sendToDevice(instruction: number, p1: number, payload: Buffer) { /* * By default transport will throw if status code is not OK. - * For some pyaloads we need to enable blind sign in the app settings + * For some payloads we need to enable blind sign in the app settings * and this is reported with StatusCodes.MISSING_CRITICAL_PARAMETER first byte prefix * so we handle it and show a user friendly error message. */ const acceptStatusList = [StatusCodes.OK, EXTRA_STATUS_CODES.BLIND_SIGNATURE_REQUIRED]; - let p2 = 0; + let p2 = P2_INIT; let payload_offset = 0; if (payload.length > MAX_PAYLOAD) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6a8a7be68e9f..7255663ec4e3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5339,9 +5339,18 @@ importers: '@ledgerhq/hw-transport': specifier: workspace:^ version: link:../hw-transport + '@ledgerhq/live-env': + specifier: workspace:* + version: link:../../../env + axios: + specifier: 1.7.7 + version: 1.7.7 bip32-path: specifier: ^0.4.2 version: 0.4.2 + semver: + specifier: 7.6.3 + version: 7.6.3 devDependencies: '@ledgerhq/hw-transport-mocker': specifier: workspace:^ @@ -5355,9 +5364,6 @@ importers: '@types/node': specifier: ^20.8.10 version: 20.12.12 - axios: - specifier: 1.7.7 - version: 1.7.7 documentation: specifier: 14.0.2 version: 14.0.2 @@ -33867,7 +33873,7 @@ snapshots: outdent: 0.5.0 prettier: 2.8.8 resolve-from: 5.0.0 - semver: 7.5.4 + semver: 7.6.3 '@changesets/assemble-release-plan@6.0.3': dependencies: @@ -33877,7 +33883,7 @@ snapshots: '@changesets/should-skip-package': 0.1.0 '@changesets/types': 6.0.0 '@manypkg/get-packages': 1.1.3 - semver: 7.5.4 + semver: 7.6.3 '@changesets/changelog-git@0.2.0': dependencies: @@ -33922,7 +33928,7 @@ snapshots: p-limit: 2.3.0 preferred-pm: 3.1.4 resolve-from: 5.0.0 - semver: 7.5.4 + semver: 7.6.3 spawndamnit: 2.0.0 term-size: 2.2.1 @@ -33946,7 +33952,7 @@ snapshots: '@manypkg/get-packages': 1.1.3 chalk: 2.4.2 fs-extra: 7.0.1 - semver: 7.5.4 + semver: 7.6.3 '@changesets/get-github-info@0.6.0(patch_hash=7jzpsqogb5i6art53pk3h33ix4)': dependencies: @@ -45524,7 +45530,7 @@ snapshots: debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 - semver: 7.5.4 + semver: 7.6.3 tsutils: 3.21.0(typescript@4.9.5) optionalDependencies: typescript: 4.9.5 @@ -45538,7 +45544,7 @@ snapshots: debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 - semver: 7.5.4 + semver: 7.6.3 tsutils: 3.21.0(typescript@5.4.3) optionalDependencies: typescript: 5.4.3 @@ -45552,7 +45558,7 @@ snapshots: debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 - semver: 7.5.4 + semver: 7.6.3 tsutils: 3.21.0(typescript@5.6.3) optionalDependencies: typescript: 5.6.3 @@ -53650,7 +53656,7 @@ snapshots: '@babel/parser': 7.24.1 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 - semver: 7.5.4 + semver: 7.6.3 transitivePeerDependencies: - supports-color @@ -55362,7 +55368,7 @@ snapshots: jest-util: 29.7.0 natural-compare: 1.4.0 pretty-format: 29.7.0 - semver: 7.5.4 + semver: 7.6.3 transitivePeerDependencies: - metro - supports-color @@ -56915,7 +56921,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.5.4 + semver: 7.6.3 make-error@1.3.6: {} From 843c476ba5a0c9563140406f9e20b85d5149c4b0 Mon Sep 17 00:00:00 2001 From: Kant Date: Mon, 2 Dec 2024 15:45:31 +0100 Subject: [PATCH 27/35] feat: update warning message for LNS --- apps/ledger-live-desktop/src/config/urls.ts | 3 ++- .../components/TransactionConfirm/index.tsx | 1 + .../families/solana/TransactionConfirmFields.tsx | 15 ++++++++------- .../src/renderer/families/types.ts | 1 + apps/ledger-live-desktop/static/i18n/en/app.json | 2 +- .../src/components/ValidateOnDevice.tsx | 3 +++ .../families/solana/TransactionConfirmFields.tsx | 14 ++++++++++---- apps/ledger-live-mobile/src/live-common-setup.ts | 6 ++++-- .../ledger-live-mobile/src/locales/en/common.json | 2 +- apps/ledger-live-mobile/src/utils/urls.tsx | 3 ++- 10 files changed, 33 insertions(+), 17 deletions(-) diff --git a/apps/ledger-live-desktop/src/config/urls.ts b/apps/ledger-live-desktop/src/config/urls.ts index f45d39ba7361..cff48e77eccb 100644 --- a/apps/ledger-live-desktop/src/config/urls.ts +++ b/apps/ledger-live-desktop/src/config/urls.ts @@ -154,7 +154,8 @@ export const urls = { }, solana: { staking: "https://support.ledger.com/article/4731749170461-zd", - splTokenInfo: "https://support.ledger.com/article/7723954701469-zd", + splTokenInfo: + "https://support.ledger.com/article/Verify-Solana-Address-from-Token-Account-Address", recipient_info: "https://support.ledger.com", ledgerByChorusOneTC: "https://chorus.one/tos", ledgerByFigmentTC: diff --git a/apps/ledger-live-desktop/src/renderer/components/TransactionConfirm/index.tsx b/apps/ledger-live-desktop/src/renderer/components/TransactionConfirm/index.tsx index 61effb690704..23968399b933 100644 --- a/apps/ledger-live-desktop/src/renderer/components/TransactionConfirm/index.tsx +++ b/apps/ledger-live-desktop/src/renderer/components/TransactionConfirm/index.tsx @@ -169,6 +169,7 @@ const TransactionConfirm = ({ parentAccount={parentAccount} transaction={transaction} status={status} + device={device} /> ) : null } diff --git a/apps/ledger-live-desktop/src/renderer/families/solana/TransactionConfirmFields.tsx b/apps/ledger-live-desktop/src/renderer/families/solana/TransactionConfirmFields.tsx index b00c77e1eff9..421f94b9e007 100644 --- a/apps/ledger-live-desktop/src/renderer/families/solana/TransactionConfirmFields.tsx +++ b/apps/ledger-live-desktop/src/renderer/families/solana/TransactionConfirmFields.tsx @@ -4,14 +4,15 @@ 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"; +import { DeviceModelId } from "@ledgerhq/devices"; +import { Link } from "@ledgerhq/react-ui"; const Title: TitleComponent = props => { - const { transaction, account, parentAccount, status } = props; + const { transaction, account, parentAccount, status, device } = props; const transferTokenHelpUrl = useLocalizedUrl(urls.solana.splTokenInfo); const fields = getDeviceTransactionConfig({ @@ -29,7 +30,10 @@ const Title: TitleComponent = props => { } }, [fields]); - if (transaction.model.commandDescriptor?.command.kind === "token.transfer") { + if ( + transaction.model.commandDescriptor?.command.kind === "token.transfer" && + device.modelId === DeviceModelId.nanoS + ) { return ( { - openURL(transferTokenHelpUrl)} - /> + openURL(transferTokenHelpUrl)} /> diff --git a/apps/ledger-live-desktop/src/renderer/families/types.ts b/apps/ledger-live-desktop/src/renderer/families/types.ts index 52c13aedefe1..b6adf482b4eb 100644 --- a/apps/ledger-live-desktop/src/renderer/families/types.ts +++ b/apps/ledger-live-desktop/src/renderer/families/types.ts @@ -169,6 +169,7 @@ export type LLDCoinFamily< parentAccount: A | null | undefined; transaction: T; status: TS; + device: Device; }>; footer?: React.ComponentType<{ diff --git a/apps/ledger-live-desktop/static/i18n/en/app.json b/apps/ledger-live-desktop/static/i18n/en/app.json index e278d3a5520a..d7255ea8cbea 100644 --- a/apps/ledger-live-desktop/static/i18n/en/app.json +++ b/apps/ledger-live-desktop/static/i18n/en/app.json @@ -3746,7 +3746,7 @@ }, "token": { "frozenStateWarning": "Account assets are frozen!", - "transferWarning": "Solana SPL tokens transactions have unique characteristics. To learn more, visit: <0>ledger.com/spl" + "transferWarning": "To verify the recipient address for Solana tokens using a Ledger Nano S™, <0>follow these instructions." } }, "ethereum": { diff --git a/apps/ledger-live-mobile/src/components/ValidateOnDevice.tsx b/apps/ledger-live-mobile/src/components/ValidateOnDevice.tsx index 44b79de4f3a2..65d9bfb4c08b 100644 --- a/apps/ledger-live-mobile/src/components/ValidateOnDevice.tsx +++ b/apps/ledger-live-mobile/src/components/ValidateOnDevice.tsx @@ -96,6 +96,7 @@ type SubComponentCommonProps = { parentAccount?: Account | null | undefined; transaction: Transaction; status: TransactionStatus; + device: Device; }; export default function ValidateOnDevice({ @@ -188,6 +189,7 @@ export default function ValidateOnDevice({ parentAccount={parentAccount} transaction={transaction} status={status} + device={device} /> ) : ( {titleWording} @@ -223,6 +225,7 @@ export default function ValidateOnDevice({ transaction={transaction} recipientWording={recipientWording} status={status} + device={device} /> ) : null} diff --git a/apps/ledger-live-mobile/src/families/solana/TransactionConfirmFields.tsx b/apps/ledger-live-mobile/src/families/solana/TransactionConfirmFields.tsx index 4bf31f76ebfd..2041e5089098 100644 --- a/apps/ledger-live-mobile/src/families/solana/TransactionConfirmFields.tsx +++ b/apps/ledger-live-mobile/src/families/solana/TransactionConfirmFields.tsx @@ -10,6 +10,8 @@ import { Transaction, TransactionStatus, } from "@ledgerhq/live-common/families/solana/types"; +import { Device } from "@ledgerhq/live-common/hw/actions/types"; +import { DeviceModelId } from "@ledgerhq/devices"; import Alert from "~/components/Alert"; import { urls } from "~/utils/urls"; import LText from "~/components/LText"; @@ -20,17 +22,21 @@ type SolanaFieldComponentProps = { transaction: Transaction; status: TransactionStatus; field: DeviceTransactionField; + device: Device; }; -const Warning = ({ transaction }: SolanaFieldComponentProps) => { +const Warning = ({ transaction, device }: SolanaFieldComponentProps) => { invariant(transaction.family === "solana", "solana transaction"); - if (transaction.model.commandDescriptor?.command.kind === "token.transfer") { + if ( + transaction.model.commandDescriptor?.command.kind === "token.transfer" && + device.modelId === DeviceModelId.nanoS + ) { return ( - + - Linking.openURL(urls.solana.splTokenInfo)} type="color" /> + Linking.openURL(urls.solana.splTokenInfo)} /> diff --git a/apps/ledger-live-mobile/src/live-common-setup.ts b/apps/ledger-live-mobile/src/live-common-setup.ts index b5579874dbaf..6132be9604e0 100644 --- a/apps/ledger-live-mobile/src/live-common-setup.ts +++ b/apps/ledger-live-mobile/src/live-common-setup.ts @@ -18,7 +18,7 @@ import { setDeviceMode } from "@ledgerhq/live-common/hw/actions/app"; import { getDeviceModel } from "@ledgerhq/devices"; import { DescriptorEvent } from "@ledgerhq/hw-transport"; import VersionNumber from "react-native-version-number"; -import type { DeviceModelId } from "@ledgerhq/types-devices"; +import { DeviceModelId } from "@ledgerhq/types-devices"; import { Platform } from "react-native"; import { setSecp256k1Instance } from "@ledgerhq/live-common/families/bitcoin/logic"; import { setGlobalOnBridgeError } from "@ledgerhq/live-common/bridge/useBridgeTransaction"; @@ -188,7 +188,9 @@ if (__DEV__ && Config.DEVICE_PROXY_URL) { map(({ type, descriptor }) => ({ type, id: `httpdebug|${descriptor}`, - deviceModel: getDeviceModel((Config?.FALLBACK_DEVICE_MODEL_ID as DeviceModelId) || "nanoX"), + deviceModel: getDeviceModel( + (Config?.FALLBACK_DEVICE_MODEL_ID as DeviceModelId) || DeviceModelId.nanoX, + ), wired: Config?.FALLBACK_DEVICE_WIRED === "YES", name: descriptor, })), diff --git a/apps/ledger-live-mobile/src/locales/en/common.json b/apps/ledger-live-mobile/src/locales/en/common.json index 8147f6a48d2d..e12d323f9b0f 100644 --- a/apps/ledger-live-mobile/src/locales/en/common.json +++ b/apps/ledger-live-mobile/src/locales/en/common.json @@ -5977,7 +5977,7 @@ }, "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" + "transferWarning": "To verify the recipient address for Solana tokens using a Ledger Nano S™, <0>follow these instructions." } }, "near": { diff --git a/apps/ledger-live-mobile/src/utils/urls.tsx b/apps/ledger-live-mobile/src/utils/urls.tsx index 726b39dd6636..00f5fb159d0c 100644 --- a/apps/ledger-live-mobile/src/utils/urls.tsx +++ b/apps/ledger-live-mobile/src/utils/urls.tsx @@ -168,7 +168,8 @@ export const urls = { solana: { supportPage: "https://support.ledger.com", stakingPage: "https://support.ledger.com/article/4731749170461-zd", - splTokenInfo: "https://support.ledger.com/article/7723954701469-zd", + splTokenInfo: + "https://support.ledger.com/article/Verify-Solana-Address-from-Token-Account-Address", }, resources: { gettingStarted: From 5d95b3bf86e1a001ce33e2d46fdb1e71cd8b7552 Mon Sep 17 00:00:00 2001 From: Kant Date: Wed, 18 Dec 2024 16:00:57 +0100 Subject: [PATCH 28/35] feat: clear sign SPL token address --- .../coin-solana/src/signOperation.ts | 27 +++++++-- libs/coin-modules/coin-solana/src/signer.ts | 7 +++ libs/ledger-live-common/package.json | 8 ++- .../src/families/solana/setup.ts | 55 ++++++++++++++++- .../src/hw/actions/transaction.ts | 1 + .../cal/src/certificate.integ.test.ts | 2 +- libs/ledger-services/cal/src/certificate.ts | 4 +- libs/ledger-services/trust/src/index.ts | 3 +- .../packages/hw-app-solana/package.json | 2 - .../packages/hw-app-solana/src/Solana.ts | 60 +++---------------- .../packages/types-live/src/bridge.ts | 2 + pnpm-lock.yaml | 13 ++-- 12 files changed, 110 insertions(+), 74 deletions(-) diff --git a/libs/coin-modules/coin-solana/src/signOperation.ts b/libs/coin-modules/coin-solana/src/signOperation.ts index a0789d9cb883..9b0c7a467fc6 100644 --- a/libs/coin-modules/coin-solana/src/signOperation.ts +++ b/libs/coin-modules/coin-solana/src/signOperation.ts @@ -21,6 +21,7 @@ import { encodeOperationId } from "@ledgerhq/coin-framework/operation"; import { assertUnreachable } from "./utils"; import { ChainAPI } from "./api"; import { SignerContext } from "@ledgerhq/coin-framework/signer"; +import { DeviceModelId } from "@ledgerhq/devices"; const buildOptimisticOperation = (account: Account, transaction: Transaction): SolanaOperation => { if (transaction.model.commandDescriptor === undefined) { @@ -45,19 +46,37 @@ const buildOptimisticOperation = (account: Account, transaction: Transaction): S return optimisticOp; }; -function getResolution(transaction: Transaction): Resolution | undefined { +function getResolution( + transaction: Transaction, + deviceModelId?: DeviceModelId, +): Resolution | undefined { if (!transaction.subAccountId || !transaction.model.commandDescriptor) return; const { command } = transaction.model.commandDescriptor; switch (command.kind) { case "token.transfer": { + if (command.recipientDescriptor.shouldCreateAsAssociatedTokenAccount) { + return { + deviceModelId, + createATA: { + address: command.recipientDescriptor.walletAddress, + mintAddress: command.mintAddress, + }, + }; + } return { + deviceModelId, tokenAddress: command.recipientDescriptor.tokenAccAddress, }; } + // Not sure we need to handle this case as we don't use the TLV descriptor on the steps of createATA case "token.createATA": { return { - tokenAddress: command.associatedTokenAccountAddress, + deviceModelId, + createATA: { + address: command.owner, + mintAddress: command.mint, + }, }; } } @@ -68,7 +87,7 @@ export const buildSignOperation = signerContext: SignerContext, api: () => Promise, ): AccountBridge["signOperation"] => - ({ account, deviceId, transaction }) => + ({ account, deviceId, deviceModelId, transaction }) => new Observable(subscriber => { const main = async () => { const [tx, recentBlockhash, signOnChainTransaction] = await buildTransactionWithAPI( @@ -85,7 +104,7 @@ export const buildSignOperation = signer.signTransaction( account.freshAddressPath, Buffer.from(tx.message.serialize()), - getResolution(transaction), + getResolution(transaction, deviceModelId), ), ); diff --git a/libs/coin-modules/coin-solana/src/signer.ts b/libs/coin-modules/coin-solana/src/signer.ts index 440e09675c9b..b3707495df9d 100644 --- a/libs/coin-modules/coin-solana/src/signer.ts +++ b/libs/coin-modules/coin-solana/src/signer.ts @@ -1,3 +1,5 @@ +import { DeviceModelId } from "@ledgerhq/devices"; + export type SolanaAddress = { address: Buffer; }; @@ -6,7 +8,12 @@ export type SolanaSignature = { }; export type Resolution = { + deviceModelId?: DeviceModelId | undefined; tokenAddress?: string; + createATA?: { + address: string; + mintAddress: string; + }; }; export interface SolanaSigner { diff --git a/libs/ledger-live-common/package.json b/libs/ledger-live-common/package.json index 76ccb78270aa..d6278ebfb373 100644 --- a/libs/ledger-live-common/package.json +++ b/libs/ledger-live-common/package.json @@ -175,9 +175,11 @@ "@ledgerhq/hw-app-trx": "workspace:^", "@ledgerhq/hw-app-vet": "workspace:^", "@ledgerhq/hw-app-xrp": "workspace:^", + "@ledgerhq/hw-bolos": "workspace:*", "@ledgerhq/hw-transport": "workspace:^", "@ledgerhq/hw-transport-mocker": "workspace:^", "@ledgerhq/ledger-cal-service": "workspace:^", + "@ledgerhq/ledger-trust-service": "workspace:*", "@ledgerhq/live-app-sdk": "^0.8.1", "@ledgerhq/live-config": "workspace:^", "@ledgerhq/live-countervalues": "workspace:^", @@ -268,6 +270,7 @@ "@types/react": "^18.2.21", "@types/uuid": "^8.3.4", "benchmark": "^2.1.4", + "buffer": "6.0.3", "camelcase": "^6.2.1", "cross-env": "^7.0.3", "env-cmd": "*", @@ -294,9 +297,8 @@ "ts-jest": "^29.1.1", "ts-node": "^10.4.0", "typescript": "5.1.3", + "undici": "6.19.2", "uuid": "^8.3.2", - "ws": "7", - "buffer": "6.0.3", - "undici": "6.19.2" + "ws": "7" } } diff --git a/libs/ledger-live-common/src/families/solana/setup.ts b/libs/ledger-live-common/src/families/solana/setup.ts index ea9dd49cdd34..aa7ad35f8e57 100644 --- a/libs/ledger-live-common/src/families/solana/setup.ts +++ b/libs/ledger-live-common/src/families/solana/setup.ts @@ -1,5 +1,6 @@ // Goal of this file is to inject all necessary device/signer dependency to coin-modules +import semver from "semver"; import Solana from "@ledgerhq/hw-app-solana"; import Transport from "@ledgerhq/hw-transport"; import type { Bridge } from "@ledgerhq/types-live"; @@ -8,11 +9,63 @@ import { createBridges } from "@ledgerhq/coin-solana/bridge/js"; import makeCliTools from "@ledgerhq/coin-solana/cli-transaction"; import solanaResolver from "@ledgerhq/coin-solana/hw-getAddress"; import { SolanaAccount, Transaction, TransactionStatus } from "@ledgerhq/coin-solana/types"; +import { DeviceModelId } from "@ledgerhq/devices"; +import { loadPKI } from "@ledgerhq/hw-bolos"; +import calService from "@ledgerhq/ledger-cal-service"; +import trustService from "@ledgerhq/ledger-trust-service"; import { CreateSigner, createResolver, executeWithSigner } from "../../bridge/setup"; import type { Resolver } from "../../hw/getAddress/types"; +const TRUSTED_NAME_MIN_VERSION = "1.6.1"; + const createSigner: CreateSigner = (transport: Transport) => { - return new Solana(transport); + const app = new Solana(transport); + return { + getAddress: app.getAddress, + signTransaction: async (path, tx, resolution) => { + if (resolution) { + if (!resolution.deviceModelId) { + throw new Error("Resolution provided without a deviceModelId"); + } + + if (resolution.deviceModelId !== DeviceModelId.nanoS) { + const { descriptor, signature } = await calService.getCertificate( + resolution.deviceModelId, + ); + + await loadPKI(transport, "TRUSTED_NAME", descriptor, signature); + + // TODO throw error to update the app and os if wrong version of the app instead of allowing non clear signing flows + const { version } = await app.getAppConfiguration(); + if (resolution.tokenAddress && semver.gte(version, TRUSTED_NAME_MIN_VERSION)) { + const challenge = await app.getChallenge(); + const { signedDescriptor } = await trustService.getOwnerAddress( + resolution.tokenAddress, + challenge, + ); + + if (signedDescriptor) { + await app.provideTrustedName(signedDescriptor); + } + } + if (resolution.createATA && semver.gte(version, TRUSTED_NAME_MIN_VERSION)) { + const challenge = await app.getChallenge(); + const { signedDescriptor } = await trustService.computedTokenAddress( + resolution.createATA.address, + resolution.createATA.mintAddress, + challenge, + ); + + if (signedDescriptor) { + await app.provideTrustedName(signedDescriptor); + } + } + } + } + + return app.signTransaction(path, tx); + }, + }; }; const bridge: Bridge = createBridges( diff --git a/libs/ledger-live-common/src/hw/actions/transaction.ts b/libs/ledger-live-common/src/hw/actions/transaction.ts index 3f5baf551b87..d70568dcc1f6 100644 --- a/libs/ledger-live-common/src/hw/actions/transaction.ts +++ b/libs/ledger-live-common/src/hw/actions/transaction.ts @@ -151,6 +151,7 @@ export const createAction = ( account: mainAccount, transaction, deviceId: device.deviceId, + deviceModelId: device.modelId, }) .pipe( catchError(error => diff --git a/libs/ledger-services/cal/src/certificate.integ.test.ts b/libs/ledger-services/cal/src/certificate.integ.test.ts index 086dc3119316..e8e15e1902f0 100644 --- a/libs/ledger-services/cal/src/certificate.integ.test.ts +++ b/libs/ledger-services/cal/src/certificate.integ.test.ts @@ -40,7 +40,7 @@ describe("getCertificate", () => { "returns all data in expected format for $device", async ({ device, descriptor, signature }) => { // When - const result = await getCertificate(device, "1.3.0", { env: "test", signatureKind: "test" }); + const result = await getCertificate(device, { env: "test", signatureKind: "test" }); // Then expect(result).toEqual({ diff --git a/libs/ledger-services/cal/src/certificate.ts b/libs/ledger-services/cal/src/certificate.ts index 56b74ba0a680..42a0937a9647 100644 --- a/libs/ledger-services/cal/src/certificate.ts +++ b/libs/ledger-services/cal/src/certificate.ts @@ -42,11 +42,9 @@ export type CertificateInfo = { /** * Retrieve PKI certificate * @param device - * @param version semver */ export async function getCertificate( device: Device, - version: string, { env = "prod", signatureKind = "prod", ref = undefined }: ServiceOption = DEFAULT_OPTION, ): Promise { const { data } = await network({ @@ -56,7 +54,7 @@ export async function getCertificate( output: "id,target_device,not_valid_after,public_key_usage,certificate_version,descriptor", target_device: DeviceModel[device], public_key_usage: "trusted_name", - note_valid_after: version, + latest: true, ref, }, }); diff --git a/libs/ledger-services/trust/src/index.ts b/libs/ledger-services/trust/src/index.ts index f2f45e4a7d65..d5844a51cbe1 100644 --- a/libs/ledger-services/trust/src/index.ts +++ b/libs/ledger-services/trust/src/index.ts @@ -4,8 +4,9 @@ * Use only exposed methods below outside of this module. */ -import { getOwnerAddress } from "./solana"; +import { getOwnerAddress, computedTokenAddress } from "./solana"; export default { getOwnerAddress, + computedTokenAddress, }; diff --git a/libs/ledgerjs/packages/hw-app-solana/package.json b/libs/ledgerjs/packages/hw-app-solana/package.json index cf26189c70a9..f40044536ab8 100644 --- a/libs/ledgerjs/packages/hw-app-solana/package.json +++ b/libs/ledgerjs/packages/hw-app-solana/package.json @@ -29,8 +29,6 @@ "dependencies": { "@ledgerhq/errors": "workspace:^", "@ledgerhq/hw-transport": "workspace:^", - "@ledgerhq/live-env": "workspace:*", - "axios": "1.7.7", "bip32-path": "^0.4.2", "semver": "7.6.3" }, diff --git a/libs/ledgerjs/packages/hw-app-solana/src/Solana.ts b/libs/ledgerjs/packages/hw-app-solana/src/Solana.ts index 8516255e648d..3e7c7b926081 100644 --- a/libs/ledgerjs/packages/hw-app-solana/src/Solana.ts +++ b/libs/ledgerjs/packages/hw-app-solana/src/Solana.ts @@ -1,11 +1,8 @@ import Transport from "@ledgerhq/hw-transport"; import { StatusCodes } from "@ledgerhq/errors"; -import { getEnv } from "@ledgerhq/live-env"; import BIPPath from "bip32-path"; -import axios from "axios"; -import semver from "semver"; const P1_NON_CONFIRM = 0x00; const P1_CONFIRM = 0x01; @@ -31,12 +28,6 @@ enum EXTRA_STATUS_CODES { BLIND_SIGNATURE_REQUIRED = 0x6808, } -const TRUSTED_NAME_MIN_VERSION = "1.6.0"; - -export type Resolution = { - tokenAddress?: string; -}; - /** * Solana API * @@ -59,7 +50,13 @@ export default class Solana { this.transport = transport; this.transport.decorateAppAPIMethods( this, - ["getAddress", "signTransaction", "getAppConfiguration"], + [ + "getAddress", + "signTransaction", + "getAppConfiguration", + "getChallenge", + "provideTrustedName", + ], scrambleKey, ); } @@ -112,17 +109,9 @@ export default class Solana { async signTransaction( path: string, txBuffer: Buffer, - resolution?: Resolution, ): Promise<{ signature: Buffer; }> { - if (resolution) { - const { version } = await this.getAppConfig(); - if (resolution.tokenAddress && semver.gte(version, TRUSTED_NAME_MIN_VERSION)) { - await this.trustedNameResolutionFlow(resolution.tokenAddress); - } - } - const pathBuffer = this.pathToBuffer(path); // Ledger app supports only a single derivation path per call ATM const pathsCountBuffer = Buffer.alloc(1); @@ -177,11 +166,6 @@ export default class Solana { * solana.getAppConfiguration().then(r => r.version) */ async getAppConfiguration(): Promise { - return this.getAppConfig(); - } - - // Created to be able to call it from signTransaction as getAppConfiguration is decorated to avoid calling it while signing - private async getAppConfig(): Promise { const [blindSigningEnabled, pubKeyDisplayMode, major, minor, patch] = await this.sendToDevice( INS.GET_VERSION, P1_NON_CONFIRM, @@ -232,36 +216,6 @@ export default class Solana { return true; } - private async trustedNameResolutionFlow(tokenAddress: string) { - const challenge = await this.getChallenge(); - const trustedNameAPDU = await this.signTokenAddressResolution(tokenAddress, challenge); - - if (trustedNameAPDU) { - await this.provideTrustedName(trustedNameAPDU); - } - } - - private async signTokenAddressResolution(tokenAddress: string, challenge: string) { - return axios - .request<{ - contract: string; - descriptorType: string; - descriptorVersion: number; - owner: string; - signedDescriptor: string; - tokenAccount: string; - }>({ - method: "GET", - url: `${getEnv("NFT_ETH_METADATA_SERVICE")}/v2/solana/owner/${tokenAddress}?challenge=${challenge}`, - }) - .then(({ data }) => { - return data.signedDescriptor; - }) - .catch(() => { - return null; - }); - } - private pathToBuffer(originalPath: string) { const path = originalPath .split("/") diff --git a/libs/ledgerjs/packages/types-live/src/bridge.ts b/libs/ledgerjs/packages/types-live/src/bridge.ts index 768de66395ae..7e296ef14af5 100644 --- a/libs/ledgerjs/packages/types-live/src/bridge.ts +++ b/libs/ledgerjs/packages/types-live/src/bridge.ts @@ -6,6 +6,7 @@ import { BigNumber } from "bignumber.js"; import type { Observable } from "rxjs"; import type { CryptoCurrency } from "@ledgerhq/types-cryptoassets"; +import type { DeviceModelId } from "@ledgerhq/types-devices"; import type { AccountLike, Account, AccountRaw, TokenAccount, TokenAccountRaw } from "./account"; import type { SignOperationEvent, @@ -68,6 +69,7 @@ export type SignOperationArg0 = account: A; transaction: T; deviceId: DeviceId; + deviceModelId?: DeviceModelId; }; /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7255663ec4e3..99aeb5b549c4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4074,6 +4074,9 @@ importers: '@ledgerhq/hw-app-xrp': specifier: workspace:^ version: link:../ledgerjs/packages/hw-app-xrp + '@ledgerhq/hw-bolos': + specifier: workspace:* + version: link:../ledgerjs/packages/hw-bolos '@ledgerhq/hw-transport': specifier: workspace:^ version: link:../ledgerjs/packages/hw-transport @@ -4083,6 +4086,9 @@ importers: '@ledgerhq/ledger-cal-service': specifier: workspace:^ version: link:../ledger-services/cal + '@ledgerhq/ledger-trust-service': + specifier: workspace:* + version: link:../ledger-services/trust '@ledgerhq/live-app-sdk': specifier: ^0.8.1 version: 0.8.2 @@ -5339,12 +5345,6 @@ importers: '@ledgerhq/hw-transport': specifier: workspace:^ version: link:../hw-transport - '@ledgerhq/live-env': - specifier: workspace:* - version: link:../../../env - axios: - specifier: 1.7.7 - version: 1.7.7 bip32-path: specifier: ^0.4.2 version: 0.4.2 @@ -26338,6 +26338,7 @@ packages: engines: {node: '>=0.6.0', teleport: '>=0.2.0'} deprecated: |- You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other. + (For a CapTP with native promises, see @endo/eventual-send and @endo/captp) qrcode-terminal@0.11.0: From 3aee663d5f4075b1853034097ba47cef162de54a Mon Sep 17 00:00:00 2001 From: Kant Date: Mon, 23 Dec 2024 10:27:04 +0100 Subject: [PATCH 29/35] fix: add error handling for older app versions and unsupported device firmware versions --- .../src/families/solana/setup.ts | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/libs/ledger-live-common/src/families/solana/setup.ts b/libs/ledger-live-common/src/families/solana/setup.ts index aa7ad35f8e57..4a5acb597ff5 100644 --- a/libs/ledger-live-common/src/families/solana/setup.ts +++ b/libs/ledger-live-common/src/families/solana/setup.ts @@ -13,11 +13,23 @@ import { DeviceModelId } from "@ledgerhq/devices"; import { loadPKI } from "@ledgerhq/hw-bolos"; import calService from "@ledgerhq/ledger-cal-service"; import trustService from "@ledgerhq/ledger-trust-service"; +import { FirmwareOrAppUpdateRequired, TransportStatusError } from "@ledgerhq/errors"; import { CreateSigner, createResolver, executeWithSigner } from "../../bridge/setup"; import type { Resolver } from "../../hw/getAddress/types"; const TRUSTED_NAME_MIN_VERSION = "1.6.1"; +async function checkVersion(app: Solana) { + const { version } = await app.getAppConfiguration(); + if (semver.lt(version, TRUSTED_NAME_MIN_VERSION)) { + throw new FirmwareOrAppUpdateRequired(); + } +} + +function isPKIUnsupportedError(err: unknown): err is TransportStatusError { + return err instanceof TransportStatusError && err.message.includes("0x6a81"); +} + const createSigner: CreateSigner = (transport: Transport) => { const app = new Solana(transport); return { @@ -33,11 +45,17 @@ const createSigner: CreateSigner = (transport: Transport) => { resolution.deviceModelId, ); - await loadPKI(transport, "TRUSTED_NAME", descriptor, signature); + try { + await loadPKI(transport, "TRUSTED_NAME", descriptor, signature); + } catch (err) { + if (isPKIUnsupportedError(err)) { + throw new FirmwareOrAppUpdateRequired(); + } + } + + if (resolution.tokenAddress) { + await checkVersion(app); - // TODO throw error to update the app and os if wrong version of the app instead of allowing non clear signing flows - const { version } = await app.getAppConfiguration(); - if (resolution.tokenAddress && semver.gte(version, TRUSTED_NAME_MIN_VERSION)) { const challenge = await app.getChallenge(); const { signedDescriptor } = await trustService.getOwnerAddress( resolution.tokenAddress, @@ -48,7 +66,9 @@ const createSigner: CreateSigner = (transport: Transport) => { await app.provideTrustedName(signedDescriptor); } } - if (resolution.createATA && semver.gte(version, TRUSTED_NAME_MIN_VERSION)) { + if (resolution.createATA) { + await checkVersion(app); + const challenge = await app.getChallenge(); const { signedDescriptor } = await trustService.computedTokenAddress( resolution.createATA.address, From 4231af32b88df2b392f9b89ad0670ee86d556d23 Mon Sep 17 00:00:00 2001 From: Kant Date: Mon, 23 Dec 2024 10:41:43 +0100 Subject: [PATCH 30/35] chore: updated doc --- .../ledgerjs/packages/hw-app-solana/README.md | 79 ++++++++----------- 1 file changed, 34 insertions(+), 45 deletions(-) diff --git a/libs/ledgerjs/packages/hw-app-solana/README.md b/libs/ledgerjs/packages/hw-app-solana/README.md index 0b853001ac72..7a9aa41b1cc9 100644 --- a/libs/ledgerjs/packages/hw-app-solana/README.md +++ b/libs/ledgerjs/packages/hw-app-solana/README.md @@ -8,7 +8,7 @@ Ledger Hardware Wallet Solana JavaScript bindings. ---- +*** ## Are you adding Ledger support to your software wallet? @@ -16,10 +16,10 @@ You may be using this package to communicate with the Solana Nano App. For a smooth and quick integration: -- See the developers’ documentation on the [Developer Portal](https://developers.ledger.com/docs/transport/overview/) and -- Go on [Discord](https://developers.ledger.com/discord-pro/) to chat with developer support and the developer community. +* See the developers’ documentation on the [Developer Portal](https://developers.ledger.com/docs/transport/overview/) and +* Go on [Discord](https://developers.ledger.com/discord-pro/) to chat with developer support and the developer community. ---- +*** ## Notes @@ -39,22 +39,23 @@ If ledger returns error `6808` - enable blind signature in settings (not needed #### Table of Contents -- [Solana](#solana) - - [Parameters](#parameters) - - [Examples](#examples) - - [getAddress](#getaddress) - - [Parameters](#parameters-1) - - [Examples](#examples-1) - - [signTransaction](#signtransaction) - - [Parameters](#parameters-2) - - [Examples](#examples-2) - - [signOffchainMessage](#signoffchainmessage) - - [Parameters](#parameters-3) - - [Examples](#examples-3) - - [getAppConfiguration](#getappconfiguration) - - [Examples](#examples-4) - - [getChallenge](#getchallenge) - - [provideTrustedName](#providetrustedname) +* [Solana](#solana) + * [Parameters](#parameters) + * [Examples](#examples) + * [getAddress](#getaddress) + * [Parameters](#parameters-1) + * [Examples](#examples-1) + * [signTransaction](#signtransaction) + * [Parameters](#parameters-2) + * [Examples](#examples-2) + * [signOffchainMessage](#signoffchainmessage) + * [Parameters](#parameters-3) + * [Examples](#examples-3) + * [getAppConfiguration](#getappconfiguration) + * [Examples](#examples-4) + * [getChallenge](#getchallenge) + * [provideTrustedName](#providetrustedname) + * [Parameters](#parameters-4) ### Solana @@ -62,8 +63,8 @@ Solana API #### Parameters -- `transport` **Transport** a transport for sending commands to a device -- `scrambleKey` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** a scramble key (optional, default `"solana_default_scramble_key"`) +* `transport` **Transport** a transport for sending commands to a device +* `scrambleKey` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** a scramble key (optional, default `"solana_default_scramble_key"`) #### Examples @@ -81,13 +82,13 @@ all derivation-path indexes will be promoted to hardened indexes. ##### Parameters -- `path` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** a BIP32 path -- `display` **[boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** flag to show display (optional, default `false`) +* `path` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** a BIP32 path +* `display` **[boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** flag to show display (optional, default `false`) ##### Examples ```javascript -solana.getAddress("44'/501'/0'").then(r => r.address); +solana.getAddress("44'/501'/0'").then(r => r.address) ``` Returns **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)<{address: [Buffer](https://nodejs.org/api/buffer.html)}>** an object with the address field @@ -98,13 +99,13 @@ Sign a Solana transaction. ##### Parameters -- `path` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** a BIP32 path -- `txBuffer` **[Buffer](https://nodejs.org/api/buffer.html)** serialized transaction +* `path` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** a BIP32 path +* `txBuffer` **[Buffer](https://nodejs.org/api/buffer.html)** serialized transaction ##### Examples ```javascript -solana.signTransaction("44'/501'/0'", txBuffer).then(r => r.signature); +solana.signTransaction("44'/501'/0'", txBuffer).then(r => r.signature) ``` Returns **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)<{signature: [Buffer](https://nodejs.org/api/buffer.html)}>** an object with the signature field @@ -115,13 +116,13 @@ Sign a Solana off-chain message. ##### Parameters -- `path` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** a BIP32 path -- `msgBuffer` **[Buffer](https://nodejs.org/api/buffer.html)** serialized off-chain message +* `path` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** a BIP32 path +* `msgBuffer` **[Buffer](https://nodejs.org/api/buffer.html)** serialized off-chain message ##### Examples ```javascript -solana.signOffchainMessage("44'/501'/0'", msgBuffer).then(r => r.signature); +solana.signOffchainMessage("44'/501'/0'", msgBuffer).then(r => r.signature) ``` Returns **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)<{signature: [Buffer](https://nodejs.org/api/buffer.html)}>** an object with the signature field @@ -133,7 +134,7 @@ Get application configuration. ##### Examples ```javascript -solana.getAppConfiguration().then(r => r.version); +solana.getAppConfiguration().then(r => r.version) ``` Returns **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)\** application config object @@ -142,12 +143,6 @@ Returns **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/ Method returning a 4 bytes TLV challenge as an hex string -##### Examples - -```javascript -solana.getChallenge().then(challenge => challenge); -``` - Returns **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)<[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)>** #### provideTrustedName @@ -156,12 +151,6 @@ Provides a trusted name to be displayed during transactions in place of the toke ##### Parameters -- `data` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** a stringified buffer of some TLV encoded data to represent the trusted name - -##### Examples - -```javascript -solana.provideTrustedName(data).then(success => success); -``` +* `data` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** a stringified buffer of some TLV encoded data to represent the trusted name Returns **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)<[boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)>** a boolean From da6758e6c81cfb05366fb19c188f502e629cc4be Mon Sep 17 00:00:00 2001 From: Kant Date: Mon, 23 Dec 2024 10:43:52 +0100 Subject: [PATCH 31/35] chore: remove unused semver dep --- libs/ledgerjs/packages/hw-app-solana/package.json | 3 +-- pnpm-lock.yaml | 4 ---- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/libs/ledgerjs/packages/hw-app-solana/package.json b/libs/ledgerjs/packages/hw-app-solana/package.json index f40044536ab8..da32869a4731 100644 --- a/libs/ledgerjs/packages/hw-app-solana/package.json +++ b/libs/ledgerjs/packages/hw-app-solana/package.json @@ -29,8 +29,7 @@ "dependencies": { "@ledgerhq/errors": "workspace:^", "@ledgerhq/hw-transport": "workspace:^", - "bip32-path": "^0.4.2", - "semver": "7.6.3" + "bip32-path": "^0.4.2" }, "devDependencies": { "@ledgerhq/hw-transport-mocker": "workspace:^", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 99aeb5b549c4..f41142fe2cfa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5348,9 +5348,6 @@ importers: bip32-path: specifier: ^0.4.2 version: 0.4.2 - semver: - specifier: 7.6.3 - version: 7.6.3 devDependencies: '@ledgerhq/hw-transport-mocker': specifier: workspace:^ @@ -26338,7 +26335,6 @@ packages: engines: {node: '>=0.6.0', teleport: '>=0.2.0'} deprecated: |- You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other. - (For a CapTP with native promises, see @endo/eventual-send and @endo/captp) qrcode-terminal@0.11.0: From 305b53715ca4f10b9d9842a071be88bdeb3f6b3a Mon Sep 17 00:00:00 2001 From: Kant Date: Mon, 6 Jan 2025 17:46:23 +0100 Subject: [PATCH 32/35] fix: use cal token id for spl and contractAddress instead of manual split on id to get the mint address --- libs/coin-modules/coin-solana/src/logic.ts | 18 +- .../coin-solana/src/prepareTransaction.ts | 6 +- .../coin-solana/src/synchronization.ts | 14 +- .../coin-solana/src/transaction.ts | 8 +- .../src/account/serialization.test.ts | 2 +- .../sortByMarketcap.test.ts.snap | 222 +++++++++--------- .../src/__snapshots__/currencies.test.ts.snap | 2 +- .../importers/spl/index.ts | 4 + .../importers/spl/spl.test.ts | 12 +- .../packages/cryptoassets/src/data/spl.json | 2 +- .../packages/cryptoassets/src/data/spl.ts | 1 + .../packages/cryptoassets/src/tokens.ts | 4 +- 12 files changed, 149 insertions(+), 146 deletions(-) diff --git a/libs/coin-modules/coin-solana/src/logic.ts b/libs/coin-modules/coin-solana/src/logic.ts index faba5b53feaa..57a93a06dabc 100644 --- a/libs/coin-modules/coin-solana/src/logic.ts +++ b/libs/coin-modules/coin-solana/src/logic.ts @@ -1,6 +1,6 @@ -import { findTokenById } from "@ledgerhq/cryptoassets"; +import { findTokenByAddressInCurrency } from "@ledgerhq/cryptoassets"; import { PublicKey } from "@solana/web3.js"; -import { AccountLike, TokenAccount } from "@ledgerhq/types-live"; +import { AccountLike } from "@ledgerhq/types-live"; import { StakeMeta } from "./api/chain/account/stake"; import { SolanaStake, SolanaTokenAccount, StakeAction } from "./types"; import { assertUnreachable } from "./utils"; @@ -36,20 +36,8 @@ export function decodeAccountIdWithTokenAccountAddress(accountIdWithTokenAccount }; } -export function toTokenId(currencyId: string, mint: string): string { - return `${currencyId}/spl/${mint}`; -} - -export function toTokenMint(tokenId: string): string { - return tokenId.split("/")[2]; -} - -export function toSubAccMint(subAcc: TokenAccount): string { - return toTokenMint(subAcc.token.id); -} - export function tokenIsListedOnLedger(currencyId: string, mint: string): boolean { - return findTokenById(toTokenId(currencyId, mint))?.type === "TokenCurrency"; + return findTokenByAddressInCurrency(mint, currencyId)?.type === "TokenCurrency"; } export function stakeActions(stake: SolanaStake): StakeAction[] { diff --git a/libs/coin-modules/coin-solana/src/prepareTransaction.ts b/libs/coin-modules/coin-solana/src/prepareTransaction.ts index 669488642712..556f30626420 100644 --- a/libs/coin-modules/coin-solana/src/prepareTransaction.ts +++ b/libs/coin-modules/coin-solana/src/prepareTransaction.ts @@ -140,8 +140,7 @@ const deriveTokenTransferCommandDescriptor = async ( validateMemoCommon(memo, errors); } - const tokenIdParts = subAccount.token.id.split("/"); - const mintAddress = tokenIdParts[tokenIdParts.length - 1]; + const mintAddress = subAccount.token.contractAddress; const mintDecimals = subAccount.token.units[0].magnitude; const senderAssociatedTokenAccountAddress = decodeAccountIdWithTokenAccountAddress( @@ -282,8 +281,7 @@ async function deriveCreateAssociatedTokenAccountCommandDescriptor( const errors: Record = {}; const token = getTokenById(model.uiState.tokenId); - const tokenIdParts = token.id.split("/"); - const mint = tokenIdParts[tokenIdParts.length - 1]; + const mint = token.contractAddress; const associatedTokenAccountAddress = await api.findAssocTokenAccAddress( mainAccount.freshAddress, diff --git a/libs/coin-modules/coin-solana/src/synchronization.ts b/libs/coin-modules/coin-solana/src/synchronization.ts index 569595b523b1..96852872843d 100644 --- a/libs/coin-modules/coin-solana/src/synchronization.ts +++ b/libs/coin-modules/coin-solana/src/synchronization.ts @@ -9,15 +9,13 @@ import { toStakeAccountWithInfo, TransactionDescriptor, } from "./api/chain/web3"; -import { getTokenById } from "@ledgerhq/cryptoassets"; +import { findTokenByAddressInCurrency } from "@ledgerhq/cryptoassets"; import { encodeOperationId } from "@ledgerhq/coin-framework/operation"; import { Awaited, encodeAccountIdWithTokenAccountAddress, isStakeLockUpInForce, tokenIsListedOnLedger, - toTokenId, - toTokenMint, withdrawableFromStake, } from "./logic"; import { @@ -80,7 +78,7 @@ export const getAccountShapeWithAPI = async ( const subAccByMint = pipe( () => mainInitialAcc?.subAccounts ?? [], filter((subAcc): subAcc is TokenAccount => subAcc.type === "TokenAccount"), - keyBy(subAcc => toTokenMint(subAcc.token.id)), + keyBy(subAcc => subAcc.token.contractAddress), v => new Map(toPairs(v)), )(); @@ -260,8 +258,12 @@ function newSubAcc({ const creationDate = new Date((firstTx.info.blockTime ?? Date.now() / 1000) * 1000); - const tokenId = toTokenId(currencyId, assocTokenAcc.info.mint.toBase58()); - const tokenCurrency = getTokenById(tokenId); + const mint = assocTokenAcc.info.mint.toBase58(); + const tokenCurrency = findTokenByAddressInCurrency(mint, currencyId); + + if (!tokenCurrency) { + throw new Error(`token for mint "${mint}" not found`); + } const accosTokenAccPubkey = assocTokenAcc.onChainAcc.pubkey; diff --git a/libs/coin-modules/coin-solana/src/transaction.ts b/libs/coin-modules/coin-solana/src/transaction.ts index 0ed1ef8c3e98..afb798cb1a24 100644 --- a/libs/coin-modules/coin-solana/src/transaction.ts +++ b/libs/coin-modules/coin-solana/src/transaction.ts @@ -20,11 +20,10 @@ import { toTransactionStatusRawCommon as toTransactionStatusRaw, } from "@ledgerhq/coin-framework/serialization"; import type { Account } from "@ledgerhq/types-live"; -import { getTokenById } from "@ledgerhq/cryptoassets/index"; +import { findTokenByAddressInCurrency } from "@ledgerhq/cryptoassets/index"; import { findSubAccountById, getAccountCurrency } from "@ledgerhq/coin-framework/account"; import { formatCurrencyUnit } from "@ledgerhq/coin-framework/currencies"; import { assertUnreachable } from "./utils"; -import { toTokenId } from "./logic"; export const fromTransactionRaw = (tr: TransactionRaw): Transaction => { const common = fromTransactionCommonRaw(tr); @@ -148,7 +147,10 @@ function formatTokenTransfer(mainAccount: Account, tx: Transaction, command: Tok } function formatCreateATA(mainAccount: Account, command: TokenCreateATACommand) { - const token = getTokenById(toTokenId(mainAccount.currency.id, command.mint)); + const token = findTokenByAddressInCurrency(command.mint, mainAccount.currency.id); + if (!token) { + throw new Error(`token for mint "${command.mint}" not found`); + } const str = [` OPT IN TOKEN: ${token.ticker}`].filter(Boolean).join("\n"); return "\n" + str; } diff --git a/libs/ledger-live-common/src/account/serialization.test.ts b/libs/ledger-live-common/src/account/serialization.test.ts index 0aa979de1482..f283c3d7e82f 100644 --- a/libs/ledger-live-common/src/account/serialization.test.ts +++ b/libs/ledger-live-common/src/account/serialization.test.ts @@ -8,7 +8,7 @@ setWalletAPIVersion(WALLET_API_VERSION); setSupportedCurrencies(["solana"]); const Solana = getCryptoCurrencyById("solana"); -const USDC = getTokenById("solana/spl/EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); +const USDC = getTokenById("solana/spl/epjfwdd5aufqssqem2qn1xzybapc8g4weggkzwytdt1v"); describe("serialization", () => { test("TokenAccount extra fields should be serialized/deserialized", () => { diff --git a/libs/ledger-live-common/src/currencies/__snapshots__/sortByMarketcap.test.ts.snap b/libs/ledger-live-common/src/currencies/__snapshots__/sortByMarketcap.test.ts.snap index fba53ae3f29c..0a0a27a95aba 100644 --- a/libs/ledger-live-common/src/currencies/__snapshots__/sortByMarketcap.test.ts.snap +++ b/libs/ledger-live-common/src/currencies/__snapshots__/sortByMarketcap.test.ts.snap @@ -923,7 +923,7 @@ exports[`sortCurrenciesByIds snapshot 1`] = ` "ethereum/erc20/ksm_starter_token", "ethereum/erc20/aditus", "ethereum/erc20/ethopt_io", - "solana/spl/SHDWyBxihqiCj6YekG2GUr7wqKLeLAMK1gHZck9pL6y", + "solana/spl/shdwybxihqicj6yekg2gur7wqklelamk1ghzck9pl6y", "ethereum/erc20/bigboom", "ethereum/erc20/realchain", "ethereum/erc20/ink_protocol", @@ -16236,115 +16236,115 @@ exports[`sortCurrenciesByIds snapshot 1`] = ` "filecoin/erc20/pfil_token", "filecoin/erc20/wrapped_fil", "filecoin/erc20/wrapped_pfil_token", - "solana/spl/27G8MtK7VtTcCHkpASjSDdkWWYfoqT6ggEuKidVJidD4", - "solana/spl/2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk", - "solana/spl/31k88G5Mq7ptbRDf3AM13HAq6wRQHXHikR8hik7wPygk", - "solana/spl/3bRTivrVsitbmCTGtqwp7hxXPsybkjn4XLNtPsHqa3zR", - "solana/spl/3dgCCb15HMQSA4Pn3Tfii5vRk7aRqTH95LJjxzsG2Mug", - "solana/spl/3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh", - "solana/spl/3psH1Mj1f7yUfaD5gh6Zj7epE8hhrMkMETgv5TshQA4o", - "solana/spl/4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R", - "solana/spl/4LLbsb5ReP3yEtYzmXewyGjcir5uXtKFURtaEUVC2AHs", - "solana/spl/4vMsoUT2BWatFweudnQM1xedRLfJgJ7hswhcpz4xgBTy", - "solana/spl/5MAYDfq5yxtudAhtfyuMBuHZjgAbaS9tbEyEQYAhDS5y", - "solana/spl/5oVNBeEEQvYi1cX3ir8Dx5n1P7pdxydbGF2X4TxVusJm", - "solana/spl/6dKCoWjpj5MFU5gWDEFdpUUeBasBLK3wLEwhUzQPAa1e", - "solana/spl/6gnCPhXtLnUD76HjQuSYPENLSZdG8RvDB1pTLM5aLSJA", - "solana/spl/7atgF8KQo4wJrD5ATGX7t1V2zVvykPJbFfNeVf1icFv1", - "solana/spl/7dHbWXmci3dT8UFYWYZweBLXgycu7Y3iL6trKn1Y7ARj", - "solana/spl/7GCihgDB8fe6KNjn2MYtkzZcRjQy3t9GHdC8uHYmW2hr", - "solana/spl/7i5KKsX2weiTkry7jA4ZwSuXGhs5eJBEjY8vVxR4pfRx", - "solana/spl/7Q2afV64in6N6SeZsAAB81TJzwDoD6zpqmHkzi9Dcavn", - "solana/spl/7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs", - "solana/spl/7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU", - "solana/spl/85VBFQZC9TZkfaptBWjvUw7YbZjy52A6mjtPGjstQAmQ", - "solana/spl/947tEoG318GUmyjVYhraNRvWpMX7fpBTDQFBoJvSkSG3", - "solana/spl/a11bdAAuV8iB2fu7X6AxAvDTo1QZ8FXB3kk5eecdasp", - "solana/spl/A1KLoBrKBde8Ty9qtNQUtq3C2ortoC3u7twggz7sEto6", - "solana/spl/AFbX8oGjGpmVFywbVouvhQSRmiW2aR1mohfahi4Y2AdB", - "solana/spl/AMUwxPsqWSd1fbCGzWsrRKDcNoduuWMkdR38qPdit8G8", - "solana/spl/7iT1GRYYhEop2nV1dyCwK2MGyLmPHq47WhPGSwiqcUg5", - "solana/spl/AT79ReYU9XtHUTF5vM6Q4oa9K8w7918Fp5SU7G1MDMQY", - "solana/spl/ATLASXmbPQxBUYbxPsV97usA3fPQYEqzQBUHgiFCUsXx", - "solana/spl/ATRLuHph8dxnPny4WSNW7fxkhbeivBrtWbY6BfB4xpLj", - "solana/spl/AURYydfxJib1ZkTir1Jn1J9ECYUtjb6rKQVmtYaixWPP", - "solana/spl/AujTJJ7aMS8LDo3bFzoyXDwT3jBALUbu4VZhzZdTZLmG", - "solana/spl/AHW5N8iqZobTcBepkSJzZ61XtAuSzBDcpxtrLG6KUKPk", - "solana/spl/BiDB55p4G3n1fGhwKFpxsokBMqgctL4qnZpDH1bVQxMD", - "solana/spl/BLZEEuZUBVqFhj8adcCFPJvPVCiCyVmh3hkJMrU8KuJA", - "solana/spl/bSo13r4TkiE4KumL71LsHTPpL2euBYLFx6h9HP3piy1", - "solana/spl/MEW1gQWJ3nEXg2qgERiKu7FAFj79PHvQVREQUzScPP5", - "solana/spl/CKaKtYvz6dKPyMvYq9Rh3UBrnNqYZAyd7iF4hJtjUvks", - "solana/spl/CvB1ztJvpYQPvdPBePtRzjL4aQidjydtUz61NWgcgQtP", - "solana/spl/4Cnk9EPnW5ixfLZatCPJjDB1PUtcRpVVgTQukm9epump", - "solana/spl/DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263", - "solana/spl/DFL1zNkaGPWm1BqAVqRjCZvHmwTFrEaJtbzJWgseoNJh", - "solana/spl/EchesyfXePKdLtoiZSL8pBe8Myagyy8ZRqsACNCFGnvp", - "solana/spl/EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm", - "solana/spl/EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", - "solana/spl/Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", - "solana/spl/ETAtLmCmsoiEEKfNrHKJ2kYy3MoABhU6NQvpSfij5tDs", - "solana/spl/FANoyuAQZx7AHCnxqsLeWq6te63F6zs6ENkbncCyYUZu", - "solana/spl/FLUXBmPhT3Fd1EDVFdg46YREqHBeNypn1h4EbnTzWERX", - "solana/spl/FoXyMu5xwXre7zEoSvzViRk3nGawHUp9kUh97y2NDhcq", - "solana/spl/FtgGSFADXBtroxq8VCausXRr2of47QBf5AS1NtZCu4GD", - "solana/spl/8wXtPeU6557ETkp9WHFY1n1EcU6NxDvbAggHGsMYiHsB", - "solana/spl/GDfnEsia2WLAW5t8yx2X5j2mkfA74i5kwGdDuZHt7XmG", - "solana/spl/GENEtH5amGSi8kHAtQoezp1XEXwZJ8vcuePYnXdKrMYz", - "solana/spl/GFX1ZjR2P15tmrSwow6FjyDYcEkoFb4p4gJCpLBjaxHD", - "solana/spl/GTH3wG3NErjwcf7VGCoXEXkgXSHvYhx5gtATeeM5JAS1", - "solana/spl/H53UGEyBrB9easo9ego8yYk7o4Zq1G5cCtkxD3E3hZav", - "solana/spl/HHjoYwUp5aU6pnrvN4s2pwEErwXNZKhxKGYjRJMoBjLw", - "solana/spl/HhJpBhRRn4g56VsyLuT8DL5Bv31HkXqsrahTTUCZeZg4", - "solana/spl/hntyVP6YFm1Hg25TN9WGLqM12b8TQmcknKrdu1oxWux", - "solana/spl/HxhWkVpk5NS4Ltg5nij2G671CKXFRKPK8vy271Ub4uEK", - "solana/spl/HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3", - "solana/spl/HzwqbKZw8HxMN6bF2yFZNrht3c2iXXzpKcFu7uBEDKtr", - "solana/spl/BZLbGTNCSFfoth2GYDtwr7e4imWzpR5jqcUuGEwr646K", - "solana/spl/iotEVVZLEywoTn1QdwNPddxPWszn3zFhEot3MfL9fns", - "solana/spl/J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn", - "solana/spl/J2LWsSXx4r3pYbJ1fwuX5Nqo7PPxjcGPpUb2zHNadWKa", - "solana/spl/jtojtomepa8beP8AuQc6eXt5FriJwfFMwQx2v2f9mCL", - "solana/spl/JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN", - "solana/spl/KMNo3nJsBXfcpJTVhZcXLW7RmTwTt4GVFE7suUBo9sS", - "solana/spl/kinXdEcpDQeHPEuQnqmUgtYykqKGVFq6CeVX5iAHJq6", - "solana/spl/LAinEtNLgpmCP9Rvsf5Hn8W6EhNiKLZQti1xfWMLy6X", - "solana/spl/LFNTYraetVioAPnGJht4yNg2aUZFXR776cMeN9VMjXp", - "solana/spl/LSTxxxnJzKDFSLr4dUkPcmCf5VyryEqzPLz5j4bpxFp", - "solana/spl/MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac", - "solana/spl/mb1eu7TzEc71KxDpsmsKoucSSuuoGLv1drys1oP2jh6", - "solana/spl/METAewgxyPbgwsseH8T16a39CQ5VyVxZi9zXiDPY18m", - "solana/spl/MNDEFzGvMt87ueuHvVU9VcTqsAP5b3fTGPsHuuPA5ey", - "solana/spl/ED5nyyWEzpPPiWimP8vYm7sD7TD3LAt3Q3gRTWHzPJBY", - "solana/spl/mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So", - "solana/spl/NeonTjSjsuo3rexg9o6vHuMXw62f9V7zvmu8M8Zut44", - "solana/spl/NFTUkR4u7wKxy9QLaX2TGvd9oZSWoMo4jqSJqdMb7Nk", - "solana/spl/nosXBVoaCTtYdLvKY6Csb4AC8JCdQKKAaWYtx2ZMoo7", - "solana/spl/NYANpAp9Cr7YarBNrby7Xx4xU6No6JKTBuohNA3yscP", - "solana/spl/octo82drBEdm8CSDaEKBymVn86TBtgmPnDdmE64PTqJ", - "solana/spl/orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE", - "solana/spl/poLisWXnNRwC6oBu1vHiuKQzFjGL4XDSu4g9qjz9qVk", - "solana/spl/WskzsKqEW3ZsmrhPAevfVZb6PuuLzWov9mJWZsfDePC", - "solana/spl/RLBxxFkseAZ4RgJH3Sqn8jXxhmGoz9jWxDNJMh8pL7a", - "solana/spl/rndrizKT3MK1iimdxRdWabcF7Zg7AR5T4nud4EkHBof", - "solana/spl/Saber2gLauYim4Mvftnrasomsv6NvAuncvMEZwcLpD1", - "solana/spl/SCSuPPNUSypLBsV4darsrYNg4ANPgaGhKhsA3GmMyjz", - "solana/spl/SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt", - "solana/spl/SHARKSYJjqaNyxVfrpnBN9pjgkhwDhatnMyicWPnr1s", - "solana/spl/7BgBvyjrZX1YKz4oh9mjb8ZScatkkwb8DzFx7LoiVkM3", - "solana/spl/SLNDpmoWTVADgEdndyvWzroNL7zSi1dF9PC3xHGtPwp", - "solana/spl/SNSNkV9zfG5ZKWQs6x4hxvBRV6s8SqMfSGCtECDvdMd", - "solana/spl/So11111111111111111111111111111111111111112", - "solana/spl/2wme8EVkw8qsfSk2B3QeX4S64ac6wxHPXb3GrdckEkio", - "solana/spl/StepAscQoEioFxxWGnh2sLBDFp9d8rvKz2Yp39iDpyT", - "solana/spl/Taki7fi3Zicv7Du1xNAWLaf6mRK7ikdn77HeGzgwvo4", - "solana/spl/TNSRxcUxoT9xBG3de7PiJyTDYu7kskLqcpddxnEJAS6", - "solana/spl/ukHH6c7mMyiWCf1b9pnWe25TSpkDDt3H5pQZgZ74J82", - "solana/spl/UXPhBoR3qG4UCiGNJfV7MqhHyFqKN68g45GoYvAeL2M", - "solana/spl/WENWENvqqNya429ubCdR81ZmD69brwQaaBYY6p3LCpk", - "solana/spl/xxxxa1sKNGwFtw2kFn8XauW9xq8hBZ5kVtcSesTT9fW", - "solana/spl/yomFPUqz1wJwYSfD5tZJUtS3bNb8xs8mx9XzBv8RL39", - "solana/spl/zebeczgi5fSEtbpfQKVZKCJ3WgYXxjkMUkNNx7fLKAF", - "solana/spl/ZEUS1aR7aX8DFFJf5QjWj2ftDDdNTroMNGo8YoQm3Gq", + "solana/spl/27g8mtk7vttcchkpasjsddkwwyfoqt6ggeukidvjidd4", + "solana/spl/2fpytwczlug1mdrwsyop4d6s1tm7hakhyrjknb5w6pxk", + "solana/spl/31k88g5mq7ptbrdf3am13haq6wrqhxhikr8hik7wpygk", + "solana/spl/3brtivrvsitbmctgtqwp7hxxpsybkjn4xlntpshqa3zr", + "solana/spl/3dgccb15hmqsa4pn3tfii5vrk7arqth95ljjxzsg2mug", + "solana/spl/3nz9jmvbmgaqocybic2c7lqcjscmgsaz6vqqtdzcqmjh", + "solana/spl/3psh1mj1f7yufad5gh6zj7epe8hhrmkmetgv5tshqa4o", + "solana/spl/4k3dyjzvzp8emzwuxbbcjevwskkk59s5icnly3qrkx6r", + "solana/spl/4llbsb5rep3yetyzmxewygjcir5uxtkfurtaeuvc2ahs", + "solana/spl/4vmsout2bwatfweudnqm1xedrlfjgj7hswhcpz4xgbty", + "solana/spl/5maydfq5yxtudahtfyumbuhzjgabas9tbeyeqyahds5y", + "solana/spl/5ovnbeeeqvyi1cx3ir8dx5n1p7pdxydbgf2x4txvusjm", + "solana/spl/6dkcowjpj5mfu5gwdefdpuuebasblk3wlewhuzqpaa1e", + "solana/spl/6gncphxtlnud76hjqusypenlszdg8rvdb1ptlm5alsja", + "solana/spl/7atgf8kqo4wjrd5atgx7t1v2zvvykpjbffnevf1icfv1", + "solana/spl/7dhbwxmci3dt8ufywyzweblxgycu7y3il6trkn1y7arj", + "solana/spl/7gcihgdb8fe6knjn2mytkzzcrjqy3t9ghdc8uhymw2hr", + "solana/spl/7i5kksx2weitkry7ja4zwsuxghs5ejbejy8vvxr4pfrx", + "solana/spl/7q2afv64in6n6sezsaab81tjzwdod6zpqmhkzi9dcavn", + "solana/spl/7vfcxtuxx5wjv5jadk17duj4ksgau7utnkj4b963voxs", + "solana/spl/7xkxtg2cw87d97txjsdpbd5jbkhetqa83tzrujosgasu", + "solana/spl/85vbfqzc9tzkfaptbwjvuw7ybzjy52a6mjtpgjstqamq", + "solana/spl/947teog318gumyjvyhranrvwpmx7fpbtdqfbojvsksg3", + "solana/spl/a11bdaauv8ib2fu7x6axavdto1qz8fxb3kk5eecdasp", + "solana/spl/a1klobrkbde8ty9qtnqutq3c2ortoc3u7twggz7seto6", + "solana/spl/afbx8ogjgpmvfywbvouvhqsrmiw2ar1mohfahi4y2adb", + "solana/spl/amuwxpsqwsd1fbcgzwsrrkdcnoduuwmkdr38qpdit8g8", + "solana/spl/analos_7it1gryyheop2nv1dycwk2mgylmphq47whpgswiqcug5", + "solana/spl/at79reyu9xthutf5vm6q4oa9k8w7918fp5su7g1mdmqy", + "solana/spl/atlasxmbpqxbuybxpsv97usa3fpqyeqzqbuhgifcusxx", + "solana/spl/atrluhph8dxnpny4wsnw7fxkhbeivbrtwby6bfb4xplj", + "solana/spl/auryydfxjib1zktir1jn1j9ecyutjb6rkqvmtyaixwpp", + "solana/spl/beer_aujtjj7ams8ldo3bfzoyxdwt3jbalubu4vzhzzdtzlmg", + "solana/spl/ben_the_dog_ahw5n8iqzobtcbepksjzz61xtauszbdcpxtrlg6kukpk", + "solana/spl/bidb55p4g3n1fghwkfpxsokbmqgctl4qnzpdh1bvqxmd", + "solana/spl/blzeeuzubvqfhj8adccfpjvpvcicyvmh3hkjmru8kuja", + "solana/spl/bso13r4tkie4kuml71lshtppl2eubylfx6h9hp3piy1", + "solana/spl/cat_in_a_dogs_world_mew1gqwj3nexg2qgeriku7fafj79phvqvrequzscpp5", + "solana/spl/ckaktyvz6dkpymvyq9rh3ubrnnqyzayd7if4hjtjuvks", + "solana/spl/cvb1ztjvpyqpvdpbeptrzjl4aqidjydtuz61nwgcgqtp", + "solana/spl/daddy_tate_4cnk9epnw5ixflzatcpjjdb1putcrpvvgtqukm9epump", + "solana/spl/dezxaz8z7pnrnrjjz3wxborgixca6xjnb7yab1ppb263", + "solana/spl/dfl1znkagpwm1bqavqrjczvhmwtfreajtbzjwgseonjh", + "solana/spl/echesyfxepkdltoizsl8pbe8myagyy8zrqsacncfgnvp", + "solana/spl/ekpqgsjtjmfqkz9kqansqyxrcf8fbopzlhyxdm65zcjm", + "solana/spl/epjfwdd5aufqssqem2qn1xzybapc8g4weggkzwytdt1v", + "solana/spl/es9vmfrzacermjfrf4h2fyd4kconky11mcce8benwnyb", + "solana/spl/etatlmcmsoieekfnrhkj2kyy3moabhu6nqvpsfij5tds", + "solana/spl/fanoyuaqzx7ahcnxqslewq6te63f6zs6enkbnccyyuzu", + "solana/spl/fluxbmpht3fd1edvfdg46yreqhbenypn1h4ebntzwerx", + "solana/spl/foxymu5xwxre7zeosvzvirk3ngawhup9kuh97y2ndhcq", + "solana/spl/ftggsfadxbtroxq8vcausxrr2of47qbf5as1ntzcu4gd", + "solana/spl/gamestop_8wxtpeu6557etkp9whfy1n1ecu6nxdvbagghgsmyihsb", + "solana/spl/gdfnesia2wlaw5t8yx2x5j2mkfa74i5kwgdduzht7xmg", + "solana/spl/geneth5amgsi8khatqoezp1xexwzj8vcuepynxdkrmyz", + "solana/spl/gfx1zjr2p15tmrswow6fjydycekofb4p4gjcplbjaxhd", + "solana/spl/gth3wg3nerjwcf7vgcoxexkgxshvyhx5gtateem5jas1", + "solana/spl/h53ugeybrb9easo9ego8yyk7o4zq1g5cctkxd3e3hzav", + "solana/spl/hhjoywup5au6pnrvn4s2pweerwxnzkhxkgyjrjmobjlw", + "solana/spl/hhjpbhrrn4g56vsylut8dl5bv31hkxqsrahttuczezg4", + "solana/spl/hntyvp6yfm1hg25tn9wglqm12b8tqmcknkrdu1oxwux", + "solana/spl/hxhwkvpk5ns4ltg5nij2g671ckxfrkpk8vy271ub4uek", + "solana/spl/hz1jovnivvgrgniiyveozevgz58xau3rkwx8eacqbct3", + "solana/spl/hzwqbkzw8hxmn6bf2yfznrht3c2ixxzpkcfu7ubedktr", + "solana/spl/io_bzlbgtncsffoth2gydtwr7e4imwzpr5jqcuugewr646k", + "solana/spl/iotevvzleywotn1qdwnpddxpwszn3zfheot3mfl9fns", + "solana/spl/j1toso1uck3rlmjorhttrvwy9hj7x8v9yyac6y7kgcpn", + "solana/spl/j2lwssxx4r3pybj1fwux5nqo7ppxjcgppub2zhnadwka", + "solana/spl/jtojtomepa8bep8auqc6ext5frijwffmwqx2v2f9mcl", + "solana/spl/jupyiwryjfskupiha7hker8vutaefosybkedznsdvcn", + "solana/spl/kamino_kmno3njsbxfcpjtvhzcxlw7rmtwtt4gvfe7suubo9ss", + "solana/spl/kinxdecpdqehpeuqnqmugtyykqkgvfq6cevx5iahjq6", + "solana/spl/lainetnlgpmcp9rvsf5hn8w6ehniklzqti1xfwmly6x", + "solana/spl/lfntyraetvioapngjht4yng2auzfxr776cmen9vmjxp", + "solana/spl/lstxxxnjzkdfslr4dukpcmcf5vyryeqzplz5j4bpxfp", + "solana/spl/mangoczj36ajzykwvj3vnyu4gtonjfvenjmvvwaxlac", + "solana/spl/mb1eu7tzec71kxdpsmskoucssuuoglv1drys1op2jh6", + "solana/spl/metaewgxypbgwsseh8t16a39cq5vyvxzi9zxidpy18m", + "solana/spl/mndefzgvmt87ueuhvvu9vctqsap5b3ftgpshuupa5ey", + "solana/spl/moo_deng_ed5nyywezpppiwimp8vym7sd7td3lat3q3grtwhzpjby", + "solana/spl/msolzycxhdygdzu16g5qsh3i5k3z3kzk7ytfqcjm7so", + "solana/spl/neontjsjsuo3rexg9o6vhumxw62f9v7zvmu8m8zut44", + "solana/spl/nftukr4u7wkxy9qlax2tgvd9ozswomo4jqsjqdmb7nk", + "solana/spl/nosxbvoacttydlvky6csb4ac8jcdqkkaawytx2zmoo7", + "solana/spl/nyan_nyanpap9cr7yarbnrby7xx4xu6no6jktbuohna3yscp", + "solana/spl/octo82drbedm8csdaekbymvn86tbtgmpnddme64ptqj", + "solana/spl/orcaektdk7lkz57vaayr9qensvepfiu6qemu1kektze", + "solana/spl/poliswxnnrwc6obu1vhiukqzfjgl4xdsu4g9qjz9qvk", + "solana/spl/pundu_wskzskqew3zsmrhpaevfvzb6puulzwov9mjwzsfdepc", + "solana/spl/rlbxxfkseaz4rgjh3sqn8jxxhmgoz9jwxdnjmh8pl7a", + "solana/spl/rndrizkt3mk1iimdxrdwabcf7zg7ar5t4nud4ekhbof", + "solana/spl/saber_protocol_token_saber2glauyim4mvftnrasomsv6nvauncvmezwclpd1", + "solana/spl/scsuppnusyplbsv4darsryng4anpgaghkhsa3gmmyjz", + "solana/spl/serum_srmuapvndxxokk5gt7xd5cuugxmbcoaz2lheuaokwrt", + "solana/spl/sharksyjjqanyxvfrpnbn9pjgkhwdhatnmyicwpnr1s", + "solana/spl/slerf_7bgbvyjrzx1ykz4oh9mjb8zscatkkwb8dzfx7loivkm3", + "solana/spl/slndpmowtvadgedndyvwzronl7zsi1df9pc3xhgtpwp", + "solana/spl/snsnkv9zfg5zkwqs6x4hxvbrv6s8sqmfsgctecdvdmd", + "solana/spl/so11111111111111111111111111111111111111112", + "solana/spl/sols_2wme8evkw8qsfsk2b3qex4s64ac6wxhpxb3grdckekio", + "solana/spl/stepascqoeiofxxwgnh2slbdfp9d8rvkz2yp39idpyt", + "solana/spl/taki7fi3zicv7du1xnawlaf6mrk7ikdn77hegzgwvo4", + "solana/spl/tnsrxcuxot9xbg3de7pijytdyu7ksklqcpddxnejas6", + "solana/spl/ukhh6c7mmyiwcf1b9pnwe25tspkddt3h5pqzgz74j82", + "solana/spl/uxphbor3qg4ucignjfv7mqhhyfqkn68g45goyvael2m", + "solana/spl/wenwenvqqnya429ubcdr81zmd69brwqaabyy6p3lcpk", + "solana/spl/xxxxa1skngwftw2kfn8xauw9xq8hbz5kvtcsestt9fw", + "solana/spl/yomfpuqz1wjwysfd5tzjuts3bnb8xs8mx9xzbv8rl39", + "solana/spl/zebeczgi5fsetbpfqkvzkcj3wgyxxjkmuknnx7flkaf", + "solana/spl/zeus1ar7ax8dffjf5qjwj2ftdddntromngo8yoqm3gq", ] `; diff --git a/libs/ledgerjs/packages/cryptoassets/src/__snapshots__/currencies.test.ts.snap b/libs/ledgerjs/packages/cryptoassets/src/__snapshots__/currencies.test.ts.snap index cf6adca489ed..a6be8e84d569 100644 --- a/libs/ledgerjs/packages/cryptoassets/src/__snapshots__/currencies.test.ts.snap +++ b/libs/ledgerjs/packages/cryptoassets/src/__snapshots__/currencies.test.ts.snap @@ -8,7 +8,7 @@ exports[`all USDT are countervalue enabled 1`] = ` "elrond/esdt/555344542d663863303863", "ethereum/erc20/usd_tether__erc20_", "polygon/erc20/(pos)_tether_usd", - "solana/spl/Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", + "solana/spl/es9vmfrzacermjfrf4h2fyd4kconky11mcce8benwnyb", "ton/jetton/eqcxe6mutqjkfngfarotkot1lzbdiix1kcixrv7nw2id_sds", "tron/trc20/tr7nhqjekqxgtci8q8zy4pl8otszgjlj6t", ] diff --git a/libs/ledgerjs/packages/cryptoassets/src/crypto-assets-importer/importers/spl/index.ts b/libs/ledgerjs/packages/cryptoassets/src/crypto-assets-importer/importers/spl/index.ts index 5bc3338ced80..564552fcd331 100644 --- a/libs/ledgerjs/packages/cryptoassets/src/crypto-assets-importer/importers/spl/index.ts +++ b/libs/ledgerjs/packages/cryptoassets/src/crypto-assets-importer/importers/spl/index.ts @@ -3,6 +3,7 @@ import path from "path"; import { fetchTokensFromCALService } from "../../fetch"; type SPLToken = [ + string, // CAL id number, // chainId string, // name string, // ticker @@ -14,6 +15,7 @@ export const importSPLTokens = async (outputDir: string) => { try { console.log("importing spl tokens..."); const { tokens, hash } = await fetchTokensFromCALService({ blockchain_name: "solana" }, [ + "id", "chain_id", "name", "ticker", @@ -21,6 +23,7 @@ export const importSPLTokens = async (outputDir: string) => { "decimals", ]); const splTokens: SPLToken[] = tokens.map(token => [ + token.id, token.chain_id, token.name, token.ticker, @@ -30,6 +33,7 @@ export const importSPLTokens = async (outputDir: string) => { const filePath = path.join(outputDir, "spl"); const splTypeStringified = `export type SPLToken = [ + string, // CAL id number, // chainId string, // name string, // ticker diff --git a/libs/ledgerjs/packages/cryptoassets/src/crypto-assets-importer/importers/spl/spl.test.ts b/libs/ledgerjs/packages/cryptoassets/src/crypto-assets-importer/importers/spl/spl.test.ts index d18194739306..7d20970c940b 100644 --- a/libs/ledgerjs/packages/cryptoassets/src/crypto-assets-importer/importers/spl/spl.test.ts +++ b/libs/ledgerjs/packages/cryptoassets/src/crypto-assets-importer/importers/spl/spl.test.ts @@ -33,6 +33,7 @@ describe("import Spl tokens", () => { it("should output the file in the correct format", async () => { const expectedFile = `export type SPLToken = [ + string, // CAL id number, // chainId string, // name string, // ticker @@ -57,7 +58,7 @@ export default tokens as SPLToken[]; params: { blockchain_name: "solana", chain_id: undefined, - output: "chain_id,name,ticker,contract_address,decimals", + output: "id,chain_id,name,ticker,contract_address,decimals", standard: undefined, }, }, @@ -66,7 +67,14 @@ export default tokens as SPLToken[]; 1, "spl.json", JSON.stringify([ - [101, "USD Coin", "USDC", "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", 6], + [ + "solana/spl/epjfwdd5aufqssqem2qn1xzybapc8g4weggkzwytdt1v", + 101, + "USD Coin", + "USDC", + "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + 6, + ], ]), ); expect(mockedFs).toHaveBeenNthCalledWith(2, "spl-hash.json", JSON.stringify("commitHash")); diff --git a/libs/ledgerjs/packages/cryptoassets/src/data/spl.json b/libs/ledgerjs/packages/cryptoassets/src/data/spl.json index 12473e06e987..51260521d916 100644 --- a/libs/ledgerjs/packages/cryptoassets/src/data/spl.json +++ b/libs/ledgerjs/packages/cryptoassets/src/data/spl.json @@ -1 +1 @@ -[[1,"Jupiter Perps LP","JLP","27G8MtK7VtTcCHkpASjSDdkWWYfoqT6ggEuKidVJidD4",6],[1,"Wrapped Ethereum (Sollet)","soETH","2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk",6],[1,"Graphite","GP","31k88G5Mq7ptbRDf3AM13HAq6wRQHXHikR8hik7wPygk",9],[1,"Only1 (LIKE)","LIKE","3bRTivrVsitbmCTGtqwp7hxXPsybkjn4XLNtPsHqa3zR",9],[1,"Honeyland","HXD","3dgCCb15HMQSA4Pn3Tfii5vRk7aRqTH95LJjxzsG2Mug",9],[1,"Wrapped BTC (Wormhole)","WBTC","3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh",8],[1,"jeo boden","boden","3psH1Mj1f7yUfaD5gh6Zj7epE8hhrMkMETgv5TshQA4o",9],[1,"Raydium","RAY","4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R",6],[1,"Parcl","PRCL","4LLbsb5ReP3yEtYzmXewyGjcir5uXtKFURtaEUVC2AHs",6],[1,"HONEY","HONEY","4vMsoUT2BWatFweudnQM1xedRLfJgJ7hswhcpz4xgBTy",9],[1,"Access Protocol","ACS","5MAYDfq5yxtudAhtfyuMBuHZjgAbaS9tbEyEQYAhDS5y",6],[1,"Socean staked SOL","scnSOL","5oVNBeEEQvYi1cX3ir8Dx5n1P7pdxydbGF2X4TxVusJm",9],[1,"CHEX","CHEX","6dKCoWjpj5MFU5gWDEFdpUUeBasBLK3wLEwhUzQPAa1e",8],[1,"BSKT","BSKT","6gnCPhXtLnUD76HjQuSYPENLSZdG8RvDB1pTLM5aLSJA",5],[1,"catwifhat","$CWIF","7atgF8KQo4wJrD5ATGX7t1V2zVvykPJbFfNeVf1icFv1",2],[1,"Lido Staked SOL","stSOL","7dHbWXmci3dT8UFYWYZweBLXgycu7Y3iL6trKn1Y7ARj",9],[1,"POPCAT","POPCAT","7GCihgDB8fe6KNjn2MYtkzZcRjQy3t9GHdC8uHYmW2hr",9],[1,"GMT","GMT","7i5KKsX2weiTkry7jA4ZwSuXGhs5eJBEjY8vVxR4pfRx",9],[1,"JPOOL Solana Token","JSOL","7Q2afV64in6N6SeZsAAB81TJzwDoD6zpqmHkzi9Dcavn",9],[1,"Wrapped Ether (Wormhole)","ETH","7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs",8],[1,"Samoyed Coin","SAMO","7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",9],[1,"Wormhole Token","W","85VBFQZC9TZkfaptBWjvUw7YbZjy52A6mjtPGjstQAmQ",6],[1,"Solchat","CHAT","947tEoG318GUmyjVYhraNRvWpMX7fpBTDQFBoJvSkSG3",9],[1,"Allbridge","ABR","a11bdAAuV8iB2fu7X6AxAvDTo1QZ8FXB3kk5eecdasp",9],[1,"Ondo US Dollar Yield","USDY","A1KLoBrKBde8Ty9qtNQUtq3C2ortoC3u7twggz7sEto6",6],[1,"GST","GST","AFbX8oGjGpmVFywbVouvhQSRmiW2aR1mohfahi4Y2AdB",9],[1,"Amulet","AMU","AMUwxPsqWSd1fbCGzWsrRKDcNoduuWMkdR38qPdit8G8",9],[1,"ANALOS","ANALOS","7iT1GRYYhEop2nV1dyCwK2MGyLmPHq47WhPGSwiqcUg5",8],[1,"SpiderSwap","SPDR","AT79ReYU9XtHUTF5vM6Q4oa9K8w7918Fp5SU7G1MDMQY",9],[1,"Star Atlas","ATLAS","ATLASXmbPQxBUYbxPsV97usA3fPQYEqzQBUHgiFCUsXx",8],[1,"Artrade Token","ATR","ATRLuHph8dxnPny4WSNW7fxkhbeivBrtWbY6BfB4xpLj",9],[1,"Aurory","AURY","AURYydfxJib1ZkTir1Jn1J9ECYUtjb6rKQVmtYaixWPP",9],[1,"BEER","$BEER","AujTJJ7aMS8LDo3bFzoyXDwT3jBALUbu4VZhzZdTZLmG",6],[1,"Ben the Dog","BENDOG","AHW5N8iqZobTcBepkSJzZ61XtAuSzBDcpxtrLG6KUKPk",9],[1,"Decimated","DIO","BiDB55p4G3n1fGhwKFpxsokBMqgctL4qnZpDH1bVQxMD",9],[1,"Blaze","BLZE","BLZEEuZUBVqFhj8adcCFPJvPVCiCyVmh3hkJMrU8KuJA",9],[1,"BlazeStake Staked SOL (bSOL)","bSOL","bSo13r4TkiE4KumL71LsHTPpL2euBYLFx6h9HP3piy1",9],[1,"cat in a dogs world","MEW","MEW1gQWJ3nEXg2qgERiKu7FAFj79PHvQVREQUzScPP5",5],[1,"Gari","GARI","CKaKtYvz6dKPyMvYq9Rh3UBrnNqYZAyd7iF4hJtjUvks",9],[1,"Epics Token","EPCT","CvB1ztJvpYQPvdPBePtRzjL4aQidjydtUz61NWgcgQtP",6],[1,"DADDY TATE","DADDY","4Cnk9EPnW5ixfLZatCPJjDB1PUtcRpVVgTQukm9epump",6],[1,"Bonk","BONK","DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263",5],[1,"DeFi Land","DFL","DFL1zNkaGPWm1BqAVqRjCZvHmwTFrEaJtbzJWgseoNJh",9],[1,"Bonfida","FIDA","EchesyfXePKdLtoiZSL8pBe8Myagyy8ZRqsACNCFGnvp",6],[1,"dogwifhat","$WIF","EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm",6],[1,"USD Coin","USDC","EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",6],[1,"USDT","USDT","Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB",6],[1,"Media Network","MEDIA","ETAtLmCmsoiEEKfNrHKJ2kYy3MoABhU6NQvpSfij5tDs",6],[1,"SuperFans.Tech","FAN","FANoyuAQZx7AHCnxqsLeWq6te63F6zs6ENkbncCyYUZu",9],[1,"FluxBot","FLUXB","FLUXBmPhT3Fd1EDVFdg46YREqHBeNypn1h4EbnTzWERX",5],[1,"Famous Fox Federation","FOXY","FoXyMu5xwXre7zEoSvzViRk3nGawHUp9kUh97y2NDhcq",0],[1,"BRZ","BRZ","FtgGSFADXBtroxq8VCausXRr2of47QBf5AS1NtZCu4GD",4],[1,"GameStop","GME","8wXtPeU6557ETkp9WHFY1n1EcU6NxDvbAggHGsMYiHsB",9],[1,"CROWN Token","CROWN","GDfnEsia2WLAW5t8yx2X5j2mkfA74i5kwGdDuZHt7XmG",9],[1,"Genopets","GENE","GENEtH5amGSi8kHAtQoezp1XEXwZJ8vcuePYnXdKrMYz",9],[1,"GooseFX","GOFX","GFX1ZjR2P15tmrSwow6FjyDYcEkoFb4p4gJCpLBjaxHD",9],[1,"Whales Market","WHALES","GTH3wG3NErjwcf7VGCoXEXkgXSHvYhx5gtATeeM5JAS1",6],[1,"MXM","MXM","H53UGEyBrB9easo9ego8yYk7o4Zq1G5cCtkxD3E3hZav",6],[1,"PIP","PIP","HHjoYwUp5aU6pnrvN4s2pwEErwXNZKhxKGYjRJMoBjLw",9],[1,"Myro","$MYRO","HhJpBhRRn4g56VsyLuT8DL5Bv31HkXqsrahTTUCZeZg4",9],[1,"Helium Network Token","HNT","hntyVP6YFm1Hg25TN9WGLqM12b8TQmcknKrdu1oxWux",8],[1,"Hxro (Portal)","HXRO","HxhWkVpk5NS4Ltg5nij2G671CKXFRKPK8vy271Ub4uEK",8],[1,"Pyth Network","PYTH","HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3",6],[1,"EURC","EURC","HzwqbKZw8HxMN6bF2yFZNrht3c2iXXzpKcFu7uBEDKtr",6],[1,"IO","IO","BZLbGTNCSFfoth2GYDtwr7e4imWzpR5jqcUuGEwr646K",8],[1,"Helium IOT","IOT","iotEVVZLEywoTn1QdwNPddxPWszn3zFhEot3MfL9fns",6],[1,"Jito Staked SOL","JITOSOL","J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn",9],[1,"DePlan","DPLN","J2LWsSXx4r3pYbJ1fwuX5Nqo7PPxjcGPpUb2zHNadWKa",6],[1,"JITO","JTO","jtojtomepa8beP8AuQc6eXt5FriJwfFMwQx2v2f9mCL",9],[1,"Jupiter","JUP","JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN",6],[1,"Kamino","KMNO","KMNo3nJsBXfcpJTVhZcXLW7RmTwTt4GVFE7suUBo9sS",6],[1,"KIN","KIN","kinXdEcpDQeHPEuQnqmUgtYykqKGVFq6CeVX5iAHJq6",5],[1,"Laine Stake Token","laineSOL","LAinEtNLgpmCP9Rvsf5Hn8W6EhNiKLZQti1xfWMLy6X",9],[1,"Lifinity","LFNTY","LFNTYraetVioAPnGJht4yNg2aUZFXR776cMeN9VMjXp",6],[1,"Liquid Staking Token","LST","LSTxxxnJzKDFSLr4dUkPcmCf5VyryEqzPLz5j4bpxFp",9],[1,"Mango","MNGO","MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac",6],[1,"Helium Mobile","MOBILE","mb1eu7TzEc71KxDpsmsKoucSSuuoGLv1drys1oP2jh6",6],[1,"Metaplex","META","METAewgxyPbgwsseH8T16a39CQ5VyVxZi9zXiDPY18m",6],[1,"Marinade","MNDE","MNDEFzGvMt87ueuHvVU9VcTqsAP5b3fTGPsHuuPA5ey",9],[1,"Moo Deng","MOODENG","ED5nyyWEzpPPiWimP8vYm7sD7TD3LAt3Q3gRTWHzPJBY",6],[1,"Marinade staked SOL (mSOL)","mSOL","mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So",9],[1,"Neon EVM Token","NEON","NeonTjSjsuo3rexg9o6vHuMXw62f9V7zvmu8M8Zut44",9],[1,"Blockasset","BLOCK","NFTUkR4u7wKxy9QLaX2TGvd9oZSWoMo4jqSJqdMb7Nk",6],[1,"Nosana","NOS","nosXBVoaCTtYdLvKY6Csb4AC8JCdQKKAaWYtx2ZMoo7",6],[1,"NYAN","NYAN","NYANpAp9Cr7YarBNrby7Xx4xU6No6JKTBuohNA3yscP",9],[1,"Octokn","OTK","octo82drBEdm8CSDaEKBymVn86TBtgmPnDdmE64PTqJ",9],[1,"Orca","ORCA","orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE",6],[1,"Star Atlas DAO","POLIS","poLisWXnNRwC6oBu1vHiuKQzFjGL4XDSu4g9qjz9qVk",8],[1,"PUNDU","PUNDU","WskzsKqEW3ZsmrhPAevfVZb6PuuLzWov9mJWZsfDePC",9],[1,"Rollbit Coin","RLB","RLBxxFkseAZ4RgJH3Sqn8jXxhmGoz9jWxDNJMh8pL7a",2],[1,"Render Token","RNDR","rndrizKT3MK1iimdxRdWabcF7Zg7AR5T4nud4EkHBof",8],[1,"Saber Protocol Token","SBR","Saber2gLauYim4Mvftnrasomsv6NvAuncvMEZwcLpD1",6],[1,"Solcasino Token","SCS","SCSuPPNUSypLBsV4darsrYNg4ANPgaGhKhsA3GmMyjz",5],[1,"Serum","SRM","SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt",6],[1,"Sharky","SHARK","SHARKSYJjqaNyxVfrpnBN9pjgkhwDhatnMyicWPnr1s",6],[1,"Shadow Token","SHDW","SHDWyBxihqiCj6YekG2GUr7wqKLeLAMK1gHZck9pL6y",9],[1,"SLERF","SLERF","7BgBvyjrZX1YKz4oh9mjb8ZScatkkwb8DzFx7LoiVkM3",9],[1,"Solend","SLND","SLNDpmoWTVADgEdndyvWzroNL7zSi1dF9PC3xHGtPwp",6],[1,"SynesisOne","SNS","SNSNkV9zfG5ZKWQs6x4hxvBRV6s8SqMfSGCtECDvdMd",9],[1,"Wrapped SOL","SOL","So11111111111111111111111111111111111111112",9],[1,"sols","sols","2wme8EVkw8qsfSk2B3QeX4S64ac6wxHPXb3GrdckEkio",9],[1,"Step","STEP","StepAscQoEioFxxWGnh2sLBDFp9d8rvKz2Yp39iDpyT",9],[1,"Taki","TAKI","Taki7fi3Zicv7Du1xNAWLaf6mRK7ikdn77HeGzgwvo4",9],[1,"Tensor","TNSR","TNSRxcUxoT9xBG3de7PiJyTDYu7kskLqcpddxnEJAS6",9],[1,"BOOK OF MEME","BOME","ukHH6c7mMyiWCf1b9pnWe25TSpkDDt3H5pQZgZ74J82",6],[1,"UXP Governance Token","UXP","UXPhBoR3qG4UCiGNJfV7MqhHyFqKN68g45GoYvAeL2M",9],[1,"Wen","WEN","WENWENvqqNya429ubCdR81ZmD69brwQaaBYY6p3LCpk",5],[1,"Solanium","SLIM","xxxxa1sKNGwFtw2kFn8XauW9xq8hBZ5kVtcSesTT9fW",6],[1,"YOM","YOM","yomFPUqz1wJwYSfD5tZJUtS3bNb8xs8mx9XzBv8RL39",9],[1,"ZEBEC","ZBC","zebeczgi5fSEtbpfQKVZKCJ3WgYXxjkMUkNNx7fLKAF",9],[1,"ZEUS","ZEUS","ZEUS1aR7aX8DFFJf5QjWj2ftDDdNTroMNGo8YoQm3Gq",6]] \ No newline at end of file +[["solana/spl/27g8mtk7vttcchkpasjsddkwwyfoqt6ggeukidvjidd4",1,"Jupiter Perps LP","JLP","27G8MtK7VtTcCHkpASjSDdkWWYfoqT6ggEuKidVJidD4",6],["solana/spl/2fpytwczlug1mdrwsyop4d6s1tm7hakhyrjknb5w6pxk",1,"Wrapped Ethereum (Sollet)","soETH","2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk",6],["solana/spl/31k88g5mq7ptbrdf3am13haq6wrqhxhikr8hik7wpygk",1,"Graphite","GP","31k88G5Mq7ptbRDf3AM13HAq6wRQHXHikR8hik7wPygk",9],["solana/spl/3brtivrvsitbmctgtqwp7hxxpsybkjn4xlntpshqa3zr",1,"Only1 (LIKE)","LIKE","3bRTivrVsitbmCTGtqwp7hxXPsybkjn4XLNtPsHqa3zR",9],["solana/spl/3dgccb15hmqsa4pn3tfii5vrk7arqth95ljjxzsg2mug",1,"Honeyland","HXD","3dgCCb15HMQSA4Pn3Tfii5vRk7aRqTH95LJjxzsG2Mug",9],["solana/spl/3nz9jmvbmgaqocybic2c7lqcjscmgsaz6vqqtdzcqmjh",1,"Wrapped BTC (Wormhole)","WBTC","3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh",8],["solana/spl/3psh1mj1f7yufad5gh6zj7epe8hhrmkmetgv5tshqa4o",1,"jeo boden","boden","3psH1Mj1f7yUfaD5gh6Zj7epE8hhrMkMETgv5TshQA4o",9],["solana/spl/4k3dyjzvzp8emzwuxbbcjevwskkk59s5icnly3qrkx6r",1,"Raydium","RAY","4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R",6],["solana/spl/4llbsb5rep3yetyzmxewygjcir5uxtkfurtaeuvc2ahs",1,"Parcl","PRCL","4LLbsb5ReP3yEtYzmXewyGjcir5uXtKFURtaEUVC2AHs",6],["solana/spl/4vmsout2bwatfweudnqm1xedrlfjgj7hswhcpz4xgbty",1,"HONEY","HONEY","4vMsoUT2BWatFweudnQM1xedRLfJgJ7hswhcpz4xgBTy",9],["solana/spl/5maydfq5yxtudahtfyumbuhzjgabas9tbeyeqyahds5y",1,"Access Protocol","ACS","5MAYDfq5yxtudAhtfyuMBuHZjgAbaS9tbEyEQYAhDS5y",6],["solana/spl/5ovnbeeeqvyi1cx3ir8dx5n1p7pdxydbgf2x4txvusjm",1,"Socean staked SOL","scnSOL","5oVNBeEEQvYi1cX3ir8Dx5n1P7pdxydbGF2X4TxVusJm",9],["solana/spl/6dkcowjpj5mfu5gwdefdpuuebasblk3wlewhuzqpaa1e",1,"CHEX","CHEX","6dKCoWjpj5MFU5gWDEFdpUUeBasBLK3wLEwhUzQPAa1e",8],["solana/spl/6gncphxtlnud76hjqusypenlszdg8rvdb1ptlm5alsja",1,"BSKT","BSKT","6gnCPhXtLnUD76HjQuSYPENLSZdG8RvDB1pTLM5aLSJA",5],["solana/spl/7atgf8kqo4wjrd5atgx7t1v2zvvykpjbffnevf1icfv1",1,"catwifhat","$CWIF","7atgF8KQo4wJrD5ATGX7t1V2zVvykPJbFfNeVf1icFv1",2],["solana/spl/7dhbwxmci3dt8ufywyzweblxgycu7y3il6trkn1y7arj",1,"Lido Staked SOL","stSOL","7dHbWXmci3dT8UFYWYZweBLXgycu7Y3iL6trKn1Y7ARj",9],["solana/spl/7gcihgdb8fe6knjn2mytkzzcrjqy3t9ghdc8uhymw2hr",1,"POPCAT","POPCAT","7GCihgDB8fe6KNjn2MYtkzZcRjQy3t9GHdC8uHYmW2hr",9],["solana/spl/7i5kksx2weitkry7ja4zwsuxghs5ejbejy8vvxr4pfrx",1,"GMT","GMT","7i5KKsX2weiTkry7jA4ZwSuXGhs5eJBEjY8vVxR4pfRx",9],["solana/spl/7q2afv64in6n6sezsaab81tjzwdod6zpqmhkzi9dcavn",1,"JPOOL Solana Token","JSOL","7Q2afV64in6N6SeZsAAB81TJzwDoD6zpqmHkzi9Dcavn",9],["solana/spl/7vfcxtuxx5wjv5jadk17duj4ksgau7utnkj4b963voxs",1,"Wrapped Ether (Wormhole)","ETH","7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs",8],["solana/spl/7xkxtg2cw87d97txjsdpbd5jbkhetqa83tzrujosgasu",1,"Samoyed Coin","SAMO","7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",9],["solana/spl/85vbfqzc9tzkfaptbwjvuw7ybzjy52a6mjtpgjstqamq",1,"Wormhole Token","W","85VBFQZC9TZkfaptBWjvUw7YbZjy52A6mjtPGjstQAmQ",6],["solana/spl/947teog318gumyjvyhranrvwpmx7fpbtdqfbojvsksg3",1,"Solchat","CHAT","947tEoG318GUmyjVYhraNRvWpMX7fpBTDQFBoJvSkSG3",9],["solana/spl/a11bdaauv8ib2fu7x6axavdto1qz8fxb3kk5eecdasp",1,"Allbridge","ABR","a11bdAAuV8iB2fu7X6AxAvDTo1QZ8FXB3kk5eecdasp",9],["solana/spl/a1klobrkbde8ty9qtnqutq3c2ortoc3u7twggz7seto6",1,"Ondo US Dollar Yield","USDY","A1KLoBrKBde8Ty9qtNQUtq3C2ortoC3u7twggz7sEto6",6],["solana/spl/afbx8ogjgpmvfywbvouvhqsrmiw2ar1mohfahi4y2adb",1,"GST","GST","AFbX8oGjGpmVFywbVouvhQSRmiW2aR1mohfahi4Y2AdB",9],["solana/spl/amuwxpsqwsd1fbcgzwsrrkdcnoduuwmkdr38qpdit8g8",1,"Amulet","AMU","AMUwxPsqWSd1fbCGzWsrRKDcNoduuWMkdR38qPdit8G8",9],["solana/spl/analos_7it1gryyheop2nv1dycwk2mgylmphq47whpgswiqcug5",1,"ANALOS","ANALOS","7iT1GRYYhEop2nV1dyCwK2MGyLmPHq47WhPGSwiqcUg5",8],["solana/spl/at79reyu9xthutf5vm6q4oa9k8w7918fp5su7g1mdmqy",1,"SpiderSwap","SPDR","AT79ReYU9XtHUTF5vM6Q4oa9K8w7918Fp5SU7G1MDMQY",9],["solana/spl/atlasxmbpqxbuybxpsv97usa3fpqyeqzqbuhgifcusxx",1,"Star Atlas","ATLAS","ATLASXmbPQxBUYbxPsV97usA3fPQYEqzQBUHgiFCUsXx",8],["solana/spl/atrluhph8dxnpny4wsnw7fxkhbeivbrtwby6bfb4xplj",1,"Artrade Token","ATR","ATRLuHph8dxnPny4WSNW7fxkhbeivBrtWbY6BfB4xpLj",9],["solana/spl/auryydfxjib1zktir1jn1j9ecyutjb6rkqvmtyaixwpp",1,"Aurory","AURY","AURYydfxJib1ZkTir1Jn1J9ECYUtjb6rKQVmtYaixWPP",9],["solana/spl/beer_aujtjj7ams8ldo3bfzoyxdwt3jbalubu4vzhzzdtzlmg",1,"BEER","$BEER","AujTJJ7aMS8LDo3bFzoyXDwT3jBALUbu4VZhzZdTZLmG",6],["solana/spl/ben_the_dog_ahw5n8iqzobtcbepksjzz61xtauszbdcpxtrlg6kukpk",1,"Ben the Dog","BENDOG","AHW5N8iqZobTcBepkSJzZ61XtAuSzBDcpxtrLG6KUKPk",9],["solana/spl/bidb55p4g3n1fghwkfpxsokbmqgctl4qnzpdh1bvqxmd",1,"Decimated","DIO","BiDB55p4G3n1fGhwKFpxsokBMqgctL4qnZpDH1bVQxMD",9],["solana/spl/blzeeuzubvqfhj8adccfpjvpvcicyvmh3hkjmru8kuja",1,"Blaze","BLZE","BLZEEuZUBVqFhj8adcCFPJvPVCiCyVmh3hkJMrU8KuJA",9],["solana/spl/bso13r4tkie4kuml71lshtppl2eubylfx6h9hp3piy1",1,"BlazeStake Staked SOL (bSOL)","bSOL","bSo13r4TkiE4KumL71LsHTPpL2euBYLFx6h9HP3piy1",9],["solana/spl/cat_in_a_dogs_world_mew1gqwj3nexg2qgeriku7fafj79phvqvrequzscpp5",1,"cat in a dogs world","MEW","MEW1gQWJ3nEXg2qgERiKu7FAFj79PHvQVREQUzScPP5",5],["solana/spl/ckaktyvz6dkpymvyq9rh3ubrnnqyzayd7if4hjtjuvks",1,"Gari","GARI","CKaKtYvz6dKPyMvYq9Rh3UBrnNqYZAyd7iF4hJtjUvks",9],["solana/spl/cvb1ztjvpyqpvdpbeptrzjl4aqidjydtuz61nwgcgqtp",1,"Epics Token","EPCT","CvB1ztJvpYQPvdPBePtRzjL4aQidjydtUz61NWgcgQtP",6],["solana/spl/daddy_tate_4cnk9epnw5ixflzatcpjjdb1putcrpvvgtqukm9epump",1,"DADDY TATE","DADDY","4Cnk9EPnW5ixfLZatCPJjDB1PUtcRpVVgTQukm9epump",6],["solana/spl/dezxaz8z7pnrnrjjz3wxborgixca6xjnb7yab1ppb263",1,"Bonk","BONK","DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263",5],["solana/spl/dfl1znkagpwm1bqavqrjczvhmwtfreajtbzjwgseonjh",1,"DeFi Land","DFL","DFL1zNkaGPWm1BqAVqRjCZvHmwTFrEaJtbzJWgseoNJh",9],["solana/spl/echesyfxepkdltoizsl8pbe8myagyy8zrqsacncfgnvp",1,"Bonfida","FIDA","EchesyfXePKdLtoiZSL8pBe8Myagyy8ZRqsACNCFGnvp",6],["solana/spl/ekpqgsjtjmfqkz9kqansqyxrcf8fbopzlhyxdm65zcjm",1,"dogwifhat","$WIF","EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm",6],["solana/spl/epjfwdd5aufqssqem2qn1xzybapc8g4weggkzwytdt1v",1,"USD Coin","USDC","EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",6],["solana/spl/es9vmfrzacermjfrf4h2fyd4kconky11mcce8benwnyb",1,"USDT","USDT","Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB",6],["solana/spl/etatlmcmsoieekfnrhkj2kyy3moabhu6nqvpsfij5tds",1,"Media Network","MEDIA","ETAtLmCmsoiEEKfNrHKJ2kYy3MoABhU6NQvpSfij5tDs",6],["solana/spl/fanoyuaqzx7ahcnxqslewq6te63f6zs6enkbnccyyuzu",1,"SuperFans.Tech","FAN","FANoyuAQZx7AHCnxqsLeWq6te63F6zs6ENkbncCyYUZu",9],["solana/spl/fluxbmpht3fd1edvfdg46yreqhbenypn1h4ebntzwerx",1,"FluxBot","FLUXB","FLUXBmPhT3Fd1EDVFdg46YREqHBeNypn1h4EbnTzWERX",5],["solana/spl/foxymu5xwxre7zeosvzvirk3ngawhup9kuh97y2ndhcq",1,"Famous Fox Federation","FOXY","FoXyMu5xwXre7zEoSvzViRk3nGawHUp9kUh97y2NDhcq",0],["solana/spl/ftggsfadxbtroxq8vcausxrr2of47qbf5as1ntzcu4gd",1,"BRZ","BRZ","FtgGSFADXBtroxq8VCausXRr2of47QBf5AS1NtZCu4GD",4],["solana/spl/gamestop_8wxtpeu6557etkp9whfy1n1ecu6nxdvbagghgsmyihsb",1,"GameStop","GME","8wXtPeU6557ETkp9WHFY1n1EcU6NxDvbAggHGsMYiHsB",9],["solana/spl/gdfnesia2wlaw5t8yx2x5j2mkfa74i5kwgdduzht7xmg",1,"CROWN Token","CROWN","GDfnEsia2WLAW5t8yx2X5j2mkfA74i5kwGdDuZHt7XmG",9],["solana/spl/geneth5amgsi8khatqoezp1xexwzj8vcuepynxdkrmyz",1,"Genopets","GENE","GENEtH5amGSi8kHAtQoezp1XEXwZJ8vcuePYnXdKrMYz",9],["solana/spl/gfx1zjr2p15tmrswow6fjydycekofb4p4gjcplbjaxhd",1,"GooseFX","GOFX","GFX1ZjR2P15tmrSwow6FjyDYcEkoFb4p4gJCpLBjaxHD",9],["solana/spl/gth3wg3nerjwcf7vgcoxexkgxshvyhx5gtateem5jas1",1,"Whales Market","WHALES","GTH3wG3NErjwcf7VGCoXEXkgXSHvYhx5gtATeeM5JAS1",6],["solana/spl/h53ugeybrb9easo9ego8yyk7o4zq1g5cctkxd3e3hzav",1,"MXM","MXM","H53UGEyBrB9easo9ego8yYk7o4Zq1G5cCtkxD3E3hZav",6],["solana/spl/hhjoywup5au6pnrvn4s2pweerwxnzkhxkgyjrjmobjlw",1,"PIP","PIP","HHjoYwUp5aU6pnrvN4s2pwEErwXNZKhxKGYjRJMoBjLw",9],["solana/spl/hhjpbhrrn4g56vsylut8dl5bv31hkxqsrahttuczezg4",1,"Myro","$MYRO","HhJpBhRRn4g56VsyLuT8DL5Bv31HkXqsrahTTUCZeZg4",9],["solana/spl/hntyvp6yfm1hg25tn9wglqm12b8tqmcknkrdu1oxwux",1,"Helium Network Token","HNT","hntyVP6YFm1Hg25TN9WGLqM12b8TQmcknKrdu1oxWux",8],["solana/spl/hxhwkvpk5ns4ltg5nij2g671ckxfrkpk8vy271ub4uek",1,"Hxro (Portal)","HXRO","HxhWkVpk5NS4Ltg5nij2G671CKXFRKPK8vy271Ub4uEK",8],["solana/spl/hz1jovnivvgrgniiyveozevgz58xau3rkwx8eacqbct3",1,"Pyth Network","PYTH","HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3",6],["solana/spl/hzwqbkzw8hxmn6bf2yfznrht3c2ixxzpkcfu7ubedktr",1,"EURC","EURC","HzwqbKZw8HxMN6bF2yFZNrht3c2iXXzpKcFu7uBEDKtr",6],["solana/spl/io_bzlbgtncsffoth2gydtwr7e4imwzpr5jqcuugewr646k",1,"IO","IO","BZLbGTNCSFfoth2GYDtwr7e4imWzpR5jqcUuGEwr646K",8],["solana/spl/iotevvzleywotn1qdwnpddxpwszn3zfheot3mfl9fns",1,"Helium IOT","IOT","iotEVVZLEywoTn1QdwNPddxPWszn3zFhEot3MfL9fns",6],["solana/spl/j1toso1uck3rlmjorhttrvwy9hj7x8v9yyac6y7kgcpn",1,"Jito Staked SOL","JITOSOL","J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn",9],["solana/spl/j2lwssxx4r3pybj1fwux5nqo7ppxjcgppub2zhnadwka",1,"DePlan","DPLN","J2LWsSXx4r3pYbJ1fwuX5Nqo7PPxjcGPpUb2zHNadWKa",6],["solana/spl/jtojtomepa8bep8auqc6ext5frijwffmwqx2v2f9mcl",1,"JITO","JTO","jtojtomepa8beP8AuQc6eXt5FriJwfFMwQx2v2f9mCL",9],["solana/spl/jupyiwryjfskupiha7hker8vutaefosybkedznsdvcn",1,"Jupiter","JUP","JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN",6],["solana/spl/kamino_kmno3njsbxfcpjtvhzcxlw7rmtwtt4gvfe7suubo9ss",1,"Kamino","KMNO","KMNo3nJsBXfcpJTVhZcXLW7RmTwTt4GVFE7suUBo9sS",6],["solana/spl/kinxdecpdqehpeuqnqmugtyykqkgvfq6cevx5iahjq6",1,"KIN","KIN","kinXdEcpDQeHPEuQnqmUgtYykqKGVFq6CeVX5iAHJq6",5],["solana/spl/lainetnlgpmcp9rvsf5hn8w6ehniklzqti1xfwmly6x",1,"Laine Stake Token","laineSOL","LAinEtNLgpmCP9Rvsf5Hn8W6EhNiKLZQti1xfWMLy6X",9],["solana/spl/lfntyraetvioapngjht4yng2auzfxr776cmen9vmjxp",1,"Lifinity","LFNTY","LFNTYraetVioAPnGJht4yNg2aUZFXR776cMeN9VMjXp",6],["solana/spl/lstxxxnjzkdfslr4dukpcmcf5vyryeqzplz5j4bpxfp",1,"Liquid Staking Token","LST","LSTxxxnJzKDFSLr4dUkPcmCf5VyryEqzPLz5j4bpxFp",9],["solana/spl/mangoczj36ajzykwvj3vnyu4gtonjfvenjmvvwaxlac",1,"Mango","MNGO","MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac",6],["solana/spl/mb1eu7tzec71kxdpsmskoucssuuoglv1drys1op2jh6",1,"Helium Mobile","MOBILE","mb1eu7TzEc71KxDpsmsKoucSSuuoGLv1drys1oP2jh6",6],["solana/spl/metaewgxypbgwsseh8t16a39cq5vyvxzi9zxidpy18m",1,"Metaplex","META","METAewgxyPbgwsseH8T16a39CQ5VyVxZi9zXiDPY18m",6],["solana/spl/mndefzgvmt87ueuhvvu9vctqsap5b3ftgpshuupa5ey",1,"Marinade","MNDE","MNDEFzGvMt87ueuHvVU9VcTqsAP5b3fTGPsHuuPA5ey",9],["solana/spl/moo_deng_ed5nyywezpppiwimp8vym7sd7td3lat3q3grtwhzpjby",1,"Moo Deng","MOODENG","ED5nyyWEzpPPiWimP8vYm7sD7TD3LAt3Q3gRTWHzPJBY",6],["solana/spl/msolzycxhdygdzu16g5qsh3i5k3z3kzk7ytfqcjm7so",1,"Marinade staked SOL (mSOL)","mSOL","mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So",9],["solana/spl/neontjsjsuo3rexg9o6vhumxw62f9v7zvmu8m8zut44",1,"Neon EVM Token","NEON","NeonTjSjsuo3rexg9o6vHuMXw62f9V7zvmu8M8Zut44",9],["solana/spl/nftukr4u7wkxy9qlax2tgvd9ozswomo4jqsjqdmb7nk",1,"Blockasset","BLOCK","NFTUkR4u7wKxy9QLaX2TGvd9oZSWoMo4jqSJqdMb7Nk",6],["solana/spl/nosxbvoacttydlvky6csb4ac8jcdqkkaawytx2zmoo7",1,"Nosana","NOS","nosXBVoaCTtYdLvKY6Csb4AC8JCdQKKAaWYtx2ZMoo7",6],["solana/spl/nyan_nyanpap9cr7yarbnrby7xx4xu6no6jktbuohna3yscp",1,"NYAN","NYAN","NYANpAp9Cr7YarBNrby7Xx4xU6No6JKTBuohNA3yscP",9],["solana/spl/octo82drbedm8csdaekbymvn86tbtgmpnddme64ptqj",1,"Octokn","OTK","octo82drBEdm8CSDaEKBymVn86TBtgmPnDdmE64PTqJ",9],["solana/spl/orcaektdk7lkz57vaayr9qensvepfiu6qemu1kektze",1,"Orca","ORCA","orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE",6],["solana/spl/poliswxnnrwc6obu1vhiukqzfjgl4xdsu4g9qjz9qvk",1,"Star Atlas DAO","POLIS","poLisWXnNRwC6oBu1vHiuKQzFjGL4XDSu4g9qjz9qVk",8],["solana/spl/pundu_wskzskqew3zsmrhpaevfvzb6puulzwov9mjwzsfdepc",1,"PUNDU","PUNDU","WskzsKqEW3ZsmrhPAevfVZb6PuuLzWov9mJWZsfDePC",9],["solana/spl/rlbxxfkseaz4rgjh3sqn8jxxhmgoz9jwxdnjmh8pl7a",1,"Rollbit Coin","RLB","RLBxxFkseAZ4RgJH3Sqn8jXxhmGoz9jWxDNJMh8pL7a",2],["solana/spl/rndrizkt3mk1iimdxrdwabcf7zg7ar5t4nud4ekhbof",1,"Render Token","RNDR","rndrizKT3MK1iimdxRdWabcF7Zg7AR5T4nud4EkHBof",8],["solana/spl/saber_protocol_token_saber2glauyim4mvftnrasomsv6nvauncvmezwclpd1",1,"Saber Protocol Token","SBR","Saber2gLauYim4Mvftnrasomsv6NvAuncvMEZwcLpD1",6],["solana/spl/scsuppnusyplbsv4darsryng4anpgaghkhsa3gmmyjz",1,"Solcasino Token","SCS","SCSuPPNUSypLBsV4darsrYNg4ANPgaGhKhsA3GmMyjz",5],["solana/spl/serum_srmuapvndxxokk5gt7xd5cuugxmbcoaz2lheuaokwrt",1,"Serum","SRM","SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt",6],["solana/spl/sharksyjjqanyxvfrpnbn9pjgkhwdhatnmyicwpnr1s",1,"Sharky","SHARK","SHARKSYJjqaNyxVfrpnBN9pjgkhwDhatnMyicWPnr1s",6],["solana/spl/shdwybxihqicj6yekg2gur7wqklelamk1ghzck9pl6y",1,"Shadow Token","SHDW","SHDWyBxihqiCj6YekG2GUr7wqKLeLAMK1gHZck9pL6y",9],["solana/spl/slerf_7bgbvyjrzx1ykz4oh9mjb8zscatkkwb8dzfx7loivkm3",1,"SLERF","SLERF","7BgBvyjrZX1YKz4oh9mjb8ZScatkkwb8DzFx7LoiVkM3",9],["solana/spl/slndpmowtvadgedndyvwzronl7zsi1df9pc3xhgtpwp",1,"Solend","SLND","SLNDpmoWTVADgEdndyvWzroNL7zSi1dF9PC3xHGtPwp",6],["solana/spl/snsnkv9zfg5zkwqs6x4hxvbrv6s8sqmfsgctecdvdmd",1,"SynesisOne","SNS","SNSNkV9zfG5ZKWQs6x4hxvBRV6s8SqMfSGCtECDvdMd",9],["solana/spl/so11111111111111111111111111111111111111112",1,"Wrapped SOL","SOL","So11111111111111111111111111111111111111112",9],["solana/spl/sols_2wme8evkw8qsfsk2b3qex4s64ac6wxhpxb3grdckekio",1,"sols","sols","2wme8EVkw8qsfSk2B3QeX4S64ac6wxHPXb3GrdckEkio",9],["solana/spl/stepascqoeiofxxwgnh2slbdfp9d8rvkz2yp39idpyt",1,"Step","STEP","StepAscQoEioFxxWGnh2sLBDFp9d8rvKz2Yp39iDpyT",9],["solana/spl/taki7fi3zicv7du1xnawlaf6mrk7ikdn77hegzgwvo4",1,"Taki","TAKI","Taki7fi3Zicv7Du1xNAWLaf6mRK7ikdn77HeGzgwvo4",9],["solana/spl/tnsrxcuxot9xbg3de7pijytdyu7ksklqcpddxnejas6",1,"Tensor","TNSR","TNSRxcUxoT9xBG3de7PiJyTDYu7kskLqcpddxnEJAS6",9],["solana/spl/ukhh6c7mmyiwcf1b9pnwe25tspkddt3h5pqzgz74j82",1,"BOOK OF MEME","BOME","ukHH6c7mMyiWCf1b9pnWe25TSpkDDt3H5pQZgZ74J82",6],["solana/spl/uxphbor3qg4ucignjfv7mqhhyfqkn68g45goyvael2m",1,"UXP Governance Token","UXP","UXPhBoR3qG4UCiGNJfV7MqhHyFqKN68g45GoYvAeL2M",9],["solana/spl/wenwenvqqnya429ubcdr81zmd69brwqaabyy6p3lcpk",1,"Wen","WEN","WENWENvqqNya429ubCdR81ZmD69brwQaaBYY6p3LCpk",5],["solana/spl/xxxxa1skngwftw2kfn8xauw9xq8hbz5kvtcsestt9fw",1,"Solanium","SLIM","xxxxa1sKNGwFtw2kFn8XauW9xq8hBZ5kVtcSesTT9fW",6],["solana/spl/yomfpuqz1wjwysfd5tzjuts3bnb8xs8mx9xzbv8rl39",1,"YOM","YOM","yomFPUqz1wJwYSfD5tZJUtS3bNb8xs8mx9XzBv8RL39",9],["solana/spl/zebeczgi5fsetbpfqkvzkcj3wgyxxjkmuknnx7flkaf",1,"ZEBEC","ZBC","zebeczgi5fSEtbpfQKVZKCJ3WgYXxjkMUkNNx7fLKAF",9],["solana/spl/zeus1ar7ax8dffjf5qjwj2ftdddntromngo8yoqm3gq",1,"ZEUS","ZEUS","ZEUS1aR7aX8DFFJf5QjWj2ftDDdNTroMNGo8YoQm3Gq",6]] \ No newline at end of file diff --git a/libs/ledgerjs/packages/cryptoassets/src/data/spl.ts b/libs/ledgerjs/packages/cryptoassets/src/data/spl.ts index 3107116dcf61..f79c2a8033de 100644 --- a/libs/ledgerjs/packages/cryptoassets/src/data/spl.ts +++ b/libs/ledgerjs/packages/cryptoassets/src/data/spl.ts @@ -1,4 +1,5 @@ export type SPLToken = [ + string, // CAL id number, // chainId string, // name string, // ticker diff --git a/libs/ledgerjs/packages/cryptoassets/src/tokens.ts b/libs/ledgerjs/packages/cryptoassets/src/tokens.ts index a1da74195b3e..48d494f738e6 100644 --- a/libs/ledgerjs/packages/cryptoassets/src/tokens.ts +++ b/libs/ledgerjs/packages/cryptoassets/src/tokens.ts @@ -406,7 +406,7 @@ function convertElrondESDTTokens([ }; } -function convertSplTokens([chainId, name, symbol, address, decimals]: SPLToken): TokenCurrency { +function convertSplTokens([id, chainId, name, symbol, address, decimals]: SPLToken): TokenCurrency { const chainIdToCurrencyId = { // Fallback in case CAL is using chainIds for vault 1: "solana", @@ -419,7 +419,7 @@ function convertSplTokens([chainId, name, symbol, address, decimals]: SPLToken): const currencyId = chainIdToCurrencyId[chainId]; return { type: "TokenCurrency", - id: `${currencyId}/spl/${address}`, + id, contractAddress: address, parentCurrency: getCryptoCurrencyById(currencyId), name, From be06064e6e78dabf2458d5d1ce4b2b12e5e7b7e4 Mon Sep 17 00:00:00 2001 From: Kant Date: Mon, 6 Jan 2025 18:40:31 +0100 Subject: [PATCH 33/35] fix: replace chainId in spl data by network to get the currencyId from the CAL instead of static data in code --- .../src/crypto-assets-importer/fetch/index.ts | 2 ++ .../crypto-assets-importer/importers/spl/index.ts | 8 ++++---- .../importers/spl/spl.test.ts | 7 ++++--- .../packages/cryptoassets/src/data/spl.json | 2 +- .../ledgerjs/packages/cryptoassets/src/data/spl.ts | 2 +- libs/ledgerjs/packages/cryptoassets/src/tokens.ts | 14 ++------------ 6 files changed, 14 insertions(+), 21 deletions(-) diff --git a/libs/ledgerjs/packages/cryptoassets/src/crypto-assets-importer/fetch/index.ts b/libs/ledgerjs/packages/cryptoassets/src/crypto-assets-importer/fetch/index.ts index ed6aa2bd5dc0..b88cb94097ba 100644 --- a/libs/ledgerjs/packages/cryptoassets/src/crypto-assets-importer/fetch/index.ts +++ b/libs/ledgerjs/packages/cryptoassets/src/crypto-assets-importer/fetch/index.ts @@ -16,6 +16,8 @@ export type CALServiceOutput = { type: string; id: string; blockchain_name: string; + network: string; + network_family: string; chain_id: number; contract_address: string; token_identifier: string; diff --git a/libs/ledgerjs/packages/cryptoassets/src/crypto-assets-importer/importers/spl/index.ts b/libs/ledgerjs/packages/cryptoassets/src/crypto-assets-importer/importers/spl/index.ts index 564552fcd331..084891bb57cf 100644 --- a/libs/ledgerjs/packages/cryptoassets/src/crypto-assets-importer/importers/spl/index.ts +++ b/libs/ledgerjs/packages/cryptoassets/src/crypto-assets-importer/importers/spl/index.ts @@ -4,7 +4,7 @@ import { fetchTokensFromCALService } from "../../fetch"; type SPLToken = [ string, // CAL id - number, // chainId + string, // network string, // name string, // ticker string, // address @@ -16,7 +16,7 @@ export const importSPLTokens = async (outputDir: string) => { console.log("importing spl tokens..."); const { tokens, hash } = await fetchTokensFromCALService({ blockchain_name: "solana" }, [ "id", - "chain_id", + "network", "name", "ticker", "contract_address", @@ -24,7 +24,7 @@ export const importSPLTokens = async (outputDir: string) => { ]); const splTokens: SPLToken[] = tokens.map(token => [ token.id, - token.chain_id, + token.network, token.name, token.ticker, token.contract_address, @@ -34,7 +34,7 @@ export const importSPLTokens = async (outputDir: string) => { const filePath = path.join(outputDir, "spl"); const splTypeStringified = `export type SPLToken = [ string, // CAL id - number, // chainId + string, // network string, // name string, // ticker string, // address diff --git a/libs/ledgerjs/packages/cryptoassets/src/crypto-assets-importer/importers/spl/spl.test.ts b/libs/ledgerjs/packages/cryptoassets/src/crypto-assets-importer/importers/spl/spl.test.ts index 7d20970c940b..9c9996c7cb94 100644 --- a/libs/ledgerjs/packages/cryptoassets/src/crypto-assets-importer/importers/spl/spl.test.ts +++ b/libs/ledgerjs/packages/cryptoassets/src/crypto-assets-importer/importers/spl/spl.test.ts @@ -6,6 +6,7 @@ const splTokens = [ { id: "solana/spl/epjfwdd5aufqssqem2qn1xzybapc8g4weggkzwytdt1v", blockchain_name: "solana", + network: "solana", chain_id: 101, contract_address: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", decimals: 6, @@ -34,7 +35,7 @@ describe("import Spl tokens", () => { it("should output the file in the correct format", async () => { const expectedFile = `export type SPLToken = [ string, // CAL id - number, // chainId + string, // network string, // name string, // ticker string, // address @@ -58,7 +59,7 @@ export default tokens as SPLToken[]; params: { blockchain_name: "solana", chain_id: undefined, - output: "id,chain_id,name,ticker,contract_address,decimals", + output: "id,network,name,ticker,contract_address,decimals", standard: undefined, }, }, @@ -69,7 +70,7 @@ export default tokens as SPLToken[]; JSON.stringify([ [ "solana/spl/epjfwdd5aufqssqem2qn1xzybapc8g4weggkzwytdt1v", - 101, + "solana", "USD Coin", "USDC", "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", diff --git a/libs/ledgerjs/packages/cryptoassets/src/data/spl.json b/libs/ledgerjs/packages/cryptoassets/src/data/spl.json index 51260521d916..7d8c9337bad8 100644 --- a/libs/ledgerjs/packages/cryptoassets/src/data/spl.json +++ b/libs/ledgerjs/packages/cryptoassets/src/data/spl.json @@ -1 +1 @@ -[["solana/spl/27g8mtk7vttcchkpasjsddkwwyfoqt6ggeukidvjidd4",1,"Jupiter Perps LP","JLP","27G8MtK7VtTcCHkpASjSDdkWWYfoqT6ggEuKidVJidD4",6],["solana/spl/2fpytwczlug1mdrwsyop4d6s1tm7hakhyrjknb5w6pxk",1,"Wrapped Ethereum (Sollet)","soETH","2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk",6],["solana/spl/31k88g5mq7ptbrdf3am13haq6wrqhxhikr8hik7wpygk",1,"Graphite","GP","31k88G5Mq7ptbRDf3AM13HAq6wRQHXHikR8hik7wPygk",9],["solana/spl/3brtivrvsitbmctgtqwp7hxxpsybkjn4xlntpshqa3zr",1,"Only1 (LIKE)","LIKE","3bRTivrVsitbmCTGtqwp7hxXPsybkjn4XLNtPsHqa3zR",9],["solana/spl/3dgccb15hmqsa4pn3tfii5vrk7arqth95ljjxzsg2mug",1,"Honeyland","HXD","3dgCCb15HMQSA4Pn3Tfii5vRk7aRqTH95LJjxzsG2Mug",9],["solana/spl/3nz9jmvbmgaqocybic2c7lqcjscmgsaz6vqqtdzcqmjh",1,"Wrapped BTC (Wormhole)","WBTC","3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh",8],["solana/spl/3psh1mj1f7yufad5gh6zj7epe8hhrmkmetgv5tshqa4o",1,"jeo boden","boden","3psH1Mj1f7yUfaD5gh6Zj7epE8hhrMkMETgv5TshQA4o",9],["solana/spl/4k3dyjzvzp8emzwuxbbcjevwskkk59s5icnly3qrkx6r",1,"Raydium","RAY","4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R",6],["solana/spl/4llbsb5rep3yetyzmxewygjcir5uxtkfurtaeuvc2ahs",1,"Parcl","PRCL","4LLbsb5ReP3yEtYzmXewyGjcir5uXtKFURtaEUVC2AHs",6],["solana/spl/4vmsout2bwatfweudnqm1xedrlfjgj7hswhcpz4xgbty",1,"HONEY","HONEY","4vMsoUT2BWatFweudnQM1xedRLfJgJ7hswhcpz4xgBTy",9],["solana/spl/5maydfq5yxtudahtfyumbuhzjgabas9tbeyeqyahds5y",1,"Access Protocol","ACS","5MAYDfq5yxtudAhtfyuMBuHZjgAbaS9tbEyEQYAhDS5y",6],["solana/spl/5ovnbeeeqvyi1cx3ir8dx5n1p7pdxydbgf2x4txvusjm",1,"Socean staked SOL","scnSOL","5oVNBeEEQvYi1cX3ir8Dx5n1P7pdxydbGF2X4TxVusJm",9],["solana/spl/6dkcowjpj5mfu5gwdefdpuuebasblk3wlewhuzqpaa1e",1,"CHEX","CHEX","6dKCoWjpj5MFU5gWDEFdpUUeBasBLK3wLEwhUzQPAa1e",8],["solana/spl/6gncphxtlnud76hjqusypenlszdg8rvdb1ptlm5alsja",1,"BSKT","BSKT","6gnCPhXtLnUD76HjQuSYPENLSZdG8RvDB1pTLM5aLSJA",5],["solana/spl/7atgf8kqo4wjrd5atgx7t1v2zvvykpjbffnevf1icfv1",1,"catwifhat","$CWIF","7atgF8KQo4wJrD5ATGX7t1V2zVvykPJbFfNeVf1icFv1",2],["solana/spl/7dhbwxmci3dt8ufywyzweblxgycu7y3il6trkn1y7arj",1,"Lido Staked SOL","stSOL","7dHbWXmci3dT8UFYWYZweBLXgycu7Y3iL6trKn1Y7ARj",9],["solana/spl/7gcihgdb8fe6knjn2mytkzzcrjqy3t9ghdc8uhymw2hr",1,"POPCAT","POPCAT","7GCihgDB8fe6KNjn2MYtkzZcRjQy3t9GHdC8uHYmW2hr",9],["solana/spl/7i5kksx2weitkry7ja4zwsuxghs5ejbejy8vvxr4pfrx",1,"GMT","GMT","7i5KKsX2weiTkry7jA4ZwSuXGhs5eJBEjY8vVxR4pfRx",9],["solana/spl/7q2afv64in6n6sezsaab81tjzwdod6zpqmhkzi9dcavn",1,"JPOOL Solana Token","JSOL","7Q2afV64in6N6SeZsAAB81TJzwDoD6zpqmHkzi9Dcavn",9],["solana/spl/7vfcxtuxx5wjv5jadk17duj4ksgau7utnkj4b963voxs",1,"Wrapped Ether (Wormhole)","ETH","7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs",8],["solana/spl/7xkxtg2cw87d97txjsdpbd5jbkhetqa83tzrujosgasu",1,"Samoyed Coin","SAMO","7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",9],["solana/spl/85vbfqzc9tzkfaptbwjvuw7ybzjy52a6mjtpgjstqamq",1,"Wormhole Token","W","85VBFQZC9TZkfaptBWjvUw7YbZjy52A6mjtPGjstQAmQ",6],["solana/spl/947teog318gumyjvyhranrvwpmx7fpbtdqfbojvsksg3",1,"Solchat","CHAT","947tEoG318GUmyjVYhraNRvWpMX7fpBTDQFBoJvSkSG3",9],["solana/spl/a11bdaauv8ib2fu7x6axavdto1qz8fxb3kk5eecdasp",1,"Allbridge","ABR","a11bdAAuV8iB2fu7X6AxAvDTo1QZ8FXB3kk5eecdasp",9],["solana/spl/a1klobrkbde8ty9qtnqutq3c2ortoc3u7twggz7seto6",1,"Ondo US Dollar Yield","USDY","A1KLoBrKBde8Ty9qtNQUtq3C2ortoC3u7twggz7sEto6",6],["solana/spl/afbx8ogjgpmvfywbvouvhqsrmiw2ar1mohfahi4y2adb",1,"GST","GST","AFbX8oGjGpmVFywbVouvhQSRmiW2aR1mohfahi4Y2AdB",9],["solana/spl/amuwxpsqwsd1fbcgzwsrrkdcnoduuwmkdr38qpdit8g8",1,"Amulet","AMU","AMUwxPsqWSd1fbCGzWsrRKDcNoduuWMkdR38qPdit8G8",9],["solana/spl/analos_7it1gryyheop2nv1dycwk2mgylmphq47whpgswiqcug5",1,"ANALOS","ANALOS","7iT1GRYYhEop2nV1dyCwK2MGyLmPHq47WhPGSwiqcUg5",8],["solana/spl/at79reyu9xthutf5vm6q4oa9k8w7918fp5su7g1mdmqy",1,"SpiderSwap","SPDR","AT79ReYU9XtHUTF5vM6Q4oa9K8w7918Fp5SU7G1MDMQY",9],["solana/spl/atlasxmbpqxbuybxpsv97usa3fpqyeqzqbuhgifcusxx",1,"Star Atlas","ATLAS","ATLASXmbPQxBUYbxPsV97usA3fPQYEqzQBUHgiFCUsXx",8],["solana/spl/atrluhph8dxnpny4wsnw7fxkhbeivbrtwby6bfb4xplj",1,"Artrade Token","ATR","ATRLuHph8dxnPny4WSNW7fxkhbeivBrtWbY6BfB4xpLj",9],["solana/spl/auryydfxjib1zktir1jn1j9ecyutjb6rkqvmtyaixwpp",1,"Aurory","AURY","AURYydfxJib1ZkTir1Jn1J9ECYUtjb6rKQVmtYaixWPP",9],["solana/spl/beer_aujtjj7ams8ldo3bfzoyxdwt3jbalubu4vzhzzdtzlmg",1,"BEER","$BEER","AujTJJ7aMS8LDo3bFzoyXDwT3jBALUbu4VZhzZdTZLmG",6],["solana/spl/ben_the_dog_ahw5n8iqzobtcbepksjzz61xtauszbdcpxtrlg6kukpk",1,"Ben the Dog","BENDOG","AHW5N8iqZobTcBepkSJzZ61XtAuSzBDcpxtrLG6KUKPk",9],["solana/spl/bidb55p4g3n1fghwkfpxsokbmqgctl4qnzpdh1bvqxmd",1,"Decimated","DIO","BiDB55p4G3n1fGhwKFpxsokBMqgctL4qnZpDH1bVQxMD",9],["solana/spl/blzeeuzubvqfhj8adccfpjvpvcicyvmh3hkjmru8kuja",1,"Blaze","BLZE","BLZEEuZUBVqFhj8adcCFPJvPVCiCyVmh3hkJMrU8KuJA",9],["solana/spl/bso13r4tkie4kuml71lshtppl2eubylfx6h9hp3piy1",1,"BlazeStake Staked SOL (bSOL)","bSOL","bSo13r4TkiE4KumL71LsHTPpL2euBYLFx6h9HP3piy1",9],["solana/spl/cat_in_a_dogs_world_mew1gqwj3nexg2qgeriku7fafj79phvqvrequzscpp5",1,"cat in a dogs world","MEW","MEW1gQWJ3nEXg2qgERiKu7FAFj79PHvQVREQUzScPP5",5],["solana/spl/ckaktyvz6dkpymvyq9rh3ubrnnqyzayd7if4hjtjuvks",1,"Gari","GARI","CKaKtYvz6dKPyMvYq9Rh3UBrnNqYZAyd7iF4hJtjUvks",9],["solana/spl/cvb1ztjvpyqpvdpbeptrzjl4aqidjydtuz61nwgcgqtp",1,"Epics Token","EPCT","CvB1ztJvpYQPvdPBePtRzjL4aQidjydtUz61NWgcgQtP",6],["solana/spl/daddy_tate_4cnk9epnw5ixflzatcpjjdb1putcrpvvgtqukm9epump",1,"DADDY TATE","DADDY","4Cnk9EPnW5ixfLZatCPJjDB1PUtcRpVVgTQukm9epump",6],["solana/spl/dezxaz8z7pnrnrjjz3wxborgixca6xjnb7yab1ppb263",1,"Bonk","BONK","DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263",5],["solana/spl/dfl1znkagpwm1bqavqrjczvhmwtfreajtbzjwgseonjh",1,"DeFi Land","DFL","DFL1zNkaGPWm1BqAVqRjCZvHmwTFrEaJtbzJWgseoNJh",9],["solana/spl/echesyfxepkdltoizsl8pbe8myagyy8zrqsacncfgnvp",1,"Bonfida","FIDA","EchesyfXePKdLtoiZSL8pBe8Myagyy8ZRqsACNCFGnvp",6],["solana/spl/ekpqgsjtjmfqkz9kqansqyxrcf8fbopzlhyxdm65zcjm",1,"dogwifhat","$WIF","EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm",6],["solana/spl/epjfwdd5aufqssqem2qn1xzybapc8g4weggkzwytdt1v",1,"USD Coin","USDC","EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",6],["solana/spl/es9vmfrzacermjfrf4h2fyd4kconky11mcce8benwnyb",1,"USDT","USDT","Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB",6],["solana/spl/etatlmcmsoieekfnrhkj2kyy3moabhu6nqvpsfij5tds",1,"Media Network","MEDIA","ETAtLmCmsoiEEKfNrHKJ2kYy3MoABhU6NQvpSfij5tDs",6],["solana/spl/fanoyuaqzx7ahcnxqslewq6te63f6zs6enkbnccyyuzu",1,"SuperFans.Tech","FAN","FANoyuAQZx7AHCnxqsLeWq6te63F6zs6ENkbncCyYUZu",9],["solana/spl/fluxbmpht3fd1edvfdg46yreqhbenypn1h4ebntzwerx",1,"FluxBot","FLUXB","FLUXBmPhT3Fd1EDVFdg46YREqHBeNypn1h4EbnTzWERX",5],["solana/spl/foxymu5xwxre7zeosvzvirk3ngawhup9kuh97y2ndhcq",1,"Famous Fox Federation","FOXY","FoXyMu5xwXre7zEoSvzViRk3nGawHUp9kUh97y2NDhcq",0],["solana/spl/ftggsfadxbtroxq8vcausxrr2of47qbf5as1ntzcu4gd",1,"BRZ","BRZ","FtgGSFADXBtroxq8VCausXRr2of47QBf5AS1NtZCu4GD",4],["solana/spl/gamestop_8wxtpeu6557etkp9whfy1n1ecu6nxdvbagghgsmyihsb",1,"GameStop","GME","8wXtPeU6557ETkp9WHFY1n1EcU6NxDvbAggHGsMYiHsB",9],["solana/spl/gdfnesia2wlaw5t8yx2x5j2mkfa74i5kwgdduzht7xmg",1,"CROWN Token","CROWN","GDfnEsia2WLAW5t8yx2X5j2mkfA74i5kwGdDuZHt7XmG",9],["solana/spl/geneth5amgsi8khatqoezp1xexwzj8vcuepynxdkrmyz",1,"Genopets","GENE","GENEtH5amGSi8kHAtQoezp1XEXwZJ8vcuePYnXdKrMYz",9],["solana/spl/gfx1zjr2p15tmrswow6fjydycekofb4p4gjcplbjaxhd",1,"GooseFX","GOFX","GFX1ZjR2P15tmrSwow6FjyDYcEkoFb4p4gJCpLBjaxHD",9],["solana/spl/gth3wg3nerjwcf7vgcoxexkgxshvyhx5gtateem5jas1",1,"Whales Market","WHALES","GTH3wG3NErjwcf7VGCoXEXkgXSHvYhx5gtATeeM5JAS1",6],["solana/spl/h53ugeybrb9easo9ego8yyk7o4zq1g5cctkxd3e3hzav",1,"MXM","MXM","H53UGEyBrB9easo9ego8yYk7o4Zq1G5cCtkxD3E3hZav",6],["solana/spl/hhjoywup5au6pnrvn4s2pweerwxnzkhxkgyjrjmobjlw",1,"PIP","PIP","HHjoYwUp5aU6pnrvN4s2pwEErwXNZKhxKGYjRJMoBjLw",9],["solana/spl/hhjpbhrrn4g56vsylut8dl5bv31hkxqsrahttuczezg4",1,"Myro","$MYRO","HhJpBhRRn4g56VsyLuT8DL5Bv31HkXqsrahTTUCZeZg4",9],["solana/spl/hntyvp6yfm1hg25tn9wglqm12b8tqmcknkrdu1oxwux",1,"Helium Network Token","HNT","hntyVP6YFm1Hg25TN9WGLqM12b8TQmcknKrdu1oxWux",8],["solana/spl/hxhwkvpk5ns4ltg5nij2g671ckxfrkpk8vy271ub4uek",1,"Hxro (Portal)","HXRO","HxhWkVpk5NS4Ltg5nij2G671CKXFRKPK8vy271Ub4uEK",8],["solana/spl/hz1jovnivvgrgniiyveozevgz58xau3rkwx8eacqbct3",1,"Pyth Network","PYTH","HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3",6],["solana/spl/hzwqbkzw8hxmn6bf2yfznrht3c2ixxzpkcfu7ubedktr",1,"EURC","EURC","HzwqbKZw8HxMN6bF2yFZNrht3c2iXXzpKcFu7uBEDKtr",6],["solana/spl/io_bzlbgtncsffoth2gydtwr7e4imwzpr5jqcuugewr646k",1,"IO","IO","BZLbGTNCSFfoth2GYDtwr7e4imWzpR5jqcUuGEwr646K",8],["solana/spl/iotevvzleywotn1qdwnpddxpwszn3zfheot3mfl9fns",1,"Helium IOT","IOT","iotEVVZLEywoTn1QdwNPddxPWszn3zFhEot3MfL9fns",6],["solana/spl/j1toso1uck3rlmjorhttrvwy9hj7x8v9yyac6y7kgcpn",1,"Jito Staked SOL","JITOSOL","J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn",9],["solana/spl/j2lwssxx4r3pybj1fwux5nqo7ppxjcgppub2zhnadwka",1,"DePlan","DPLN","J2LWsSXx4r3pYbJ1fwuX5Nqo7PPxjcGPpUb2zHNadWKa",6],["solana/spl/jtojtomepa8bep8auqc6ext5frijwffmwqx2v2f9mcl",1,"JITO","JTO","jtojtomepa8beP8AuQc6eXt5FriJwfFMwQx2v2f9mCL",9],["solana/spl/jupyiwryjfskupiha7hker8vutaefosybkedznsdvcn",1,"Jupiter","JUP","JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN",6],["solana/spl/kamino_kmno3njsbxfcpjtvhzcxlw7rmtwtt4gvfe7suubo9ss",1,"Kamino","KMNO","KMNo3nJsBXfcpJTVhZcXLW7RmTwTt4GVFE7suUBo9sS",6],["solana/spl/kinxdecpdqehpeuqnqmugtyykqkgvfq6cevx5iahjq6",1,"KIN","KIN","kinXdEcpDQeHPEuQnqmUgtYykqKGVFq6CeVX5iAHJq6",5],["solana/spl/lainetnlgpmcp9rvsf5hn8w6ehniklzqti1xfwmly6x",1,"Laine Stake Token","laineSOL","LAinEtNLgpmCP9Rvsf5Hn8W6EhNiKLZQti1xfWMLy6X",9],["solana/spl/lfntyraetvioapngjht4yng2auzfxr776cmen9vmjxp",1,"Lifinity","LFNTY","LFNTYraetVioAPnGJht4yNg2aUZFXR776cMeN9VMjXp",6],["solana/spl/lstxxxnjzkdfslr4dukpcmcf5vyryeqzplz5j4bpxfp",1,"Liquid Staking Token","LST","LSTxxxnJzKDFSLr4dUkPcmCf5VyryEqzPLz5j4bpxFp",9],["solana/spl/mangoczj36ajzykwvj3vnyu4gtonjfvenjmvvwaxlac",1,"Mango","MNGO","MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac",6],["solana/spl/mb1eu7tzec71kxdpsmskoucssuuoglv1drys1op2jh6",1,"Helium Mobile","MOBILE","mb1eu7TzEc71KxDpsmsKoucSSuuoGLv1drys1oP2jh6",6],["solana/spl/metaewgxypbgwsseh8t16a39cq5vyvxzi9zxidpy18m",1,"Metaplex","META","METAewgxyPbgwsseH8T16a39CQ5VyVxZi9zXiDPY18m",6],["solana/spl/mndefzgvmt87ueuhvvu9vctqsap5b3ftgpshuupa5ey",1,"Marinade","MNDE","MNDEFzGvMt87ueuHvVU9VcTqsAP5b3fTGPsHuuPA5ey",9],["solana/spl/moo_deng_ed5nyywezpppiwimp8vym7sd7td3lat3q3grtwhzpjby",1,"Moo Deng","MOODENG","ED5nyyWEzpPPiWimP8vYm7sD7TD3LAt3Q3gRTWHzPJBY",6],["solana/spl/msolzycxhdygdzu16g5qsh3i5k3z3kzk7ytfqcjm7so",1,"Marinade staked SOL (mSOL)","mSOL","mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So",9],["solana/spl/neontjsjsuo3rexg9o6vhumxw62f9v7zvmu8m8zut44",1,"Neon EVM Token","NEON","NeonTjSjsuo3rexg9o6vHuMXw62f9V7zvmu8M8Zut44",9],["solana/spl/nftukr4u7wkxy9qlax2tgvd9ozswomo4jqsjqdmb7nk",1,"Blockasset","BLOCK","NFTUkR4u7wKxy9QLaX2TGvd9oZSWoMo4jqSJqdMb7Nk",6],["solana/spl/nosxbvoacttydlvky6csb4ac8jcdqkkaawytx2zmoo7",1,"Nosana","NOS","nosXBVoaCTtYdLvKY6Csb4AC8JCdQKKAaWYtx2ZMoo7",6],["solana/spl/nyan_nyanpap9cr7yarbnrby7xx4xu6no6jktbuohna3yscp",1,"NYAN","NYAN","NYANpAp9Cr7YarBNrby7Xx4xU6No6JKTBuohNA3yscP",9],["solana/spl/octo82drbedm8csdaekbymvn86tbtgmpnddme64ptqj",1,"Octokn","OTK","octo82drBEdm8CSDaEKBymVn86TBtgmPnDdmE64PTqJ",9],["solana/spl/orcaektdk7lkz57vaayr9qensvepfiu6qemu1kektze",1,"Orca","ORCA","orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE",6],["solana/spl/poliswxnnrwc6obu1vhiukqzfjgl4xdsu4g9qjz9qvk",1,"Star Atlas DAO","POLIS","poLisWXnNRwC6oBu1vHiuKQzFjGL4XDSu4g9qjz9qVk",8],["solana/spl/pundu_wskzskqew3zsmrhpaevfvzb6puulzwov9mjwzsfdepc",1,"PUNDU","PUNDU","WskzsKqEW3ZsmrhPAevfVZb6PuuLzWov9mJWZsfDePC",9],["solana/spl/rlbxxfkseaz4rgjh3sqn8jxxhmgoz9jwxdnjmh8pl7a",1,"Rollbit Coin","RLB","RLBxxFkseAZ4RgJH3Sqn8jXxhmGoz9jWxDNJMh8pL7a",2],["solana/spl/rndrizkt3mk1iimdxrdwabcf7zg7ar5t4nud4ekhbof",1,"Render Token","RNDR","rndrizKT3MK1iimdxRdWabcF7Zg7AR5T4nud4EkHBof",8],["solana/spl/saber_protocol_token_saber2glauyim4mvftnrasomsv6nvauncvmezwclpd1",1,"Saber Protocol Token","SBR","Saber2gLauYim4Mvftnrasomsv6NvAuncvMEZwcLpD1",6],["solana/spl/scsuppnusyplbsv4darsryng4anpgaghkhsa3gmmyjz",1,"Solcasino Token","SCS","SCSuPPNUSypLBsV4darsrYNg4ANPgaGhKhsA3GmMyjz",5],["solana/spl/serum_srmuapvndxxokk5gt7xd5cuugxmbcoaz2lheuaokwrt",1,"Serum","SRM","SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt",6],["solana/spl/sharksyjjqanyxvfrpnbn9pjgkhwdhatnmyicwpnr1s",1,"Sharky","SHARK","SHARKSYJjqaNyxVfrpnBN9pjgkhwDhatnMyicWPnr1s",6],["solana/spl/shdwybxihqicj6yekg2gur7wqklelamk1ghzck9pl6y",1,"Shadow Token","SHDW","SHDWyBxihqiCj6YekG2GUr7wqKLeLAMK1gHZck9pL6y",9],["solana/spl/slerf_7bgbvyjrzx1ykz4oh9mjb8zscatkkwb8dzfx7loivkm3",1,"SLERF","SLERF","7BgBvyjrZX1YKz4oh9mjb8ZScatkkwb8DzFx7LoiVkM3",9],["solana/spl/slndpmowtvadgedndyvwzronl7zsi1df9pc3xhgtpwp",1,"Solend","SLND","SLNDpmoWTVADgEdndyvWzroNL7zSi1dF9PC3xHGtPwp",6],["solana/spl/snsnkv9zfg5zkwqs6x4hxvbrv6s8sqmfsgctecdvdmd",1,"SynesisOne","SNS","SNSNkV9zfG5ZKWQs6x4hxvBRV6s8SqMfSGCtECDvdMd",9],["solana/spl/so11111111111111111111111111111111111111112",1,"Wrapped SOL","SOL","So11111111111111111111111111111111111111112",9],["solana/spl/sols_2wme8evkw8qsfsk2b3qex4s64ac6wxhpxb3grdckekio",1,"sols","sols","2wme8EVkw8qsfSk2B3QeX4S64ac6wxHPXb3GrdckEkio",9],["solana/spl/stepascqoeiofxxwgnh2slbdfp9d8rvkz2yp39idpyt",1,"Step","STEP","StepAscQoEioFxxWGnh2sLBDFp9d8rvKz2Yp39iDpyT",9],["solana/spl/taki7fi3zicv7du1xnawlaf6mrk7ikdn77hegzgwvo4",1,"Taki","TAKI","Taki7fi3Zicv7Du1xNAWLaf6mRK7ikdn77HeGzgwvo4",9],["solana/spl/tnsrxcuxot9xbg3de7pijytdyu7ksklqcpddxnejas6",1,"Tensor","TNSR","TNSRxcUxoT9xBG3de7PiJyTDYu7kskLqcpddxnEJAS6",9],["solana/spl/ukhh6c7mmyiwcf1b9pnwe25tspkddt3h5pqzgz74j82",1,"BOOK OF MEME","BOME","ukHH6c7mMyiWCf1b9pnWe25TSpkDDt3H5pQZgZ74J82",6],["solana/spl/uxphbor3qg4ucignjfv7mqhhyfqkn68g45goyvael2m",1,"UXP Governance Token","UXP","UXPhBoR3qG4UCiGNJfV7MqhHyFqKN68g45GoYvAeL2M",9],["solana/spl/wenwenvqqnya429ubcdr81zmd69brwqaabyy6p3lcpk",1,"Wen","WEN","WENWENvqqNya429ubCdR81ZmD69brwQaaBYY6p3LCpk",5],["solana/spl/xxxxa1skngwftw2kfn8xauw9xq8hbz5kvtcsestt9fw",1,"Solanium","SLIM","xxxxa1sKNGwFtw2kFn8XauW9xq8hBZ5kVtcSesTT9fW",6],["solana/spl/yomfpuqz1wjwysfd5tzjuts3bnb8xs8mx9xzbv8rl39",1,"YOM","YOM","yomFPUqz1wJwYSfD5tZJUtS3bNb8xs8mx9XzBv8RL39",9],["solana/spl/zebeczgi5fsetbpfqkvzkcj3wgyxxjkmuknnx7flkaf",1,"ZEBEC","ZBC","zebeczgi5fSEtbpfQKVZKCJ3WgYXxjkMUkNNx7fLKAF",9],["solana/spl/zeus1ar7ax8dffjf5qjwj2ftdddntromngo8yoqm3gq",1,"ZEUS","ZEUS","ZEUS1aR7aX8DFFJf5QjWj2ftDDdNTroMNGo8YoQm3Gq",6]] \ No newline at end of file +[["solana/spl/27g8mtk7vttcchkpasjsddkwwyfoqt6ggeukidvjidd4","solana","Jupiter Perps LP","JLP","27G8MtK7VtTcCHkpASjSDdkWWYfoqT6ggEuKidVJidD4",6],["solana/spl/2fpytwczlug1mdrwsyop4d6s1tm7hakhyrjknb5w6pxk","solana","Wrapped Ethereum (Sollet)","soETH","2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk",6],["solana/spl/31k88g5mq7ptbrdf3am13haq6wrqhxhikr8hik7wpygk","solana","Graphite","GP","31k88G5Mq7ptbRDf3AM13HAq6wRQHXHikR8hik7wPygk",9],["solana/spl/3brtivrvsitbmctgtqwp7hxxpsybkjn4xlntpshqa3zr","solana","Only1 (LIKE)","LIKE","3bRTivrVsitbmCTGtqwp7hxXPsybkjn4XLNtPsHqa3zR",9],["solana/spl/3dgccb15hmqsa4pn3tfii5vrk7arqth95ljjxzsg2mug","solana","Honeyland","HXD","3dgCCb15HMQSA4Pn3Tfii5vRk7aRqTH95LJjxzsG2Mug",9],["solana/spl/3nz9jmvbmgaqocybic2c7lqcjscmgsaz6vqqtdzcqmjh","solana","Wrapped BTC (Wormhole)","WBTC","3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh",8],["solana/spl/3psh1mj1f7yufad5gh6zj7epe8hhrmkmetgv5tshqa4o","solana","jeo boden","boden","3psH1Mj1f7yUfaD5gh6Zj7epE8hhrMkMETgv5TshQA4o",9],["solana/spl/4k3dyjzvzp8emzwuxbbcjevwskkk59s5icnly3qrkx6r","solana","Raydium","RAY","4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R",6],["solana/spl/4llbsb5rep3yetyzmxewygjcir5uxtkfurtaeuvc2ahs","solana","Parcl","PRCL","4LLbsb5ReP3yEtYzmXewyGjcir5uXtKFURtaEUVC2AHs",6],["solana/spl/4vmsout2bwatfweudnqm1xedrlfjgj7hswhcpz4xgbty","solana","HONEY","HONEY","4vMsoUT2BWatFweudnQM1xedRLfJgJ7hswhcpz4xgBTy",9],["solana/spl/5maydfq5yxtudahtfyumbuhzjgabas9tbeyeqyahds5y","solana","Access Protocol","ACS","5MAYDfq5yxtudAhtfyuMBuHZjgAbaS9tbEyEQYAhDS5y",6],["solana/spl/5ovnbeeeqvyi1cx3ir8dx5n1p7pdxydbgf2x4txvusjm","solana","Socean staked SOL","scnSOL","5oVNBeEEQvYi1cX3ir8Dx5n1P7pdxydbGF2X4TxVusJm",9],["solana/spl/6dkcowjpj5mfu5gwdefdpuuebasblk3wlewhuzqpaa1e","solana","CHEX","CHEX","6dKCoWjpj5MFU5gWDEFdpUUeBasBLK3wLEwhUzQPAa1e",8],["solana/spl/6gncphxtlnud76hjqusypenlszdg8rvdb1ptlm5alsja","solana","BSKT","BSKT","6gnCPhXtLnUD76HjQuSYPENLSZdG8RvDB1pTLM5aLSJA",5],["solana/spl/7atgf8kqo4wjrd5atgx7t1v2zvvykpjbffnevf1icfv1","solana","catwifhat","$CWIF","7atgF8KQo4wJrD5ATGX7t1V2zVvykPJbFfNeVf1icFv1",2],["solana/spl/7dhbwxmci3dt8ufywyzweblxgycu7y3il6trkn1y7arj","solana","Lido Staked SOL","stSOL","7dHbWXmci3dT8UFYWYZweBLXgycu7Y3iL6trKn1Y7ARj",9],["solana/spl/7gcihgdb8fe6knjn2mytkzzcrjqy3t9ghdc8uhymw2hr","solana","POPCAT","POPCAT","7GCihgDB8fe6KNjn2MYtkzZcRjQy3t9GHdC8uHYmW2hr",9],["solana/spl/7i5kksx2weitkry7ja4zwsuxghs5ejbejy8vvxr4pfrx","solana","GMT","GMT","7i5KKsX2weiTkry7jA4ZwSuXGhs5eJBEjY8vVxR4pfRx",9],["solana/spl/7q2afv64in6n6sezsaab81tjzwdod6zpqmhkzi9dcavn","solana","JPOOL Solana Token","JSOL","7Q2afV64in6N6SeZsAAB81TJzwDoD6zpqmHkzi9Dcavn",9],["solana/spl/7vfcxtuxx5wjv5jadk17duj4ksgau7utnkj4b963voxs","solana","Wrapped Ether (Wormhole)","ETH","7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs",8],["solana/spl/7xkxtg2cw87d97txjsdpbd5jbkhetqa83tzrujosgasu","solana","Samoyed Coin","SAMO","7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",9],["solana/spl/85vbfqzc9tzkfaptbwjvuw7ybzjy52a6mjtpgjstqamq","solana","Wormhole Token","W","85VBFQZC9TZkfaptBWjvUw7YbZjy52A6mjtPGjstQAmQ",6],["solana/spl/947teog318gumyjvyhranrvwpmx7fpbtdqfbojvsksg3","solana","Solchat","CHAT","947tEoG318GUmyjVYhraNRvWpMX7fpBTDQFBoJvSkSG3",9],["solana/spl/a11bdaauv8ib2fu7x6axavdto1qz8fxb3kk5eecdasp","solana","Allbridge","ABR","a11bdAAuV8iB2fu7X6AxAvDTo1QZ8FXB3kk5eecdasp",9],["solana/spl/a1klobrkbde8ty9qtnqutq3c2ortoc3u7twggz7seto6","solana","Ondo US Dollar Yield","USDY","A1KLoBrKBde8Ty9qtNQUtq3C2ortoC3u7twggz7sEto6",6],["solana/spl/afbx8ogjgpmvfywbvouvhqsrmiw2ar1mohfahi4y2adb","solana","GST","GST","AFbX8oGjGpmVFywbVouvhQSRmiW2aR1mohfahi4Y2AdB",9],["solana/spl/amuwxpsqwsd1fbcgzwsrrkdcnoduuwmkdr38qpdit8g8","solana","Amulet","AMU","AMUwxPsqWSd1fbCGzWsrRKDcNoduuWMkdR38qPdit8G8",9],["solana/spl/analos_7it1gryyheop2nv1dycwk2mgylmphq47whpgswiqcug5","solana","ANALOS","ANALOS","7iT1GRYYhEop2nV1dyCwK2MGyLmPHq47WhPGSwiqcUg5",8],["solana/spl/at79reyu9xthutf5vm6q4oa9k8w7918fp5su7g1mdmqy","solana","SpiderSwap","SPDR","AT79ReYU9XtHUTF5vM6Q4oa9K8w7918Fp5SU7G1MDMQY",9],["solana/spl/atlasxmbpqxbuybxpsv97usa3fpqyeqzqbuhgifcusxx","solana","Star Atlas","ATLAS","ATLASXmbPQxBUYbxPsV97usA3fPQYEqzQBUHgiFCUsXx",8],["solana/spl/atrluhph8dxnpny4wsnw7fxkhbeivbrtwby6bfb4xplj","solana","Artrade Token","ATR","ATRLuHph8dxnPny4WSNW7fxkhbeivBrtWbY6BfB4xpLj",9],["solana/spl/auryydfxjib1zktir1jn1j9ecyutjb6rkqvmtyaixwpp","solana","Aurory","AURY","AURYydfxJib1ZkTir1Jn1J9ECYUtjb6rKQVmtYaixWPP",9],["solana/spl/beer_aujtjj7ams8ldo3bfzoyxdwt3jbalubu4vzhzzdtzlmg","solana","BEER","$BEER","AujTJJ7aMS8LDo3bFzoyXDwT3jBALUbu4VZhzZdTZLmG",6],["solana/spl/ben_the_dog_ahw5n8iqzobtcbepksjzz61xtauszbdcpxtrlg6kukpk","solana","Ben the Dog","BENDOG","AHW5N8iqZobTcBepkSJzZ61XtAuSzBDcpxtrLG6KUKPk",9],["solana/spl/bidb55p4g3n1fghwkfpxsokbmqgctl4qnzpdh1bvqxmd","solana","Decimated","DIO","BiDB55p4G3n1fGhwKFpxsokBMqgctL4qnZpDH1bVQxMD",9],["solana/spl/blzeeuzubvqfhj8adccfpjvpvcicyvmh3hkjmru8kuja","solana","Blaze","BLZE","BLZEEuZUBVqFhj8adcCFPJvPVCiCyVmh3hkJMrU8KuJA",9],["solana/spl/bso13r4tkie4kuml71lshtppl2eubylfx6h9hp3piy1","solana","BlazeStake Staked SOL (bSOL)","bSOL","bSo13r4TkiE4KumL71LsHTPpL2euBYLFx6h9HP3piy1",9],["solana/spl/cat_in_a_dogs_world_mew1gqwj3nexg2qgeriku7fafj79phvqvrequzscpp5","solana","cat in a dogs world","MEW","MEW1gQWJ3nEXg2qgERiKu7FAFj79PHvQVREQUzScPP5",5],["solana/spl/ckaktyvz6dkpymvyq9rh3ubrnnqyzayd7if4hjtjuvks","solana","Gari","GARI","CKaKtYvz6dKPyMvYq9Rh3UBrnNqYZAyd7iF4hJtjUvks",9],["solana/spl/cvb1ztjvpyqpvdpbeptrzjl4aqidjydtuz61nwgcgqtp","solana","Epics Token","EPCT","CvB1ztJvpYQPvdPBePtRzjL4aQidjydtUz61NWgcgQtP",6],["solana/spl/daddy_tate_4cnk9epnw5ixflzatcpjjdb1putcrpvvgtqukm9epump","solana","DADDY TATE","DADDY","4Cnk9EPnW5ixfLZatCPJjDB1PUtcRpVVgTQukm9epump",6],["solana/spl/dezxaz8z7pnrnrjjz3wxborgixca6xjnb7yab1ppb263","solana","Bonk","BONK","DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263",5],["solana/spl/dfl1znkagpwm1bqavqrjczvhmwtfreajtbzjwgseonjh","solana","DeFi Land","DFL","DFL1zNkaGPWm1BqAVqRjCZvHmwTFrEaJtbzJWgseoNJh",9],["solana/spl/echesyfxepkdltoizsl8pbe8myagyy8zrqsacncfgnvp","solana","Bonfida","FIDA","EchesyfXePKdLtoiZSL8pBe8Myagyy8ZRqsACNCFGnvp",6],["solana/spl/ekpqgsjtjmfqkz9kqansqyxrcf8fbopzlhyxdm65zcjm","solana","dogwifhat","$WIF","EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm",6],["solana/spl/epjfwdd5aufqssqem2qn1xzybapc8g4weggkzwytdt1v","solana","USD Coin","USDC","EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",6],["solana/spl/es9vmfrzacermjfrf4h2fyd4kconky11mcce8benwnyb","solana","USDT","USDT","Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB",6],["solana/spl/etatlmcmsoieekfnrhkj2kyy3moabhu6nqvpsfij5tds","solana","Media Network","MEDIA","ETAtLmCmsoiEEKfNrHKJ2kYy3MoABhU6NQvpSfij5tDs",6],["solana/spl/fanoyuaqzx7ahcnxqslewq6te63f6zs6enkbnccyyuzu","solana","SuperFans.Tech","FAN","FANoyuAQZx7AHCnxqsLeWq6te63F6zs6ENkbncCyYUZu",9],["solana/spl/fluxbmpht3fd1edvfdg46yreqhbenypn1h4ebntzwerx","solana","FluxBot","FLUXB","FLUXBmPhT3Fd1EDVFdg46YREqHBeNypn1h4EbnTzWERX",5],["solana/spl/foxymu5xwxre7zeosvzvirk3ngawhup9kuh97y2ndhcq","solana","Famous Fox Federation","FOXY","FoXyMu5xwXre7zEoSvzViRk3nGawHUp9kUh97y2NDhcq",0],["solana/spl/ftggsfadxbtroxq8vcausxrr2of47qbf5as1ntzcu4gd","solana","BRZ","BRZ","FtgGSFADXBtroxq8VCausXRr2of47QBf5AS1NtZCu4GD",4],["solana/spl/gamestop_8wxtpeu6557etkp9whfy1n1ecu6nxdvbagghgsmyihsb","solana","GameStop","GME","8wXtPeU6557ETkp9WHFY1n1EcU6NxDvbAggHGsMYiHsB",9],["solana/spl/gdfnesia2wlaw5t8yx2x5j2mkfa74i5kwgdduzht7xmg","solana","CROWN Token","CROWN","GDfnEsia2WLAW5t8yx2X5j2mkfA74i5kwGdDuZHt7XmG",9],["solana/spl/geneth5amgsi8khatqoezp1xexwzj8vcuepynxdkrmyz","solana","Genopets","GENE","GENEtH5amGSi8kHAtQoezp1XEXwZJ8vcuePYnXdKrMYz",9],["solana/spl/gfx1zjr2p15tmrswow6fjydycekofb4p4gjcplbjaxhd","solana","GooseFX","GOFX","GFX1ZjR2P15tmrSwow6FjyDYcEkoFb4p4gJCpLBjaxHD",9],["solana/spl/gth3wg3nerjwcf7vgcoxexkgxshvyhx5gtateem5jas1","solana","Whales Market","WHALES","GTH3wG3NErjwcf7VGCoXEXkgXSHvYhx5gtATeeM5JAS1",6],["solana/spl/h53ugeybrb9easo9ego8yyk7o4zq1g5cctkxd3e3hzav","solana","MXM","MXM","H53UGEyBrB9easo9ego8yYk7o4Zq1G5cCtkxD3E3hZav",6],["solana/spl/hhjoywup5au6pnrvn4s2pweerwxnzkhxkgyjrjmobjlw","solana","PIP","PIP","HHjoYwUp5aU6pnrvN4s2pwEErwXNZKhxKGYjRJMoBjLw",9],["solana/spl/hhjpbhrrn4g56vsylut8dl5bv31hkxqsrahttuczezg4","solana","Myro","$MYRO","HhJpBhRRn4g56VsyLuT8DL5Bv31HkXqsrahTTUCZeZg4",9],["solana/spl/hntyvp6yfm1hg25tn9wglqm12b8tqmcknkrdu1oxwux","solana","Helium Network Token","HNT","hntyVP6YFm1Hg25TN9WGLqM12b8TQmcknKrdu1oxWux",8],["solana/spl/hxhwkvpk5ns4ltg5nij2g671ckxfrkpk8vy271ub4uek","solana","Hxro (Portal)","HXRO","HxhWkVpk5NS4Ltg5nij2G671CKXFRKPK8vy271Ub4uEK",8],["solana/spl/hz1jovnivvgrgniiyveozevgz58xau3rkwx8eacqbct3","solana","Pyth Network","PYTH","HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3",6],["solana/spl/hzwqbkzw8hxmn6bf2yfznrht3c2ixxzpkcfu7ubedktr","solana","EURC","EURC","HzwqbKZw8HxMN6bF2yFZNrht3c2iXXzpKcFu7uBEDKtr",6],["solana/spl/io_bzlbgtncsffoth2gydtwr7e4imwzpr5jqcuugewr646k","solana","IO","IO","BZLbGTNCSFfoth2GYDtwr7e4imWzpR5jqcUuGEwr646K",8],["solana/spl/iotevvzleywotn1qdwnpddxpwszn3zfheot3mfl9fns","solana","Helium IOT","IOT","iotEVVZLEywoTn1QdwNPddxPWszn3zFhEot3MfL9fns",6],["solana/spl/j1toso1uck3rlmjorhttrvwy9hj7x8v9yyac6y7kgcpn","solana","Jito Staked SOL","JITOSOL","J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn",9],["solana/spl/j2lwssxx4r3pybj1fwux5nqo7ppxjcgppub2zhnadwka","solana","DePlan","DPLN","J2LWsSXx4r3pYbJ1fwuX5Nqo7PPxjcGPpUb2zHNadWKa",6],["solana/spl/jtojtomepa8bep8auqc6ext5frijwffmwqx2v2f9mcl","solana","JITO","JTO","jtojtomepa8beP8AuQc6eXt5FriJwfFMwQx2v2f9mCL",9],["solana/spl/jupyiwryjfskupiha7hker8vutaefosybkedznsdvcn","solana","Jupiter","JUP","JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN",6],["solana/spl/kamino_kmno3njsbxfcpjtvhzcxlw7rmtwtt4gvfe7suubo9ss","solana","Kamino","KMNO","KMNo3nJsBXfcpJTVhZcXLW7RmTwTt4GVFE7suUBo9sS",6],["solana/spl/kinxdecpdqehpeuqnqmugtyykqkgvfq6cevx5iahjq6","solana","KIN","KIN","kinXdEcpDQeHPEuQnqmUgtYykqKGVFq6CeVX5iAHJq6",5],["solana/spl/lainetnlgpmcp9rvsf5hn8w6ehniklzqti1xfwmly6x","solana","Laine Stake Token","laineSOL","LAinEtNLgpmCP9Rvsf5Hn8W6EhNiKLZQti1xfWMLy6X",9],["solana/spl/lfntyraetvioapngjht4yng2auzfxr776cmen9vmjxp","solana","Lifinity","LFNTY","LFNTYraetVioAPnGJht4yNg2aUZFXR776cMeN9VMjXp",6],["solana/spl/lstxxxnjzkdfslr4dukpcmcf5vyryeqzplz5j4bpxfp","solana","Liquid Staking Token","LST","LSTxxxnJzKDFSLr4dUkPcmCf5VyryEqzPLz5j4bpxFp",9],["solana/spl/mangoczj36ajzykwvj3vnyu4gtonjfvenjmvvwaxlac","solana","Mango","MNGO","MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac",6],["solana/spl/mb1eu7tzec71kxdpsmskoucssuuoglv1drys1op2jh6","solana","Helium Mobile","MOBILE","mb1eu7TzEc71KxDpsmsKoucSSuuoGLv1drys1oP2jh6",6],["solana/spl/metaewgxypbgwsseh8t16a39cq5vyvxzi9zxidpy18m","solana","Metaplex","META","METAewgxyPbgwsseH8T16a39CQ5VyVxZi9zXiDPY18m",6],["solana/spl/mndefzgvmt87ueuhvvu9vctqsap5b3ftgpshuupa5ey","solana","Marinade","MNDE","MNDEFzGvMt87ueuHvVU9VcTqsAP5b3fTGPsHuuPA5ey",9],["solana/spl/moo_deng_ed5nyywezpppiwimp8vym7sd7td3lat3q3grtwhzpjby","solana","Moo Deng","MOODENG","ED5nyyWEzpPPiWimP8vYm7sD7TD3LAt3Q3gRTWHzPJBY",6],["solana/spl/msolzycxhdygdzu16g5qsh3i5k3z3kzk7ytfqcjm7so","solana","Marinade staked SOL (mSOL)","mSOL","mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So",9],["solana/spl/neontjsjsuo3rexg9o6vhumxw62f9v7zvmu8m8zut44","solana","Neon EVM Token","NEON","NeonTjSjsuo3rexg9o6vHuMXw62f9V7zvmu8M8Zut44",9],["solana/spl/nftukr4u7wkxy9qlax2tgvd9ozswomo4jqsjqdmb7nk","solana","Blockasset","BLOCK","NFTUkR4u7wKxy9QLaX2TGvd9oZSWoMo4jqSJqdMb7Nk",6],["solana/spl/nosxbvoacttydlvky6csb4ac8jcdqkkaawytx2zmoo7","solana","Nosana","NOS","nosXBVoaCTtYdLvKY6Csb4AC8JCdQKKAaWYtx2ZMoo7",6],["solana/spl/nyan_nyanpap9cr7yarbnrby7xx4xu6no6jktbuohna3yscp","solana","NYAN","NYAN","NYANpAp9Cr7YarBNrby7Xx4xU6No6JKTBuohNA3yscP",9],["solana/spl/octo82drbedm8csdaekbymvn86tbtgmpnddme64ptqj","solana","Octokn","OTK","octo82drBEdm8CSDaEKBymVn86TBtgmPnDdmE64PTqJ",9],["solana/spl/orcaektdk7lkz57vaayr9qensvepfiu6qemu1kektze","solana","Orca","ORCA","orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE",6],["solana/spl/poliswxnnrwc6obu1vhiukqzfjgl4xdsu4g9qjz9qvk","solana","Star Atlas DAO","POLIS","poLisWXnNRwC6oBu1vHiuKQzFjGL4XDSu4g9qjz9qVk",8],["solana/spl/pundu_wskzskqew3zsmrhpaevfvzb6puulzwov9mjwzsfdepc","solana","PUNDU","PUNDU","WskzsKqEW3ZsmrhPAevfVZb6PuuLzWov9mJWZsfDePC",9],["solana/spl/rlbxxfkseaz4rgjh3sqn8jxxhmgoz9jwxdnjmh8pl7a","solana","Rollbit Coin","RLB","RLBxxFkseAZ4RgJH3Sqn8jXxhmGoz9jWxDNJMh8pL7a",2],["solana/spl/rndrizkt3mk1iimdxrdwabcf7zg7ar5t4nud4ekhbof","solana","Render Token","RNDR","rndrizKT3MK1iimdxRdWabcF7Zg7AR5T4nud4EkHBof",8],["solana/spl/saber_protocol_token_saber2glauyim4mvftnrasomsv6nvauncvmezwclpd1","solana","Saber Protocol Token","SBR","Saber2gLauYim4Mvftnrasomsv6NvAuncvMEZwcLpD1",6],["solana/spl/scsuppnusyplbsv4darsryng4anpgaghkhsa3gmmyjz","solana","Solcasino Token","SCS","SCSuPPNUSypLBsV4darsrYNg4ANPgaGhKhsA3GmMyjz",5],["solana/spl/serum_srmuapvndxxokk5gt7xd5cuugxmbcoaz2lheuaokwrt","solana","Serum","SRM","SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt",6],["solana/spl/sharksyjjqanyxvfrpnbn9pjgkhwdhatnmyicwpnr1s","solana","Sharky","SHARK","SHARKSYJjqaNyxVfrpnBN9pjgkhwDhatnMyicWPnr1s",6],["solana/spl/shdwybxihqicj6yekg2gur7wqklelamk1ghzck9pl6y","solana","Shadow Token","SHDW","SHDWyBxihqiCj6YekG2GUr7wqKLeLAMK1gHZck9pL6y",9],["solana/spl/slerf_7bgbvyjrzx1ykz4oh9mjb8zscatkkwb8dzfx7loivkm3","solana","SLERF","SLERF","7BgBvyjrZX1YKz4oh9mjb8ZScatkkwb8DzFx7LoiVkM3",9],["solana/spl/slndpmowtvadgedndyvwzronl7zsi1df9pc3xhgtpwp","solana","Solend","SLND","SLNDpmoWTVADgEdndyvWzroNL7zSi1dF9PC3xHGtPwp",6],["solana/spl/snsnkv9zfg5zkwqs6x4hxvbrv6s8sqmfsgctecdvdmd","solana","SynesisOne","SNS","SNSNkV9zfG5ZKWQs6x4hxvBRV6s8SqMfSGCtECDvdMd",9],["solana/spl/so11111111111111111111111111111111111111112","solana","Wrapped SOL","SOL","So11111111111111111111111111111111111111112",9],["solana/spl/sols_2wme8evkw8qsfsk2b3qex4s64ac6wxhpxb3grdckekio","solana","sols","sols","2wme8EVkw8qsfSk2B3QeX4S64ac6wxHPXb3GrdckEkio",9],["solana/spl/stepascqoeiofxxwgnh2slbdfp9d8rvkz2yp39idpyt","solana","Step","STEP","StepAscQoEioFxxWGnh2sLBDFp9d8rvKz2Yp39iDpyT",9],["solana/spl/taki7fi3zicv7du1xnawlaf6mrk7ikdn77hegzgwvo4","solana","Taki","TAKI","Taki7fi3Zicv7Du1xNAWLaf6mRK7ikdn77HeGzgwvo4",9],["solana/spl/tnsrxcuxot9xbg3de7pijytdyu7ksklqcpddxnejas6","solana","Tensor","TNSR","TNSRxcUxoT9xBG3de7PiJyTDYu7kskLqcpddxnEJAS6",9],["solana/spl/ukhh6c7mmyiwcf1b9pnwe25tspkddt3h5pqzgz74j82","solana","BOOK OF MEME","BOME","ukHH6c7mMyiWCf1b9pnWe25TSpkDDt3H5pQZgZ74J82",6],["solana/spl/uxphbor3qg4ucignjfv7mqhhyfqkn68g45goyvael2m","solana","UXP Governance Token","UXP","UXPhBoR3qG4UCiGNJfV7MqhHyFqKN68g45GoYvAeL2M",9],["solana/spl/wenwenvqqnya429ubcdr81zmd69brwqaabyy6p3lcpk","solana","Wen","WEN","WENWENvqqNya429ubCdR81ZmD69brwQaaBYY6p3LCpk",5],["solana/spl/xxxxa1skngwftw2kfn8xauw9xq8hbz5kvtcsestt9fw","solana","Solanium","SLIM","xxxxa1sKNGwFtw2kFn8XauW9xq8hBZ5kVtcSesTT9fW",6],["solana/spl/yomfpuqz1wjwysfd5tzjuts3bnb8xs8mx9xzbv8rl39","solana","YOM","YOM","yomFPUqz1wJwYSfD5tZJUtS3bNb8xs8mx9XzBv8RL39",9],["solana/spl/zebeczgi5fsetbpfqkvzkcj3wgyxxjkmuknnx7flkaf","solana","ZEBEC","ZBC","zebeczgi5fSEtbpfQKVZKCJ3WgYXxjkMUkNNx7fLKAF",9],["solana/spl/zeus1ar7ax8dffjf5qjwj2ftdddntromngo8yoqm3gq","solana","ZEUS","ZEUS","ZEUS1aR7aX8DFFJf5QjWj2ftDDdNTroMNGo8YoQm3Gq",6]] \ No newline at end of file diff --git a/libs/ledgerjs/packages/cryptoassets/src/data/spl.ts b/libs/ledgerjs/packages/cryptoassets/src/data/spl.ts index f79c2a8033de..6a2038797f0c 100644 --- a/libs/ledgerjs/packages/cryptoassets/src/data/spl.ts +++ b/libs/ledgerjs/packages/cryptoassets/src/data/spl.ts @@ -1,6 +1,6 @@ export type SPLToken = [ string, // CAL id - number, // chainId + string, // network string, // name string, // ticker string, // address diff --git a/libs/ledgerjs/packages/cryptoassets/src/tokens.ts b/libs/ledgerjs/packages/cryptoassets/src/tokens.ts index 48d494f738e6..156d978da70b 100644 --- a/libs/ledgerjs/packages/cryptoassets/src/tokens.ts +++ b/libs/ledgerjs/packages/cryptoassets/src/tokens.ts @@ -406,22 +406,12 @@ function convertElrondESDTTokens([ }; } -function convertSplTokens([id, chainId, name, symbol, address, decimals]: SPLToken): TokenCurrency { - const chainIdToCurrencyId = { - // Fallback in case CAL is using chainIds for vault - 1: "solana", - 2: "solana_testnet", - 3: "solana_devnet", - 101: "solana", - 102: "solana_testnet", - 103: "solana_devnet", - }; - const currencyId = chainIdToCurrencyId[chainId]; +function convertSplTokens([id, network, name, symbol, address, decimals]: SPLToken): TokenCurrency { return { type: "TokenCurrency", id, contractAddress: address, - parentCurrency: getCryptoCurrencyById(currencyId), + parentCurrency: getCryptoCurrencyById(network), name, tokenType: "spl", ticker: symbol, From 97073a573e47a1a3c006b05fb8c140b0a71a515f Mon Sep 17 00:00:00 2001 From: Kant Date: Fri, 10 Jan 2025 10:35:37 +0100 Subject: [PATCH 34/35] feat: add better error for sol --- .../src/renderer/components/ErrorDisplay.tsx | 22 ++++++++++++++++++- .../src/families/solana/setup.ts | 10 ++++++--- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/apps/ledger-live-desktop/src/renderer/components/ErrorDisplay.tsx b/apps/ledger-live-desktop/src/renderer/components/ErrorDisplay.tsx index dc83243daab9..fcbf57dc5edb 100644 --- a/apps/ledger-live-desktop/src/renderer/components/ErrorDisplay.tsx +++ b/apps/ledger-live-desktop/src/renderer/components/ErrorDisplay.tsx @@ -1,3 +1,5 @@ +import { ManagerNotEnoughSpaceError, UpdateYourApp } from "@ledgerhq/errors"; +import { LatestFirmwareVersionRequired, OutdatedApp } from "@ledgerhq/live-common/errors"; import { useTranslation } from "react-i18next"; import { renderError } from "~/renderer/components/DeviceAction/rendering"; @@ -22,7 +24,25 @@ const ErrorDisplay = ({ }: ErrorDisplayProps) => { const { t } = useTranslation(); - return renderError({ t, error, onRetry, withExportLogs, list, supportLink, warning, Icon }); + const managerAppName = + error instanceof ManagerNotEnoughSpaceError || + (error as unknown) instanceof OutdatedApp || + (error as unknown) instanceof UpdateYourApp + ? (error as unknown as { managerAppName: string }).managerAppName + : undefined; + + return renderError({ + t, + error, + onRetry, + managerAppName, + requireFirmwareUpdate: error instanceof LatestFirmwareVersionRequired, + withExportLogs, + list, + supportLink, + warning, + Icon, + }); }; export default ErrorDisplay; diff --git a/libs/ledger-live-common/src/families/solana/setup.ts b/libs/ledger-live-common/src/families/solana/setup.ts index 4a5acb597ff5..82e12120bf6a 100644 --- a/libs/ledger-live-common/src/families/solana/setup.ts +++ b/libs/ledger-live-common/src/families/solana/setup.ts @@ -13,16 +13,20 @@ import { DeviceModelId } from "@ledgerhq/devices"; import { loadPKI } from "@ledgerhq/hw-bolos"; import calService from "@ledgerhq/ledger-cal-service"; import trustService from "@ledgerhq/ledger-trust-service"; -import { FirmwareOrAppUpdateRequired, TransportStatusError } from "@ledgerhq/errors"; +import { TransportStatusError, UpdateYourApp } from "@ledgerhq/errors"; import { CreateSigner, createResolver, executeWithSigner } from "../../bridge/setup"; +import { LatestFirmwareVersionRequired } from "../../errors"; import type { Resolver } from "../../hw/getAddress/types"; const TRUSTED_NAME_MIN_VERSION = "1.6.1"; +const MANAGER_APP_NAME = "Solana"; async function checkVersion(app: Solana) { const { version } = await app.getAppConfiguration(); if (semver.lt(version, TRUSTED_NAME_MIN_VERSION)) { - throw new FirmwareOrAppUpdateRequired(); + throw new UpdateYourApp(undefined, { + managerAppName: MANAGER_APP_NAME, + }); } } @@ -49,7 +53,7 @@ const createSigner: CreateSigner = (transport: Transport) => { await loadPKI(transport, "TRUSTED_NAME", descriptor, signature); } catch (err) { if (isPKIUnsupportedError(err)) { - throw new FirmwareOrAppUpdateRequired(); + throw new LatestFirmwareVersionRequired("LatestFirmwareVersionRequired"); } } From 460f55f901481d759148c61d29fdcc6a36f50fc1 Mon Sep 17 00:00:00 2001 From: Kant Date: Wed, 15 Jan 2025 10:25:50 +0100 Subject: [PATCH 35/35] feat: LLM better error on send validate --- .../src/components/DeviceAction/index.tsx | 9 +-- .../src/components/ValidateError.tsx | 80 ++++++++++++++----- apps/ledger-live-mobile/src/types/error.ts | 6 ++ 3 files changed, 69 insertions(+), 26 deletions(-) create mode 100644 apps/ledger-live-mobile/src/types/error.ts diff --git a/apps/ledger-live-mobile/src/components/DeviceAction/index.tsx b/apps/ledger-live-mobile/src/components/DeviceAction/index.tsx index 377c55079a55..80133c6e669a 100644 --- a/apps/ledger-live-mobile/src/components/DeviceAction/index.tsx +++ b/apps/ledger-live-mobile/src/components/DeviceAction/index.tsx @@ -25,7 +25,6 @@ import { InitSwapResult, } from "@ledgerhq/live-common/exchange/swap/types"; import { AppAndVersion } from "@ledgerhq/live-common/hw/connectApp"; -import { LedgerErrorConstructor } from "@ledgerhq/errors/lib/helpers"; import { StackNavigationProp } from "@react-navigation/stack"; import { setLastSeenDeviceInfo } from "~/actions/settings"; import ValidateOnDevice from "../ValidateOnDevice"; @@ -58,18 +57,14 @@ import ModalLock from "../ModalLock"; import { walletSelector } from "~/reducers/wallet"; import { settingsStoreSelector } from "~/reducers/settings"; import { RootStackParamList } from "../RootNavigator/types/RootNavigator"; - -type LedgerError = InstanceType>; +import { LedgerError } from "~/types/error"; type Status = PartialNullable<{ appAndVersion: AppAndVersion; device: Device; unresponsive: boolean; isLocked: boolean; - error: LedgerError & { - name?: string; - managerAppName?: string; - }; + error: LedgerError; isLoading: boolean; allowManagerRequested: boolean; allowRenamingRequested: boolean; diff --git a/apps/ledger-live-mobile/src/components/ValidateError.tsx b/apps/ledger-live-mobile/src/components/ValidateError.tsx index 4ba708c9107a..48351dde3321 100644 --- a/apps/ledger-live-mobile/src/components/ValidateError.tsx +++ b/apps/ledger-live-mobile/src/components/ValidateError.tsx @@ -1,19 +1,55 @@ -import React, { memo } from "react"; +import React, { memo, useCallback } from "react"; import { View, StyleSheet } from "react-native"; -import { Trans } from "react-i18next"; -import { useTheme } from "@react-navigation/native"; +import { useTranslation } from "react-i18next"; +import { useTheme, useNavigation } from "@react-navigation/native"; import GenericErrorView from "./GenericErrorView"; import Button from "./Button"; import NeedHelp from "./NeedHelp"; +import { BaseNavigation } from "./RootNavigator/types/helpers"; +import { NavigatorName, ScreenName } from "~/const"; +import { MANAGER_TABS } from "~/const/manager"; +import { LatestFirmwareVersionRequired } from "@ledgerhq/live-common/errors"; +import { RequiredFirmwareUpdate } from "./DeviceAction/rendering"; +import { useSelector } from "react-redux"; +import { lastConnectedDeviceSelector } from "~/reducers/settings"; +import { LedgerError } from "~/types/error"; type Props = { - error: Error; + error: LedgerError; onClose: () => void; onRetry?: () => void; }; function ValidateError({ error, onClose, onRetry }: Props) { + const navigation = useNavigation(); + const { t } = useTranslation(); const { colors } = useTheme(); + + const managerAppName = error?.name === "UpdateYourApp" ? error.managerAppName : undefined; + + const lastConnectedDevice = useSelector(lastConnectedDeviceSelector); + + const onPress = useCallback(() => { + if (managerAppName && navigation) { + navigation.navigate(NavigatorName.Base, { + screen: NavigatorName.Main, + params: { + screen: NavigatorName.MyLedger, + params: { + screen: ScreenName.MyLedgerChooseDevice, + params: { + tab: MANAGER_TABS.INSTALLED_APPS, + updateModalOpened: true, + device: lastConnectedDevice, + }, + }, + }, + }); + } else if (onRetry) { + onRetry(); + } + }, [lastConnectedDevice, managerAppName, navigation, onRetry]); + return ( - -