mirror of
https://github.com/RoboSats/robosats.git
synced 2025-01-18 12:11:35 +00:00
Merge pull request #1495 from RoboSats/use-nostr-in-book-page
Use nostr in book page
This commit is contained in:
commit
7bf1d5110e
134
frontend/package-lock.json
generated
134
frontend/package-lock.json
generated
@ -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"
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
|
@ -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}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
</>
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
</>
|
||||
)}
|
||||
|
@ -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>
|
||||
|
@ -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'>
|
||||
|
@ -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 />
|
||||
|
@ -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}
|
||||
|
@ -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]);
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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 => {
|
||||
|
@ -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,
|
||||
};
|
||||
|
||||
|
@ -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');
|
||||
});
|
||||
|
@ -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;
|
||||
|
101
frontend/src/services/RoboPool/index.ts
Normal file
101
frontend/src/services/RoboPool/index.ts
Normal 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;
|
@ -32,7 +32,7 @@ class RoboGenerator {
|
||||
|
||||
setTimeout(() => {
|
||||
this.waitingForLibrary = false;
|
||||
}, 2500);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
public generate: (hash: string, size: 'small' | 'large') => Promise<string> = async (
|
||||
|
@ -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 });
|
||||
});
|
||||
|
@ -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 &&
|
||||
|
99
frontend/src/utils/nostr.ts
Normal file
99
frontend/src/utils/nostr.ts
Normal 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;
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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ネットワークに接続中",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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 网络",
|
||||
|
@ -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
9920
frontend/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@ -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');
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user