diff --git a/tauri-app/src/lib/components/TanstackPageContentDetail.test.svelte b/tauri-app/src/lib/components/detail/TanstackPageContentDetail.test.svelte
similarity index 100%
rename from tauri-app/src/lib/components/TanstackPageContentDetail.test.svelte
rename to tauri-app/src/lib/components/detail/TanstackPageContentDetail.test.svelte
diff --git a/tauri-app/src/lib/components/TanstackPageContentDetail.test.ts b/tauri-app/src/lib/components/detail/TanstackPageContentDetail.test.ts
similarity index 100%
rename from tauri-app/src/lib/components/TanstackPageContentDetail.test.ts
rename to tauri-app/src/lib/components/detail/TanstackPageContentDetail.test.ts
diff --git a/tauri-app/src/lib/components/detail/VaultDetail.svelte b/tauri-app/src/lib/components/detail/VaultDetail.svelte
new file mode 100644
index 000000000..481984476
--- /dev/null
+++ b/tauri-app/src/lib/components/detail/VaultDetail.svelte
@@ -0,0 +1,136 @@
+
+
+
+
+
+ {data.token.name}
+
+
+ {#if $walletAddressMatchesOrBlank(data.owner)}
+
handleDepositModal(data)}
+ > Deposit
+
handleWithdrawModal(data)}
+ > Withdraw
+ {/if}
+
+
+
+
+ Vault ID
+ {bigintStringToHex(data.vault_id)}
+
+
+
+ Owner Address
+
+
+
+
+
+
+ Token address
+
+
+
+
+
+
+ Balance
+ {formatUnits(BigInt(data.balance), Number(data.token.decimals ?? 0))}
+ {data.token.symbol}
+
+
+
+ Orders as input
+
+
+ {#if data.orders_as_input && data.orders_as_input.length > 0}
+ {#each data.orders_as_input as order}
+ goto(`/orders/${order.id}`)}
+ >
+
+
+ {/each}
+ {:else}
+ None
+ {/if}
+
+
+
+
+
+ Orders as output
+
+
+ {#if data.orders_as_output && data.orders_as_output.length > 0}
+ {#each data.orders_as_output as order}
+ goto(`/orders/${order.id}`)}
+ >
+
+
+ {/each}
+ {:else}
+ None
+ {/if}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tauri-app/src/lib/components/detail/VaultDetail.test.ts b/tauri-app/src/lib/components/detail/VaultDetail.test.ts
new file mode 100644
index 000000000..7b912de39
--- /dev/null
+++ b/tauri-app/src/lib/components/detail/VaultDetail.test.ts
@@ -0,0 +1,346 @@
+import { render, screen, waitFor } from '@testing-library/svelte';
+import { test, vi } from 'vitest';
+import { expect } from '$lib/test/matchers';
+import { QueryClient } from '@tanstack/svelte-query';
+import VaultDetail from './VaultDetail.svelte';
+import { mockIPC } from '@tauri-apps/api/mocks';
+import type { Vault } from '$lib/typeshare/vaultDetail';
+import { goto } from '$app/navigation';
+import { handleDepositModal, handleWithdrawModal } from '$lib/services/modal';
+
+const { mockWalletAddressMatchesOrBlankStore } = await vi.hoisted(
+ () => import('$lib/mocks/wallets'),
+);
+
+vi.mock('$lib/stores/wallets', async () => {
+ return {
+ walletAddressMatchesOrBlank: mockWalletAddressMatchesOrBlankStore,
+ };
+});
+
+vi.mock('$lib/stores/settings', async (importOriginal) => {
+ const { writable } = await import('svelte/store');
+ const { mockSettingsStore } = await import('$lib/mocks/settings');
+
+ const _activeOrderbook = writable();
+
+ return {
+ ...((await importOriginal()) as object),
+ settings: mockSettingsStore,
+ subgraphUrl: writable('https://example.com'),
+ activeOrderbook: {
+ ..._activeOrderbook,
+ load: vi.fn(() => _activeOrderbook.set(true)),
+ },
+ };
+});
+
+vi.mock('$lib/services/modal', async () => {
+ return {
+ handleDepositGenericModal: vi.fn(),
+ handleDepositModal: vi.fn(),
+ handleWithdrawModal: vi.fn(),
+ };
+});
+
+vi.mock('$app/navigation', () => ({
+ goto: vi.fn(),
+}));
+
+vi.mock('lightweight-charts', async (importOriginal) => ({
+ ...((await importOriginal()) as object),
+ createChart: vi.fn(() => ({
+ addLineSeries: vi.fn(),
+ remove(): void {},
+ applyOptions: vi.fn(),
+ })),
+}));
+
+test('calls the vault detail query fn with the correct vault id', async () => {
+ let receivedId: string;
+ mockIPC((cmd, args) => {
+ if (cmd === 'vault_detail') {
+ receivedId = args.id as string;
+ return [];
+ }
+ });
+
+ const queryClient = new QueryClient();
+
+ render(VaultDetail, {
+ props: { id: '100' },
+ context: new Map([['$$_queryClient', queryClient]]),
+ });
+
+ await waitFor(() => expect(receivedId).toEqual('100'));
+});
+
+test('shows the correct empty message when the query returns no data', async () => {
+ mockIPC((cmd) => {
+ if (cmd === 'vault_detail') {
+ return null;
+ }
+ });
+
+ const queryClient = new QueryClient();
+
+ render(VaultDetail, {
+ props: { id: '100' },
+ context: new Map([['$$_queryClient', queryClient]]),
+ });
+
+ await waitFor(() => expect(screen.getByTestId('emptyMessage')).toBeInTheDocument());
+});
+
+test('shows the correct data when the query returns data', async () => {
+ const mockData: Vault = {
+ id: '1',
+ vault_id: '0xabc',
+ owner: '0x123',
+ token: {
+ id: '0x456',
+ address: '0x456',
+ name: 'USDC coin',
+ symbol: 'USDC',
+ decimals: '6',
+ },
+ balance: '100000000000',
+ orders_as_input: [],
+ orders_as_output: [],
+ balance_changes: [],
+ };
+ mockIPC((cmd) => {
+ if (cmd === 'vault_detail') {
+ return mockData;
+ }
+ });
+
+ const queryClient = new QueryClient();
+
+ render(VaultDetail, {
+ props: { id: '100' },
+ context: new Map([['$$_queryClient', queryClient]]),
+ });
+
+ await waitFor(() => {
+ expect(screen.getByTestId('vaultDetailTokenName')).toHaveTextContent('USDC coin');
+ expect(screen.getByTestId('vaultDetailVaultId')).toHaveTextContent('Vault ID 0xabc');
+ expect(screen.getByTestId('vaultDetailOwnerAddress')).toHaveTextContent(
+ 'Owner Address 0x123...0x123',
+ );
+ expect(screen.getByTestId('vaultDetailTokenAddress')).toHaveTextContent(
+ 'Token address 0x456...0x456',
+ );
+ expect(screen.getByTestId('vaultDetailBalance')).toHaveTextContent('Balance 100000 USDC');
+ expect(screen.queryByTestId('vaultDetailOrdersAsInput')).toHaveTextContent('None');
+ expect(screen.queryByTestId('vaulDetailOrdersAsOutput')).toHaveTextContent('None');
+ });
+});
+
+test('shows the correct data when the query returns data with orders', async () => {
+ const mockData: Vault = {
+ id: '1',
+ vault_id: '0xabc',
+ owner: '0x123',
+ token: {
+ id: '0x456',
+ address: '0x456',
+ name: 'USDC coin',
+ symbol: 'USDC',
+ decimals: '6',
+ },
+ balance: '100000000000',
+ orders_as_input: [
+ {
+ id: '1',
+ order_hash: '0x123',
+ active: true,
+ },
+ {
+ id: '2',
+ order_hash: '0x456',
+ active: false,
+ },
+ ],
+ orders_as_output: [
+ {
+ id: '3',
+ order_hash: '0x789',
+ active: true,
+ },
+ {
+ id: '4',
+ order_hash: '0xabc',
+ active: false,
+ },
+ ],
+ balance_changes: [],
+ };
+ mockIPC((cmd) => {
+ if (cmd === 'vault_detail') {
+ return mockData;
+ }
+ });
+
+ const queryClient = new QueryClient();
+
+ render(VaultDetail, {
+ props: { id: '100' },
+ context: new Map([['$$_queryClient', queryClient]]),
+ });
+
+ await waitFor(async () => {
+ expect(
+ await screen.findAllByTestId('vaultDetailOrderAsInputOrder', { exact: false }),
+ ).toHaveLength(2);
+ expect(
+ await screen.findAllByTestId('vaultDetailOrderAsOutputOrder', { exact: false }),
+ ).toHaveLength(2);
+ });
+
+ const orderAsInputOrders = screen.getAllByTestId('vaultDetailOrderAsInputOrder', {
+ exact: false,
+ });
+ expect(orderAsInputOrders[0]).toHaveTextContent('0x123...0x123');
+ expect(orderAsInputOrders[1]).toHaveTextContent('0x456...0x456');
+
+ const orderAsOutputOrders = screen.getAllByTestId('vaultDetailOrderAsOutputOrder', {
+ exact: false,
+ });
+
+ expect(orderAsOutputOrders[0]).toHaveTextContent('0x789...0x789');
+ expect(orderAsOutputOrders[1]).toHaveTextContent('0xabc...0xabc');
+});
+
+test('orders link to the correct order', async () => {
+ const mockData: Vault = {
+ id: '1',
+ vault_id: '0xabc',
+ owner: '0x123',
+ token: {
+ id: '0x456',
+ address: '0x456',
+ name: 'USDC coin',
+ symbol: 'USDC',
+ decimals: '6',
+ },
+ balance: '100000000000',
+ orders_as_input: [
+ {
+ id: '1',
+ order_hash: '0x123',
+ active: true,
+ },
+ {
+ id: '2',
+ order_hash: '0x456',
+ active: false,
+ },
+ ],
+ orders_as_output: [
+ {
+ id: '3',
+ order_hash: '0x789',
+ active: true,
+ },
+ {
+ id: '4',
+ order_hash: '0xabc',
+ active: false,
+ },
+ ],
+ balance_changes: [],
+ };
+ mockIPC((cmd) => {
+ if (cmd === 'vault_detail') {
+ return mockData;
+ }
+ });
+
+ const queryClient = new QueryClient();
+
+ render(VaultDetail, {
+ props: { id: '100' },
+ context: new Map([['$$_queryClient', queryClient]]),
+ });
+
+ let ordersAsOutput, ordersAsInput;
+
+ await waitFor(async () => {
+ ordersAsInput = await screen.findAllByTestId('vaultDetailOrderAsInputOrder', { exact: false });
+ await Promise.all(
+ ordersAsInput.map(async (order) => {
+ await order.click();
+ expect(goto).toHaveBeenCalledWith(`/orders/${order.getAttribute('data-order')}`);
+ }),
+ );
+ });
+
+ await waitFor(async () => {
+ ordersAsOutput = await screen.findAllByTestId('vaultDetailOrderAsOutputOrder', {
+ exact: false,
+ });
+ await Promise.all(
+ ordersAsOutput.map(async (order) => {
+ await order.click();
+ expect(goto).toHaveBeenCalledWith(`/orders/${order.getAttribute('data-order')}`);
+ }),
+ );
+ });
+});
+
+test('shows deposit and withdraw buttons if owner wallet matches, opens correct modals', async () => {
+ const mockData: Vault = {
+ id: '1',
+ vault_id: '0xabc',
+ owner: '0x123',
+ token: {
+ id: '0x456',
+ address: '0x456',
+ name: 'USDC coin',
+ symbol: 'USDC',
+ decimals: '6',
+ },
+ balance: '100000000000',
+ orders_as_input: [],
+ orders_as_output: [],
+ balance_changes: [],
+ };
+
+ mockIPC((cmd) => {
+ if (cmd === 'vault_detail') {
+ return mockData;
+ }
+ });
+
+ const queryClient = new QueryClient();
+
+ render(VaultDetail, {
+ props: { id: '100' },
+ context: new Map([['$$_queryClient', queryClient]]),
+ });
+
+ await waitFor(() => {
+ expect(screen.queryByTestId('vaultDetailDepositButton')).not.toBeInTheDocument();
+ expect(screen.queryByTestId('vaultDetailWithdrawButton')).not.toBeInTheDocument();
+ });
+
+ mockWalletAddressMatchesOrBlankStore.set(() => true);
+
+ await waitFor(() => {
+ expect(screen.queryByTestId('vaultDetailDepositButton')).toBeInTheDocument();
+ expect(screen.queryByTestId('vaultDetailWithdrawButton')).toBeInTheDocument();
+ });
+
+ screen.getByTestId('vaultDetailDepositButton').click();
+
+ await waitFor(() => {
+ expect(handleDepositModal).toHaveBeenCalledWith(mockData);
+ });
+
+ screen.getByTestId('vaultDetailWithdrawButton').click();
+
+ await waitFor(() => {
+ expect(handleWithdrawModal).toHaveBeenCalledWith(mockData);
+ });
+});
diff --git a/tauri-app/src/lib/components/tables/VaultBalanceChangesTable.svelte b/tauri-app/src/lib/components/tables/VaultBalanceChangesTable.svelte
new file mode 100644
index 000000000..114202a8e
--- /dev/null
+++ b/tauri-app/src/lib/components/tables/VaultBalanceChangesTable.svelte
@@ -0,0 +1,72 @@
+
+
+
+
+ Vault Balance Changes
+
+
+ Date
+ Sender
+ Transaction Hash
+ Balance Change
+ Balance
+ Type
+
+
+
+
+ {formatTimestampSecondsAsLocal(BigInt(item.timestamp))}
+
+
+
+
+
+
+
+
+ {formatUnits(BigInt(item.amount), Number(item.vault.token.decimals ?? 0))}
+ {item.vault.token.symbol}
+
+
+ {formatUnits(BigInt(item.new_vault_balance), Number(item.vault.token.decimals ?? 0))}
+ {item.vault.token.symbol}
+
+
+ {item.__typename}
+
+
+
diff --git a/tauri-app/src/lib/components/tables/VaultBalanceChangesTable.test.ts b/tauri-app/src/lib/components/tables/VaultBalanceChangesTable.test.ts
new file mode 100644
index 000000000..c47a8d670
--- /dev/null
+++ b/tauri-app/src/lib/components/tables/VaultBalanceChangesTable.test.ts
@@ -0,0 +1,161 @@
+import { render, screen, waitFor } from '@testing-library/svelte';
+import { test, vi } from 'vitest';
+import { expect } from '$lib/test/matchers';
+import { QueryClient } from '@tanstack/svelte-query';
+import { mockIPC } from '@tauri-apps/api/mocks';
+import VaultBalanceChangesTable from './VaultBalanceChangesTable.svelte';
+import type { VaultBalanceChange } from '$lib/typeshare/vaultBalanceChangesList';
+import { formatTimestampSecondsAsLocal } from '$lib/utils/time';
+
+vi.mock('$lib/stores/settings', async (importOriginal) => {
+ const { writable } = await import('svelte/store');
+ const { mockSettingsStore } = await import('$lib/mocks/settings');
+
+ const _activeOrderbook = writable();
+
+ return {
+ ...((await importOriginal()) as object),
+ settings: mockSettingsStore,
+ subgraphUrl: writable('https://example.com'),
+ activeOrderbook: {
+ ..._activeOrderbook,
+ load: vi.fn(() => _activeOrderbook.set(true)),
+ },
+ };
+});
+
+test('renders the vault list table with correct data', async () => {
+ const queryClient = new QueryClient();
+
+ const mockVaultBalanceChanges: VaultBalanceChange[] = [
+ {
+ __typename: 'Withdrawal',
+ amount: '1000',
+ old_vault_balance: '5000',
+ new_vault_balance: '4000',
+ vault: {
+ id: 'vault1',
+ token: {
+ id: 'token1',
+ address: '0xTokenAddress1',
+ name: 'Token1',
+ symbol: 'TKN1',
+ decimals: '18',
+ },
+ },
+ timestamp: '1625247600',
+ transaction: {
+ id: 'tx1',
+ from: '0xUser1',
+ },
+ },
+ {
+ __typename: 'TradeVaultBalanceChange',
+ amount: '1500',
+ old_vault_balance: '4000',
+ new_vault_balance: '2500',
+ vault: {
+ id: 'vault2',
+ token: {
+ id: 'token2',
+ address: '0xTokenAddress2',
+ name: 'Token2',
+ symbol: 'TKN2',
+ decimals: '18',
+ },
+ },
+ timestamp: '1625347600',
+ transaction: {
+ id: 'tx2',
+ from: '0xUser2',
+ },
+ },
+ {
+ __typename: 'Deposit',
+ amount: '2000',
+ old_vault_balance: '2500',
+ new_vault_balance: '4500',
+ vault: {
+ id: 'vault3',
+ token: {
+ id: 'token3',
+ address: '0xTokenAddress3',
+ name: 'Token3',
+ symbol: 'TKN3',
+ decimals: '18',
+ },
+ },
+ timestamp: '1625447600',
+ transaction: {
+ id: 'tx3',
+ from: '0xUser3',
+ },
+ },
+ ];
+
+ mockIPC((cmd) => {
+ if (cmd === 'vault_balance_changes_list') {
+ return mockVaultBalanceChanges;
+ }
+ });
+
+ render(VaultBalanceChangesTable, {
+ props: { id: '100' },
+ context: new Map([['$$_queryClient', queryClient]]),
+ });
+
+ await waitFor(() => {
+ const rows = screen.getAllByTestId('bodyRow');
+ expect(rows).toHaveLength(3);
+ });
+});
+
+test('it shows the correct data in the table', async () => {
+ const queryClient = new QueryClient();
+
+ const mockVaultBalanceChanges: VaultBalanceChange[] = [
+ {
+ __typename: 'Withdrawal',
+ amount: '1000',
+ old_vault_balance: '5000',
+ new_vault_balance: '4000',
+ vault: {
+ id: 'vault1',
+ token: {
+ id: 'token1',
+ address: '0xTokenAddress1',
+ name: 'Token1',
+ symbol: 'TKN1',
+ decimals: '4',
+ },
+ },
+ timestamp: '1625247600',
+ transaction: {
+ id: 'tx1',
+ from: '0xUser1',
+ },
+ },
+ ];
+
+ mockIPC((cmd) => {
+ if (cmd === 'vault_balance_changes_list') {
+ return mockVaultBalanceChanges;
+ }
+ });
+
+ render(VaultBalanceChangesTable, {
+ props: { id: '100' },
+ context: new Map([['$$_queryClient', queryClient]]),
+ });
+
+ await waitFor(() => {
+ expect(screen.getByTestId('vaultBalanceChangesTableDate')).toHaveTextContent(
+ formatTimestampSecondsAsLocal(BigInt('1625247600')),
+ );
+ expect(screen.getByTestId('vaultBalanceChangesTableFrom')).toHaveTextContent('0xUse...User1');
+ expect(screen.getByTestId('vaultBalanceChangesTableTx')).toHaveTextContent('tx1');
+ expect(screen.getByTestId('vaultBalanceChangesTableBalanceChange')).toHaveTextContent('1 TKN1');
+ expect(screen.getByTestId('vaultBalanceChangesTableBalance')).toHaveTextContent('0.4 TKN1');
+ expect(screen.getByTestId('vaultBalanceChangesTableType')).toHaveTextContent('Withdrawal');
+ });
+});
diff --git a/tauri-app/src/routes/orders/[id]/+page.svelte b/tauri-app/src/routes/orders/[id]/+page.svelte
index ded56672f..23422b7a3 100644
--- a/tauri-app/src/routes/orders/[id]/+page.svelte
+++ b/tauri-app/src/routes/orders/[id]/+page.svelte
@@ -11,7 +11,7 @@
import Hash from '$lib/components/Hash.svelte';
import { HashType } from '$lib/types/hash';
import { sortBy } from 'lodash';
- import LightweightChartLine from '$lib/components/LightweightChartLine.svelte';
+ import LightweightChartLine from '$lib/components/charts/LightweightChartLine.svelte';
import PageContentDetail from '$lib/components/PageContentDetail.svelte';
import CodeMirrorRainlang from '$lib/components/CodeMirrorRainlang.svelte';
import { colorTheme } from '$lib/stores/darkMode';
diff --git a/tauri-app/src/routes/vaults/[id]/+page.svelte b/tauri-app/src/routes/vaults/[id]/+page.svelte
index cedf6ea6a..a0dec9207 100644
--- a/tauri-app/src/routes/vaults/[id]/+page.svelte
+++ b/tauri-app/src/routes/vaults/[id]/+page.svelte
@@ -1,210 +1,9 @@
-
-
-
- {data?.token.name}
-
-
- {#if data && $walletAddressMatchesOrBlank(data.owner)}
-
handleDepositModal(data)}
- > Deposit
-
handleWithdrawModal(data)}
- > Withdraw
- {/if}
-
-
-
- {#if data}
-
- Vault ID
- {bigintStringToHex(data.vault_id)}
-
-
-
- Owner Address
-
-
-
-
-
-
- Token address
-
-
-
-
-
-
- Balance
- {formatUnits(BigInt(data.balance), Number(data.token.decimals ?? 0))}
- {data.token.symbol}
-
-
-
- Orders as input
-
- {#if data.orders_as_input && data.orders_as_input.length > 0}
-
- {#each data.orders_as_input as order}
- goto(`/orders/${order.id}`)}
- >
- {/each}
-
- {:else}
- None
- {/if}
-
-
-
-
- Orders as output
-
- {#if data.orders_as_output && data.orders_as_output.length > 0}
-
- {#each data.orders_as_output as order}
- goto(`/orders/${order.id}`)}
- >
- {/each}
-
- {:else}
- None{/if}
-
-
- {/if}
-
-
-
- {#if data}
-
- {/if}
-
-
-
-
-
- Vault Balance Changes
-
-
- Date
- Sender
- Transaction Hash
- Balance Change
- Balance
- Type
-
-
-
-
- {formatTimestampSecondsAsLocal(BigInt(item.timestamp))}
-
-
-
-
-
-
-
-
- {formatUnits(BigInt(item.amount), Number(item.vault.token.decimals ?? 0))}
- {item.vault.token.symbol}
-
-
- {formatUnits(BigInt(item.new_vault_balance), Number(item.vault.token.decimals ?? 0))}
- {item.vault.token.symbol}
-
-
- {item.__typename}
-
-
-
-
-
+