From 38fc8bddd236a2d7ee0e2ebb8bd5b3780459b5f1 Mon Sep 17 00:00:00 2001 From: Antonio Ventilii Date: Tue, 7 Jan 2025 13:15:39 +0100 Subject: [PATCH 01/10] refactor(frontend): explicit type in solanaHttpRpc --- src/frontend/src/sol/providers/sol-rpc.providers.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/frontend/src/sol/providers/sol-rpc.providers.ts b/src/frontend/src/sol/providers/sol-rpc.providers.ts index a73060d659..c67c130888 100644 --- a/src/frontend/src/sol/providers/sol-rpc.providers.ts +++ b/src/frontend/src/sol/providers/sol-rpc.providers.ts @@ -9,7 +9,7 @@ import { type SolRpcConnectionConfig, type SolanaNetworkType } from '$sol/types/network'; -import { createSolanaRpc } from '@solana/rpc'; +import { createSolanaRpc, type Rpc, type SolanaRpcApi } from '@solana/rpc'; const rpcs: Record = { [SolanaNetworks.mainnet]: { @@ -28,8 +28,8 @@ const rpcs: Record = { const solanaRpcConfig = (network: SolanaNetworkType): SolRpcConnectionConfig => rpcs[network]; -export const solanaHttpRpc = (network: SolanaNetworkType): ReturnType => { - const rpc = solanaRpcConfig(network); +export const solanaHttpRpc = (network: SolanaNetworkType): Rpc => { + const { httpUrl } = solanaRpcConfig(network); - return createSolanaRpc(rpc.httpUrl); + return createSolanaRpc(httpUrl); }; From a70e52bf178d7f379c08515e1657cd78720db471 Mon Sep 17 00:00:00 2001 From: Robert Schlittler Date: Wed, 8 Jan 2025 12:43:52 +0100 Subject: [PATCH 02/10] feat(frontend): implement sol api for txns --- src/frontend/src/sol/api/solana.api.ts | 92 ++++++++ src/frontend/src/sol/types/sol-transaction.ts | 6 +- .../src/tests/mocks/sol-signatures.mock.ts | 22 ++ .../src/tests/sol/api/solana.api.spec.ts | 209 +++++++++++++++++- 4 files changed, 325 insertions(+), 4 deletions(-) create mode 100644 src/frontend/src/tests/mocks/sol-signatures.mock.ts diff --git a/src/frontend/src/sol/api/solana.api.ts b/src/frontend/src/sol/api/solana.api.ts index e1f9d9bb4a..396110a556 100644 --- a/src/frontend/src/sol/api/solana.api.ts +++ b/src/frontend/src/sol/api/solana.api.ts @@ -1,8 +1,16 @@ +import { WALLET_PAGINATION } from '$lib/constants/app.constants'; import type { SolAddress } from '$lib/types/address'; +import { last } from '$lib/utils/array.utils'; import { solanaHttpRpc } from '$sol/providers/sol-rpc.providers'; +import type { SolCertifiedTransaction } from '$sol/stores/sol-transactions.store'; import type { SolanaNetworkType } from '$sol/types/network'; +import type { SolSignature } from '$sol/types/sol-transaction'; +import { mapSolTransactionUi } from '$sol/utils/sol-transactions.utils'; +import { isNullish, nonNullish } from '@dfinity/utils'; import { address as solAddress } from '@solana/addresses'; +import { signature } from '@solana/keys'; import type { Lamports } from '@solana/rpc-types'; +import type { Writeable } from 'zod'; //lamports are like satoshis: https://solana.com/docs/terminology#lamport export const loadSolLamportsBalance = async ({ @@ -19,3 +27,87 @@ export const loadSolLamportsBalance = async ({ return balance; }; + +export const getSolTransactions = async ({ + address, + network, + before, + limit = Number(WALLET_PAGINATION) +}: { + address: SolAddress; + network: SolanaNetworkType; + before?: string; + limit?: number; +}): Promise => { + const { getSignaturesForAddress } = solanaHttpRpc(network); + const wallet = solAddress(address); + + const transactions: SolCertifiedTransaction[] = []; + let lastSignature = nonNullish(before) ? signature(before) : undefined; + + while (transactions.length < limit) { + const signatures = await getSignaturesForAddress(wallet, { + before: lastSignature, + limit + }).send(); + + if (signatures.length === 0) { + break; + } + + lastSignature = last(signatures as Writeable)?.signature; + + const transactionDetails = await Promise.all( + signatures + .filter(({ err }) => isNullish(err)) + .map(async (signature) => await getTransactionDetailForSignature({ signature, network })) + ); + + const uiTransactions: SolCertifiedTransaction[] = transactionDetails + .filter(nonNullish) + + .map((transaction) => ({ + data: mapSolTransactionUi({ + transaction, + address + }), + certified: false + })); + + transactions.push(...uiTransactions); + + const hasNoMoreSignaturesLeft = signatures.length < limit; + const hasLoadedEnoughTransactions = transactions.length >= limit; + + if (hasNoMoreSignaturesLeft || hasLoadedEnoughTransactions) { + break; + } + } + + return transactions.slice(0, limit); +}; + +const getTransactionDetailForSignature = async ({ + signature: { confirmationStatus, signature }, + network +}: { + signature: SolSignature; + network: SolanaNetworkType; +}) => { + const { getTransaction } = solanaHttpRpc(network); + + const rpcTransaction = await getTransaction(signature, { + maxSupportedTransactionVersion: 0 + }).send(); + + if (isNullish(rpcTransaction)) { + return null; + } + + return { + ...rpcTransaction, + version: rpcTransaction.version, + confirmationStatus: confirmationStatus, + id: signature.toString() + }; +}; diff --git a/src/frontend/src/sol/types/sol-transaction.ts b/src/frontend/src/sol/types/sol-transaction.ts index a05532cfc6..faa99a8a0e 100644 --- a/src/frontend/src/sol/types/sol-transaction.ts +++ b/src/frontend/src/sol/types/sol-transaction.ts @@ -1,6 +1,6 @@ import { solTransactionTypes } from '$lib/schema/transaction.schema'; import type { TransactionType, TransactionUiCommon } from '$lib/types/transaction'; -import type { GetTransactionApi } from '@solana/rpc'; +import type { GetSignaturesForAddressApi, GetTransactionApi } from '@solana/rpc'; import type { Commitment } from '@solana/rpc-types'; export type SolTransactionType = Extract< @@ -20,3 +20,7 @@ export type SolRpcTransaction = NonNullable[number]; diff --git a/src/frontend/src/tests/mocks/sol-signatures.mock.ts b/src/frontend/src/tests/mocks/sol-signatures.mock.ts new file mode 100644 index 0000000000..1126a97cd4 --- /dev/null +++ b/src/frontend/src/tests/mocks/sol-signatures.mock.ts @@ -0,0 +1,22 @@ +import { signature } from '@solana/keys'; + +export const mockSolSignature = () => { + const chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; + let result = ''; + for (let i = 0; i < 87; i++) { + result += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return signature(result); +}; + +export const mockSolSignatureResponse = () => ({ + signature: mockSolSignature(), + err: null, + confirmationStatus: 'finalized' +}); + +export const mockSolSignatureWithErrorResponse = () => ({ + signature: mockSolSignature(), + err: 'Some error', + confirmationStatus: 'finalized' +}); diff --git a/src/frontend/src/tests/sol/api/solana.api.spec.ts b/src/frontend/src/tests/sol/api/solana.api.spec.ts index fe9894f54f..e0e99f36ff 100644 --- a/src/frontend/src/tests/sol/api/solana.api.spec.ts +++ b/src/frontend/src/tests/sol/api/solana.api.spec.ts @@ -1,6 +1,15 @@ -import { loadSolLamportsBalance } from '$sol/api/solana.api'; +import { getSolTransactions, loadSolLamportsBalance } from '$sol/api/solana.api'; import * as solRpcProviders from '$sol/providers/sol-rpc.providers'; import { SolanaNetworks } from '$sol/types/network'; +import { + mockSolSignature, + mockSolSignatureResponse, + mockSolSignatureWithErrorResponse +} from '$tests/mocks/sol-signatures.mock'; +import { + mockSolRpcReceiveTransaction, + mockSolRpcSendTransaction +} from '$tests/mocks/sol-transactions.mock'; import { mockSolAddress } from '$tests/mocks/sol.mock'; import { lamports } from '@solana/rpc-types'; import type { MockInstance } from 'vitest'; @@ -9,15 +18,29 @@ vi.mock('$sol/providers/sol-rpc.providers'); describe('solana.api', () => { let mockGetBalance: MockInstance; + let mockGetSignaturesForAddress: MockInstance; + let mockGetTransaction: MockInstance; beforeEach(() => { vi.clearAllMocks(); - // Mock RPC provider mockGetBalance = vi .fn() .mockReturnValue({ send: () => Promise.resolve({ value: lamports(500000n) }) }); - const mockSolanaHttpRpc = vi.fn().mockReturnValue({ getBalance: mockGetBalance }); + + mockGetSignaturesForAddress = vi.fn().mockReturnValue({ + send: () => Promise.resolve([mockSolSignatureResponse(), mockSolSignatureResponse()]) + }); + + mockGetTransaction = vi.fn().mockReturnValue({ + send: () => Promise.resolve(mockSolRpcSendTransaction) + }); + + const mockSolanaHttpRpc = vi.fn().mockReturnValue({ + getBalance: mockGetBalance, + getSignaturesForAddress: mockGetSignaturesForAddress, + getTransaction: mockGetTransaction + }); vi.mocked(solRpcProviders.solanaHttpRpc).mockImplementation(mockSolanaHttpRpc); }); @@ -64,4 +87,184 @@ describe('solana.api', () => { ).rejects.toThrow(); }); }); + + describe('getSolTransactions', () => { + it('should fetch transactions successfully', async () => { + const transactions = await getSolTransactions({ + address: mockSolAddress, + network: SolanaNetworks.mainnet + }); + + expect(transactions).toHaveLength(2); + expect(mockGetSignaturesForAddress).toHaveBeenCalledTimes(1); + expect(mockGetTransaction).toHaveBeenCalledTimes(2); + }); + + it('should handle before parameter', async () => { + const signature = mockSolSignature(); + await getSolTransactions({ + address: mockSolAddress, + network: SolanaNetworks.mainnet, + before: signature + }); + + expect(mockGetSignaturesForAddress).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + before: signature + }) + ); + }); + + it('should handle limit parameter', async () => { + const transactions = await getSolTransactions({ + address: mockSolAddress, + network: SolanaNetworks.mainnet, + limit: 5 + }); + + expect(transactions).toHaveLength(5); + expect(mockGetSignaturesForAddress).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + limit: 5 + }) + ); + }); + + it('should fetch transactions in multiple batches until limit is reached', async () => { + const lastSignatureFromFirstBatch = mockSolSignatureResponse(); + + mockGetSignaturesForAddress + .mockReturnValueOnce({ + send: () => + Promise.resolve([ + mockSolSignatureResponse(), + mockSolSignatureResponse(), + mockSolSignatureResponse(), + mockSolSignatureWithErrorResponse(), + lastSignatureFromFirstBatch + ]) + }) + .mockReturnValueOnce({ + send: () => Promise.resolve([mockSolSignatureResponse(), mockSolSignatureResponse()]) + }); + + const transactions = await getSolTransactions({ + address: mockSolAddress, + network: SolanaNetworks.mainnet, + limit: 5 + }); + + expect(transactions).toHaveLength(5); + expect(mockGetSignaturesForAddress).toHaveBeenCalledTimes(2); + // Verify the second call uses the last signature from first batch for pagination + expect(mockGetSignaturesForAddress).toHaveBeenLastCalledWith( + expect.anything(), + expect.objectContaining({ + before: lastSignatureFromFirstBatch.signature + }) + ); + }); + + it('should stop fetching when no more signatures are available', async () => { + mockGetSignaturesForAddress + .mockReturnValueOnce({ + send: () => Promise.resolve([mockSolSignatureResponse(), mockSolSignatureResponse()]) + }) + // Second batch returns empty array + .mockReturnValueOnce({ + send: () => Promise.resolve([]) + }); + + const transactions = await getSolTransactions({ + address: mockSolAddress, + network: SolanaNetworks.mainnet, + limit: 5 + }); + + expect(transactions).toHaveLength(2); + expect(mockGetSignaturesForAddress).toHaveBeenCalledTimes(1); + }); + + it('should handle empty signatures response', async () => { + mockGetSignaturesForAddress.mockReturnValue({ + send: () => Promise.resolve([]) + }); + + const transactions = await getSolTransactions({ + address: mockSolAddress, + network: SolanaNetworks.mainnet + }); + + expect(transactions).toHaveLength(0); + expect(mockGetTransaction).not.toHaveBeenCalled(); + }); + + it('should filter out signatures with errors', async () => { + mockGetSignaturesForAddress.mockReturnValue({ + send: () => + Promise.resolve([mockSolSignatureResponse(), mockSolSignatureWithErrorResponse()]) + }); + + const transactions = await getSolTransactions({ + address: mockSolAddress, + network: SolanaNetworks.mainnet + }); + + expect(transactions).toHaveLength(1); + expect(mockGetTransaction).toHaveBeenCalledTimes(1); + }); + + it('should handle null transaction responses', async () => { + mockGetSignaturesForAddress.mockReturnValue({ + send: () => Promise.resolve([mockSolSignatureResponse()]) + }); + mockGetTransaction.mockReturnValue({ + send: () => Promise.resolve(null) + }); + + const transactions = await getSolTransactions({ + address: mockSolAddress, + network: SolanaNetworks.mainnet + }); + + expect(transactions).toHaveLength(0); + }); + + it('should handle mixed transaction types', async () => { + mockGetSignaturesForAddress.mockReturnValue({ + send: () => Promise.resolve([mockSolSignatureResponse(), mockSolSignatureResponse()]) + }); + mockGetTransaction + .mockReturnValueOnce({ + send: () => Promise.resolve(mockSolRpcSendTransaction) + }) + .mockReturnValueOnce({ + send: () => Promise.resolve(mockSolRpcReceiveTransaction) + }); + + const transactions = await getSolTransactions({ + address: mockSolAddress, + network: SolanaNetworks.mainnet + }); + + expect(transactions).toHaveLength(2); + expect(transactions[0].data.type).toBe('send'); + expect(transactions[1].data.type).toBe('receive'); + }); + + it('should handle RPC errors gracefully', async () => { + mockGetSignaturesForAddress.mockReturnValue({ + send: () => Promise.reject(new Error('RPC Error')) + }); + + await expect( + getSolTransactions({ + address: mockSolAddress, + network: SolanaNetworks.mainnet + }) + ).rejects.toThrow('RPC Error'); + }); + }); }); From 1bb40507bbb000bfaf31f822966d909b507a94bf Mon Sep 17 00:00:00 2001 From: Robert Schlittler Date: Wed, 8 Jan 2025 12:51:44 +0100 Subject: [PATCH 03/10] feat(frontend): fix test --- src/frontend/src/tests/sol/api/solana.api.spec.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/frontend/src/tests/sol/api/solana.api.spec.ts b/src/frontend/src/tests/sol/api/solana.api.spec.ts index e0e99f36ff..15318c8b44 100644 --- a/src/frontend/src/tests/sol/api/solana.api.spec.ts +++ b/src/frontend/src/tests/sol/api/solana.api.spec.ts @@ -117,13 +117,12 @@ describe('solana.api', () => { }); it('should handle limit parameter', async () => { - const transactions = await getSolTransactions({ + await getSolTransactions({ address: mockSolAddress, network: SolanaNetworks.mainnet, limit: 5 }); - expect(transactions).toHaveLength(5); expect(mockGetSignaturesForAddress).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ From 0819d9191f719c0be31ac6cba334438331397505 Mon Sep 17 00:00:00 2001 From: Robert Schlittler Date: Wed, 8 Jan 2025 17:20:15 +0100 Subject: [PATCH 04/10] feat(frontend): add sol-transactions.services.ts --- .../sol/services/sol-transactions.services.ts | 84 +++++++++++++ .../sol-transactions.services.spec.ts | 117 ++++++++++++++++++ 2 files changed, 201 insertions(+) create mode 100644 src/frontend/src/sol/services/sol-transactions.services.ts create mode 100644 src/frontend/src/tests/sol/services/sol-transactions.services.spec.ts diff --git a/src/frontend/src/sol/services/sol-transactions.services.ts b/src/frontend/src/sol/services/sol-transactions.services.ts new file mode 100644 index 0000000000..29a6962eb4 --- /dev/null +++ b/src/frontend/src/sol/services/sol-transactions.services.ts @@ -0,0 +1,84 @@ +import type { SolAddress } from '$lib/types/address'; +import { getSolTransactions } from '$sol/api/solana.api'; +import { type SolCertifiedTransaction, solTransactionsStore } from '$sol/stores/sol-transactions.store'; +import { SolanaNetworks, type SolanaNetworkType } from '$sol/types/network'; +import { + SOLANA_DEVNET_TOKEN_ID, + SOLANA_LOCAL_TOKEN_ID, + SOLANA_TESTNET_TOKEN_ID, + SOLANA_TOKEN_ID +} from '$env/tokens/tokens.sol.env'; + +export const loadNextSolTransactions = async ({ + address, + network, + before, + limit, + signalEnd +}: { + address: SolAddress; + network: SolanaNetworkType; + before?: string; + limit?: number; + signalEnd: () => void; +}): Promise => { + const transactions = await loadSolTransactions({ + address, + network, + before, + limit + }); + + if (transactions.length === 0) { + signalEnd(); + return []; + } + + return transactions; +}; + +const networkToSolTokenIdMap = { + [SolanaNetworks.mainnet]: SOLANA_TOKEN_ID, + [SolanaNetworks.testnet]: SOLANA_TESTNET_TOKEN_ID, + [SolanaNetworks.devnet]: SOLANA_DEVNET_TOKEN_ID, + [SolanaNetworks.local]: SOLANA_LOCAL_TOKEN_ID +}; + +const loadSolTransactions = async ({ + address, + network, + before, + limit +}: { + address: SolAddress; + network: SolanaNetworkType; + before?: string; + limit?: number; +}): Promise => { + + const solTokenIdForNetwork = networkToSolTokenIdMap[network]; + + try { + const transactions = await getSolTransactions({ + address, + network, + before, + limit + }); + + solTransactionsStore.append({ + tokenId: solTokenIdForNetwork, + transactions: transactions + }); + + return transactions; + } catch (error: unknown) { + solTransactionsStore.reset(solTokenIdForNetwork); + + console.error( + `Failed to load transactions for ${solTokenIdForNetwork.description}:`, + error + ); + return []; + } +}; diff --git a/src/frontend/src/tests/sol/services/sol-transactions.services.spec.ts b/src/frontend/src/tests/sol/services/sol-transactions.services.spec.ts new file mode 100644 index 0000000000..b057bc3a25 --- /dev/null +++ b/src/frontend/src/tests/sol/services/sol-transactions.services.spec.ts @@ -0,0 +1,117 @@ +import { SOLANA_TOKEN_ID } from '$env/tokens/tokens.sol.env'; +import * as solanaApi from '$sol/api/solana.api'; +import { loadNextSolTransactions } from '$sol/services/sol-transactions.services'; +import { solTransactionsStore } from '$sol/stores/sol-transactions.store'; +import { SolanaNetworks } from '$sol/types/network'; +import { mockSolCertifiedTransactions } from '$tests/mocks/sol-transactions.mock'; +import { mockSolAddress } from '$tests/mocks/sol.mock'; +import { get } from 'svelte/store'; +import type { MockInstance } from 'vitest'; +import { mockSolSignature } from '$tests/mocks/sol-signatures.mock'; + +describe('sol-transactions.services', () => { + let spyGetTransactions: MockInstance; + const signalEnd = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + solTransactionsStore.reset(SOLANA_TOKEN_ID); + spyGetTransactions = vi.spyOn(solanaApi, 'getSolTransactions'); + }); + + describe('loadNextSolTransactions', () => { + it('should load and return transactions successfully', async () => { + spyGetTransactions.mockResolvedValue(mockSolCertifiedTransactions); + + const transactions = await loadNextSolTransactions({ + address: mockSolAddress, + network: SolanaNetworks.mainnet, + signalEnd + }); + + expect(transactions).toEqual(mockSolCertifiedTransactions); + expect(signalEnd).not.toHaveBeenCalled(); + expect(spyGetTransactions).toHaveBeenCalledWith({ + address: mockSolAddress, + network: SolanaNetworks.mainnet + }); + }); + + it('should handle pagination parameters', async () => { + spyGetTransactions.mockResolvedValue(mockSolCertifiedTransactions); + const before = mockSolSignature(); + const limit = 10; + + await loadNextSolTransactions({ + address: mockSolAddress, + network: SolanaNetworks.mainnet, + before, + limit, + signalEnd + }); + + expect(spyGetTransactions).toHaveBeenCalledWith({ + address: mockSolAddress, + network: SolanaNetworks.mainnet, + before, + limit + }); + }); + + it('should signal end when no transactions are returned', async () => { + spyGetTransactions.mockResolvedValue([]); + + const transactions = await loadNextSolTransactions({ + address: mockSolAddress, + network: SolanaNetworks.mainnet, + signalEnd + }); + + expect(transactions).toEqual([]); + expect(signalEnd).toHaveBeenCalled(); + }); + + it('should append transactions to the store', async () => { + spyGetTransactions.mockResolvedValue(mockSolCertifiedTransactions); + + await loadNextSolTransactions({ + address: mockSolAddress, + network: SolanaNetworks.mainnet, + signalEnd + }); + + const storeData = get(solTransactionsStore)?.[SOLANA_TOKEN_ID]; + expect(storeData).toEqual(mockSolCertifiedTransactions); + }); + + it('should handle errors and reset store', async () => { + const error = new Error('Failed to load transactions'); + spyGetTransactions.mockRejectedValue(error); + + const transactions = await loadNextSolTransactions({ + address: mockSolAddress, + network: SolanaNetworks.mainnet, + signalEnd + }); + + expect(transactions).toEqual([]); + const storeData = get(solTransactionsStore)?.[SOLANA_TOKEN_ID]; + expect(storeData).toBeNull(); + }); + + it('should work with different networks', async () => { + spyGetTransactions.mockResolvedValue(mockSolCertifiedTransactions); + + await loadNextSolTransactions({ + address: mockSolAddress, + network: SolanaNetworks.devnet, + signalEnd + }); + + expect(spyGetTransactions).toHaveBeenCalledWith({ + address: mockSolAddress, + network: SolanaNetworks.devnet + }); + }); + }); +}); \ No newline at end of file From c15f886ecbe2bde159c5dd9baa782753e0fe8588 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 8 Jan 2025 16:24:10 +0000 Subject: [PATCH 05/10] =?UTF-8?q?=F0=9F=A4=96=20Apply=20formatting=20chang?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sol/services/sol-transactions.services.ts | 17 ++++++++--------- .../services/sol-transactions.services.spec.ts | 4 ++-- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/frontend/src/sol/services/sol-transactions.services.ts b/src/frontend/src/sol/services/sol-transactions.services.ts index 29a6962eb4..589bf37427 100644 --- a/src/frontend/src/sol/services/sol-transactions.services.ts +++ b/src/frontend/src/sol/services/sol-transactions.services.ts @@ -1,13 +1,16 @@ -import type { SolAddress } from '$lib/types/address'; -import { getSolTransactions } from '$sol/api/solana.api'; -import { type SolCertifiedTransaction, solTransactionsStore } from '$sol/stores/sol-transactions.store'; -import { SolanaNetworks, type SolanaNetworkType } from '$sol/types/network'; import { SOLANA_DEVNET_TOKEN_ID, SOLANA_LOCAL_TOKEN_ID, SOLANA_TESTNET_TOKEN_ID, SOLANA_TOKEN_ID } from '$env/tokens/tokens.sol.env'; +import type { SolAddress } from '$lib/types/address'; +import { getSolTransactions } from '$sol/api/solana.api'; +import { + solTransactionsStore, + type SolCertifiedTransaction +} from '$sol/stores/sol-transactions.store'; +import { SolanaNetworks, type SolanaNetworkType } from '$sol/types/network'; export const loadNextSolTransactions = async ({ address, @@ -55,7 +58,6 @@ const loadSolTransactions = async ({ before?: string; limit?: number; }): Promise => { - const solTokenIdForNetwork = networkToSolTokenIdMap[network]; try { @@ -75,10 +77,7 @@ const loadSolTransactions = async ({ } catch (error: unknown) { solTransactionsStore.reset(solTokenIdForNetwork); - console.error( - `Failed to load transactions for ${solTokenIdForNetwork.description}:`, - error - ); + console.error(`Failed to load transactions for ${solTokenIdForNetwork.description}:`, error); return []; } }; diff --git a/src/frontend/src/tests/sol/services/sol-transactions.services.spec.ts b/src/frontend/src/tests/sol/services/sol-transactions.services.spec.ts index b057bc3a25..0509803c10 100644 --- a/src/frontend/src/tests/sol/services/sol-transactions.services.spec.ts +++ b/src/frontend/src/tests/sol/services/sol-transactions.services.spec.ts @@ -3,11 +3,11 @@ import * as solanaApi from '$sol/api/solana.api'; import { loadNextSolTransactions } from '$sol/services/sol-transactions.services'; import { solTransactionsStore } from '$sol/stores/sol-transactions.store'; import { SolanaNetworks } from '$sol/types/network'; +import { mockSolSignature } from '$tests/mocks/sol-signatures.mock'; import { mockSolCertifiedTransactions } from '$tests/mocks/sol-transactions.mock'; import { mockSolAddress } from '$tests/mocks/sol.mock'; import { get } from 'svelte/store'; import type { MockInstance } from 'vitest'; -import { mockSolSignature } from '$tests/mocks/sol-signatures.mock'; describe('sol-transactions.services', () => { let spyGetTransactions: MockInstance; @@ -114,4 +114,4 @@ describe('sol-transactions.services', () => { }); }); }); -}); \ No newline at end of file +}); From b95429001b066ac758c7b7389ba773e166319870 Mon Sep 17 00:00:00 2001 From: Robert Schlittler Date: Wed, 8 Jan 2025 17:26:51 +0100 Subject: [PATCH 06/10] feat(frontend): fix tests --- .../src/tests/mocks/sol-transactions.mock.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/frontend/src/tests/mocks/sol-transactions.mock.ts b/src/frontend/src/tests/mocks/sol-transactions.mock.ts index 734e98aa01..679a564192 100644 --- a/src/frontend/src/tests/mocks/sol-transactions.mock.ts +++ b/src/frontend/src/tests/mocks/sol-transactions.mock.ts @@ -1,3 +1,4 @@ +import type { SolCertifiedTransaction } from '$sol/stores/sol-transactions.store'; import type { SolRpcTransaction, SolTransactionUi } from '$sol/types/sol-transaction'; import { mockSolAddress } from '$tests/mocks/sol.mock'; import { address } from '@solana/addresses'; @@ -18,6 +19,17 @@ export const createMockSolTransactionUi = (id: string): SolTransactionUi => ({ status: 'finalized' }); +export const mockSolCertifiedTransactions: SolCertifiedTransaction[] = [ + { + data: createMockSolTransactionUi('tx1'), + certified: false + }, + { + data: createMockSolTransactionUi('tx2'), + certified: false + } +]; + export const mockSolRpcReceiveTransaction: SolRpcTransaction = { blockTime: 1736257946n as UnixTimestamp, confirmationStatus: 'finalized', From 8bf52f00d96f5a5899675883d2c172d146ac9b89 Mon Sep 17 00:00:00 2001 From: Robert Schlittler Date: Fri, 10 Jan 2025 09:53:37 +0100 Subject: [PATCH 07/10] feat(frontend): consolidate typing, add mapping that was removed in api --- src/frontend/src/sol/api/solana.api.ts | 10 ++--- .../sol/services/sol-transactions.services.ts | 37 +++++++++---------- src/frontend/src/sol/types/sol-api.ts | 9 +++++ 3 files changed, 31 insertions(+), 25 deletions(-) create mode 100644 src/frontend/src/sol/types/sol-api.ts diff --git a/src/frontend/src/sol/api/solana.api.ts b/src/frontend/src/sol/api/solana.api.ts index 6c5bd90037..52777f239e 100644 --- a/src/frontend/src/sol/api/solana.api.ts +++ b/src/frontend/src/sol/api/solana.api.ts @@ -9,6 +9,7 @@ import { address as solAddress, type Address } from '@solana/addresses'; import { signature, type Signature } from '@solana/keys'; import type { Lamports } from '@solana/rpc-types'; import type { Writeable } from 'zod'; +import type { GetSolTransactionsParams } from '$sol/types/sol-api'; //lamports are like satoshis: https://solana.com/docs/terminology#lamport export const loadSolLamportsBalance = async ({ @@ -26,6 +27,8 @@ export const loadSolLamportsBalance = async ({ return balance; }; + + /** * Fetches transactions without an error for a given wallet address. */ @@ -34,12 +37,7 @@ export const getSolTransactions = async ({ network, before, limit = Number(WALLET_PAGINATION) -}: { - address: SolAddress; - network: SolanaNetworkType; - before?: string; - limit?: number; -}): Promise => { +}: GetSolTransactionsParams): Promise => { const wallet = solAddress(address); const beforeSignature = nonNullish(before) ? signature(before) : undefined; const signatures = await fetchSignatures({ network, wallet, before: beforeSignature, limit }); diff --git a/src/frontend/src/sol/services/sol-transactions.services.ts b/src/frontend/src/sol/services/sol-transactions.services.ts index 589bf37427..90522279b8 100644 --- a/src/frontend/src/sol/services/sol-transactions.services.ts +++ b/src/frontend/src/sol/services/sol-transactions.services.ts @@ -4,13 +4,18 @@ import { SOLANA_TESTNET_TOKEN_ID, SOLANA_TOKEN_ID } from '$env/tokens/tokens.sol.env'; -import type { SolAddress } from '$lib/types/address'; import { getSolTransactions } from '$sol/api/solana.api'; import { - solTransactionsStore, - type SolCertifiedTransaction + type SolCertifiedTransaction, + solTransactionsStore } from '$sol/stores/sol-transactions.store'; -import { SolanaNetworks, type SolanaNetworkType } from '$sol/types/network'; +import { SolanaNetworks } from '$sol/types/network'; +import type { GetSolTransactionsParams } from '$sol/types/sol-api'; +import { mapSolTransactionUi } from '$sol/utils/sol-transactions.utils'; + +interface LoadNextSolTransactionsParams extends GetSolTransactionsParams { + signalEnd: () => void; +} export const loadNextSolTransactions = async ({ address, @@ -18,13 +23,7 @@ export const loadNextSolTransactions = async ({ before, limit, signalEnd -}: { - address: SolAddress; - network: SolanaNetworkType; - before?: string; - limit?: number; - signalEnd: () => void; -}): Promise => { +}: LoadNextSolTransactionsParams): Promise => { const transactions = await loadSolTransactions({ address, network, @@ -52,12 +51,7 @@ const loadSolTransactions = async ({ network, before, limit -}: { - address: SolAddress; - network: SolanaNetworkType; - before?: string; - limit?: number; -}): Promise => { +}: GetSolTransactionsParams): Promise => { const solTokenIdForNetwork = networkToSolTokenIdMap[network]; try { @@ -68,12 +62,17 @@ const loadSolTransactions = async ({ limit }); + const certifiedTransactions = transactions.map((transaction) => ({ + data: mapSolTransactionUi({ transaction, address }), + certified: false + })); + solTransactionsStore.append({ tokenId: solTokenIdForNetwork, - transactions: transactions + transactions: certifiedTransactions }); - return transactions; + return certifiedTransactions; } catch (error: unknown) { solTransactionsStore.reset(solTokenIdForNetwork); diff --git a/src/frontend/src/sol/types/sol-api.ts b/src/frontend/src/sol/types/sol-api.ts new file mode 100644 index 0000000000..3f0d96b032 --- /dev/null +++ b/src/frontend/src/sol/types/sol-api.ts @@ -0,0 +1,9 @@ +import type { SolAddress } from '$lib/types/address'; +import type { SolanaNetworkType } from '$sol/types/network'; + +export interface GetSolTransactionsParams { + address: SolAddress; + network: SolanaNetworkType; + before?: string; + limit?: number; +} \ No newline at end of file From d9bbab6cb474efd56294634f804eb83b9eeefc13 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 10 Jan 2025 08:55:21 +0000 Subject: [PATCH 08/10] =?UTF-8?q?=F0=9F=A4=96=20Apply=20formatting=20chang?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/frontend/src/sol/api/solana.api.ts | 4 +--- src/frontend/src/sol/services/sol-transactions.services.ts | 4 ++-- src/frontend/src/sol/types/sol-api.ts | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/frontend/src/sol/api/solana.api.ts b/src/frontend/src/sol/api/solana.api.ts index 52777f239e..e142c98e1c 100644 --- a/src/frontend/src/sol/api/solana.api.ts +++ b/src/frontend/src/sol/api/solana.api.ts @@ -3,13 +3,13 @@ import type { SolAddress } from '$lib/types/address'; import { last } from '$lib/utils/array.utils'; import { solanaHttpRpc } from '$sol/providers/sol-rpc.providers'; import type { SolanaNetworkType } from '$sol/types/network'; +import type { GetSolTransactionsParams } from '$sol/types/sol-api'; import type { SolRpcTransaction, SolSignature } from '$sol/types/sol-transaction'; import { isNullish, nonNullish } from '@dfinity/utils'; import { address as solAddress, type Address } from '@solana/addresses'; import { signature, type Signature } from '@solana/keys'; import type { Lamports } from '@solana/rpc-types'; import type { Writeable } from 'zod'; -import type { GetSolTransactionsParams } from '$sol/types/sol-api'; //lamports are like satoshis: https://solana.com/docs/terminology#lamport export const loadSolLamportsBalance = async ({ @@ -27,8 +27,6 @@ export const loadSolLamportsBalance = async ({ return balance; }; - - /** * Fetches transactions without an error for a given wallet address. */ diff --git a/src/frontend/src/sol/services/sol-transactions.services.ts b/src/frontend/src/sol/services/sol-transactions.services.ts index 90522279b8..e9c81c32ce 100644 --- a/src/frontend/src/sol/services/sol-transactions.services.ts +++ b/src/frontend/src/sol/services/sol-transactions.services.ts @@ -6,8 +6,8 @@ import { } from '$env/tokens/tokens.sol.env'; import { getSolTransactions } from '$sol/api/solana.api'; import { - type SolCertifiedTransaction, - solTransactionsStore + solTransactionsStore, + type SolCertifiedTransaction } from '$sol/stores/sol-transactions.store'; import { SolanaNetworks } from '$sol/types/network'; import type { GetSolTransactionsParams } from '$sol/types/sol-api'; diff --git a/src/frontend/src/sol/types/sol-api.ts b/src/frontend/src/sol/types/sol-api.ts index 3f0d96b032..b7c026495a 100644 --- a/src/frontend/src/sol/types/sol-api.ts +++ b/src/frontend/src/sol/types/sol-api.ts @@ -6,4 +6,4 @@ export interface GetSolTransactionsParams { network: SolanaNetworkType; before?: string; limit?: number; -} \ No newline at end of file +} From 04af1ff266d2fe2641667a426176369e88e0fde2 Mon Sep 17 00:00:00 2001 From: Robert Schlittler Date: Fri, 10 Jan 2025 10:09:03 +0100 Subject: [PATCH 09/10] feat(frontend): fix tests with correct txns --- .../src/tests/mocks/sol-transactions.mock.ts | 28 +++++++++++-------- .../sol-transactions.services.spec.ts | 16 +++++++---- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/src/frontend/src/tests/mocks/sol-transactions.mock.ts b/src/frontend/src/tests/mocks/sol-transactions.mock.ts index 679a564192..d33f2e7793 100644 --- a/src/frontend/src/tests/mocks/sol-transactions.mock.ts +++ b/src/frontend/src/tests/mocks/sol-transactions.mock.ts @@ -3,11 +3,12 @@ import type { SolRpcTransaction, SolTransactionUi } from '$sol/types/sol-transac import { mockSolAddress } from '$tests/mocks/sol.mock'; import { address } from '@solana/addresses'; import { + type Base58EncodedBytes, blockhash, lamports, - type Base58EncodedBytes, type UnixTimestamp } from '@solana/rpc-types'; +import { mapSolTransactionUi } from '$sol/utils/sol-transactions.utils'; export const createMockSolTransactionUi = (id: string): SolTransactionUi => ({ id, @@ -19,17 +20,6 @@ export const createMockSolTransactionUi = (id: string): SolTransactionUi => ({ status: 'finalized' }); -export const mockSolCertifiedTransactions: SolCertifiedTransaction[] = [ - { - data: createMockSolTransactionUi('tx1'), - certified: false - }, - { - data: createMockSolTransactionUi('tx2'), - certified: false - } -]; - export const mockSolRpcReceiveTransaction: SolRpcTransaction = { blockTime: 1736257946n as UnixTimestamp, confirmationStatus: 'finalized', @@ -233,3 +223,17 @@ export const mockSolRpcSendToMyselfTransaction: SolRpcTransaction = { }, version: 'legacy' }; + +export const mockSolCertifiedTransactions: SolCertifiedTransaction[] = [ + { + data: mapSolTransactionUi({ + transaction: mockSolRpcReceiveTransaction, + address: mockSolAddress + }), + certified: false + }, + { + data: mapSolTransactionUi({ transaction: mockSolRpcSendTransaction, address: mockSolAddress }), + certified: false + } +]; diff --git a/src/frontend/src/tests/sol/services/sol-transactions.services.spec.ts b/src/frontend/src/tests/sol/services/sol-transactions.services.spec.ts index 0509803c10..0dda7f1180 100644 --- a/src/frontend/src/tests/sol/services/sol-transactions.services.spec.ts +++ b/src/frontend/src/tests/sol/services/sol-transactions.services.spec.ts @@ -4,7 +4,11 @@ import { loadNextSolTransactions } from '$sol/services/sol-transactions.services import { solTransactionsStore } from '$sol/stores/sol-transactions.store'; import { SolanaNetworks } from '$sol/types/network'; import { mockSolSignature } from '$tests/mocks/sol-signatures.mock'; -import { mockSolCertifiedTransactions } from '$tests/mocks/sol-transactions.mock'; +import { + mockSolCertifiedTransactions, + mockSolRpcReceiveTransaction, + mockSolRpcSendTransaction +} from '$tests/mocks/sol-transactions.mock'; import { mockSolAddress } from '$tests/mocks/sol.mock'; import { get } from 'svelte/store'; import type { MockInstance } from 'vitest'; @@ -13,6 +17,8 @@ describe('sol-transactions.services', () => { let spyGetTransactions: MockInstance; const signalEnd = vi.fn(); + const mockTransactions = [mockSolRpcReceiveTransaction, mockSolRpcSendTransaction] + beforeEach(() => { vi.clearAllMocks(); solTransactionsStore.reset(SOLANA_TOKEN_ID); @@ -21,7 +27,7 @@ describe('sol-transactions.services', () => { describe('loadNextSolTransactions', () => { it('should load and return transactions successfully', async () => { - spyGetTransactions.mockResolvedValue(mockSolCertifiedTransactions); + spyGetTransactions.mockResolvedValue(mockTransactions); const transactions = await loadNextSolTransactions({ address: mockSolAddress, @@ -38,7 +44,7 @@ describe('sol-transactions.services', () => { }); it('should handle pagination parameters', async () => { - spyGetTransactions.mockResolvedValue(mockSolCertifiedTransactions); + spyGetTransactions.mockResolvedValue(mockTransactions); const before = mockSolSignature(); const limit = 10; @@ -72,7 +78,7 @@ describe('sol-transactions.services', () => { }); it('should append transactions to the store', async () => { - spyGetTransactions.mockResolvedValue(mockSolCertifiedTransactions); + spyGetTransactions.mockResolvedValue(mockTransactions); await loadNextSolTransactions({ address: mockSolAddress, @@ -100,7 +106,7 @@ describe('sol-transactions.services', () => { }); it('should work with different networks', async () => { - spyGetTransactions.mockResolvedValue(mockSolCertifiedTransactions); + spyGetTransactions.mockResolvedValue(mockTransactions); await loadNextSolTransactions({ address: mockSolAddress, From efbcbf3ef546d45923e235f4c474e4613d063553 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 10 Jan 2025 09:10:43 +0000 Subject: [PATCH 10/10] =?UTF-8?q?=F0=9F=A4=96=20Apply=20formatting=20chang?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/frontend/src/tests/mocks/sol-transactions.mock.ts | 4 ++-- .../src/tests/sol/services/sol-transactions.services.spec.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/frontend/src/tests/mocks/sol-transactions.mock.ts b/src/frontend/src/tests/mocks/sol-transactions.mock.ts index d33f2e7793..b930c0f8a0 100644 --- a/src/frontend/src/tests/mocks/sol-transactions.mock.ts +++ b/src/frontend/src/tests/mocks/sol-transactions.mock.ts @@ -1,14 +1,14 @@ import type { SolCertifiedTransaction } from '$sol/stores/sol-transactions.store'; import type { SolRpcTransaction, SolTransactionUi } from '$sol/types/sol-transaction'; +import { mapSolTransactionUi } from '$sol/utils/sol-transactions.utils'; import { mockSolAddress } from '$tests/mocks/sol.mock'; import { address } from '@solana/addresses'; import { - type Base58EncodedBytes, blockhash, lamports, + type Base58EncodedBytes, type UnixTimestamp } from '@solana/rpc-types'; -import { mapSolTransactionUi } from '$sol/utils/sol-transactions.utils'; export const createMockSolTransactionUi = (id: string): SolTransactionUi => ({ id, diff --git a/src/frontend/src/tests/sol/services/sol-transactions.services.spec.ts b/src/frontend/src/tests/sol/services/sol-transactions.services.spec.ts index 0dda7f1180..254c18ffd5 100644 --- a/src/frontend/src/tests/sol/services/sol-transactions.services.spec.ts +++ b/src/frontend/src/tests/sol/services/sol-transactions.services.spec.ts @@ -17,7 +17,7 @@ describe('sol-transactions.services', () => { let spyGetTransactions: MockInstance; const signalEnd = vi.fn(); - const mockTransactions = [mockSolRpcReceiveTransaction, mockSolRpcSendTransaction] + const mockTransactions = [mockSolRpcReceiveTransaction, mockSolRpcSendTransaction]; beforeEach(() => { vi.clearAllMocks();