Merge pull request #1495 from RoboSats/use-nostr-in-book-page

Use nostr in book page
This commit is contained in:
KoalaSat 2024-10-18 20:31:40 +00:00 committed by GitHub
commit 7bf1d5110e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
50 changed files with 10631 additions and 290 deletions

View File

@ -31,8 +31,10 @@
"i18next-http-backend": "^2.5.0",
"install": "^0.13.0",
"js-sha256": "^0.11.0",
"latlon-geohash": "^2.0.0",
"leaflet": "^1.9.4",
"light-bolt11-decoder": "^3.1.1",
"nostr-tools": "^2.7.2",
"npm": "^10.8.1",
"openpgp": "^5.11.0",
"react": "^18.2.0",
@ -60,6 +62,7 @@
"@babel/preset-typescript": "^7.22.5",
"@babel/runtime": "^7.22.6",
"@types/jest": "^29.5.3",
"@types/latlon-geohash": "^2.0.3",
"@types/leaflet": "^1.9.7",
"@types/react": "^18.2.21",
"@types/react-dom": "^18.2.7",
@ -3931,6 +3934,51 @@
"react": ">= 16.14.0 < 19.0.0"
}
},
"node_modules/@noble/ciphers": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.5.3.tgz",
"integrity": "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==",
"license": "MIT",
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/curves": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz",
"integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "1.3.2"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/curves/node_modules/@noble/hashes": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz",
"integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==",
"license": "MIT",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/hashes": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz",
"integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==",
"license": "MIT",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -4076,6 +4124,45 @@
}
]
},
"node_modules/@scure/bip32": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.1.tgz",
"integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==",
"license": "MIT",
"dependencies": {
"@noble/curves": "~1.1.0",
"@noble/hashes": "~1.3.1",
"@scure/base": "~1.1.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@scure/bip32/node_modules/@noble/curves": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz",
"integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "1.3.1"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@scure/bip39": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz",
"integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "~1.3.0",
"@scure/base": "~1.1.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@sinclair/typebox": {
"version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
@ -4513,6 +4600,13 @@
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
"dev": true
},
"node_modules/@types/latlon-geohash": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/latlon-geohash/-/latlon-geohash-2.0.3.tgz",
"integrity": "sha512-VP6CWnHN4GT48Ra83JQl31SN/qSRp0OI2lb3TPPH+PhZpVzSxZbtjMNbUQQNZ2SdIOHMctOCv+q+EIyJQ5EaKw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/leaflet": {
"version": "1.9.7",
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.7.tgz",
@ -11726,6 +11820,12 @@
"node": ">=6"
}
},
"node_modules/latlon-geohash": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/latlon-geohash/-/latlon-geohash-2.0.0.tgz",
"integrity": "sha512-OKBswTwrvTdtenV+9C9euBmvgGuqyjJNAzpQCarRz1m8/pYD2nz9fKkXmLs2S3jeXaLi3Ry76twQplKKUlgS/g==",
"license": "MIT"
},
"node_modules/lazystream": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
@ -12482,6 +12582,38 @@
"node": ">=0.10.0"
}
},
"node_modules/nostr-tools": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.7.2.tgz",
"integrity": "sha512-Bq3Ug0SZFtgtL1+0wCnAe8AJtI7yx/00/a2nUug9SkhfOwlKS92Tef12iCK9FdwXw+oFZWMtRnSwcLayQso+xA==",
"license": "Unlicense",
"dependencies": {
"@noble/ciphers": "^0.5.1",
"@noble/curves": "1.2.0",
"@noble/hashes": "1.3.1",
"@scure/base": "1.1.1",
"@scure/bip32": "1.3.1",
"@scure/bip39": "1.2.1"
},
"optionalDependencies": {
"nostr-wasm": "v0.1.0"
},
"peerDependencies": {
"typescript": ">=5.0.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/nostr-wasm": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/nostr-wasm/-/nostr-wasm-0.1.0.tgz",
"integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==",
"license": "MIT",
"optional": true
},
"node_modules/npm": {
"version": "10.8.1",
"resolved": "https://registry.npmjs.org/npm/-/npm-10.8.1.tgz",
@ -17800,7 +17932,7 @@
"version": "5.5.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
"integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
"dev": true,
"devOptional": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"

View File

@ -22,6 +22,7 @@
"@babel/preset-typescript": "^7.22.5",
"@babel/runtime": "^7.22.6",
"@types/jest": "^29.5.3",
"@types/latlon-geohash": "^2.0.3",
"@types/leaflet": "^1.9.7",
"@types/react": "^18.2.21",
"@types/react-dom": "^18.2.7",
@ -71,8 +72,10 @@
"i18next-http-backend": "^2.5.0",
"install": "^0.13.0",
"js-sha256": "^0.11.0",
"latlon-geohash": "^2.0.0",
"leaflet": "^1.9.4",
"light-bolt11-decoder": "^3.1.1",
"nostr-tools": "^2.7.2",
"npm": "^10.8.1",
"openpgp": "^5.11.0",
"react": "^18.2.0",

View File

@ -1,4 +1,4 @@
import React, { useContext, useMemo, useState } from 'react';
import React, { useContext, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { Grid, Paper, Collapse, Typography } from '@mui/material';
@ -13,7 +13,7 @@ import { FederationContext, type UseFederationStoreType } from '../../contexts/F
import { GarageContext, type UseGarageStoreType } from '../../contexts/GarageContext';
const MakerPage = (): JSX.Element => {
const { fav, windowSize, navbarHeight } = useContext<UseAppStoreType>(AppContext);
const { fav, windowSize, navbarHeight, page } = useContext<UseAppStoreType>(AppContext);
const { federation } = useContext<UseFederationStoreType>(FederationContext);
const { garage, maker } = useContext<UseGarageStoreType>(GarageContext);
const { t } = useTranslation();
@ -25,7 +25,7 @@ const MakerPage = (): JSX.Element => {
const matches = useMemo(() => {
return filterOrders({
orders: federation.book,
federation,
baseFilter: {
currency: fav.currency === 0 ? 1 : fav.currency,
type: fav.type,

View File

@ -22,6 +22,7 @@ import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
import { genBase62Token } from '../../utils';
import { LoadingButton } from '@mui/lab';
import { GarageContext, type UseGarageStoreType } from '../../contexts/GarageContext';
import { FederationContext, UseFederationStoreType } from '../../contexts/FederationContext';
interface RobotProfileProps {
robot: Robot;
@ -45,6 +46,7 @@ const RobotProfile = ({
}: RobotProfileProps): JSX.Element => {
const { windowSize, client } = useContext<UseAppStoreType>(AppContext);
const { garage, slotUpdatedAt } = useContext<UseGarageStoreType>(GarageContext);
const { federation } = useContext<UseFederationStoreType>(FederationContext);
const { t } = useTranslation();
const theme = useTheme();
@ -75,10 +77,6 @@ const RobotProfile = ({
const slot = garage.getSlot();
const robot = slot?.getRobot();
const loadingCoordinators = Object.values(slot?.robots ?? {}).filter(
(robot) => robot.loading,
).length;
return (
<Grid container direction='column' alignItems='center' spacing={1} padding={1} paddingTop={2}>
<Grid
@ -154,7 +152,7 @@ const RobotProfile = ({
)}
</Grid>
{loadingCoordinators > 0 && !slot?.activeOrder?.id ? (
{federation.loading && !slot?.activeOrder?.id ? (
<Grid>
<b>{t('Looking for orders!')}</b>
<LinearProgress />
@ -208,7 +206,7 @@ const RobotProfile = ({
</Grid>
) : null}
{!slot?.activeOrder && !slot?.lastOrder && loadingCoordinators === 0 ? (
{!slot?.activeOrder && !slot?.lastOrder && !federation.loading ? (
<Grid item>{t('No existing orders found')}</Grid>
) : null}

View File

@ -26,7 +26,7 @@ import { GarageContext, type UseGarageStoreType } from '../../contexts/GarageCon
const RobotPage = (): JSX.Element => {
const { torStatus, windowSize, settings, page, client } = useContext<UseAppStoreType>(AppContext);
const { garage } = useContext<UseGarageStoreType>(GarageContext);
const { federation, sortedCoordinators } = useContext<UseFederationStoreType>(FederationContext);
const { federation } = useContext<UseFederationStoreType>(FederationContext);
const { t } = useTranslation();
const params = useParams();
const urlToken = settings.selfhostedClient ? params.token : null;
@ -64,7 +64,7 @@ const RobotPage = (): JSX.Element => {
setInputToken(token);
genKey(token)
.then((key) => {
garage.createRobot(token, sortedCoordinators, {
garage.createRobot(token, Object.keys(federation.coordinators), {
token,
pubKey: key.publicKeyArmored,
encPrivKey: key.encryptedPrivateKeyArmored,

View File

@ -1,5 +1,5 @@
import React, { useContext, useState } from 'react';
import { Button, Grid, List, ListItem, Paper, TextField, Typography } from '@mui/material';
import React, { useContext, useEffect, useState } from 'react';
import { Box, Button, Grid, List, ListItem, Paper, TextField, Typography } from '@mui/material';
import SettingsForm from '../../components/SettingsForm';
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
import FederationTable from '../../components/FederationTable';
@ -8,7 +8,7 @@ import { FederationContext, type UseFederationStoreType } from '../../contexts/F
import { GarageContext, type UseGarageStoreType } from '../../contexts/GarageContext';
const SettingsPage = (): JSX.Element => {
const { windowSize, navbarHeight } = useContext<UseAppStoreType>(AppContext);
const { windowSize, navbarHeight, page } = useContext<UseAppStoreType>(AppContext);
const { federation, addNewCoordinator } = useContext<UseFederationStoreType>(FederationContext);
const { garage } = useContext<UseGarageStoreType>(GarageContext);
const maxHeight = (windowSize.height - navbarHeight) * 0.85 - 3;
@ -49,52 +49,60 @@ const SettingsPage = (): JSX.Element => {
}}
>
<Grid container>
<Grid item>
<Grid item xs={12}>
<SettingsForm />
</Grid>
<Grid item>
<Grid item xs={12}>
<FederationTable maxHeight={18} />
</Grid>
<Grid item>
<Grid item xs={12}>
<Typography align='center' component='h2' variant='subtitle2' color='secondary'>
{error}
</Typography>
</Grid>
<List>
<ListItem>
<TextField
id='outlined-basic'
label={t('Alias')}
variant='outlined'
size='small'
value={newAlias}
onChange={(e) => {
setNewAlias(e.target.value);
}}
/>
<TextField
id='outlined-basic'
label={t('URL')}
variant='outlined'
size='small'
value={newUrl}
onChange={(e) => {
setNewUrl(e.target.value);
}}
/>
<Button
sx={{ maxHeight: 38 }}
disabled={false}
onClick={addCoordinator}
variant='contained'
color='primary'
size='small'
type='submit'
>
{t('Add')}
</Button>
</ListItem>
</List>
<Grid item xs={12}>
<List>
<ListItem>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<TextField
id='outlined-basic'
label={t('Alias')}
variant='outlined'
size='small'
value={newAlias}
onChange={(e) => {
setNewAlias(e.target.value);
}}
/>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<TextField
id='outlined-basic'
label={t('URL')}
variant='outlined'
size='small'
value={newUrl}
onChange={(e) => {
setNewUrl(e.target.value);
}}
/>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Button
sx={{ maxHeight: 38 }}
disabled={false}
onClick={addCoordinator}
variant='contained'
color='primary'
size='small'
type='submit'
>
{t('Add')}
</Button>
</Box>
</ListItem>
</List>
</Grid>
</Grid>
</Paper>
);

View File

@ -350,8 +350,9 @@ const BookControl = ({
<FlagWithProps code='ANY' />
</div>
</MenuItem>
{Object.values(federation.coordinators).map((coordinator) =>
coordinator.enabled ? (
{Object.values(federation.coordinators)
.filter((coord) => coord.enabled)
.map((coordinator) => (
<MenuItem
key={coordinator.shortAlias}
value={coordinator.shortAlias}
@ -367,10 +368,7 @@ const BookControl = ({
/>
</div>
</MenuItem>
) : (
<></>
),
)}
))}
</Select>
</Grid>
</>

View File

@ -92,7 +92,7 @@ const BookTable = ({
const { t } = useTranslation();
const theme = useTheme();
const orders = orderList ?? federation.book;
const orders = orderList ?? Object.values(federation.book);
const [paginationModel, setPaginationModel] = useState<GridPaginationModel>({
pageSize: 0,
@ -425,6 +425,11 @@ const BookTable = ({
width: width * fontSize,
renderCell: (params: any) => {
const currencyCode = String(currencyDict[params.row.currency.toString()]);
const coordinator = federation.getCoordinator(params.row.coordinatorShortAlias);
const premium = parseFloat(params.row.premium);
const price =
(coordinator.limits[params.row.currency.toString()]?.price ?? 1) * (1 + premium / 100);
return (
<div
style={{ cursor: 'pointer' }}
@ -432,7 +437,7 @@ const BookTable = ({
onOrderClicked(params.row.id, params.row.coordinatorShortAlias);
}}
>
{`${pn(params.row.price)} ${currencyCode}/BTC`}
{`${pn(Math.round(price))} ${currencyCode}/BTC`}
</div>
);
},
@ -575,6 +580,15 @@ const BookTable = ({
type: 'number',
width: width * fontSize,
renderCell: (params: any) => {
const coordinator = federation.getCoordinator(params.row.coordinatorShortAlias);
const amount = Boolean(params.row.has_range)
? parseFloat(params.row.max_amount)
: parseFloat(params.row.amount);
const premium = parseFloat(params.row.premium);
const price =
(coordinator.limits[params.row.currency.toString()]?.price ?? 1) * (1 + premium / 100);
const satoshisNow = (100000000 * amount) / price;
return (
<div
style={{ cursor: 'pointer' }}
@ -582,9 +596,9 @@ const BookTable = ({
onOrderClicked(params.row.id, params.row.coordinatorShortAlias);
}}
>
{params.row.satoshis_now > 1000000
? `${pn(Math.round(params.row.satoshis_now / 10000) / 100)} M`
: `${pn(Math.round(params.row.satoshis_now / 1000))} K`}
{satoshisNow > 1000000
? `${pn(Math.round(satoshisNow / 10000) / 100)} M`
: `${pn(Math.round(satoshisNow / 1000))} K`}
</div>
);
},
@ -813,7 +827,7 @@ const BookTable = ({
<Grid item xs={6}>
<IconButton
onClick={() => {
void federation.updateBook();
void federation.loadBook();
}}
>
<Refresh />
@ -881,17 +895,13 @@ const BookTable = ({
const filteredOrders = useMemo(() => {
return showControls
? filterOrders({
orders,
federation,
baseFilter: fav,
paymentMethods,
})
: orders;
}, [showControls, orders, fav, paymentMethods]);
const loadingPercentage =
((federation.exchange.enabledCoordinators - federation.exchange.loadingCoordinators) /
federation.exchange.enabledCoordinators) *
100;
if (!fullscreen) {
return (
<Paper
@ -924,8 +934,8 @@ const BookTable = ({
setPaymentMethods,
},
loadingOverlay: {
variant: loadingPercentage === 0 ? 'indeterminate' : 'determinate',
value: loadingPercentage,
variant: 'indeterminate',
value: federation.loading ? 0 : 100,
},
}}
paginationModel={paginationModel}

View File

@ -66,15 +66,24 @@ const DepthChart: React.FC<DepthChartProps> = ({
}, [fav.currency]);
useEffect(() => {
if (federation.book.length > 0) {
const enriched = federation.book.map((order) => {
// We need to transform all currencies to the same base (ex. USD), we don't have the exchange rate
// for EUR -> USD, but we know the rate of both to BTC, so we get advantage of it and apply a
// simple rule of three
if (order.coordinatorShortAlias != null) {
if (Object.values(federation.book).length > 0) {
const enriched = Object.values(federation.book).map((order) => {
if (order.coordinatorShortAlias != null && order.currency) {
const limits = federation.getCoordinator(order.coordinatorShortAlias).limits;
const price = limits[currencyCode] ? limits[currencyCode].price : 0;
order.base_amount = (order.price * price) / price;
const originalPrice =
(limits[order.currency].price ?? 0) * (1 + parseFloat(order.premium) / 100);
const currencyPrice =
(limits[currencyCode].price ?? 0) * (1 + parseFloat(order.premium) / 100);
const originalAmount =
order.has_range && order.max_amount
? parseFloat(order.max_amount)
: parseFloat(order.amount);
const currencyAmount = (currencyPrice * originalAmount) / originalPrice;
order.base_price = currencyPrice;
order.satoshis_now = (100000000 * currencyAmount) / currencyPrice;
}
return order;
});
@ -89,8 +98,8 @@ const DepthChart: React.FC<DepthChartProps> = ({
}, [enrichedOrders, xRange]);
useEffect(() => {
if (xType === 'base_amount') {
const prices: number[] = enrichedOrders.map((order) => order?.base_amount ?? 0);
if (xType === 'base_price') {
const prices: number[] = enrichedOrders.map((order) => order?.base_price ?? 0);
const medianValue = ~~matchMedian(prices);
const maxValue = prices.sort((a, b) => b - a).slice(0, 1)[0] ?? 1500;
@ -114,9 +123,9 @@ const DepthChart: React.FC<DepthChartProps> = ({
const generateSeries: () => void = () => {
const sortedOrders: PublicOrder[] =
xType === 'base_amount'
xType === 'base_price'
? enrichedOrders.sort(
(order1, order2) => (order1?.base_amount ?? 0) - (order2?.base_amount ?? 0),
(order1, order2) => (order1?.base_price ?? 0) - (order2?.base_price ?? 0),
)
: enrichedOrders.sort((order1, order2) => order1.premium - order2.premium);
@ -152,16 +161,17 @@ const DepthChart: React.FC<DepthChartProps> = ({
let serie: Datum[] = [];
orders.forEach((order) => {
const lastSumOrders = sumOrders;
sumOrders += (order.satoshis_now ?? 0) / 100000000;
const datum: Datum[] = [
{
// Vertical Line
x: xType === 'base_amount' ? order.base_amount : order.premium,
x: xType === 'base_price' ? order.base_price : order.premium,
y: lastSumOrders,
},
{
// PublicOrder Point
x: xType === 'base_amount' ? order.base_amount : order.premium,
x: xType === 'base_price' ? order.base_price : order.premium,
y: sumOrders,
order,
},
@ -226,7 +236,7 @@ const DepthChart: React.FC<DepthChartProps> = ({
};
const formatAxisX = (value: number): string => {
if (xType === 'base_amount') {
if (xType === 'base_price') {
return value.toString();
}
return `${value}%`;
@ -285,7 +295,7 @@ const DepthChart: React.FC<DepthChartProps> = ({
{t('Premium')}
</div>
</MenuItem>
<MenuItem value={'base_amount'}>
<MenuItem value={'base_price'}>
<div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap' }}>
{t('Price')}
</div>
@ -306,7 +316,7 @@ const DepthChart: React.FC<DepthChartProps> = ({
</Grid>
<Grid item>
<Box justifyContent='center'>
{xType === 'base_amount'
{xType === 'base_price'
? `${center} ${String(currencyDict[currencyCode])}`
: `${String(center.toPrecision(3))}%`}
</Box>
@ -339,14 +349,14 @@ const DepthChart: React.FC<DepthChartProps> = ({
axisBottom={{
tickSize: 5,
tickRotation:
xType === 'base_amount' ? (width < 40 ? 45 : 0) : width < 25 ? 45 : 0,
xType === 'base_price' ? (width < 40 ? 45 : 0) : width < 25 ? 45 : 0,
format: formatAxisX,
}}
margin={{
left: 4.64 * em,
right: 0.714 * em,
bottom:
xType === 'base_amount'
xType === 'base_price'
? width < 40
? 2.7 * em
: 1.78 * em

View File

@ -84,7 +84,7 @@ const MapChart: React.FC<MapChartProps> = ({
</DialogActions>
</Dialog>
<Paper variant='outlined' style={{ width: '100%', height: '100%', justifyContent: 'center' }}>
{federation.book.length < 1 ? (
{Object.values(federation.book).length < 1 ? (
<div
style={{
display: 'flex',
@ -130,7 +130,11 @@ const MapChart: React.FC<MapChartProps> = ({
</Tooltip>
</Grid>
<div style={{ height: `${height - 3.1}em` }}>
<Map useTiles={useTiles} orders={federation.book} onOrderClicked={onOrderClicked} />
<Map
useTiles={useTiles}
orders={Object.values(federation.book)}
onOrderClicked={onOrderClicked}
/>
</div>
</>
)}

View File

@ -1,4 +1,4 @@
import React, { useContext, useState } from 'react';
import React, { useContext, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
@ -68,8 +68,9 @@ import {
} from '../Icons';
import { AppContext } from '../../contexts/AppContext';
import { systemClient } from '../../services/System';
import { type Badges } from '../../models/Coordinator.model';
import Coordinator, { type Badges } from '../../models/Coordinator.model';
import { type UseFederationStoreType, FederationContext } from '../../contexts/FederationContext';
import { width } from '@mui/system';
interface Props {
open: boolean;
@ -348,18 +349,28 @@ const CoordinatorDialog = ({ open = false, onClose, shortAlias }: Props): JSX.El
const { t } = useTranslation();
const { clientVersion, page, settings, origin } = useContext(AppContext);
const { federation } = useContext<UseFederationStoreType>(FederationContext);
const coordinator = federation.getCoordinator(shortAlias ?? '');
const [expanded, setExpanded] = useState<'summary' | 'stats' | 'policies' | undefined>(undefined);
const [coordinator, setCoordinator] = useState<Coordinator>(
federation.getCoordinator(shortAlias ?? ''),
);
const listItemProps = { sx: { maxHeight: '3em', width: '100%' } };
const coordinatorVersion = `v${coordinator?.info?.version?.major ?? '?'}.${
coordinator?.info?.version?.minor ?? '?'
}.${coordinator?.info?.version?.patch ?? '?'}`;
useEffect(() => {
setCoordinator(federation.getCoordinator(shortAlias ?? ''));
}, [shortAlias]);
useEffect(() => {
if (open) federation.getCoordinator(shortAlias ?? '')?.loadInfo();
}, [open]);
return (
<Dialog open={open} onClose={onClose}>
<DialogContent>
<DialogContent style={{ width: 600 }}>
<Typography align='center' component='h5' variant='h5'>
{String(coordinator?.longAlias)}
</Typography>
@ -483,7 +494,7 @@ const CoordinatorDialog = ({ open = false, onClose, shortAlias }: Props): JSX.El
</ListItemButton>
</List>
{coordinator?.loadingInfo ? (
{!coordinator || coordinator?.loadingInfo ? (
<Box style={{ display: 'flex', justifyContent: 'center' }}>
<CircularProgress />
</Box>

View File

@ -35,18 +35,11 @@ interface Props {
const ExchangeDialog = ({ open = false, onClose }: Props): JSX.Element => {
const { t } = useTranslation();
const { federation, federationUpdatedAt } = useContext(FederationContext);
const [loadingProgress, setLoadingProgress] = useState<number>(0);
useEffect(() => {
const loadedCoordinators =
federation.exchange.enabledCoordinators - federation.exchange.loadingCoordinators;
setLoadingProgress((loadedCoordinators / federation.exchange.enabledCoordinators) * 100);
}, [open, federationUpdatedAt]);
return (
<Dialog open={open} onClose={onClose}>
<div style={loadingProgress < 100 ? {} : { display: 'none' }}>
<LinearProgress variant='determinate' value={loadingProgress} />
<div style={federation.loading ? {} : { display: 'none' }}>
<LinearProgress variant='indeterminate' />
</div>
<DialogContent>
<Typography component='h5' variant='h5'>

View File

@ -33,15 +33,13 @@ const ProfileDialog = ({ open = false, onClose }: Props): JSX.Element => {
const slot = garage.getSlot();
const [loading, setLoading] = useState<boolean>(true);
const [loadingCoordinators, setLoadingCoordinators] = useState<number>(
const [loadingRobots, setLoadingRobots] = useState<number>(
Object.values(slot?.robots ?? {}).length,
);
useEffect(() => {
setLoading(!garage.getSlot()?.hashId);
setLoadingCoordinators(
Object.values(slot?.robots ?? {}).filter((robot) => robot.loading).length,
);
setLoadingRobots(Object.values(slot?.robots ?? {}).filter((robot) => robot.loading).length);
}, [slotUpdatedAt]);
return (
@ -85,7 +83,7 @@ const ProfileDialog = ({ open = false, onClose }: Props): JSX.Element => {
)}
</Typography>
{loadingCoordinators > 0 ? (
{loadingRobots > 0 ? (
<>
<b>{t('Looking for your robot!')}</b>
<LinearProgress />

View File

@ -21,8 +21,7 @@ const FederationTable = ({
fillContainer = false,
}: FederationTableProps): JSX.Element => {
const { t } = useTranslation();
const { federation, sortedCoordinators, federationUpdatedAt } =
useContext<UseFederationStoreType>(FederationContext);
const { federation, federationUpdatedAt } = useContext<UseFederationStoreType>(FederationContext);
const { setOpen, settings } = useContext<UseAppStoreType>(AppContext);
const theme = useTheme();
const [pageSize, setPageSize] = useState<number>(0);
@ -128,9 +127,9 @@ const FederationTable = ({
onClickCoordinator(params.row.shortAlias);
}}
>
{Boolean(params.row.loadingInfo) && Boolean(params.row.enabled) ? (
{Boolean(params.row.loadingLimits) && Boolean(params.row.enabled) ? (
<CircularProgress thickness={0.35 * fontSize} size={1.5 * fontSize} />
) : params.row.info !== undefined ? (
) : params.row.limits !== undefined ? (
<Link color='success' />
) : (
<LinkOff color='error' />
@ -214,14 +213,6 @@ const FederationTable = ({
}
};
const reorderedCoordinators = useMemo(() => {
return sortedCoordinators.reduce((coordinators, key) => {
coordinators[key] = federation.coordinators[key];
return coordinators;
}, {});
}, [settings.network, federationUpdatedAt]);
return (
<Box
sx={
@ -235,7 +226,7 @@ const FederationTable = ({
localeText={localeText}
rowHeight={3.714 * theme.typography.fontSize}
headerHeight={3.25 * theme.typography.fontSize}
rows={Object.values(reorderedCoordinators)}
rows={Object.values(federation.coordinators)}
getRowId={(params: Coordinator) => params.shortAlias}
columns={columns}
checkboxSelection={false}

View File

@ -502,7 +502,6 @@ const MakerForm = ({
(!makerHasAmountRange && maker.amount <= 0) ||
(maker.isExplicit && (maker.badSatoshisText !== '' || maker.satoshis === '')) ||
(!maker.isExplicit && maker.badPremiumText !== '') ||
federation.getCoordinator(maker.coordinator)?.info === undefined ||
federation.getCoordinator(maker.coordinator)?.limits === undefined
);
}, [maker, amountLimits, federationUpdatedAt, fav.type, makerHasAmountRange]);

View File

@ -26,7 +26,7 @@ const SelectCoordinator: React.FC<SelectCoordinatorProps> = ({
setCoordinator,
}) => {
const { setOpen } = useContext<UseAppStoreType>(AppContext);
const { federation, sortedCoordinators } = useContext<UseFederationStoreType>(FederationContext);
const { federation } = useContext<UseFederationStoreType>(FederationContext);
const theme = useTheme();
const { t } = useTranslation();
@ -86,7 +86,7 @@ const SelectCoordinator: React.FC<SelectCoordinatorProps> = ({
flipHorizontally={false}
small={true}
/>
{(coordinator?.info === undefined ||
{(coordinator?.limits === undefined ||
Object.keys(coordinator?.limits).length === 0) && (
<CircularProgress
size={49}
@ -109,18 +109,20 @@ const SelectCoordinator: React.FC<SelectCoordinatorProps> = ({
onChange={handleCoordinatorChange}
disableUnderline
>
{sortedCoordinators.map((shortAlias: string): JSX.Element | null => {
let row: JSX.Element | null = null;
const item = federation.getCoordinator(shortAlias);
if (item.enabled === true) {
row = (
<MenuItem key={shortAlias} value={shortAlias}>
<Typography>{item.longAlias}</Typography>
</MenuItem>
);
}
return row;
})}
{Object.keys(federation.coordinators).map(
(shortAlias: string): JSX.Element | null => {
let row: JSX.Element | null = null;
const item = federation.getCoordinator(shortAlias);
if (item.enabled === true) {
row = (
<MenuItem key={shortAlias} value={shortAlias}>
<Typography>{item.longAlias}</Typography>
</MenuItem>
);
}
return row;
},
)}
</Select>
</Grid>
</Grid>

View File

@ -23,21 +23,23 @@ import {
DarkMode,
SettingsOverscan,
Link,
AccountBalance,
AttachMoney,
QrCode,
SettingsInputAntenna,
Dns,
} from '@mui/icons-material';
import { systemClient } from '../../services/System';
import { TorIcon } from '../Icons';
import SwapCalls from '@mui/icons-material/SwapCalls';
import { apiClient } from '../../services/api';
import Nostr from '../Icons/Nostr';
interface SettingsFormProps {
dense?: boolean;
}
const SettingsForm = ({ dense = false }: SettingsFormProps): JSX.Element => {
const { fav, setFav, settings, setSettings, client } = useContext<UseAppStoreType>(AppContext);
const { settings, setSettings, client } = useContext<UseAppStoreType>(AppContext);
const theme = useTheme();
const { t } = useTranslation();
const fontSizes = [
@ -49,8 +51,8 @@ const SettingsForm = ({ dense = false }: SettingsFormProps): JSX.Element => {
];
return (
<Grid container spacing={1}>
<Grid item>
<Grid container spacing={2}>
<Grid item xs={12}>
<List dense={dense}>
<ListItem>
<ListItemIcon>
@ -196,22 +198,22 @@ const SettingsForm = ({ dense = false }: SettingsFormProps): JSX.Element => {
<ListItem>
<ListItemIcon>
<AccountBalance />
<SettingsInputAntenna />
</ListItemIcon>
<ToggleButtonGroup
sx={{ width: '100%' }}
exclusive={true}
value={fav.mode}
onChange={(e, mode) => {
setFav({ ...fav, mode, currency: mode === 'fiat' ? 0 : 1000 });
value={settings.connection}
onChange={(_e, connection) => {
setSettings({ ...settings, connection });
systemClient.setItem('settings_connection', connection);
}}
>
<ToggleButton value='fiat' color='primary'>
<AttachMoney />
{t('Fiat')}
<ToggleButton value='api' color='primary' sx={{ flexGrow: 1 }}>
{t('API')}
</ToggleButton>
<ToggleButton value='swap' color='secondary'>
<SwapCalls />
{t('Swaps')}
<ToggleButton value='nostr' color='secondary' sx={{ flexGrow: 1 }}>
{t('nostr')}
</ToggleButton>
</ToggleButtonGroup>
</ListItem>
@ -221,17 +223,18 @@ const SettingsForm = ({ dense = false }: SettingsFormProps): JSX.Element => {
<Link />
</ListItemIcon>
<ToggleButtonGroup
sx={{ width: '100%' }}
exclusive={true}
value={settings.network}
onChange={(e, network) => {
onChange={(_e, network) => {
setSettings({ ...settings, network });
systemClient.setItem('settings_network', network);
}}
>
<ToggleButton value='mainnet' color='primary'>
<ToggleButton value='mainnet' color='primary' sx={{ flexGrow: 1 }}>
{t('Mainnet')}
</ToggleButton>
<ToggleButton value='testnet' color='secondary'>
<ToggleButton value='testnet' color='secondary' sx={{ flexGrow: 1 }}>
{t('Testnet')}
</ToggleButton>
</ToggleButtonGroup>

View File

@ -223,9 +223,6 @@ export const AppContextProvider = ({ children }: AppContextProviderProps): JSX.E
useEffect(() => {
void i18n.changeLanguage(settings.language);
}, []);
useEffect(() => {
window.addEventListener('torStatus', (event) => {
// Trick to improve UX on Android webview: delay the "Connected to TOR" status by 5 secs to avoid long waits on the first request.
setTimeout(

View File

@ -14,7 +14,7 @@ import { federationLottery } from '../utils';
import { AppContext, type UseAppStoreType } from './AppContext';
import { GarageContext, type UseGarageStoreType } from './GarageContext';
import { type Origin, type Origins } from '../models/Coordinator.model';
import Coordinator, { type Origin, type Origins } from '../models/Coordinator.model';
export interface CurrentOrderIdProps {
id: number | null;
@ -27,7 +27,6 @@ export interface FederationContextProviderProps {
export interface UseFederationStoreType {
federation: Federation;
sortedCoordinators: string[];
coordinatorUpdatedAt: string;
federationUpdatedAt: string;
addNewCoordinator: (alias: string, url: string) => void;
@ -35,7 +34,6 @@ export interface UseFederationStoreType {
export const initialFederationContext: UseFederationStoreType = {
federation: new Federation('onion', new Settings(), ''),
sortedCoordinators: [],
coordinatorUpdatedAt: '',
federationUpdatedAt: '',
addNewCoordinator: () => {},
@ -50,7 +48,6 @@ export const FederationContextProvider = ({
useContext<UseAppStoreType>(AppContext);
const { setMaker, garage } = useContext<UseGarageStoreType>(GarageContext);
const [federation] = useState(new Federation(origin, settings, hostUrl));
const [sortedCoordinators, setSortedCoordinators] = useState(federationLottery(federation));
const [coordinatorUpdatedAt, setCoordinatorUpdatedAt] = useState<string>(
new Date().toISOString(),
);
@ -58,7 +55,7 @@ export const FederationContextProvider = ({
useEffect(() => {
setMaker((maker) => {
return { ...maker, coordinator: sortedCoordinators[0] };
return { ...maker, coordinator: Object.keys(federation.coordinators)[0] };
}); // default MakerForm coordinator is decided via sorted lottery
federation.registerHook('onFederationUpdate', () => {
setFederationUpdatedAt(new Date().toISOString());
@ -68,10 +65,14 @@ export const FederationContextProvider = ({
useEffect(() => {
if (client !== 'mobile' || torStatus === 'ON' || !settings.useProxy) {
void federation.updateUrl(origin, settings, hostUrl);
void federation.update();
void federation.loadLimits();
}
}, [settings.network, settings.useProxy, torStatus]);
useEffect(() => {
federation.setConnection(settings);
}, [settings.connection]);
const addNewCoordinator: (alias: string, url: string) => void = (alias, url) => {
if (!federation.coordinators[alias]) {
const attributes: Record<any, any> = {
@ -91,8 +92,8 @@ export const FederationContextProvider = ({
attributes.testnet = origins;
}
federation.addCoordinator(origin, settings, hostUrl, attributes);
const newCoordinator = federation.coordinators[alias];
newCoordinator.update(() => {
const newCoordinator: Coordinator = federation.coordinators[alias];
newCoordinator.loadLimits(() => {
setCoordinatorUpdatedAt(new Date().toISOString());
});
garage.syncCoordinator(federation, alias);
@ -102,7 +103,7 @@ export const FederationContextProvider = ({
};
useEffect(() => {
if (page === 'offers') void federation.updateBook();
if (page === 'offers') void federation.loadBook();
}, [page]);
// use effects to fetchRobots on Profile open
@ -118,7 +119,6 @@ export const FederationContextProvider = ({
<FederationContext.Provider
value={{
federation,
sortedCoordinators,
coordinatorUpdatedAt,
federationUpdatedAt,
addNewCoordinator,

View File

@ -66,7 +66,7 @@ export const GarageContextProvider = ({ children }: GarageContextProviderProps):
// All garage data structured
const { settings, torStatus, open, page, client } = useContext<UseAppStoreType>(AppContext);
const pageRef = useRef(page);
const { federation, sortedCoordinators } = useContext<UseFederationStoreType>(FederationContext);
const { federation } = useContext<UseFederationStoreType>(FederationContext);
const [garage] = useState<Garage>(initialGarageContext.garage);
const [maker, setMaker] = useState<Maker>(initialGarageContext.maker);
const [slotUpdatedAt, setSlotUpdatedAt] = useState<string>(new Date().toISOString());
@ -83,7 +83,7 @@ export const GarageContextProvider = ({ children }: GarageContextProviderProps):
useEffect(() => {
setMaker((maker) => {
return { ...maker, coordinator: sortedCoordinators[0] };
return { ...maker, coordinator: Object.keys(federation.coordinators)[0] };
}); // default MakerForm coordinator is decided via sorted lottery
garage.registerHook('onSlotUpdate', onSlotUpdated);
clearInterval(timer);

View File

@ -3,26 +3,26 @@ export interface PublicOrder {
created_at: Date;
expires_at: Date;
type: number;
currency: number;
currency: number | null;
amount: string;
base_amount?: number;
base_price?: number;
has_range: boolean;
min_amount: number;
max_amount: number;
min_amount: string | null;
max_amount: string | null;
payment_method: string;
is_explicit: false;
premium: number;
satoshis: number;
satoshis_now: number;
latitude: number;
longitude: number;
bond_size: number;
maker: number;
premium: string;
satoshis: number | null;
satoshis_now: number | null;
latitude: number | null;
longitude: number | null;
bond_size: string;
maker: number | null;
escrow_duration: number;
maker_nick: string;
maker_hash_id: string;
price: number;
maker_status: 'Active' | 'Seen recently' | 'Inactive';
price: number | null;
maker_status?: 'Active' | 'Seen recently' | 'Inactive';
coordinatorShortAlias?: string;
}

View File

@ -126,7 +126,7 @@ export class Coordinator {
this.longAlias = value.longAlias;
this.shortAlias = value.shortAlias;
this.description = value.description;
this.federated = value.federated;
this.federated = value.federated ?? false;
this.motto = value.motto;
this.color = value.color;
this.size_limit = value.badges.isFounder ? 21 * 100000000 : calculateSizeLimit(established);
@ -165,7 +165,7 @@ export class Coordinator {
public basePath: string;
// These properties are fetched from coordinator API
public book: PublicOrder[] = [];
public book: Record<string, PublicOrder> = {};
public loadingBook: boolean = false;
public info?: Info | undefined = undefined;
public loadingInfo: boolean = false;
@ -182,24 +182,8 @@ export class Coordinator {
}
};
update = async (onUpdate: (shortAlias: string) => void = () => {}): Promise<void> => {
const onDataLoad = (): void => {
if (this.isUpdated()) onUpdate(this.shortAlias);
};
this.loadBook(onDataLoad);
this.loadLimits(onDataLoad);
this.loadInfo(onDataLoad);
};
updateBook = async (onUpdate: (shortAlias: string) => void = () => {}): Promise<void> => {
this.loadBook(() => {
onUpdate(this.shortAlias);
});
};
generateAllMakerAvatars = async (data: [PublicOrder]): Promise<void> => {
for (const order of data) {
generateAllMakerAvatars = async (): Promise<void> => {
for (const order of Object.values(this.book)) {
void roboidentitiesClient.generateRobohash(order.maker_hash_id, 'small');
}
};
@ -210,20 +194,19 @@ export class Coordinator {
if (this.loadingBook) return;
this.loadingBook = true;
this.book = [];
this.book = {};
apiClient
.get(this.url, `${this.basePath}/api/book/`)
.then((data) => {
if (!data?.not_found) {
this.book = (data as PublicOrder[]).map((order) => {
this.book = (data as PublicOrder[]).reduce<Record<string, PublicOrder>>((book, order) => {
order.coordinatorShortAlias = this.shortAlias;
return order;
});
void this.generateAllMakerAvatars(data);
return { ...book, [this.shortAlias + order.id]: order };
}, {});
void this.generateAllMakerAvatars();
onDataLoad();
} else {
this.book = [];
onDataLoad();
}
})
@ -289,7 +272,7 @@ export class Coordinator {
enable = (onEnabled: () => void = () => {}): void => {
this.enabled = true;
void this.update(() => {
void this.loadLimits(() => {
onEnabled();
});
};
@ -298,11 +281,7 @@ export class Coordinator {
this.enabled = false;
this.info = undefined;
this.limits = {};
this.book = [];
};
isUpdated = (): boolean => {
return !((this.loadingBook === this.loadingInfo) === this.loadingLimits);
this.book = {};
};
getBaseUrl = (): string => {

View File

@ -36,20 +36,18 @@ export const updateExchangeInfo = (federation: Federation): ExchangeInfo => {
'lifetime_volume',
];
Object.values(federation.coordinators)
.filter((coor) => coor.isUpdated())
.forEach((coordinator, index) => {
if (coordinator.info !== undefined) {
premiums[index] = coordinator.info.last_day_nonkyc_btc_premium;
volumes[index] = coordinator.info.last_day_volume;
highestVersion = getHigherVer(highestVersion, coordinator.info.version);
active_robots_today = Math.max(active_robots_today, coordinator.info.active_robots_today);
Object.values(federation.coordinators).forEach((coordinator, index) => {
if (coordinator.info !== undefined) {
premiums[index] = coordinator.info.last_day_nonkyc_btc_premium;
volumes[index] = coordinator.info.last_day_volume;
highestVersion = getHigherVer(highestVersion, coordinator.info.version);
active_robots_today = Math.max(active_robots_today, coordinator.info.active_robots_today);
aggregations.forEach((key: any) => {
info[key] = Number(info[key]) + Number(coordinator.info[key]);
});
}
});
aggregations.forEach((key: any) => {
info[key] = Number(info[key]) + Number(coordinator.info[key]);
});
}
});
info.last_day_nonkyc_btc_premium = weightedMean(premiums, volumes);
info.version = highestVersion;
@ -63,6 +61,7 @@ export interface Exchange {
enabledCoordinators: number;
onlineCoordinators: number;
loadingCoordinators: number;
loadingCache: number;
totalCoordinators: number;
}
@ -80,6 +79,7 @@ export const defaultExchange: Exchange = {
enabledCoordinators: 0,
onlineCoordinators: 0,
loadingCoordinators: 0,
loadingCache: 0,
totalCoordinators: 0,
};

View File

@ -8,40 +8,49 @@ import {
} from '.';
import defaultFederation from '../../static/federation.json';
import { systemClient } from '../services/System';
import { getHost } from '../utils';
import { federationLottery, getHost } from '../utils';
import { coordinatorDefaultValues } from './Coordinator.model';
import { updateExchangeInfo } from './Exchange.model';
import eventToPublicOrder from '../utils/nostr';
import { SubCloser } from 'nostr-tools/lib/types/pool';
import RoboPool from '../services/RoboPool';
type FederationHooks = 'onFederationUpdate';
export class Federation {
constructor(origin: Origin, settings: Settings, hostUrl: string) {
this.coordinators = Object.entries(defaultFederation).reduce(
const coordinators = Object.entries(defaultFederation).reduce(
(acc: Record<string, Coordinator>, [key, value]: [string, any]) => {
if (getHost() !== '127.0.0.1:8000' && key === 'local') {
// Do not add `Local Dev` unless it is running on localhost
return acc;
} else {
acc[key] = new Coordinator(value, origin, settings, hostUrl);
acc[key].federated = true;
return acc;
}
},
{},
);
this.coordinators = {};
federationLottery().forEach((alias) => {
if (coordinators[alias]) this.coordinators[alias] = coordinators[alias];
});
this.exchange = {
...defaultExchange,
totalCoordinators: Object.keys(this.coordinators).length,
};
this.book = [];
this.book = {};
this.hooks = {
onFederationUpdate: [],
};
Object.keys(defaultFederation).forEach((key) => {
Object.keys(this.coordinators).forEach((key) => {
if (key !== 'local' || getHost() === '127.0.0.1:8000') {
// Do not add `Local Dev` unless it is running on localhost
this.addCoordinator(origin, settings, hostUrl, defaultFederation[key]);
this.addCoordinator(origin, settings, hostUrl, this.coordinators[key]);
}
});
@ -55,15 +64,57 @@ export class Federation {
return Object.values(coor.testnet).includes(url);
});
if (tesnetHost) settings.network = 'testnet';
this.connection = null;
this.roboPool = new RoboPool(settings, origin);
}
public coordinators: Record<string, Coordinator>;
public exchange: Exchange;
public book: PublicOrder[];
public book: Record<string, PublicOrder>;
public loading: boolean;
public connection: 'api' | 'nostr' | null;
public hooks: Record<FederationHooks, Array<() => void>>;
public roboPool: RoboPool;
setConnection = (settings: Settings): void => {
this.connection = settings.connection;
if (this.connection === 'nostr') {
this.roboPool.connect();
this.loadBookNostr();
} else {
this.roboPool.close();
this.loadBook();
}
};
loadBookNostr = (): void => {
this.loading = true;
this.book = {};
this.exchange.loadingCache = this.roboPool.relays.length;
this.roboPool.subscribeBook({
onevent: (event) => {
const { dTag, publicOrder } = eventToPublicOrder(event);
if (publicOrder) {
this.book[dTag] = publicOrder;
} else {
delete this.book[dTag];
}
},
oneose: () => {
this.exchange.loadingCache = this.exchange.loadingCache - 1;
this.loading = this.exchange.loadingCache > 0 && this.exchange.loadingCoordinators > 0;
this.updateExchange();
this.triggerHook('onFederationUpdate');
},
});
};
addCoordinator = (
origin: Origin,
settings: Settings,
@ -92,12 +143,17 @@ export class Federation {
};
onCoordinatorSaved = (): void => {
this.book = Object.values(this.coordinators).reduce<PublicOrder[]>((array, coordinator) => {
return [...array, ...coordinator.book];
}, []);
if (this.connection === 'api') {
this.book = Object.values(this.coordinators).reduce<Record<string, PublicOrder>>(
(book, coordinator) => {
return { ...book, ...coordinator.book };
},
{},
);
}
this.exchange.loadingCoordinators =
this.exchange.loadingCoordinators < 1 ? 0 : this.exchange.loadingCoordinators - 1;
this.loading = this.exchange.loadingCoordinators > 0;
this.loading = this.exchange.loadingCache > 0 && this.exchange.loadingCoordinators > 0;
this.updateExchange();
this.triggerHook('onFederationUpdate');
};
@ -111,8 +167,7 @@ export class Federation {
systemClient.setCookie('federation', JSON.stringify(federationUrls));
};
update = async (): Promise<void> => {
this.loading = true;
loadInfo = async (): Promise<void> => {
this.exchange.info = {
num_public_buy_orders: 0,
num_public_sell_orders: 0,
@ -123,24 +178,38 @@ export class Federation {
lifetime_volume: 0,
version: { major: 0, minor: 0, patch: 0 },
};
this.updateEnabledCoordinators();
for (const coor of Object.values(this.coordinators)) {
void coor.loadInfo(() => {
this.onCoordinatorSaved();
});
}
};
loadLimits = async (): Promise<void> => {
this.loading = true;
this.exchange.onlineCoordinators = 0;
this.exchange.loadingCoordinators = Object.keys(this.coordinators).length;
this.updateEnabledCoordinators();
for (const coor of Object.values(this.coordinators)) {
void coor.update(() => {
void coor.loadLimits(() => {
this.exchange.onlineCoordinators = this.exchange.onlineCoordinators + 1;
this.onCoordinatorSaved();
});
}
};
updateBook = async (): Promise<void> => {
loadBook = async (): Promise<void> => {
if (this.connection !== 'api') return;
this.loading = true;
this.book = [];
this.book = {};
this.triggerHook('onFederationUpdate');
this.exchange.loadingCoordinators = Object.keys(this.coordinators).length;
for (const coor of Object.values(this.coordinators)) {
void coor.updateBook(() => {
void coor.loadBook(() => {
this.onCoordinatorSaved();
this.triggerHook('onFederationUpdate');
});

View File

@ -42,6 +42,9 @@ class BaseSettings {
? 'en'
: i18n.resolvedLanguage.substring(0, 2);
const connection = systemClient.getItem('settings_connection');
this.connection = connection && connection !== '' ? connection : 'api';
const networkCookie = systemClient.getItem('settings_network');
this.network = networkCookie && networkCookie !== '' ? networkCookie : 'mainnet';
this.host = getHost();
@ -63,6 +66,7 @@ class BaseSettings {
public language?: Language;
public freezeViewports: boolean = false;
public network: 'mainnet' | 'testnet' = 'mainnet';
public connection: 'api' | 'nostr' = 'api';
public host?: string;
public unsafeClient: boolean = false;
public selfhostedClient: boolean = false;

View File

@ -0,0 +1,101 @@
import { Event } from 'nostr-tools';
import { Settings } from '../../models';
import defaultFederation from '../../../static/federation.json';
import { Origins } from '../../models/Coordinator.model';
interface RoboPoolEvents {
onevent: (event: Event) => void;
oneose: () => void;
}
class RoboPool {
constructor(settings: Settings, origin: string) {
this.network = settings.network ?? 'mainnet';
this.relays = Object.values(defaultFederation)
.map((coord) => {
const url = coord[this.network][settings.selfhostedClient ? 'onion' : origin];
if (!url) return;
return `ws://${url.replace(/^https?:\/\//, '')}/nostr`;
})
.filter((item) => item !== undefined);
}
public relays: string[];
public network: string;
public webSockets: WebSocket[] = [];
private messageHandlers: Array<(url: string, event: MessageEvent) => void> = [];
connect = () => {
this.relays.forEach((url) => {
if (this.webSockets.find((w: WebSocket) => w.url === url)) return;
let ws: WebSocket;
const connect = () => {
ws = new WebSocket(url);
// Add event listeners for the WebSocket
ws.onopen = () => {
console.log(`Connected to ${url}`);
};
ws.onmessage = (event) => {
this.messageHandlers.forEach((handler) => handler(url, event));
};
ws.onerror = (error) => {
console.error(`WebSocket error on ${url}:`, error);
};
ws.onclose = () => {
console.log(`Disconnected from ${url}. Attempting to reconnect...`);
setTimeout(connect, 1000); // Reconnect after 1 second
};
};
connect();
this.webSockets.push(ws);
});
};
close = () => {
this.webSockets.forEach((ws) => ws.close());
};
sendMessage = (message: string) => {
const send = (index: number, message: string) => {
const ws = this.webSockets[index];
if (ws.readyState === WebSocket.OPEN) {
ws.send(message);
} else if (ws.readyState === WebSocket.CONNECTING) {
setTimeout(send, 500, index, message);
}
};
this.webSockets.forEach((_ws, index) => send(index, message));
};
subscribeBook = (events: RoboPoolEvents) => {
const authors = Object.values(defaultFederation)
.map((f) => f.nostrHexPubkey)
.filter((item) => item !== undefined);
const request = ['REQ', 'subscribeBook', { authors, kinds: [38383], '#n': [this.network] }];
this.messageHandlers.push((_url: string, messageEvent: MessageEvent) => {
const jsonMessage = JSON.parse(messageEvent.data);
if (jsonMessage[0] === 'EVENT') {
events.onevent(jsonMessage[2]);
} else if (jsonMessage[0] === 'EOSE') {
events.oneose();
}
});
this.sendMessage(JSON.stringify(request));
};
}
export default RoboPool;

View File

@ -32,7 +32,7 @@ class RoboGenerator {
setTimeout(() => {
this.waitingForLibrary = false;
}, 2500);
}, 3000);
}
public generate: (hash: string, size: 'small' | 'large') => Promise<string> = async (

View File

@ -11,14 +11,14 @@
// donate to the development fund. This is the only way envisioned to incentivize
// donations to the development fund.
import { type Federation } from '../models';
import defaultFederation from '../../static/federation.json';
export default function federationLottery(federation: Federation): string[] {
export default function federationLottery(): string[] {
// Create an array to store the coordinator short aliases and their corresponding weights (chance)
const coordinatorChance: Array<{ shortAlias: string; chance: number }> = [];
// Convert the `federation` object into an array of {shortAlias, chance}
Object.values(federation.coordinators).forEach((coor) => {
Object.values(defaultFederation).forEach((coor) => {
const chance = coor.badges.donatesToDevFund > 50 ? 50 : coor.badges?.donatesToDevFund;
coordinatorChance.push({ shortAlias: coor.shortAlias, chance });
});

View File

@ -1,4 +1,4 @@
import { type PublicOrder, type Favorites } from '../models';
import { type PublicOrder, type Favorites, Federation } from '../models';
interface AmountFilter {
amount: string;
@ -8,9 +8,9 @@ interface AmountFilter {
}
interface FilterOrders {
orders: PublicOrder[];
federation: Federation;
baseFilter: Favorites;
premium: number | null;
premium?: number | null;
amountFilter?: AmountFilter | null;
paymentMethods?: string[];
}
@ -60,13 +60,17 @@ const filterByPremium = function (order: PublicOrder, premium: number): boolean
};
const filterOrders = function ({
orders,
federation,
baseFilter,
premium = null,
paymentMethods = [],
amountFilter = null,
}: FilterOrders): PublicOrder[] {
const filteredOrders = orders.filter((order) => {
const enabledCoordinators = Object.values(federation.coordinators)
.filter((coord) => coord.enabled)
.map((coord) => coord.shortAlias);
const filteredOrders = Object.values(federation.book).filter((order) => {
const coordinatorCheck = enabledCoordinators.includes(order.coordinatorShortAlias ?? '');
const typeChecks = order.type === baseFilter.type || baseFilter.type == null;
const modeChecks = baseFilter.mode === 'fiat' ? !(order.currency === 1000) : true;
const premiumChecks = premium !== null ? filterByPremium(order, premium) : true;
@ -77,6 +81,7 @@ const filterOrders = function ({
const hostChecks =
baseFilter.coordinator !== 'any' ? filterByHost(order, baseFilter.coordinator) : true;
return (
coordinatorCheck &&
typeChecks &&
modeChecks &&
premiumChecks &&

View File

@ -0,0 +1,99 @@
import { Event } from 'nostr-tools';
import { Federation, PublicOrder } from '../models';
import { fromUnixTime } from 'date-fns';
import Geohash from 'latlon-geohash';
import currencyDict from '../../static/assets/currencies.json';
import defaultFederation from '../../static/federation.json';
const eventToPublicOrder = (event: Event): { dTag: string; publicOrder: PublicOrder | null } => {
const publicOrder: PublicOrder = {
id: 0,
coordinatorShortAlias: '',
created_at: new Date(),
expires_at: new Date(),
type: 1,
currency: null,
amount: '',
has_range: false,
min_amount: null,
max_amount: null,
payment_method: '',
is_explicit: false,
premium: '',
satoshis: null,
maker: null,
escrow_duration: 0,
bond_size: '',
latitude: null,
longitude: null,
maker_nick: '',
maker_hash_id: '',
satoshis_now: null,
price: null,
};
const statusTag = event.tags.find((t) => t[0] === 's') ?? [];
const dTag = event.tags.find((t) => t[0] === 'd') ?? [];
if (statusTag[1] !== 'pending') return { dTag: dTag[1], publicOrder: null };
event.tags.forEach((tag) => {
switch (tag[0]) {
case 'k':
publicOrder.type = tag[1] === 'sell' ? 1 : 0;
break;
case 'expiration':
publicOrder.expires_at = fromUnixTime(parseInt(tag[1], 10));
publicOrder.escrow_duration = parseInt(tag[2], 10);
break;
case 'fa':
if (tag[2]) {
publicOrder.has_range = true;
publicOrder.min_amount = tag[1] ?? null;
publicOrder.max_amount = tag[2] ?? null;
} else {
publicOrder.amount = tag[1];
}
break;
case 'bond':
publicOrder.bond_size = tag[1];
break;
case 'name':
publicOrder.maker_nick = tag[1];
publicOrder.maker_hash_id = tag[2];
break;
case 'premium':
publicOrder.premium = tag[1];
break;
case 'pm':
tag.shift();
publicOrder.payment_method = tag.join(' ');
break;
case 'g':
const { lat, lon } = Geohash.decode(tag[1]);
publicOrder.latitude = lat;
publicOrder.longitude = lon;
break;
case 'f':
const currencyNumber = Object.entries(currencyDict).find(
([_key, value]) => value === tag[1],
);
publicOrder.currency = currencyNumber?.[0] ? parseInt(currencyNumber[0], 10) : null;
break;
case 'source':
const orderUrl = tag[1].split('/');
publicOrder.id = parseInt(orderUrl[orderUrl.length - 1] ?? '0');
const coordinatorIdentifier = orderUrl[orderUrl.length - 2] ?? '';
publicOrder.coordinatorShortAlias = Object.entries(defaultFederation).find(
([key, value]) => value.identifier === coordinatorIdentifier,
)?.[0];
break;
default:
break;
}
});
return { dTag: dTag[1], publicOrder };
};
export default eventToPublicOrder;

View File

@ -2,10 +2,12 @@
"temple": {
"longAlias": "Temple of Sats",
"shortAlias": "temple",
"identifier": "templeofsats",
"description": "I am passionate about joining Robosats as a coordinator because I believe that peer-to-peer, non-KYC Bitcoin transactions are vital for the community's empowerment and autonomy. I aim to champion users' privacy, and provide a seamless experience for genuine Bitcoin enthusiasts.",
"motto": "Privacy and Integrity: Temple of Sats, where Bitcoin's essence thrives.",
"color": "#000",
"established": "2023-12-02",
"nostrHexPubkey": "74001620297035daa61475c069f90b6950087fea0d0134b795fac758c34e7191",
"contact": {
"email": "coordinator@templeofsats.org",
"telegram": "templeofsats",
@ -50,10 +52,12 @@
"lake": {
"longAlias": "TheBigLake",
"shortAlias": "lake",
"identifier": "thebiglake",
"description": "Becoming a RoboSats coordinator represents boosting intrinsic values of decentralization and economic freedom. RoboSats solves the problem of KYC and loss of privacy that big Exchanges are forced to comply with. I believe that decentralizing the lightning nodes will enhance the robustness of the tool, allowing more users to join. I am excited to be part of this new phase of growth.",
"motto": "TheBigLake: The Lake of Economic Freedom.",
"color": "#000D28",
"established": "2023-12-30",
"nostrHexPubkey": "f2d4855df39a7db6196666e8469a07a131cddc08dcaa744a344343ffcf54a10c",
"contact": {
"email": "gabbygator184@proton.me",
"telegram": "gabbygator184",
@ -95,10 +99,12 @@
"veneto": {
"longAlias": "BitcoinVeneto",
"shortAlias": "veneto",
"identifier": "bitcoinveneto",
"description": "Born as a group of computer scientists with different experiences, we discovered bitcoin at the end of 2013 and became enthusiastic and dedicated to bitcoin, blockchain and cryptocurrencies in general, in particular helping, informing and following all companies on the path to literacy in the world of digital currency. and the private individuals who have placed their trust in us in recent years.",
"motto": "Your NON-Virtual Guides on Bitcoin, Blockchain and Crypto.",
"color": "#000D27",
"established": "2024-02-24",
"nostrHexPubkey": "c8dc40a80bbb41fe7430fca9d0451b37a2341486ab65f890955528e4732da34a",
"contact": {
"email": "bitcoinveneto@proton.me",
"telegram": "BitcoinVeneto",

View File

@ -490,14 +490,14 @@
"Your last order #{{orderID}}": "La teva última ordre #{{orderID}}",
"finished order": "Ordre finalitzada",
"#43": "Phrases in components/SettingsForm/index.tsx",
"API": "API",
"Build-in": "Build-in",
"Dark": "Fosc",
"Disabled": "Disabled",
"Fiat": "Fiat",
"Light": "Clar",
"Mainnet": "Mainnet",
"Swaps": "Intercanvis",
"Testnet": "Testnet",
"nostr": "nostr",
"#44": "Phrases in components/TorConnection/index.tsx",
"Connected to TOR network": "Connectat a la xarxa TOR",
"Connecting to TOR network": "Connectant a la xarxa TOR",

View File

@ -490,14 +490,14 @@
"Your last order #{{orderID}}": "Tvá poslední nabídka #{{orderID}}",
"finished order": "finished order",
"#43": "Phrases in components/SettingsForm/index.tsx",
"API": "API",
"Build-in": "Build-in",
"Dark": "Dark",
"Disabled": "Disabled",
"Fiat": "Fiat",
"Light": "Light",
"Mainnet": "Mainnet",
"Swaps": "Swaps",
"Testnet": "Testnet",
"nostr": "nostr",
"#44": "Phrases in components/TorConnection/index.tsx",
"Connected to TOR network": "Connected to TOR network",
"Connecting to TOR network": "Connecting to TOR network",

View File

@ -490,14 +490,14 @@
"Your last order #{{orderID}}": "Deine letzte Order #{{orderID}}",
"finished order": "finished order",
"#43": "Phrases in components/SettingsForm/index.tsx",
"API": "API",
"Build-in": "Build-in",
"Dark": "Dark",
"Disabled": "Disabled",
"Fiat": "Fiat",
"Light": "Light",
"Mainnet": "Mainnet",
"Swaps": "Swaps",
"Testnet": "Testnet",
"nostr": "nostr",
"#44": "Phrases in components/TorConnection/index.tsx",
"Connected to TOR network": "Connected to TOR network",
"Connecting to TOR network": "Connecting to TOR network",

View File

@ -490,14 +490,14 @@
"Your last order #{{orderID}}": "Your last order #{{orderID}}",
"finished order": "finished order",
"#43": "Phrases in components/SettingsForm/index.tsx",
"API": "API",
"Build-in": "Build-in",
"Dark": "Dark",
"Disabled": "Disabled",
"Fiat": "Fiat",
"Light": "Light",
"Mainnet": "Mainnet",
"Swaps": "Swaps",
"Testnet": "Testnet",
"nostr": "nostr",
"#44": "Phrases in components/TorConnection/index.tsx",
"Connected to TOR network": "Connected to TOR network",
"Connecting to TOR network": "Connecting to TOR network",

View File

@ -490,14 +490,14 @@
"Your last order #{{orderID}}": "Tu última orden #{{orderID}}",
"finished order": "finished order",
"#43": "Phrases in components/SettingsForm/index.tsx",
"API": "API",
"Build-in": "Build-in",
"Dark": "Oscuro",
"Disabled": "Disabled",
"Fiat": "Fiat",
"Light": "Claro",
"Mainnet": "Mainnet",
"Swaps": "Swaps",
"Testnet": "Testnet",
"nostr": "nostr",
"#44": "Phrases in components/TorConnection/index.tsx",
"Connected to TOR network": "Conectado a la red TOR",
"Connecting to TOR network": "Conectando a la red TOR",

View File

@ -490,14 +490,14 @@
"Your last order #{{orderID}}": "Zure azken eskaera #{{orderID}}",
"finished order": "finished order",
"#43": "Phrases in components/SettingsForm/index.tsx",
"API": "API",
"Build-in": "Build-in",
"Dark": "Dark",
"Disabled": "Disabled",
"Fiat": "Fiat",
"Light": "Light",
"Mainnet": "Mainnet",
"Swaps": "Swaps",
"Testnet": "Testnet",
"nostr": "nostr",
"#44": "Phrases in components/TorConnection/index.tsx",
"Connected to TOR network": "Connected to TOR network",
"Connecting to TOR network": "Connecting to TOR network",

View File

@ -490,14 +490,14 @@
"Your last order #{{orderID}}": "Votre dernière commande #{{orderID}}",
"finished order": "finished order",
"#43": "Phrases in components/SettingsForm/index.tsx",
"API": "API",
"Build-in": "Build-in",
"Dark": "Sombre",
"Disabled": "Disabled",
"Fiat": "Fiat",
"Light": "Light",
"Mainnet": "Mainnet",
"Swaps": "Échanges",
"Testnet": "Testnet",
"nostr": "nostr",
"#44": "Phrases in components/TorConnection/index.tsx",
"Connected to TOR network": "Connecté au réseau TOR",
"Connecting to TOR network": "Connexion au réseau TOR",

View File

@ -490,14 +490,14 @@
"Your last order #{{orderID}}": "Il tuo ultimo ordine #{{orderID}}",
"finished order": "finished order",
"#43": "Phrases in components/SettingsForm/index.tsx",
"API": "API",
"Build-in": "Build-in",
"Dark": "Scuro",
"Disabled": "Disabled",
"Fiat": "Fiat",
"Light": "Chiaro",
"Mainnet": "Mainnet",
"Swaps": "Swaps",
"Testnet": "Testnet",
"nostr": "nostr",
"#44": "Phrases in components/TorConnection/index.tsx",
"Connected to TOR network": "Connesso alla rete TOR",
"Connecting to TOR network": "Connessione alla rete TOR",

View File

@ -490,14 +490,14 @@
"Your last order #{{orderID}}": "前回のオーダー #{{orderID}}",
"finished order": "finished order",
"#43": "Phrases in components/SettingsForm/index.tsx",
"API": "API",
"Build-in": "Build-in",
"Dark": "ダーク",
"Disabled": "Disabled",
"Fiat": "フィアット",
"Light": "ライト",
"Mainnet": "メインネット",
"Swaps": "スワップ",
"Testnet": "テストネット",
"nostr": "nostr",
"#44": "Phrases in components/TorConnection/index.tsx",
"Connected to TOR network": "TORネットワークに接続しました",
"Connecting to TOR network": "TORネットワークに接続中",

View File

@ -490,14 +490,14 @@
"Your last order #{{orderID}}": "Your last order #{{orderID}}",
"finished order": "finished order",
"#43": "Phrases in components/SettingsForm/index.tsx",
"API": "API",
"Build-in": "Build-in",
"Dark": "Dark",
"Disabled": "Disabled",
"Fiat": "Fiat",
"Light": "Light",
"Mainnet": "Mainnet",
"Swaps": "Swaps",
"Testnet": "Testnet",
"nostr": "nostr",
"#44": "Phrases in components/TorConnection/index.tsx",
"Connected to TOR network": "Connected to TOR network",
"Connecting to TOR network": "Connecting to TOR network",

View File

@ -490,14 +490,14 @@
"Your last order #{{orderID}}": "Sua última ordem #{{orderID}}",
"finished order": "ordem finalizada",
"#43": "Phrases in components/SettingsForm/index.tsx",
"API": "API",
"Build-in": "Build-in",
"Dark": "Dark",
"Disabled": "Disabled",
"Fiat": "Fiat",
"Light": "Light",
"Mainnet": "Mainnet",
"Swaps": "Swaps",
"Testnet": "Testnet",
"nostr": "nostr",
"#44": "Phrases in components/TorConnection/index.tsx",
"Connected to TOR network": "Conectado à rede TOR",
"Connecting to TOR network": "Conectando à rede TOR",

View File

@ -490,14 +490,14 @@
"Your last order #{{orderID}}": "Ваш последний ордер #{{orderID}}",
"finished order": "finished order",
"#43": "Phrases in components/SettingsForm/index.tsx",
"API": "API",
"Build-in": "Build-in",
"Dark": "Темный",
"Disabled": "Disabled",
"Fiat": "Фиат",
"Light": "Светлый",
"Mainnet": "Основная сеть",
"Swaps": "Обмен",
"Testnet": "Тестовая сеть",
"nostr": "nostr",
"#44": "Phrases in components/TorConnection/index.tsx",
"Connected to TOR network": "Подключено к сети TOR",
"Connecting to TOR network": "Подключение к сети TOR",

View File

@ -490,14 +490,14 @@
"Your last order #{{orderID}}": "Din senaste order #{{orderID}}",
"finished order": "finished order",
"#43": "Phrases in components/SettingsForm/index.tsx",
"API": "API",
"Build-in": "Build-in",
"Dark": "Dark",
"Disabled": "Disabled",
"Fiat": "Fiat",
"Light": "Light",
"Mainnet": "Mainnet",
"Swaps": "Swaps",
"Testnet": "Testnet",
"nostr": "nostr",
"#44": "Phrases in components/TorConnection/index.tsx",
"Connected to TOR network": "Connected to TOR network",
"Connecting to TOR network": "Connecting to TOR network",

View File

@ -490,14 +490,14 @@
"Your last order #{{orderID}}": "Amri yako ya mwisho #{{orderID}}",
"finished order": "finished order",
"#43": "Phrases in components/SettingsForm/index.tsx",
"API": "API",
"Build-in": "Build-in",
"Dark": "Giza",
"Disabled": "Disabled",
"Fiat": "Fiat",
"Light": "Nuru",
"Mainnet": "Mainnet",
"Swaps": "Swaps",
"Testnet": "Testnet",
"nostr": "nostr",
"#44": "Phrases in components/TorConnection/index.tsx",
"Connected to TOR network": "Kuunganishwa kwa mtandao wa TOR",
"Connecting to TOR network": "Kuunganisha kwa mtandao wa TOR",

View File

@ -490,14 +490,14 @@
"Your last order #{{orderID}}": "รายการล่าสุดของคุณ #{{orderID}}",
"finished order": "finished order",
"#43": "Phrases in components/SettingsForm/index.tsx",
"API": "API",
"Build-in": "Build-in",
"Dark": "Dark",
"Disabled": "Disabled",
"Fiat": "Fiat",
"Light": "Light",
"Mainnet": "Mainnet",
"Swaps": "Swaps",
"Testnet": "Testnet",
"nostr": "nostr",
"#44": "Phrases in components/TorConnection/index.tsx",
"Connected to TOR network": "Connected to TOR network",
"Connecting to TOR network": "Connecting to TOR network",

View File

@ -490,14 +490,14 @@
"Your last order #{{orderID}}": "你的上一笔交易 #{{orderID}}",
"finished order": "finished order",
"#43": "Phrases in components/SettingsForm/index.tsx",
"API": "API",
"Build-in": "Build-in",
"Dark": "深色",
"Disabled": "Disabled",
"Fiat": "法币",
"Light": "浅色",
"Mainnet": "主网",
"Swaps": "交换",
"Testnet": "测试网",
"nostr": "nostr",
"#44": "Phrases in components/TorConnection/index.tsx",
"Connected to TOR network": "已连线 TOR 网络",
"Connecting to TOR network": "正在连线 TOR 网络",

View File

@ -490,14 +490,14 @@
"Your last order #{{orderID}}": "你的上一筆交易 #{{orderID}}",
"finished order": "finished order",
"#43": "Phrases in components/SettingsForm/index.tsx",
"API": "API",
"Build-in": "Build-in",
"Dark": "深色",
"Disabled": "Disabled",
"Fiat": "法幣",
"Light": "淺色",
"Mainnet": "主網",
"Swaps": "交換",
"Testnet": "測試網",
"nostr": "nostr",
"#44": "Phrases in components/TorConnection/index.tsx",
"Connected to TOR network": "已連線 TOR 網絡",
"Connecting to TOR network": "正在連線 TOR 網絡",

9920
frontend/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@ -93,6 +93,7 @@ const App = () => {
loadCookie('settings_mode');
loadCookie('settings_light_qr');
loadCookie('settings_network');
loadCookie('settings_connection');
loadCookie('settings_use_proxy').then((useProxy) => {
SystemModule.useProxy(useProxy ?? 'true');
});