diff --git a/frontend/src/stores/crypto.test.ts b/frontend/src/stores/crypto.test.ts index 5799191..ae0d7f9 100644 --- a/frontend/src/stores/crypto.test.ts +++ b/frontend/src/stores/crypto.test.ts @@ -105,6 +105,50 @@ describe('crypto store', () => { expect(crypto.pendingQuote).toBeNull() }) + it('applies a browser preview tick without replacing account state', async () => { + const crypto = useCryptoStore() + crypto.data = { + ...bootstrap, + cashBalance: '100.00', + holdings: [ + { + assetId: 'aurora', + averagePrice: '100.00', + quantity: '2.000000', + value: '200.00', + }, + ], + markets: [ + { + changePercent: 0, + color: '#25d9ad', + enabled: true, + high24h: '100.00', + id: 'aurora', + issuedSupply: '1000000.000000', + logo: '◈', + low24h: '100.00', + name: 'Aurora', + price: '100.00', + sparkline: [0, 1], + symbol: 'AUR', + treasuryAvailable: '850000.000000', + }, + ], + portfolioValue: '300.00', + } + mockNuiCall.mockResolvedValueOnce({ + data: [{ ...crypto.data.markets[0], price: '101.25' }], + success: true, + }) + + expect(await crypto.previewMarketTick()).toBe(true) + expect(mockNuiCall).toHaveBeenCalledWith('crypto:market-tick', {}) + expect(crypto.data.holdings[0].value).toBe('202.50') + expect(crypto.data.portfolioValue).toBe('302.50') + expect(crypto.data.cashBalance).toBe('100.00') + }) + it('sends only market, side and quantity when requesting a quote', async () => { mockNuiCall.mockResolvedValueOnce({ data: quote, success: true }) const crypto = useCryptoStore() diff --git a/frontend/src/stores/crypto.ts b/frontend/src/stores/crypto.ts index ec1e5e8..c7648eb 100644 --- a/frontend/src/stores/crypto.ts +++ b/frontend/src/stores/crypto.ts @@ -54,6 +54,12 @@ export const useCryptoStore = defineStore('crypto', { this.pendingQuote = null } }, + async previewMarketTick(): Promise { + const response = await nuiCall('crypto:market-tick', {}) + if (!response.success || !response.data) return false + this.applyMarketUpdate(response.data) + return true + }, async call(endpoint: string, payload: Record = {}) { this.isLoading = true this.error = '' diff --git a/frontend/src/views/apps/CryptoApp.contract.test.ts b/frontend/src/views/apps/CryptoApp.contract.test.ts index 3c811d6..955f8b7 100644 --- a/frontend/src/views/apps/CryptoApp.contract.test.ts +++ b/frontend/src/views/apps/CryptoApp.contract.test.ts @@ -230,6 +230,7 @@ describe('VaultX crypto app contracts', () => { it('streams server-driven market movement into live portfolio values', () => { expect(config).toContain('PriceTickMinimumSeconds') expect(config).toContain('MarketsPerTickMaximum') + expect(config).toContain('TickMovementDivisor') expect(config).toContain('MarketShockChanceBasisPoints') expect(server).toContain('local market_dynamics = {}') expect(server).toContain('global_market_trend') diff --git a/frontend/src/views/apps/CryptoApp.vue b/frontend/src/views/apps/CryptoApp.vue index a634589..b5d463b 100644 --- a/frontend/src/views/apps/CryptoApp.vue +++ b/frontend/src/views/apps/CryptoApp.vue @@ -24,7 +24,7 @@ import { WalletCards, X, } from 'lucide-vue-next' -import { computed, onMounted, ref, watch } from 'vue' +import { computed, onMounted, onUnmounted, ref, watch } from 'vue' import cryptoHeaderLogo from '@/assets/img/app-icons/crypto-header-logo.png' import CryptoLogo from '@/components/crypto/CryptoLogo.vue' import { useCryptoStore } from '@/stores/crypto' @@ -86,6 +86,7 @@ const CHART_PERIOD_CONFIG: Record< const crypto = useCryptoStore() const easyShare = useEasyShareStore() const phone = usePhoneStore() +let marketPreviewTimer: number | undefined const tab = ref('portfolio') const authMode = ref<'login' | 'register'>('login') const activityFilter = ref<'all' | 'trades' | 'wallet'>('all') @@ -348,11 +349,14 @@ function t(key: string) { return phone.t(`Apps.crypto.${key}`) } function money(value: string | number) { + const numericValue = Number(value) || 0 + const absolute = Math.abs(numericValue) return new Intl.NumberFormat(locale.value, { currency: 'USD', - maximumFractionDigits: 2, + maximumFractionDigits: absolute > 0 && absolute < 1 ? 4 : 2, + minimumFractionDigits: 2, style: 'currency', - }).format(Number(value) || 0) + }).format(numericValue) } function signedMoney(value: number) { const formatted = money(Math.abs(value)) @@ -666,7 +670,23 @@ watch(markets, (value) => { value.find((market) => market.id === selectedMarket.value?.id) ?? null } }) -onMounted(() => void crypto.load()) +function scheduleDevelopmentMarketTick() { + if (!import.meta.env.DEV) return + marketPreviewTimer = window.setTimeout( + async () => { + await crypto.previewMarketTick() + scheduleDevelopmentMarketTick() + }, + 1600 + Math.random() * 1800, + ) +} +onMounted(async () => { + await crypto.load() + scheduleDevelopmentMarketTick() +}) +onUnmounted(() => { + if (marketPreviewTimer !== undefined) window.clearTimeout(marketPreviewTimer) +})