commit 8f6423172d39b54f33edd216ee5770ef5102240d Author: wireless_purple Date: Sun Jul 14 14:50:03 2024 +0000 initial commit diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6467f34 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +VITE_DB_PATH= # absolute path to the haveno_statistics/xmr_mainnet/db/ folder generate by the haveno-statsnode \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8ae61f8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.DS_Store +node_modules +/build +/.svelte-kit +/package +.env +.env.* +!.env.example +vite.config.js.timestamp-* +vite.config.ts.timestamp-* +src/lib/data \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/README.md b/README.md new file mode 100644 index 0000000..895e8d8 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# haveno-markets +## Requirements +* Haveno +* ./haveno-statsnode --dumpStatistics=true +* bun \ No newline at end of file diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..c7a339c --- /dev/null +++ b/biome.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.7.3/schema.json", + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "style": { + "useNodejsImportProtocol": "off", + "useConst": "off" + } + } + }, + "overrides": [ + { + "include": ["*.svelte"] + } + ] +} diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..8e9af19 Binary files /dev/null and b/bun.lockb differ diff --git a/package.json b/package.json new file mode 100644 index 0000000..3bbea06 --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "haveno-markets", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "fix": "biome check --write *", + "lint": "biome check *" + }, + "devDependencies": { + "@biomejs/biome": "^1.8.3", + "@sveltejs/kit": "^2.5.18", + "@sveltejs/vite-plugin-svelte": "^3.1.1", + "sass": "^1.77.8", + "svelte": "^4.2.18", + "svelte-adapter-bun": "^0.5.2", + "svelte-preprocess": "^5.1.4", + "vite": "^5.3.3" + }, + "type": "module", + "dependencies": { + "lightweight-charts": "^4.1.7", + "svelte-lightweight-charts": "^2.2.0" + } +} diff --git a/src/app.html b/src/app.html new file mode 100644 index 0000000..419db4b --- /dev/null +++ b/src/app.html @@ -0,0 +1,13 @@ + + + + + + Haveno Markets + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/src/lib/formatPrice.js b/src/lib/formatPrice.js new file mode 100644 index 0000000..ded7f5f --- /dev/null +++ b/src/lib/formatPrice.js @@ -0,0 +1,60 @@ +import { get, writable } from "svelte/store"; + +const crypto = writable([]); +const fiat = writable([]); + +const isMoneroQuote = (currency) => { + return !!get(crypto).find((e) => e.code === currency); +}; + +const getAsset = (currency) => { + return ( + get(crypto).find((e) => e.code === currency) || + get(fiat).find((e) => e.code === currency) + ); +}; + +const getSignificantDigits = (price) => { + const avg = + price.length > 0 ? price.reduce((a, b) => a + b) / price.length : price; + let i = -4; + for (; i < 20; i++) { + if (Math.floor(avg * 10 ** i) >= 1000) break; + } + if (i <= 1) i = 2; + return i; +}; + +const getPrice = (price, currency = "XMR", useQuote = true) => { + return isMoneroQuote(currency) && useQuote + ? 10 ** getAsset(currency).precision / price + : price / 10 ** getAsset(currency).precision; +}; + +const formatPrice = ( + price, + currency = "XMR", + showSign = false, + useQuote = true, +) => { + const calculatedPrice = getPrice(price, currency, useQuote); + return ( + (showSign + ? getAsset(isMoneroQuote(currency) ? "XMR" : currency).sign || "" + : "") + + calculatedPrice.toLocaleString(undefined, { + minimumFractionDigits: getSignificantDigits(calculatedPrice), + maximumFractionDigits: getSignificantDigits(calculatedPrice), + }) + ); +}; + +export { + formatPrice, + getPrice, + getSignificantDigits, + getAsset, + isMoneroQuote, + crypto, + fiat, +}; diff --git a/src/lib/server/context.js b/src/lib/server/context.js new file mode 100644 index 0000000..9f4f5f5 --- /dev/null +++ b/src/lib/server/context.js @@ -0,0 +1,88 @@ +import { watch } from "fs"; +import { writable } from "svelte/store"; + +const offers = writable([]); +const trades = writable([]); +const crypto = writable([]); +const fiat = writable([]); + +const formatTrades = (e) => { + return e.map((e) => { + const crypto = e.primaryMarketTradeVolume === e.tradeAmount; + + return { + currency: e.currency, + price: e.tradePrice, + xmrAmount: e.tradeAmount, + amount: crypto ? e.primaryMarketTradeAmount : e.primaryMarketTradeVolume, + date: e.tradeDate, + }; + }); +}; +const formatOffers = (e) => { + return Object.groupBy(e, ({ currencyCode }) => currencyCode); +}; +const formatCrypto = (e) => { + e[e.findIndex((e) => e.code === "XMR")].precision = 12; + e[e.findIndex((e) => e.code === "XMR")].sign = "ɱ"; + return e; +}; +const formatFiat = (e) => { + e[e.findIndex((e) => e.code === "USD")].sign = "$"; + e[e.findIndex((e) => e.code === "EUR")].sign = "€"; + e[e.findIndex((e) => e.code === "GBP")].sign = "£"; + e[e.findIndex((e) => e.code === "AUD")].sign = "$"; + e[e.findIndex((e) => e.code === "CAD")].sign = "$"; + e[e.findIndex((e) => e.code === "SEK")].sign = "kr"; + return e; +}; + +Bun.file(`${import.meta.env.VITE_DB_PATH}offers_statistics.json`) + .json() + .then((j) => { + offers.set(formatOffers(j)); + }); +Bun.file(`${import.meta.env.VITE_DB_PATH}trade_statistics.json`) + .json() + .then((j) => { + trades.set(formatTrades(j)); + }); +Bun.file(`${import.meta.env.VITE_DB_PATH}crypto_currency_list.json`) + .json() + .then((j) => { + crypto.set(formatCrypto(j)); + }); +Bun.file(`${import.meta.env.VITE_DB_PATH}/traditional_currency_list.json`) + .json() + .then((j) => { + fiat.set(formatFiat(j)); + }); + +const watcher = watch(import.meta.env.VITE_DB_PATH, async (_, filename) => { + const file = Bun.file(import.meta.env.VITE_DB_PATH + filename); + const contents = await file.json(); + switch (filename) { + case "offers_statistics.json": + offers.set(formatOffers(contents)); + break; + case "trade_statistics.json": + trades.set(formatTrades(contents)); + break; + case "crypto_currency_list.json": + crypto.set(formatCrypto(contents)); + break; + case "traditional_currency_list.json": + fiat.set(formatFiat(contents)); + break; + } +}); + +process.on("SIGINT", () => { + // close watcher when Ctrl-C is pressed + console.log("Closing watcher..."); + watcher.close(); + + process.exit(0); +}); + +export { offers, trades, crypto, fiat }; diff --git a/src/routes/+layout.js b/src/routes/+layout.js new file mode 100644 index 0000000..51b6cc2 --- /dev/null +++ b/src/routes/+layout.js @@ -0,0 +1,6 @@ +import { crypto, fiat } from "$lib/formatPrice"; + +export function load({ data }) { + crypto.set(data.crypto); + fiat.set(data.fiat); +} diff --git a/src/routes/+layout.server.js b/src/routes/+layout.server.js new file mode 100644 index 0000000..4b56f77 --- /dev/null +++ b/src/routes/+layout.server.js @@ -0,0 +1,6 @@ +import { crypto, fiat } from "$lib/server/context"; +import { get } from "svelte/store"; + +export function load() { + return { crypto: get(crypto), fiat: get(fiat) }; +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte new file mode 100644 index 0000000..04feee7 --- /dev/null +++ b/src/routes/+layout.svelte @@ -0,0 +1,121 @@ +
+ +
+ +
+ +
+ \ No newline at end of file diff --git a/src/routes/+page.server.js b/src/routes/+page.server.js new file mode 100644 index 0000000..84f7fa0 --- /dev/null +++ b/src/routes/+page.server.js @@ -0,0 +1,6 @@ +import { offers, trades } from "$lib/server/context"; +import { get } from "svelte/store"; + +export function load() { + return { trades: get(trades), offers: get(offers) }; +} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte new file mode 100644 index 0000000..0be8b67 --- /dev/null +++ b/src/routes/+page.svelte @@ -0,0 +1,202 @@ + + + + Haveno Markets + + +
+
+

XMR/USD

+ {formatPrice(grouped["USD"][0].price, "USD", true)} +
+
+

Liquidity

+ {formatPrice(liquidity, "XMR", true, false)} +
+
+ +
+
+

Price XMR/

+ + + + + +
+
+

+ Volume

+ + + + + + + + + +
+
+
+
+

Markets

+ + + + + + + {#each Object.values(Object.groupBy(data.trades, ({currency}) => currency)).toSorted((a,b) => b.length - a.length || (b[0].currency < a[0].currency ? 1 : -1)).slice(0, 16) as market} + + + + + + {/each} +
CurrencyPriceTrades
{getAsset(market[0].currency).name} ({market[0].currency}){formatPrice(market[0].price, market[0].currency, true, false)}{market.length}
+

View more »

+
+
+

Trades

+ + + + + + + {#each data.trades.slice(0, 16) as trade} + + + + + + {/each} +
DateAmount (XMR)Amount
{new Date(trade.date).toISOString().replace("T", " ").replace(/\.\d*Z/, "")}{formatPrice(trade.xmrAmount, "XMR", false, false)}{formatPrice(trade.amount, trade.currency, false, false)} {trade.currency}
+

View more »

+
+
\ No newline at end of file diff --git a/src/routes/market/[market]/+page.server.js b/src/routes/market/[market]/+page.server.js new file mode 100644 index 0000000..5cc064a --- /dev/null +++ b/src/routes/market/[market]/+page.server.js @@ -0,0 +1,16 @@ +import { offers, trades } from "$lib/server/context"; +import { get } from "svelte/store"; + +export async function load({ params }) { + let groupedOffers = { BUY: [], SELL: [] }; + if (get(offers)[params.market]?.length > 0) { + groupedOffers = Object.groupBy( + get(offers)[params.market], + ({ direction }) => direction, + ); + } + return { + trades: get(trades).filter(({ currency }) => currency === params.market), + offers: groupedOffers, + }; +} diff --git a/src/routes/market/[market]/+page.svelte b/src/routes/market/[market]/+page.svelte new file mode 100644 index 0000000..1640179 --- /dev/null +++ b/src/routes/market/[market]/+page.svelte @@ -0,0 +1,138 @@ + + + {marketPair} - Haveno Markets + +
+
+

{marketPair}

+ {formatPrice(data.trades?.[0]?.price, market, true, false)} + + + + +
+
+
+
+

Buy Offers

+ + + + + + + {#each data.offers["BUY"]?.toSorted((a,b) => a.price < b.price ? 1 : -1)||[] as offer} + + + + + + {/each} +
PriceAmount (XMR)Amount ({market})
{formatPrice(offer.price, market, false, false)}{formatPrice(offer.amount, "XMR", false, false)}{formatPrice(offer.primaryMarketAmount, market, false, false)}
+
+
+

Sell Offers

+ + + + + + + {#each data.offers["SELL"]?.toSorted((a,b) => a.price > b.price ? 1 : -1)||[] as offer} + + + + + + {/each} +
PriceAmount (XMR)Amount ({market})
{formatPrice(offer.price, market, false, false)}{formatPrice(offer.amount, "XMR", false, false)}{formatPrice(offer.primaryMarketAmount, market, false, false)}
+
+
+
+
+

Last Trades

+ + + + + + + + {#each data.trades as trade} + + + + + + + {/each} +
DatePriceAmount (XMR)Amount ({market})
{new Date(trade.date).toISOString().replace("T", " ").replace(/\.\d*Z/, "")}{formatPrice(trade.price, trade.currency, false, false)}{formatPrice(trade.xmrAmount, "XMR", false, false)}{formatPrice(trade.amount, trade.currency, false, false)}
+
+
\ No newline at end of file diff --git a/src/routes/markets/+page.svelte b/src/routes/markets/+page.svelte new file mode 100644 index 0000000..b9c4d8d --- /dev/null +++ b/src/routes/markets/+page.svelte @@ -0,0 +1 @@ +Under construction \ No newline at end of file diff --git a/src/routes/trades/+page.svelte b/src/routes/trades/+page.svelte new file mode 100644 index 0000000..b9c4d8d --- /dev/null +++ b/src/routes/trades/+page.svelte @@ -0,0 +1 @@ +Under construction \ No newline at end of file diff --git a/static/haveno_logo_icon.png b/static/haveno_logo_icon.png new file mode 100644 index 0000000..c031b77 Binary files /dev/null and b/static/haveno_logo_icon.png differ diff --git a/static/monero-symbol-on-white-1280.png b/static/monero-symbol-on-white-1280.png new file mode 100644 index 0000000..65360cb Binary files /dev/null and b/static/monero-symbol-on-white-1280.png differ diff --git a/svelte.config.js b/svelte.config.js new file mode 100644 index 0000000..f6c3133 --- /dev/null +++ b/svelte.config.js @@ -0,0 +1,12 @@ +import adapter from "svelte-adapter-bun"; +import preprocess from "svelte-preprocess"; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + kit: { + adapter: adapter(), + }, + preprocess: [preprocess()], +}; + +export default config; diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..6b9eb5d --- /dev/null +++ b/vite.config.js @@ -0,0 +1,6 @@ +import { sveltekit } from "@sveltejs/kit/vite"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [sveltekit()], +});