mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-28 17:01:18 +00:00
ENH - expand VaultX trading experience
This commit is contained in:
@@ -15,7 +15,17 @@ const bootstrap: CryptoBootstrap = {
|
||||
holdings: [],
|
||||
markets: [],
|
||||
portfolioValue: '25000',
|
||||
profile: { handle: 'skyline', id: 'profile-1', status: 'active' },
|
||||
profile: {
|
||||
createdAt: Date.now() - 86_400_000,
|
||||
handle: 'skyline',
|
||||
hideBalances: false,
|
||||
id: 'profile-1',
|
||||
priceAlerts: true,
|
||||
status: 'active',
|
||||
totalTrades: 12,
|
||||
totalVolume: '18462.80',
|
||||
tradeConfirmations: true,
|
||||
},
|
||||
}
|
||||
const quote: CryptoQuote = {
|
||||
expiresAt: Date.now() + 8000,
|
||||
@@ -84,4 +94,26 @@ describe('crypto store', () => {
|
||||
expect(crypto.data).toEqual(bootstrap)
|
||||
expect(crypto.error).toBe('quote_expired')
|
||||
})
|
||||
|
||||
it('updates profile preferences through the authenticated server endpoint', async () => {
|
||||
mockNuiCall.mockResolvedValueOnce({ data: bootstrap, success: true })
|
||||
const crypto = useCryptoStore()
|
||||
|
||||
expect(
|
||||
await crypto.updateProfile({
|
||||
handle: 'skyline',
|
||||
hideBalances: true,
|
||||
password: '',
|
||||
priceAlerts: false,
|
||||
tradeConfirmations: true,
|
||||
}),
|
||||
).toBe(true)
|
||||
expect(mockNuiCall).toHaveBeenCalledWith('crypto:update-profile', {
|
||||
handle: 'skyline',
|
||||
hideBalances: true,
|
||||
password: '',
|
||||
priceAlerts: false,
|
||||
tradeConfirmations: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -95,5 +95,20 @@ export const useCryptoStore = defineStore('crypto', {
|
||||
this.pendingQuote = null
|
||||
return true
|
||||
},
|
||||
async updateProfile(payload: {
|
||||
handle: string
|
||||
hideBalances: boolean
|
||||
password: string
|
||||
priceAlerts: boolean
|
||||
tradeConfirmations: boolean
|
||||
}): Promise<boolean> {
|
||||
const response = await this.call<CryptoBootstrap>(
|
||||
'update-profile',
|
||||
payload,
|
||||
)
|
||||
if (!response.success || !response.data) return false
|
||||
this.data = response.data
|
||||
return true
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -440,7 +440,12 @@ const cryptoFallbackLocales = {
|
||||
loginAction: 'Open portfolio',
|
||||
registerAction: 'Create secure profile',
|
||||
},
|
||||
tabs: { portfolio: 'Portfolio', markets: 'Markets', activity: 'Activity' },
|
||||
tabs: {
|
||||
portfolio: 'Portfolio',
|
||||
markets: 'Markets',
|
||||
activity: 'Activity',
|
||||
profile: 'Profile',
|
||||
},
|
||||
portfolio: {
|
||||
total: 'Total portfolio',
|
||||
cash: 'Available cash',
|
||||
@@ -448,7 +453,11 @@ const cryptoFallbackLocales = {
|
||||
empty: 'No assets yet',
|
||||
emptyBody: 'Open Markets to request a protected quote.',
|
||||
avg: 'Avg.',
|
||||
insightTitle: 'Portfolio insight',
|
||||
insightBody:
|
||||
'Your holdings are valued against the latest synthetic server prices.',
|
||||
},
|
||||
quick: { trade: 'Trade', more: 'More' },
|
||||
actions: { deposit: 'Deposit', withdraw: 'Withdraw' },
|
||||
markets: {
|
||||
title: 'Fictional markets',
|
||||
@@ -456,6 +465,22 @@ const cryptoFallbackLocales = {
|
||||
noticeTitle: 'In-game exchange',
|
||||
notice:
|
||||
'Prices are synthetic and have no real-world value. Every trade is checked against finite exchange liquidity.',
|
||||
eyebrow: 'Market pulse',
|
||||
movers: 'Top movers',
|
||||
today: 'Today',
|
||||
},
|
||||
marketDetail: {
|
||||
back: 'Back to markets',
|
||||
today: 'Today',
|
||||
description:
|
||||
'The reference price is generated by the server. Executable prices use a protected short-lived quote.',
|
||||
investment: 'Your investment',
|
||||
totalValue: 'Current value',
|
||||
statistics: 'Market statistics',
|
||||
high24h: '24h high',
|
||||
low24h: '24h low',
|
||||
supply: 'Issued supply',
|
||||
liquidity: 'Exchange liquidity',
|
||||
},
|
||||
trade: {
|
||||
buy: 'Buy',
|
||||
@@ -482,6 +507,31 @@ const cryptoFallbackLocales = {
|
||||
title: 'Financial activity',
|
||||
empty: 'No activity',
|
||||
emptyBody: 'Deposits, withdrawals and trades will appear here.',
|
||||
volume: 'Lifetime trading volume',
|
||||
completedTrades: 'completed trades',
|
||||
cash: 'Cash',
|
||||
filters: { all: 'All', trades: 'Trades', wallet: 'Wallet' },
|
||||
},
|
||||
profile: {
|
||||
verified: 'Character ownership verified',
|
||||
trades: 'Trades',
|
||||
volume: 'Volume',
|
||||
memberSince: 'Member since',
|
||||
preferences: 'Preferences',
|
||||
priceAlerts: 'Price alerts',
|
||||
priceAlertsBody: 'Notify me about notable market moves.',
|
||||
confirmations: 'Trade confirmations',
|
||||
confirmationsBody: 'Keep an extra confirmation before execution.',
|
||||
hideBalances: 'Privacy mode',
|
||||
hideBalancesBody: 'Hide balances across VaultX.',
|
||||
identity: 'Profile identity',
|
||||
passwordForChange: 'Password for handle change',
|
||||
passwordOptional: 'Only required when changing handle',
|
||||
saved: 'Profile saved securely.',
|
||||
save: 'Save profile',
|
||||
securityTitle: 'Protected profile',
|
||||
securityBody:
|
||||
'Your profile stays bound to this framework character and financial session.',
|
||||
},
|
||||
activityTypes: {
|
||||
buy: 'Asset purchase',
|
||||
@@ -516,6 +566,7 @@ const cryptoFallbackLocales = {
|
||||
settlement_pending: 'A money transfer is already pending review.',
|
||||
service_unavailable: 'VaultX is temporarily unavailable.',
|
||||
request_failed: 'The secure exchange request failed.',
|
||||
invalid_profile: 'Check your profile settings and handle.',
|
||||
default: 'VaultX could not complete the request.',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -4,11 +4,15 @@ export type CryptoMarket = {
|
||||
changePercent: number
|
||||
color: string
|
||||
enabled: boolean
|
||||
high24h: string
|
||||
id: string
|
||||
issuedSupply: string
|
||||
low24h: string
|
||||
name: string
|
||||
price: string
|
||||
sparkline: number[]
|
||||
symbol: string
|
||||
treasuryAvailable: string
|
||||
}
|
||||
|
||||
export type CryptoHolding = {
|
||||
@@ -28,9 +32,15 @@ export type CryptoActivity = {
|
||||
}
|
||||
|
||||
export type CryptoProfile = {
|
||||
createdAt: number
|
||||
handle: string
|
||||
hideBalances: boolean
|
||||
id: string
|
||||
priceAlerts: boolean
|
||||
status: 'active' | 'frozen' | 'closed'
|
||||
totalTrades: number
|
||||
totalVolume: string
|
||||
tradeConfirmations: boolean
|
||||
}
|
||||
|
||||
export type CryptoBootstrap = {
|
||||
|
||||
@@ -36,6 +36,15 @@ describe('VaultX crypto app contracts', () => {
|
||||
expect(source).toContain('<WalletCards')
|
||||
expect(source).toContain('<ChartNoAxesCombined')
|
||||
expect(source).toContain('<History')
|
||||
expect(source).toContain('<UserRound')
|
||||
})
|
||||
|
||||
it('includes advanced market detail and persistent profile controls', () => {
|
||||
expect(source).toContain('detail.sparkline')
|
||||
expect(source).toContain("t('marketDetail.statistics')")
|
||||
expect(source).toContain('<SkyToggle')
|
||||
expect(server).toContain('sky_phone:crypto:update-profile')
|
||||
expect(server).toContain('`price_alerts`')
|
||||
})
|
||||
|
||||
it('keeps all consequential calculations and state transitions on the server', () => {
|
||||
@@ -71,7 +80,7 @@ describe('VaultX crypto app contracts', () => {
|
||||
expect(passwordProvider).toContain('scryptSync')
|
||||
expect(passwordProvider).toContain('timingSafeEqual')
|
||||
expect(passwordProvider).toContain('randomBytes(16)')
|
||||
expect(server).not.toContain('data.price')
|
||||
expect(server).not.toContain('data.fee')
|
||||
expect(server).not.toMatch(/data\.price\b/)
|
||||
expect(server).not.toMatch(/data\.fee\b/)
|
||||
})
|
||||
})
|
||||
|
||||
+1062
-514
File diff suppressed because it is too large
Load Diff
@@ -211,6 +211,10 @@ const cryptoMarkets = [
|
||||
price: '128.50',
|
||||
changePercent: 4.82,
|
||||
enabled: true,
|
||||
high24h: '132.80',
|
||||
low24h: '119.40',
|
||||
issuedSupply: '1000000',
|
||||
treasuryAvailable: '849882.5',
|
||||
sparkline: [0.12, 0.24, 0.2, 0.38, 0.51, 0.46, 0.68, 0.62, 0.81, 0.92],
|
||||
},
|
||||
{
|
||||
@@ -221,6 +225,10 @@ const cryptoMarkets = [
|
||||
price: '42.75',
|
||||
changePercent: -1.37,
|
||||
enabled: true,
|
||||
high24h: '46.30',
|
||||
low24h: '40.90',
|
||||
issuedSupply: '2500000',
|
||||
treasuryAvailable: '2099914.75',
|
||||
sparkline: [0.84, 0.72, 0.76, 0.61, 0.69, 0.52, 0.46, 0.41, 0.35, 0.39],
|
||||
},
|
||||
{
|
||||
@@ -231,9 +239,24 @@ const cryptoMarkets = [
|
||||
price: '9.80',
|
||||
changePercent: 7.21,
|
||||
enabled: true,
|
||||
high24h: '10.24',
|
||||
low24h: '8.76',
|
||||
issuedSupply: '8000000',
|
||||
treasuryAvailable: '6800000',
|
||||
sparkline: [0.08, 0.11, 0.18, 0.26, 0.23, 0.4, 0.55, 0.64, 0.79, 0.93],
|
||||
},
|
||||
]
|
||||
let cryptoProfile = {
|
||||
createdAt: Date.now() - 42 * 86400000,
|
||||
handle: 'skyline',
|
||||
hideBalances: false,
|
||||
id: 'crypto-profile-demo',
|
||||
priceAlerts: true,
|
||||
status: 'active',
|
||||
totalTrades: 12,
|
||||
totalVolume: '18462.80',
|
||||
tradeConfirmations: true,
|
||||
}
|
||||
let cryptoHoldings = [
|
||||
{
|
||||
assetId: 'aurora',
|
||||
@@ -6544,9 +6567,7 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
0,
|
||||
),
|
||||
),
|
||||
profile: cryptoAuthenticated
|
||||
? { handle: 'skyline', id: 'crypto-profile-demo', status: 'active' }
|
||||
: null,
|
||||
profile: cryptoAuthenticated ? cryptoProfile : null,
|
||||
})
|
||||
const billingInvoice = (invoice) => ({
|
||||
...invoice,
|
||||
@@ -8044,13 +8065,18 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
return
|
||||
}
|
||||
cryptoAuthenticated = true
|
||||
response.json({
|
||||
success: true,
|
||||
data: {
|
||||
...cryptoOverview(),
|
||||
profile: { handle, id: 'crypto-profile-new', status: 'active' },
|
||||
},
|
||||
})
|
||||
cryptoProfile = {
|
||||
createdAt: Date.now(),
|
||||
handle,
|
||||
hideBalances: false,
|
||||
id: 'crypto-profile-new',
|
||||
priceAlerts: true,
|
||||
status: 'active',
|
||||
totalTrades: 0,
|
||||
totalVolume: '0',
|
||||
tradeConfirmations: true,
|
||||
}
|
||||
response.json({ success: true, data: cryptoOverview() })
|
||||
return
|
||||
}
|
||||
if (endpoint === 'crypto:logout') {
|
||||
@@ -8059,6 +8085,37 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
response.json({ success: true })
|
||||
return
|
||||
}
|
||||
if (endpoint === 'crypto:update-profile') {
|
||||
const handle = String(request.body.handle ?? '').trim()
|
||||
if (!/^[A-Za-z0-9][A-Za-z0-9._]{1,18}[A-Za-z0-9]$/.test(handle)) {
|
||||
response.json({ success: false, error: 'invalid_profile' })
|
||||
return
|
||||
}
|
||||
if (
|
||||
handle.toLowerCase() !== cryptoProfile.handle.toLowerCase() &&
|
||||
request.body.password !== cryptoPassword
|
||||
) {
|
||||
response.json({ success: false, error: 'invalid_credentials' })
|
||||
return
|
||||
}
|
||||
if (
|
||||
typeof request.body.priceAlerts !== 'boolean' ||
|
||||
typeof request.body.tradeConfirmations !== 'boolean' ||
|
||||
typeof request.body.hideBalances !== 'boolean'
|
||||
) {
|
||||
response.json({ success: false, error: 'invalid_profile' })
|
||||
return
|
||||
}
|
||||
cryptoProfile = {
|
||||
...cryptoProfile,
|
||||
handle,
|
||||
hideBalances: request.body.hideBalances,
|
||||
priceAlerts: request.body.priceAlerts,
|
||||
tradeConfirmations: request.body.tradeConfirmations,
|
||||
}
|
||||
response.json({ success: true, data: cryptoOverview() })
|
||||
return
|
||||
}
|
||||
if (endpoint === 'crypto:deposit' || endpoint === 'crypto:withdraw') {
|
||||
const amount = Number(request.body.amount)
|
||||
if (!Number.isSafeInteger(amount) || amount < 10) {
|
||||
|
||||
@@ -114,6 +114,10 @@ function verifyBrowserTestData(dataByEndpoint) {
|
||||
)
|
||||
|
||||
expectItems(dataByEndpoint.get('health:overview').days, 'health history', 7)
|
||||
const crypto = dataByEndpoint.get('crypto:bootstrap')
|
||||
expectItems(crypto.markets, 'crypto markets', 3)
|
||||
assert.equal(typeof crypto.profile.priceAlerts, 'boolean')
|
||||
assert.equal(typeof crypto.markets[0].issuedSupply, 'string')
|
||||
expectItems(
|
||||
dataByEndpoint.get('billing:list').invoices,
|
||||
'billing invoices',
|
||||
|
||||
@@ -1054,16 +1054,19 @@ Locales["de"] = {
|
||||
auth = {
|
||||
eyebrow = "Charaktergebundene Börse", loginTitle = "Willkommen zurück", registerTitle = "Erstelle dein VaultX-Profil", body = "Handle fiktive digitale Assets mit dem Ingame-Geld deines Charakters.", login = "Einloggen", register = "Registrieren", handle = "VaultX-Benutzername", handlePlaceholder = "3–20 Buchstaben oder Zahlen", password = "Passwort", passwordPlaceholder = "8–72 Zeichen", showPassword = "Passwort anzeigen", hidePassword = "Passwort ausblenden", security = "Nutze ein In-Character-Passwort. Verwende niemals ein echtes Passwort erneut.", loginAction = "Portfolio öffnen", registerAction = "Sicheres Profil erstellen",
|
||||
},
|
||||
tabs = { portfolio = "Portfolio", markets = "Märkte", activity = "Aktivität" },
|
||||
portfolio = { total = "Gesamtes Portfolio", cash = "Verfügbares Guthaben", holdings = "Deine Assets", empty = "Noch keine Assets", emptyBody = "Öffne Märkte und fordere ein geschütztes Angebot an.", avg = "Ø" },
|
||||
tabs = { portfolio = "Portfolio", markets = "Märkte", activity = "Aktivität", profile = "Profil" },
|
||||
portfolio = { total = "Gesamtes Portfolio", cash = "Verfügbares Guthaben", holdings = "Deine Assets", empty = "Noch keine Assets", emptyBody = "Öffne Märkte und fordere ein geschütztes Angebot an.", avg = "Ø", insightTitle = "Portfolio-Einblick", insightBody = "Deine Bestände werden mit den neuesten synthetischen Serverkursen bewertet." },
|
||||
quick = { trade = "Handeln", more = "Mehr" },
|
||||
actions = { deposit = "Einzahlen", withdraw = "Auszahlen" },
|
||||
markets = { title = "Fiktive Märkte", live = "Serverpreise", noticeTitle = "Ingame-Börse", notice = "Die Kurse sind synthetisch und haben keinen realen Wert. Jeder Handel wird gegen die begrenzte Liquidität der Börse geprüft." },
|
||||
markets = { title = "Fiktive Märkte", live = "Serverpreise", noticeTitle = "Ingame-Börse", notice = "Die Kurse sind synthetisch und haben keinen realen Wert. Jeder Handel wird gegen die begrenzte Liquidität der Börse geprüft.", eyebrow = "Marktpuls", movers = "Top-Beweger", today = "Heute" },
|
||||
marketDetail = { back = "Zurück zu den Märkten", today = "Heute", description = "Der Referenzkurs wird vom Server erzeugt. Ausführbare Kurse verwenden ein geschütztes, kurzlebiges Angebot.", investment = "Deine Investition", totalValue = "Aktueller Wert", statistics = "Marktstatistik", high24h = "24h-Hoch", low24h = "24h-Tief", supply = "Ausgegebene Menge", liquidity = "Börsenliquidität" },
|
||||
trade = { buy = "Kaufen", sell = "Verkaufen", quantity = "Menge", available = "Verfügbar:", price = "Angebotspreis", gross = "Brutto", fee = "Börsengebühr", total = "Gesamt", quoteExpiry = "Dieses Serverangebot läuft nach 8 Sekunden ab und kann nur einmal genutzt werden.", getQuote = "Geschütztes Angebot", confirm = "Handel bestätigen" },
|
||||
settlement = { depositBody = "Übertrage Bankguthaben in dein verbuchtes VaultX-Guthaben.", withdrawBody = "Zahle verbuchtes VaultX-Guthaben auf das Bankkonto deines Charakters aus.", amount = "Ganzer Bankbetrag", confirm = "Sicher bestätigen" },
|
||||
activity = { title = "Finanzaktivität", empty = "Keine Aktivität", emptyBody = "Einzahlungen, Auszahlungen und Handel erscheinen hier." },
|
||||
activity = { title = "Finanzaktivität", empty = "Keine Aktivität", emptyBody = "Einzahlungen, Auszahlungen und Handel erscheinen hier.", volume = "Handelsvolumen gesamt", completedTrades = "abgeschlossene Trades", cash = "Guthaben", filters = { all = "Alle", trades = "Trades", wallet = "Wallet" } },
|
||||
profile = { verified = "Charakterbesitz verifiziert", trades = "Trades", volume = "Volumen", memberSince = "Mitglied seit", preferences = "Einstellungen", priceAlerts = "Kurswarnungen", priceAlertsBody = "Bei auffälligen Marktbewegungen benachrichtigen.", confirmations = "Handelsbestätigung", confirmationsBody = "Zusätzliche Bestätigung vor der Ausführung behalten.", hideBalances = "Privatsphäre-Modus", hideBalancesBody = "Beträge in VaultX verbergen.", identity = "Profilidentität", passwordForChange = "Passwort für Benutzernamenänderung", passwordOptional = "Nur bei Änderung des Benutzernamens nötig", saved = "Profil sicher gespeichert.", save = "Profil speichern", securityTitle = "Geschütztes Profil", securityBody = "Dein Profil bleibt an diesen Framework-Charakter und die Finanzsitzung gebunden." },
|
||||
activityTypes = { buy = "Asset-Kauf", sell = "Asset-Verkauf", deposit = "Bankeinzahlung", withdrawal = "Bankauszahlung" },
|
||||
statuses = { completed = "Abgeschlossen", pending = "Prüfung ausstehend", failed = "Fehlgeschlagen", manual_review = "Manuelle Prüfung" },
|
||||
errors = { invalid_handle = "Nutze 3–20 Buchstaben, Zahlen, Punkte oder Unterstriche.", handle_taken = "Dieser VaultX-Benutzername ist bereits vergeben.", profile_exists = "Dieser Charakter besitzt bereits ein VaultX-Profil.", invalid_password = "Das Passwort muss 8–72 Zeichen lang sein.", invalid_credentials = "Das Passwort ist falsch.", locked = "Zu viele Versuche. Versuche es später erneut.", not_authenticated = "Logge dich zuerst bei VaultX ein.", invalid_amount = "Gib einen positiven ganzen Betrag ein.", invalid_quantity = "Gib eine Menge mit bis zu sechs Nachkommastellen ein.", insufficient_funds = "Dein verfügbares Guthaben reicht nicht aus.", insufficient_liquidity = "Die Börse kann diese Menge derzeit nicht ausführen.", quote_expired = "Das Angebot ist abgelaufen. Fordere ein neues an.", quote_unavailable = "Das Angebot ist nicht mehr verfügbar.", market_unavailable = "Dieser Markt ist vorübergehend pausiert.", limit_exceeded = "Die Anfrage überschreitet ein Börsenlimit.", duplicate_request = "Diese Anfrage wurde bereits verarbeitet.", rate_limited = "Zu viele Anfragen. Versuche es gleich erneut.", settlement_pending = "Eine Geldbewegung wartet bereits auf Prüfung.", service_unavailable = "VaultX ist vorübergehend nicht verfügbar.", request_failed = "Die sichere Börsenanfrage ist fehlgeschlagen.", default = "VaultX konnte die Anfrage nicht abschließen." },
|
||||
errors = { invalid_profile = "Prüfe deine VaultX-Profileinstellungen.", invalid_handle = "Nutze 3–20 Buchstaben, Zahlen, Punkte oder Unterstriche.", handle_taken = "Dieser VaultX-Benutzername ist bereits vergeben.", profile_exists = "Dieser Charakter besitzt bereits ein VaultX-Profil.", invalid_password = "Das Passwort muss 8–72 Zeichen lang sein.", invalid_credentials = "Das Passwort ist falsch.", locked = "Zu viele Versuche. Versuche es später erneut.", not_authenticated = "Logge dich zuerst bei VaultX ein.", invalid_amount = "Gib einen positiven ganzen Betrag ein.", invalid_quantity = "Gib eine Menge mit bis zu sechs Nachkommastellen ein.", insufficient_funds = "Dein verfügbares Guthaben reicht nicht aus.", insufficient_liquidity = "Die Börse kann diese Menge derzeit nicht ausführen.", quote_expired = "Das Angebot ist abgelaufen. Fordere ein neues an.", quote_unavailable = "Das Angebot ist nicht mehr verfügbar.", market_unavailable = "Dieser Markt ist vorübergehend pausiert.", limit_exceeded = "Die Anfrage überschreitet ein Börsenlimit.", duplicate_request = "Diese Anfrage wurde bereits verarbeitet.", rate_limited = "Zu viele Anfragen. Versuche es gleich erneut.", settlement_pending = "Eine Geldbewegung wartet bereits auf Prüfung.", service_unavailable = "VaultX ist vorübergehend nicht verfügbar.", request_failed = "Die sichere Börsenanfrage ist fehlgeschlagen.", default = "VaultX konnte die Anfrage nicht abschließen." },
|
||||
},
|
||||
banking = {
|
||||
name = "Banking", welcome = "Willkommen zurück", totalBalance = "Gesamtsaldo", recentPeriod = "in jüngster Zeit",
|
||||
|
||||
@@ -1054,16 +1054,19 @@ Locales["en"] = {
|
||||
auth = {
|
||||
eyebrow = "Character-bound exchange", loginTitle = "Welcome back", registerTitle = "Create your VaultX profile", body = "Trade fictional digital assets with your character's in-game funds.", login = "Log in", register = "Register", handle = "VaultX handle", handlePlaceholder = "3–20 letters or numbers", password = "Password", passwordPlaceholder = "8–72 characters", showPassword = "Show password", hidePassword = "Hide password", security = "Use an in-character password. Never reuse a real password.", loginAction = "Open portfolio", registerAction = "Create secure profile",
|
||||
},
|
||||
tabs = { portfolio = "Portfolio", markets = "Markets", activity = "Activity" },
|
||||
portfolio = { total = "Total portfolio", cash = "Available cash", holdings = "Your assets", empty = "No assets yet", emptyBody = "Open Markets to request a protected quote.", avg = "Avg." },
|
||||
tabs = { portfolio = "Portfolio", markets = "Markets", activity = "Activity", profile = "Profile" },
|
||||
portfolio = { total = "Total portfolio", cash = "Available cash", holdings = "Your assets", empty = "No assets yet", emptyBody = "Open Markets to request a protected quote.", avg = "Avg.", insightTitle = "Portfolio insight", insightBody = "Your holdings are valued against the latest synthetic server prices." },
|
||||
quick = { trade = "Trade", more = "More" },
|
||||
actions = { deposit = "Deposit", withdraw = "Withdraw" },
|
||||
markets = { title = "Fictional markets", live = "Server priced", noticeTitle = "In-game exchange", notice = "Prices are synthetic and have no real-world value. Every trade is checked against finite exchange liquidity." },
|
||||
markets = { title = "Fictional markets", live = "Server priced", noticeTitle = "In-game exchange", notice = "Prices are synthetic and have no real-world value. Every trade is checked against finite exchange liquidity.", eyebrow = "Market pulse", movers = "Top movers", today = "Today" },
|
||||
marketDetail = { back = "Back to markets", today = "Today", description = "The reference price is generated by the server. Executable prices use a protected short-lived quote.", investment = "Your investment", totalValue = "Current value", statistics = "Market statistics", high24h = "24h high", low24h = "24h low", supply = "Issued supply", liquidity = "Exchange liquidity" },
|
||||
trade = { buy = "Buy", sell = "Sell", quantity = "Quantity", available = "Available:", price = "Quote price", gross = "Gross", fee = "Exchange fee", total = "Total", quoteExpiry = "This server quote expires after 8 seconds and can only be used once.", getQuote = "Get protected quote", confirm = "Confirm trade" },
|
||||
settlement = { depositBody = "Move bank funds into your settled VaultX cash balance.", withdrawBody = "Return settled VaultX cash to your character's bank account.", amount = "Whole bank amount", confirm = "Confirm securely" },
|
||||
activity = { title = "Financial activity", empty = "No activity", emptyBody = "Deposits, withdrawals and trades will appear here." },
|
||||
activity = { title = "Financial activity", empty = "No activity", emptyBody = "Deposits, withdrawals and trades will appear here.", volume = "Lifetime trading volume", completedTrades = "completed trades", cash = "Cash", filters = { all = "All", trades = "Trades", wallet = "Wallet" } },
|
||||
profile = { verified = "Character ownership verified", trades = "Trades", volume = "Volume", memberSince = "Member since", preferences = "Preferences", priceAlerts = "Price alerts", priceAlertsBody = "Notify me about notable market moves.", confirmations = "Trade confirmations", confirmationsBody = "Keep an extra confirmation before execution.", hideBalances = "Privacy mode", hideBalancesBody = "Hide balances across VaultX.", identity = "Profile identity", passwordForChange = "Password for handle change", passwordOptional = "Only required when changing handle", saved = "Profile saved securely.", save = "Save profile", securityTitle = "Protected profile", securityBody = "Your profile stays bound to this framework character and financial session." },
|
||||
activityTypes = { buy = "Asset purchase", sell = "Asset sale", deposit = "Bank deposit", withdrawal = "Bank withdrawal" },
|
||||
statuses = { completed = "Completed", pending = "Pending review", failed = "Failed", manual_review = "Manual review" },
|
||||
errors = { invalid_handle = "Use 3–20 letters, numbers, dots or underscores.", handle_taken = "That VaultX handle is already taken.", profile_exists = "This character already owns a VaultX profile.", invalid_password = "Password must be 8–72 characters.", invalid_credentials = "The password is incorrect.", locked = "Too many attempts. Try again later.", not_authenticated = "Sign in to VaultX first.", invalid_amount = "Enter a positive whole amount.", invalid_quantity = "Enter a quantity with up to six decimals.", insufficient_funds = "Your available balance is too low.", insufficient_liquidity = "The exchange cannot fill that amount right now.", quote_expired = "This quote expired. Request a new one.", quote_unavailable = "The quote is no longer available.", market_unavailable = "This market is temporarily paused.", limit_exceeded = "This request exceeds an exchange limit.", duplicate_request = "This request was already processed.", rate_limited = "Too many requests. Try again shortly.", settlement_pending = "A money transfer is already pending review.", service_unavailable = "VaultX is temporarily unavailable.", request_failed = "The secure exchange request failed.", default = "VaultX could not complete the request." },
|
||||
errors = { invalid_profile = "Check your VaultX profile settings.", invalid_handle = "Use 3–20 letters, numbers, dots or underscores.", handle_taken = "That VaultX handle is already taken.", profile_exists = "This character already owns a VaultX profile.", invalid_password = "Password must be 8–72 characters.", invalid_credentials = "The password is incorrect.", locked = "Too many attempts. Try again later.", not_authenticated = "Sign in to VaultX first.", invalid_amount = "Enter a positive whole amount.", invalid_quantity = "Enter a quantity with up to six decimals.", insufficient_funds = "Your available balance is too low.", insufficient_liquidity = "The exchange cannot fill that amount right now.", quote_expired = "This quote expired. Request a new one.", quote_unavailable = "The quote is no longer available.", market_unavailable = "This market is temporarily paused.", limit_exceeded = "This request exceeds an exchange limit.", duplicate_request = "This request was already processed.", rate_limited = "Too many requests. Try again shortly.", settlement_pending = "A money transfer is already pending review.", service_unavailable = "VaultX is temporarily unavailable.", request_failed = "The secure exchange request failed.", default = "VaultX could not complete the request." },
|
||||
},
|
||||
banking = {
|
||||
name = "Banking", welcome = "Welcome back", totalBalance = "Total Balance", recentPeriod = "in recent activity",
|
||||
|
||||
@@ -250,6 +250,7 @@ local server_callbacks = {
|
||||
"crypto:execute",
|
||||
"crypto:deposit",
|
||||
"crypto:withdraw",
|
||||
"crypto:update-profile",
|
||||
"billing:overview",
|
||||
"billing:list",
|
||||
"billing:detail",
|
||||
|
||||
@@ -14,6 +14,9 @@ local function ensure_schema()
|
||||
`account_id` BIGINT UNSIGNED NULL,
|
||||
`handle` VARCHAR(20) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL,
|
||||
`password_hash` VARCHAR(255) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
`price_alerts` TINYINT(1) UNSIGNED NOT NULL DEFAULT 1,
|
||||
`trade_confirmations` TINYINT(1) UNSIGNED NOT NULL DEFAULT 1,
|
||||
`hide_balances` TINYINT(1) UNSIGNED NOT NULL DEFAULT 0,
|
||||
`status` ENUM('active','frozen','closed') NOT NULL DEFAULT 'active',
|
||||
`failed_logins` TINYINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`locked_until` DATETIME NULL,
|
||||
@@ -138,6 +141,9 @@ local function ensure_schema()
|
||||
for _, statement in ipairs(statements) do
|
||||
Bridge.Database.Query(statement, {})
|
||||
end
|
||||
Bridge.Database.Query("ALTER TABLE `sky_phone_crypto_profiles` ADD COLUMN IF NOT EXISTS `price_alerts` TINYINT(1) UNSIGNED NOT NULL DEFAULT 1 AFTER `password_hash`", {})
|
||||
Bridge.Database.Query("ALTER TABLE `sky_phone_crypto_profiles` ADD COLUMN IF NOT EXISTS `trade_confirmations` TINYINT(1) UNSIGNED NOT NULL DEFAULT 1 AFTER `price_alerts`", {})
|
||||
Bridge.Database.Query("ALTER TABLE `sky_phone_crypto_profiles` ADD COLUMN IF NOT EXISTS `hide_balances` TINYINT(1) UNSIGNED NOT NULL DEFAULT 0 AFTER `trade_confirmations`", {})
|
||||
end
|
||||
|
||||
local function new_id()
|
||||
@@ -282,6 +288,8 @@ end
|
||||
local function profile_by_owner(identifier)
|
||||
return Bridge.Database.Query([[
|
||||
SELECT `id`,`owner_identifier`,`account_id`,`handle`,`status`,`failed_logins`,
|
||||
`price_alerts`,`trade_confirmations`,`hide_balances`,
|
||||
UNIX_TIMESTAMP(`created_at`) AS `created_at`,
|
||||
UNIX_TIMESTAMP(`locked_until`) AS `locked_until`
|
||||
FROM `sky_phone_crypto_profiles` WHERE `owner_identifier` = ? LIMIT 1
|
||||
]], { identifier })[1]
|
||||
@@ -385,6 +393,10 @@ local function market_dtos()
|
||||
price = decimal_string(price, Config.Crypto.PriceScale),
|
||||
changePercent = first > 0 and ((price - first) / first) * 100 or 0,
|
||||
enabled = row.status == "active",
|
||||
high24h = decimal_string(math.max(table.unpack(prices)), Config.Crypto.PriceScale),
|
||||
low24h = decimal_string(math.min(table.unpack(prices)), Config.Crypto.PriceScale),
|
||||
issuedSupply = decimal_string(config.IssuedSupply * Config.Crypto.AssetScale, Config.Crypto.AssetScale),
|
||||
treasuryAvailable = decimal_string(balance("treasury", market_id), Config.Crypto.AssetScale),
|
||||
sparkline = sparkline,
|
||||
}
|
||||
end
|
||||
@@ -446,9 +458,24 @@ local function bootstrap(profile)
|
||||
portfolio = portfolio + value
|
||||
end
|
||||
end
|
||||
local metrics = Bridge.Database.Query([[
|
||||
SELECT COUNT(*) AS `total_trades`, COALESCE(SUM(`amount`), 0) AS `total_volume`
|
||||
FROM `sky_phone_crypto_operations`
|
||||
WHERE `profile_id` = ? AND `type` IN ('buy','sell') AND `status` = 'completed'
|
||||
]], { profile.id })[1]
|
||||
return {
|
||||
authenticated = true,
|
||||
profile = { id = profile.id, handle = profile.handle, status = profile.status },
|
||||
profile = {
|
||||
id = profile.id,
|
||||
handle = profile.handle,
|
||||
status = profile.status,
|
||||
createdAt = (tonumber(profile.created_at) or 0) * 1000,
|
||||
hideBalances = tonumber(profile.hide_balances) == 1,
|
||||
priceAlerts = tonumber(profile.price_alerts) == 1,
|
||||
tradeConfirmations = tonumber(profile.trade_confirmations) == 1,
|
||||
totalTrades = tonumber(metrics and metrics.total_trades) or 0,
|
||||
totalVolume = decimal_string(metrics and metrics.total_volume, Config.Crypto.PriceScale),
|
||||
},
|
||||
cashBalance = decimal_string(cash, Config.Crypto.PriceScale),
|
||||
portfolioValue = decimal_string(portfolio, Config.Crypto.PriceScale),
|
||||
holdings = holdings,
|
||||
@@ -628,6 +655,49 @@ Bridge.Callbacks.Register("sky_phone:crypto:logout", function(source)
|
||||
return { success = true }
|
||||
end)
|
||||
|
||||
Bridge.Callbacks.Register("sky_phone:crypto:update-profile", function(source, data)
|
||||
if not SkyPhone.AllowOperation(source, "crypto:update-profile", 8, 60) then
|
||||
return { success = false, error = "rate_limited" }
|
||||
end
|
||||
local profile, error_response = authenticated_profile(source)
|
||||
if not profile then
|
||||
return error_response
|
||||
end
|
||||
data = type(data) == "table" and data or {}
|
||||
local handle = valid_handle(data.handle)
|
||||
if not handle or type(data.priceAlerts) ~= "boolean"
|
||||
or type(data.tradeConfirmations) ~= "boolean" or type(data.hideBalances) ~= "boolean"
|
||||
then
|
||||
return { success = false, error = "invalid_profile" }
|
||||
end
|
||||
if handle:lower() ~= profile.handle:lower() then
|
||||
if not verify_password(profile.id, data.password) then
|
||||
audit(profile.id, profile.owner_identifier, "profile_update_reauth_failed", "")
|
||||
return { success = false, error = "invalid_credentials" }
|
||||
end
|
||||
local duplicate = Bridge.Database.Query(
|
||||
"SELECT 1 FROM `sky_phone_crypto_profiles` WHERE `handle` = ? AND `id` <> ? LIMIT 1",
|
||||
{ handle, profile.id }
|
||||
)
|
||||
if duplicate[1] then
|
||||
return { success = false, error = "handle_taken" }
|
||||
end
|
||||
end
|
||||
Bridge.Database.Query([[
|
||||
UPDATE `sky_phone_crypto_profiles`
|
||||
SET `handle` = ?, `price_alerts` = ?, `trade_confirmations` = ?, `hide_balances` = ?
|
||||
WHERE `id` = ?
|
||||
]], {
|
||||
handle,
|
||||
data.priceAlerts and 1 or 0,
|
||||
data.tradeConfirmations and 1 or 0,
|
||||
data.hideBalances and 1 or 0,
|
||||
profile.id,
|
||||
})
|
||||
audit(profile.id, profile.owner_identifier, "profile_updated", handle)
|
||||
return { success = true, data = bootstrap(profile_by_owner(profile.owner_identifier)) }
|
||||
end)
|
||||
|
||||
Bridge.Callbacks.Register("sky_phone:crypto:quote", function(source, data)
|
||||
if not SkyPhone.AllowOperation(source, "crypto:quote", Config.Crypto.ActionsPerMinute, 60) then
|
||||
return { success = false, error = "rate_limited" }
|
||||
|
||||
@@ -1351,6 +1351,9 @@ CREATE TABLE IF NOT EXISTS `sky_phone_crypto_profiles` (
|
||||
`account_id` BIGINT UNSIGNED NULL,
|
||||
`handle` VARCHAR(20) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL,
|
||||
`password_hash` VARCHAR(255) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
`price_alerts` TINYINT(1) UNSIGNED NOT NULL DEFAULT 1,
|
||||
`trade_confirmations` TINYINT(1) UNSIGNED NOT NULL DEFAULT 1,
|
||||
`hide_balances` TINYINT(1) UNSIGNED NOT NULL DEFAULT 0,
|
||||
`status` ENUM('active','frozen','closed') NOT NULL DEFAULT 'active',
|
||||
`failed_logins` TINYINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`locked_until` DATETIME NULL,
|
||||
|
||||
Reference in New Issue
Block a user