diff --git a/packages/solana-wallet-snap/snap.manifest.json b/packages/solana-wallet-snap/snap.manifest.json index 3db6ea623..95ba62813 100644 --- a/packages/solana-wallet-snap/snap.manifest.json +++ b/packages/solana-wallet-snap/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/internal-snaps.git" }, "source": { - "shasum": "V3iOdFzSye/NKphktjgWLlyZ6npSWHr+dgR0tkVtJvU=", + "shasum": "AEtvrNFWywA98I2okgZahTBeW0PeNhyIpS5NzT9bheM=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/solana-wallet-snap/src/core/services/assets/adapters/SnapAssetsAdapter.test.ts b/packages/solana-wallet-snap/src/core/services/assets/adapters/SnapAssetsAdapter.test.ts index 1f9be1274..1a673a63c 100644 --- a/packages/solana-wallet-snap/src/core/services/assets/adapters/SnapAssetsAdapter.test.ts +++ b/packages/solana-wallet-snap/src/core/services/assets/adapters/SnapAssetsAdapter.test.ts @@ -6,13 +6,16 @@ import { InMemoryCache } from '../../../caching/InMemoryCache'; import { MOCK_NFTS_LIST_RESPONSE_MAPPED } from '../../../clients/nft-api/mocks/mockNftsListResponseMapped'; import type { NftApiClient } from '../../../clients/nft-api/NftApiClient'; import type { TokenApiClient } from '../../../clients/token-api-client/TokenApiClient'; +import { Network } from '../../../constants/solana'; import { MOCK_ASSET_ENTITY_0, MOCK_ASSET_ENTITY_1, MOCK_ASSET_ENTITY_2, } from '../../../test/mocks/asset-entities'; +import { MOCK_SOLANA_KEYRING_ACCOUNT_0 } from '../../../test/mocks/solana-keyring-accounts'; import { mockLogger } from '../../__mocks__/logger'; import { createMockConnection } from '../../__mocks__/mockConnection'; +import { MOCK_SOLANA_RPC_GET_TOKEN_ACCOUNTS_BY_OWNER_RESPONSE } from '../../__mocks__/mockSolanaRpcResponses'; import type { AccountsService } from '../../accounts/AccountsService'; import type { ConfigProvider } from '../../config'; import type { SolanaConnection } from '../../connection'; @@ -115,4 +118,58 @@ describe('SnapAssetsAdapter', () => { expect(SnapAssetsAdapter.hasChanged(asset, assetsLookup)).toBe(false); }); }); + + describe('fetch', () => { + it('aggregates token accounts for the same mint', async () => { + jest + .spyOn(mockConfigProvider, 'getActiveNetworks') + .mockImplementation() + .mockResolvedValue([Network.Mainnet]); + jest + .spyOn(mockTokenApiClient, 'getTokensMetadata') + .mockImplementation() + .mockResolvedValue({}); + + const [firstTokenAccount] = + MOCK_SOLANA_RPC_GET_TOKEN_ACCOUNTS_BY_OWNER_RESPONSE.result.value; + if (!firstTokenAccount) { + throw new Error('Missing token account fixture'); + } + const secondTokenAccount = cloneDeep(firstTokenAccount); + secondTokenAccount.pubkey = + '7Gg2Y8vCj3v5nQj5xFfC3uT7wJ9sN4mK2pL8rH6dQ1eA'; + secondTokenAccount.account.data.parsed.info.tokenAmount.amount = + '1000000'; + secondTokenAccount.account.data.parsed.info.tokenAmount.uiAmountString = + '1'; + + jest.spyOn(mockConnection, 'getRpc').mockReturnValue({ + getBalance: jest.fn().mockReturnValue({ + send: jest.fn().mockResolvedValue({ value: 1000000000 }), + }), + getTokenAccountsByOwner: jest + .fn() + .mockReturnValueOnce({ + send: jest.fn().mockResolvedValue({ + value: [firstTokenAccount, secondTokenAccount], + }), + }) + .mockReturnValue({ + send: jest.fn().mockResolvedValue({ value: [] }), + }), + } as unknown as ReturnType); + + expect( + await snapAssetsAdapter.fetch(MOCK_SOLANA_KEYRING_ACCOUNT_0), + ).toStrictEqual( + expect.arrayContaining([ + expect.objectContaining({ + mint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', + rawAmount: '124456789', + uiAmount: '124.456789', + }), + ]), + ); + }); + }); }); diff --git a/packages/solana-wallet-snap/src/core/services/assets/adapters/SnapAssetsAdapter.ts b/packages/solana-wallet-snap/src/core/services/assets/adapters/SnapAssetsAdapter.ts index c84d13e3e..53db06e30 100644 --- a/packages/solana-wallet-snap/src/core/services/assets/adapters/SnapAssetsAdapter.ts +++ b/packages/solana-wallet-snap/src/core/services/assets/adapters/SnapAssetsAdapter.ts @@ -21,6 +21,7 @@ import type { Address, } from '@solana/kit'; import { address as asAddress } from '@solana/kit'; +import { BigNumber } from 'bignumber.js'; import type { AssetEntity, @@ -360,26 +361,42 @@ export class SnapAssetsAdapter { const tokensMetadata = await this.#tokenApiClient.getTokensMetadata(assetTypes); - const tokenAssets: TokenAsset[] = tokenAccounts - .filter((tokenAccount) => tokenAccount.assetType.includes('/token:')) - .map((tokenAccount) => { - const { assetType } = tokenAccount; - const { decimals, amount, uiAmountString } = - tokenAccount.token.account.data.parsed.info.tokenAmount; + const tokenAssetsByKey = new Map(); - return { + tokenAccounts + .filter((tokenAccount) => tokenAccount.assetType.includes('/token:')) + .forEach((tokenAccount) => { + const { assetType, scope, keyringAccount } = tokenAccount; + const { mint, tokenAmount } = + tokenAccount.token.account.data.parsed.info; + const { decimals, amount, uiAmountString } = tokenAmount; + const key = `${keyringAccount.id}:${scope}:${mint}`; + const existingAsset = tokenAssetsByKey.get(key); + + if (existingAsset) { + existingAsset.rawAmount = ( + BigInt(existingAsset.rawAmount) + BigInt(amount) + ).toString(); + existingAsset.uiAmount = new BigNumber(existingAsset.uiAmount) + .plus(uiAmountString ?? fromTokenUnits(amount, decimals)) + .toFixed(); + return; + } + + tokenAssetsByKey.set(key, { assetType, - keyringAccountId: tokenAccount.keyringAccount.id, - network: tokenAccount.scope, - mint: tokenAccount.token.account.data.parsed.info.mint, - pubkey: tokenAccount.token.pubkey, + keyringAccountId: keyringAccount.id, + network: scope, + mint, symbol: tokensMetadata[assetType]?.symbol ?? 'UNKNOWN', decimals, rawAmount: amount, uiAmount: uiAmountString ?? fromTokenUnits(amount, decimals), - }; + }); }); + const tokenAssets = [...tokenAssetsByKey.values()]; + // const nftAssets = await this.#fetchNftAssets(account, tokenAccounts.filter( // (token) => token.assetType.includes('/nft:'), // )); diff --git a/packages/solana-wallet-snap/src/core/services/send/SendService.test.ts b/packages/solana-wallet-snap/src/core/services/send/SendService.test.ts index 511982d7e..545073fa6 100644 --- a/packages/solana-wallet-snap/src/core/services/send/SendService.test.ts +++ b/packages/solana-wallet-snap/src/core/services/send/SendService.test.ts @@ -435,7 +435,6 @@ describe('SendService', () => { keyringAccountId: mockAccount.id, network: Network.Mainnet, mint: 'token-address-123', - pubkey: 'token-address-123', symbol: 'USDC', decimals: 6, rawAmount: '1000000', @@ -503,7 +502,6 @@ describe('SendService', () => { keyringAccountId: mockAccount.id, network: Network.Mainnet, mint: 'token-address-123', - pubkey: 'token-address-123', symbol: 'USDC', decimals: 6, rawAmount: '100000000000', @@ -544,7 +542,6 @@ describe('SendService', () => { keyringAccountId: mockAccount.id, network: Network.Mainnet, mint: 'token-address-123', - pubkey: 'token-address-123', symbol: 'USDC', decimals: 6, rawAmount: '100000000000', diff --git a/packages/solana-wallet-snap/src/core/services/subscriptions/KeyringAccountMonitor.test.ts b/packages/solana-wallet-snap/src/core/services/subscriptions/KeyringAccountMonitor.test.ts index 8ae4a7e88..fd7a7d485 100644 --- a/packages/solana-wallet-snap/src/core/services/subscriptions/KeyringAccountMonitor.test.ts +++ b/packages/solana-wallet-snap/src/core/services/subscriptions/KeyringAccountMonitor.test.ts @@ -535,7 +535,6 @@ describe('KeyringAccountMonitor', () => { keyringAccountId: account.id, network: Network.Mainnet, mint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', - pubkey: '9wt9PfjPD3JCy5r7o4K1cTGiuTG7fq2pQhdDCdQALKjg', symbol: 'USDC', decimals: 6, rawAmount: '123456789', diff --git a/packages/solana-wallet-snap/src/core/services/subscriptions/KeyringAccountMonitor.ts b/packages/solana-wallet-snap/src/core/services/subscriptions/KeyringAccountMonitor.ts index bf8541683..62970ee00 100644 --- a/packages/solana-wallet-snap/src/core/services/subscriptions/KeyringAccountMonitor.ts +++ b/packages/solana-wallet-snap/src/core/services/subscriptions/KeyringAccountMonitor.ts @@ -411,7 +411,6 @@ export class KeyringAccountMonitor { keyringAccountId: keyringAccount.id, network, mint, - pubkey, symbol: metadata?.symbol ?? 'UNKNOWN', decimals, rawAmount: amount, diff --git a/packages/solana-wallet-snap/src/core/services/transactions/TransactionsService.test.ts b/packages/solana-wallet-snap/src/core/services/transactions/TransactionsService.test.ts index 6f9ea34ce..ed483b768 100644 --- a/packages/solana-wallet-snap/src/core/services/transactions/TransactionsService.test.ts +++ b/packages/solana-wallet-snap/src/core/services/transactions/TransactionsService.test.ts @@ -1,7 +1,9 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import type { Transaction } from '@metamask/keyring-api'; +import { TOKEN_PROGRAM_ADDRESS } from '@solana-program/token'; +import { TOKEN_2022_PROGRAM_ADDRESS } from '@solana-program/token-2022'; import { address as asAddress } from '@solana/kit'; -import type { NativeAsset } from 'src/entities'; +import type { NativeAsset, TokenAsset } from 'src/entities'; import { KnownCaip19Id, Network } from '../../constants/solana'; import { @@ -19,6 +21,19 @@ import type { TransactionMapper } from './TransactionMapper'; import type { TransactionsRepository } from './TransactionsRepository'; import { TransactionsService } from './TransactionsService'; +const MOCK_TOKEN_ACCOUNT_RESPONSE = { + account: { + data: { + parsed: { + info: { + mint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', + }, + }, + }, + }, + pubkey: asAddress('9wt9PfjPD3JCy5r7o4K1cTGiuTG7fq2pQhdDCdQALKjg'), +}; + jest.mock('../../utils/errors', () => ({ trackError: jest.fn().mockResolvedValue('tracked-error-id'), })); @@ -82,7 +97,7 @@ describe('TransactionsService', () => { getTransaction: jest.fn().mockReturnValue({ send: jest.fn().mockResolvedValue(mockTransactionData), }), - } as any); + } as unknown as ReturnType); jest .spyOn(mockTransactionMapper, 'mapRpcTransaction') @@ -102,7 +117,7 @@ describe('TransactionsService', () => { getTransaction: jest.fn().mockReturnValue({ send: jest.fn().mockResolvedValue(null), }), - } as any); + } as unknown as ReturnType); const result = await service.fetchBySignature( mockSignature, @@ -120,7 +135,7 @@ describe('TransactionsService', () => { getSignaturesForAddress: jest.fn().mockReturnValue({ send: jest.fn().mockResolvedValue(MOCK_GET_SIGNATURES_FOR_ADDRESS), }), - } as any); + } as unknown as ReturnType); const result = await service.fetchLatestSignatures( Network.Localnet, @@ -195,12 +210,184 @@ describe('TransactionsService', () => { getTransaction: jest.fn().mockReturnValue({ send: jest.fn().mockRejectedValue(error), }), - } as any); + } as unknown as ReturnType); expect(await service.fetchAssetsTransactions([asset])).toStrictEqual([]); expect(trackError).toHaveBeenCalledTimes(1); expect(trackError).toHaveBeenCalledWith(error); }); + + it('fetches signatures for every token account, including non-ATA accounts', async () => { + const asset: TokenAsset = { + assetType: KnownCaip19Id.UsdcMainnet, + keyringAccountId: MOCK_SOLANA_KEYRING_ACCOUNT_0.id, + network: Network.Mainnet, + mint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', + symbol: 'USDC', + decimals: 6, + rawAmount: '1', + uiAmount: '0.000001', + }; + const ata = '9wt9PfjPD3JCy5r7o4K1cTGiuTG7fq2pQhdDCdQALKjg'; + const nonAta = '7Gg2Y8vCj3v5nQj5xFfC3uT7wJ9sN4mK2pL8rH6dQ1eA'; + const signature = MOCK_GET_SIGNATURES_FOR_ADDRESS[0]!; + const getSignaturesForAddress = jest.fn((address: string) => ({ + send: jest.fn().mockResolvedValue( + address === nonAta + ? [ + { + ...signature, + signature: 'non-ata-signature', + }, + ] + : [], + ), + })); + + jest + .spyOn(mockAccountsService, 'getAll') + .mockResolvedValue([MOCK_SOLANA_KEYRING_ACCOUNT_0]); + jest.spyOn(mockAssetsService, 'getAssetsMetadata').mockResolvedValue({}); + jest.spyOn(mockTransactionsRepository, 'getAll').mockResolvedValue([]); + jest.spyOn(mockConnection, 'getRpc').mockReturnValue({ + getTokenAccountsByOwner: jest.fn( + (_owner: string, filter: { programId: string }) => ({ + send: jest.fn().mockResolvedValue({ + value: + filter.programId === TOKEN_PROGRAM_ADDRESS + ? [ + { + ...MOCK_TOKEN_ACCOUNT_RESPONSE, + pubkey: asAddress(ata), + }, + { + ...MOCK_TOKEN_ACCOUNT_RESPONSE, + pubkey: asAddress(nonAta), + }, + ] + : [], + }), + }), + ), + getSignaturesForAddress, + getTransaction: jest.fn().mockReturnValue({ + send: jest.fn().mockResolvedValue(null), + }), + } as unknown as ReturnType); + + await service.fetchAssetsTransactions([asset]); + + expect(getSignaturesForAddress).toHaveBeenCalledTimes(2); + expect(getSignaturesForAddress).toHaveBeenCalledWith(ata, { + limit: 5, + }); + expect(getSignaturesForAddress).toHaveBeenCalledWith(nonAta, { + limit: 5, + }); + }); + + it('continues when one token program request fails', async () => { + const asset: TokenAsset = { + assetType: KnownCaip19Id.UsdcMainnet, + keyringAccountId: MOCK_SOLANA_KEYRING_ACCOUNT_0.id, + network: Network.Mainnet, + mint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', + symbol: 'USDC', + decimals: 6, + rawAmount: '1', + uiAmount: '0.000001', + }; + const tokenAccount = '9wt9PfjPD3JCy5r7o4K1cTGiuTG7fq2pQhdDCdQALKjg'; + const getSignaturesForAddress = jest.fn().mockReturnValue({ + send: jest.fn().mockResolvedValue([]), + }); + + jest + .spyOn(mockAccountsService, 'getAll') + .mockResolvedValue([MOCK_SOLANA_KEYRING_ACCOUNT_0]); + jest.spyOn(mockAssetsService, 'getAssetsMetadata').mockResolvedValue({}); + jest.spyOn(mockTransactionsRepository, 'getAll').mockResolvedValue([]); + jest.spyOn(mockConnection, 'getRpc').mockReturnValue({ + getTokenAccountsByOwner: jest.fn( + (_owner: string, filter: { programId: string }) => ({ + send: + filter.programId === TOKEN_PROGRAM_ADDRESS + ? jest.fn().mockResolvedValue({ + value: [ + { + ...MOCK_TOKEN_ACCOUNT_RESPONSE, + pubkey: asAddress(tokenAccount), + }, + ], + }) + : jest.fn().mockRejectedValue(new Error('Token-2022 failed')), + }), + ), + getSignaturesForAddress, + } as unknown as ReturnType); + + expect(await service.fetchAssetsTransactions([asset])).toStrictEqual([]); + expect(getSignaturesForAddress).toHaveBeenCalledWith(tokenAccount, { + limit: 5, + }); + expect(trackError).toHaveBeenCalledWith( + expect.objectContaining({ message: 'Token-2022 failed' }), + ); + }); + + it('queries Token-2022 token accounts as well', async () => { + const asset: TokenAsset = { + assetType: KnownCaip19Id.UsdcMainnet, + keyringAccountId: MOCK_SOLANA_KEYRING_ACCOUNT_0.id, + network: Network.Mainnet, + mint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', + symbol: 'USDC', + decimals: 6, + rawAmount: '1', + uiAmount: '0.000001', + }; + const token2022Account = '7Gg2Y8vCj3v5nQj5xFfC3uT7wJ9sN4mK2pL8rH6dQ1eA'; + const getTokenAccountsByOwner = jest.fn( + (_owner: string, filter: { programId: string }) => ({ + send: jest.fn().mockResolvedValue({ + value: + filter.programId === TOKEN_2022_PROGRAM_ADDRESS + ? [ + { + ...MOCK_TOKEN_ACCOUNT_RESPONSE, + pubkey: asAddress(token2022Account), + }, + ] + : [], + }), + }), + ); + + jest + .spyOn(mockAccountsService, 'getAll') + .mockResolvedValue([MOCK_SOLANA_KEYRING_ACCOUNT_0]); + jest.spyOn(mockAssetsService, 'getAssetsMetadata').mockResolvedValue({}); + jest.spyOn(mockTransactionsRepository, 'getAll').mockResolvedValue([]); + jest.spyOn(mockConnection, 'getRpc').mockReturnValue({ + getTokenAccountsByOwner, + getSignaturesForAddress: jest.fn().mockReturnValue({ + send: jest.fn().mockResolvedValue([]), + }), + } as unknown as ReturnType); + + await service.fetchAssetsTransactions([asset]); + + expect(getTokenAccountsByOwner).toHaveBeenCalledWith( + asAddress(MOCK_SOLANA_KEYRING_ACCOUNT_0.address), + { programId: TOKEN_PROGRAM_ADDRESS }, + { encoding: 'jsonParsed' }, + ); + expect(getTokenAccountsByOwner).toHaveBeenCalledWith( + asAddress(MOCK_SOLANA_KEYRING_ACCOUNT_0.address), + { programId: TOKEN_2022_PROGRAM_ADDRESS }, + { encoding: 'jsonParsed' }, + ); + }); }); describe('save', () => { diff --git a/packages/solana-wallet-snap/src/core/services/transactions/TransactionsService.ts b/packages/solana-wallet-snap/src/core/services/transactions/TransactionsService.ts index 6a80ed81e..3d8a1ea45 100644 --- a/packages/solana-wallet-snap/src/core/services/transactions/TransactionsService.ts +++ b/packages/solana-wallet-snap/src/core/services/transactions/TransactionsService.ts @@ -1,6 +1,8 @@ import { KeyringEvent } from '@metamask/keyring-api'; import type { Transaction } from '@metamask/keyring-api'; import { emitSnapKeyringEvent } from '@metamask/keyring-snap-sdk'; +import { TOKEN_PROGRAM_ADDRESS } from '@solana-program/token'; +import { TOKEN_2022_PROGRAM_ADDRESS } from '@solana-program/token-2022'; import type { Address, Commitment, Signature, Slot } from '@solana/kit'; import { address as asAddress, signature as asSignature } from '@solana/kit'; import { get, groupBy } from 'lodash'; @@ -81,16 +83,20 @@ export class TransactionsService { limit?: number; }, ): Promise { - const accounts = await this.#accountsService.getAll(); - const assetTypes = assets.map((asset) => asset.assetType); - const assetsMetadata = - await this.#assetsService.getAssetsMetadata(assetTypes); + const [accounts, assetsMetadata, savedTransactions] = await Promise.all([ + this.#accountsService.getAll(), + this.#assetsService.getAssetsMetadata(assetTypes), + this.#transactionsRepository.getAll(), + ]); - const savedTransactions = await this.#transactionsRepository.getAll(); + const findAccountById = (id: string): SolanaKeyringAccount | undefined => + accounts.find((account) => account.id === id); - const findLatestTransactionForAsset = async (asset: AssetEntity) => { + const findLatestTransactionForAsset = async ( + asset: AssetEntity, + ): Promise => { const { network } = asset; const addressOrMint = 'mint' in asset ? asset.mint : asset.address; @@ -117,6 +123,59 @@ export class TransactionsService { return existingTransaction; }; + const tokenAccountsByOwnerAndNetwork = new Map< + string, + Promise> + >(); + + const getTokenAccounts = async ( + account: SolanaKeyringAccount, + network: Network, + ): Promise> => { + const key = `${account.id}:${network}`; + const cached = tokenAccountsByOwnerAndNetwork.get(key); + if (cached) { + return cached; + } + + const request: Promise> = (async () => { + const rpc = this.#connection.getRpc(network); + const responses = await Promise.allSettled( + [TOKEN_PROGRAM_ADDRESS, TOKEN_2022_PROGRAM_ADDRESS].map( + async (tokenProgram) => + rpc + .getTokenAccountsByOwner( + asAddress(account.address), + { programId: tokenProgram }, + { encoding: 'jsonParsed' }, + ) + .send(), + ), + ); + + const accountsByMint = new Map(); + for (const response of responses) { + if (response.status === 'rejected') { + await trackError(response.reason); + continue; + } + + const { value } = response.value; + for (const tokenAccount of value) { + const { mint } = tokenAccount.account.data.parsed.info; + const accountsForMint = accountsByMint.get(mint) ?? []; + accountsForMint.push(tokenAccount.pubkey); + accountsByMint.set(mint, accountsForMint); + } + } + + return accountsByMint; + })(); + + tokenAccountsByOwnerAndNetwork.set(key, request); + return request; + }; + type SignatureWithAsset = { signatureResponse: { signature: Signature; @@ -129,29 +188,51 @@ export class TransactionsService { asset: AssetEntity, ): Promise => { const { network } = asset; - const addressOrPubkey = 'pubkey' in asset ? asset.pubkey : asset.address; + const account = findAccountById(asset.keyringAccountId); + + if (!account) { + return []; + } + const addresses = + 'mint' in asset + ? ((await getTokenAccounts(account, network)).get(asset.mint) ?? []) + : [asAddress(asset.address)]; const latestTransaction = await findLatestTransactionForAsset(asset); const latestSignature = latestTransaction - ? asSignature(latestTransaction?.id) + ? asSignature(latestTransaction.id) : undefined; - const response = await this.#connection - .getRpc(network) - .getSignaturesForAddress(asAddress(addressOrPubkey), { - limit: 5, - ...(latestSignature ? { until: latestSignature } : {}), - }) - .send(); - - return response.map((item) => ({ - signatureResponse: { - signature: item.signature, - blockTime: Number(item.blockTime ?? 0), - }, - asset, - })); + const responses = await Promise.allSettled( + addresses.map(async (address) => { + const response = await this.#connection + .getRpc(network) + .getSignaturesForAddress(address, { + limit: 5, + ...(latestSignature ? { until: latestSignature } : {}), + }) + .send(); + return response.map((item) => ({ + signatureResponse: { + signature: item.signature, + blockTime: Number(item.blockTime ?? 0), + }, + asset, + })); + }), + ); + + const signatureResponses: SignatureWithAsset[][] = []; + for (const response of responses) { + if (response.status === 'rejected') { + await trackError(response.reason); + continue; + } + signatureResponses.push(response.value); + } + + return signatureResponses.flat(); }; const signatures = ( @@ -159,15 +240,27 @@ export class TransactionsService { ).flat(); // If limit is provided, only fetch the most recent signatures with the limit + const uniqueSignatures = [ + ...new Map( + signatures.map( + (item) => + [ + `${item.asset.keyringAccountId}:${item.asset.network}:${item.signatureResponse.signature}`, + item, + ] as const, + ), + ).values(), + ]; + const signaturesToFetch = options?.limit - ? signatures + ? uniqueSignatures .sort( (a, b) => (b.signatureResponse.blockTime ?? 0) - (a.signatureResponse.blockTime ?? 0), ) .slice(0, options.limit) - : signatures; + : uniqueSignatures; type TransactionWithAsset = { transaction: SolanaTransaction | null; @@ -199,12 +292,9 @@ export class TransactionsService { await Promise.all(signaturesToFetch.map(fetchTransaction)) ).filter((item) => item !== null); - const findAccountById = (id: string) => - accounts.find((account) => account.id === id); - const mapTransaction = async ( transactionWithAsset: TransactionWithAsset, - ) => { + ): Promise => { const { transaction, asset } = transactionWithAsset; if (!transaction) { return null; diff --git a/packages/solana-wallet-snap/src/core/test/mocks/asset-entities.ts b/packages/solana-wallet-snap/src/core/test/mocks/asset-entities.ts index e6286204c..5b777b049 100644 --- a/packages/solana-wallet-snap/src/core/test/mocks/asset-entities.ts +++ b/packages/solana-wallet-snap/src/core/test/mocks/asset-entities.ts @@ -21,7 +21,6 @@ export const MOCK_ASSET_ENTITY_1: TokenAsset = { network: Network.Mainnet, symbol: 'USDC', mint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', - pubkey: '9wt9PfjPD3JCy5r7o4K1cTGiuTG7fq2pQhdDCdQALKjg', decimals: 6, rawAmount: '123456789', uiAmount: '123.456789', @@ -33,7 +32,6 @@ export const MOCK_ASSET_ENTITY_2: TokenAsset = { network: Network.Mainnet, symbol: 'AI16Z', mint: 'HeLp6NuQkmYB4pYWo2zYs22mESHXPQYzXbB8n4V98jwC', - pubkey: 'DJGpJufSnVDriDczovhcQRyxamKtt87PHQ7TJEcVB6ta', decimals: 9, rawAmount: '987654321', uiAmount: '987.654321', diff --git a/packages/solana-wallet-snap/src/entities/assets.ts b/packages/solana-wallet-snap/src/entities/assets.ts index 06883eaff..10c80152d 100644 --- a/packages/solana-wallet-snap/src/entities/assets.ts +++ b/packages/solana-wallet-snap/src/entities/assets.ts @@ -21,7 +21,6 @@ export type TokenAsset = { keyringAccountId: string; network: Network; mint: string; - pubkey: string; symbol: string; decimals: number; rawAmount: string; // Without decimals nor multiplier @@ -33,7 +32,6 @@ export type NftAsset = { keyringAccountId: string; network: Network; mint: string; - pubkey: string; symbol: string; rawAmount: string; // Without decimals uiAmount: string; // With decimals