diff --git a/package.json b/package.json index ad1d7e2..835e558 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "scripts": { "start": "react-scripts start", "dev": "react-scripts start", + "dev:mock": "REACT_APP_USE_MOCK_DATA=true react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "test:ci": "react-scripts test --watchAll=false --maxWorkers=50%", diff --git a/public/exchanges/gate.ico b/public/exchanges/gate.ico new file mode 100644 index 0000000..bfa050a Binary files /dev/null and b/public/exchanges/gate.ico differ diff --git a/public/exchanges/htx.ico b/public/exchanges/htx.ico new file mode 100644 index 0000000..7c05c74 Binary files /dev/null and b/public/exchanges/htx.ico differ diff --git a/public/exchanges/kucoin.svg b/public/exchanges/kucoin.svg new file mode 100644 index 0000000..e71f0f0 --- /dev/null +++ b/public/exchanges/kucoin.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/exchanges/lbank.png b/public/exchanges/lbank.png new file mode 100644 index 0000000..037078d Binary files /dev/null and b/public/exchanges/lbank.png differ diff --git a/src/data/exchanges.js b/src/data/exchanges.js index 63eb2f3..6929a7a 100644 --- a/src/data/exchanges.js +++ b/src/data/exchanges.js @@ -1,13 +1,18 @@ const SYS_EXCHANGES = [ { - name: 'Binance', - logo: 'https://coin-images.coingecko.com/markets/images/52/small/binance.jpg?1706864274', - href: 'https://www.binance.com/en/trade/SYS_USDT', + name: 'Bitget', + logo: 'https://coin-images.coingecko.com/markets/images/540/small/2023-07-25_21.47.43.jpg?1706864507', + href: 'https://www.bitget.com/spot/SYSUSDT', + }, + { + name: 'Gate', + logo: '/exchanges/gate.ico', + href: 'https://www.gate.com/trade/SYS_USDT', }, { - name: 'Pionex', - logo: 'https://coin-images.coingecko.com/markets/images/1026/small/pionex.png?1706865056', - href: 'https://www.pionex.com/en/trade/SYS_USDT', + name: 'KuCoin', + logo: '/exchanges/kucoin.svg', + href: 'https://www.kucoin.com/trade/SYS-USDT', }, { name: 'MEXC', @@ -15,19 +20,14 @@ const SYS_EXCHANGES = [ href: 'https://www.mexc.com/exchange/SYS_USDT', }, { - name: 'Bitget', - logo: 'https://coin-images.coingecko.com/markets/images/540/small/2023-07-25_21.47.43.jpg?1706864507', - href: 'https://www.bitget.com/spot/SYSUSDT', - }, - { - name: 'Phemex', - logo: 'https://coin-images.coingecko.com/markets/images/564/small/phemex-exchange-new-logo.png?1763018222', - href: 'https://phemex.com/spot/trade/SYSUSDT', + name: 'HTX', + logo: '/exchanges/htx.ico', + href: 'https://www.huobi.com/en-us/exchange/sys_usdt', }, { - name: 'XT.COM', - logo: 'https://coin-images.coingecko.com/markets/images/404/small/xt_logo_%E7%BB%BF.png?1761205934', - href: 'https://www.xt.com/en/trade/sys_usdt', + name: 'LBank', + logo: '/exchanges/lbank.png', + href: 'https://www.lbank.com/trade/sys_usdt', }, ]; diff --git a/src/data/mockApi.js b/src/data/mockApi.js new file mode 100644 index 0000000..423c0a2 --- /dev/null +++ b/src/data/mockApi.js @@ -0,0 +1,253 @@ +const nowSec = Math.floor(Date.now() / 1000); +const daySec = 24 * 60 * 60; + +const nextVotingDeadlineSec = nowSec + 4 * daySec + 2 * 60 * 60 + 9 * 60; +const nextSuperblockSec = nowSec + 7 * daySec + 2 * 60 * 60 + 13 * 60; +const nextVotingDeadlineIso = new Date(nextVotingDeadlineSec * 1000).toISOString(); +const nextSuperblockIso = new Date(nextSuperblockSec * 1000).toISOString(); + +function clone(value) { + return JSON.parse(JSON.stringify(value)); +} + +function formatUtcClock(epochSec) { + return new Intl.DateTimeFormat('en-US', { + hour: 'numeric', + minute: '2-digit', + hour12: true, + timeZone: 'UTC', + }).format(new Date(epochSec * 1000)); +} + +function makeHistory(days, startUsers, dailyStep) { + return Array.from({ length: days }, function buildEntry(_, index) { + const dayOffset = days - index - 1; + const epochMs = (nowSec - dayOffset * daySec) * 1000; + const cycle = Math.sin(index / 8) * 18; + const drift = (index % 11) - 5; + + return { + date: new Date(epochMs).toISOString(), + users: Math.round(startUsers + index * dailyStep + cycle + drift), + }; + }); +} + +function makeProposal({ + key, + name, + title, + absoluteYesCount, + yesCount, + noCount, + paymentAmount, + createdDaysAgo, + startDaysAgo, + endDaysFromNow, + url, +}) { + return { + AbsoluteYesCount: absoluteYesCount, + CreationTime: nowSec - createdDaysAgo * daySec, + Hash: key, + Key: key, + NoCount: noCount, + ObectType: 1, + YesCount: yesCount, + end_epoch: nowSec + endDaysFromNow * daySec, + fCachedDelete: false, + name, + payment_amount: paymentAmount, + start_epoch: nowSec - startDaysAgo * daySec, + title, + url, + }; +} + +export const MOCK_API_LATENCY_MS = 180; + +export const mockNetworkStats = { + stats: { + blockchain_stats: { + avg_block: '2.5 min', + connections: 2365400, + genesis: '2014-08-16T00:00:00Z', + protocol: '70931', + sub_version: '/SyscoinCore:4.4.2/', + version: '4.4.2', + }, + income_stats: { + sys: { + daily: '21.4 SYS', + monthly: '652 SYS', + yearly: '7,823 SYS', + }, + usd: { + daily: '$1.87', + monthly: '$56.83', + yearly: '$682', + }, + }, + income_stats_seniority_one_year: { + sys: { + daily: '23.6 SYS', + monthly: '719 SYS', + yearly: '8,626 SYS', + }, + usd: { + daily: '$2.06', + monthly: '$62.69', + yearly: '$752', + }, + }, + income_stats_seniority_two_year: { + sys: { + daily: '25.8 SYS', + monthly: '786 SYS', + yearly: '9,435 SYS', + }, + usd: { + daily: '$2.25', + monthly: '$68.53', + yearly: '$822', + }, + }, + mn_stats: { + coins_percent_locked: 31.77, + collateral_req: 100000, + current_supply: 750500000, + enabled: 2384, + masternode_price_usd: 8725, + payout_frequency: 'every 10.7 days', + pose_banned: 62, + roi: '7.82%', + roi_one: '8.61%', + roi_two: '9.48%', + total: 2446, + total_locked: 238400000, + }, + price_stats: { + circulating_supply: 749500000, + market_cap_usd: 65390000, + price_btc: 0.00000124, + price_change: 3.42, + price_usd: 0.08725, + volume_usd: 1986500, + }, + superblock_stats: { + budget: 76543, + next_superblock: `${formatUtcClock(nextSuperblockSec)} (UTC)`, + superblock_date: nextSuperblockIso, + superblock_next_epoch_sec: nextSuperblockSec, + voting_deadline: nextVotingDeadlineIso, + }, + }, + mapData: { + USA: { masternodes: 482 }, + DEU: { masternodes: 341 }, + NLD: { masternodes: 260 }, + SGP: { masternodes: 228 }, + CAN: { masternodes: 216 }, + FIN: { masternodes: 182 }, + FRA: { masternodes: 171 }, + GBR: { masternodes: 160 }, + SWE: { masternodes: 146 }, + LTU: { masternodes: 120 }, + POL: { masternodes: 78 }, + }, +}; + +export const mockNodeHistory = makeHistory(220, 2142, 1.05); + +export const mockGovernanceFeed = [ + makeProposal({ + key: 'a1'.repeat(32), + name: 'SMT', + title: 'Scaling the Ecosystem', + absoluteYesCount: 462, + yesCount: 478, + noCount: 16, + paymentAmount: 12000, + createdDaysAgo: 110, + startDaysAgo: 12, + endDaysFromNow: 48, + url: 'https://syscoin.org/news', + }), + makeProposal({ + key: 'b2'.repeat(32), + name: 'Foundation', + title: 'Budget Proposal', + absoluteYesCount: 388, + yesCount: 406, + noCount: 18, + paymentAmount: 18000, + createdDaysAgo: 96, + startDaysAgo: 10, + endDaysFromNow: 44, + url: 'https://syscoin.org', + }), + makeProposal({ + key: 'c3'.repeat(32), + name: 'Lunos', + title: 'R&D for Compliance-First Edge-Chain on zkSys', + absoluteYesCount: 341, + yesCount: 356, + noCount: 15, + paymentAmount: 22000, + createdDaysAgo: 138, + startDaysAgo: 9, + endDaysFromNow: 43, + url: 'https://syscoin.org', + }), + makeProposal({ + key: 'd4'.repeat(32), + name: 'Core Contributors', + title: 'Developer Tooling Sprint', + absoluteYesCount: 248, + yesCount: 259, + noCount: 11, + paymentAmount: 30000, + createdDaysAgo: 64, + startDaysAgo: 7, + endDaysFromNow: 37, + url: 'https://syscoin.org', + }), + makeProposal({ + key: 'e5'.repeat(32), + name: 'Syscoin Growth', + title: 'Exchange Liquidity Program', + absoluteYesCount: 205, + yesCount: 213, + noCount: 8, + paymentAmount: 15000, + createdDaysAgo: 52, + startDaysAgo: 6, + endDaysFromNow: 31, + url: 'https://syscoin.org', + }), + makeProposal({ + key: 'f6'.repeat(32), + name: 'Community DAO', + title: 'Regional Community Events', + absoluteYesCount: 129, + yesCount: 142, + noCount: 13, + paymentAmount: 8000, + createdDaysAgo: 40, + startDaysAgo: 4, + endDaysFromNow: 28, + url: 'https://syscoin.org', + }), +]; + +export function getMockNetworkStats() { + return clone(mockNetworkStats); +} + +export function getMockNodeHistory() { + return clone(mockNodeHistory); +} + +export function getMockGovernanceFeed() { + return clone(mockGovernanceFeed); +} diff --git a/src/lib/api.js b/src/lib/api.js index 093682f..fb9f602 100644 --- a/src/lib/api.js +++ b/src/lib/api.js @@ -1,4 +1,10 @@ import axios from 'axios'; +import { + getMockGovernanceFeed, + getMockNetworkStats, + getMockNodeHistory, + MOCK_API_LATENCY_MS, +} from '../data/mockApi'; // Base URL for the anonymous public sysnode-backend endpoints // (`/mnstats`, `/mncount`, `/govlist`). Kept in lockstep with the @@ -23,6 +29,10 @@ const DEFAULT_BASE = ? '' : process.env.REACT_APP_API_BASE || 'http://localhost:3001'); +const USE_MOCK_DATA = /^(1|true|yes)$/i.test( + process.env.REACT_APP_USE_MOCK_DATA || '' +); + const client = axios.create({ baseURL: DEFAULT_BASE, headers: { @@ -32,17 +42,37 @@ const client = axios.create({ timeout: 15000, }); +function respondWithMockData(factory) { + return new Promise(function resolveMock(resolve) { + globalThis.setTimeout(function sendMockData() { + resolve(factory()); + }, MOCK_API_LATENCY_MS); + }); +} + export async function fetchNetworkStats() { + if (USE_MOCK_DATA) { + return respondWithMockData(getMockNetworkStats); + } + const response = await client.get('/mnstats'); return response.data; } export async function fetchNodeHistory() { + if (USE_MOCK_DATA) { + return respondWithMockData(getMockNodeHistory); + } + const response = await client.get('/mncount'); return response.data; } export async function fetchGovernanceFeed() { + if (USE_MOCK_DATA) { + return respondWithMockData(getMockGovernanceFeed); + } + const response = await client.post('/govlist', []); return response.data; } diff --git a/src/lib/formatters.js b/src/lib/formatters.js index 3f9a82a..ed07374 100644 --- a/src/lib/formatters.js +++ b/src/lib/formatters.js @@ -1,4 +1,5 @@ const COUNTRY_NAMES = { + ARE: 'UAE', AUT: 'Austria', BGR: 'Bulgaria', BRA: 'Brazil', @@ -20,6 +21,7 @@ const COUNTRY_NAMES = { SGP: 'Singapore', SWE: 'Sweden', TUR: 'Turkey', + UAE: 'UAE', USA: 'United States', }; diff --git a/src/lib/formatters.test.js b/src/lib/formatters.test.js index 55c5b49..ae88df9 100644 --- a/src/lib/formatters.test.js +++ b/src/lib/formatters.test.js @@ -1,4 +1,9 @@ -import { formatDayMonth, formatShortDate, formatUtcTime } from './formatters'; +import { + formatDayMonth, + formatShortDate, + formatUtcTime, + getCountryName, +} from './formatters'; describe('date formatters', () => { const superblockDate = 'April 20th 2026, 2:15:21 pm'; @@ -12,3 +17,10 @@ describe('date formatters', () => { expect(formatUtcTime(superblockDate)).toBe('2:15 PM'); }); }); + +describe('country labels', () => { + test('shortens United Arab Emirates to UAE for common feed codes', () => { + expect(getCountryName('ARE')).toBe('UAE'); + expect(getCountryName('UAE')).toBe('UAE'); + }); +});