Nostr test

This commit is contained in:
koalasat 2024-09-12 10:10:27 +02:00
parent d4b3a708b2
commit 49c2469bb1
No known key found for this signature in database
GPG Key ID: 2F7F61C6146AB157
13 changed files with 10302 additions and 67 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

@ -25,7 +25,7 @@ const MakerPage = (): JSX.Element => {
const matches = useMemo(() => {
return filterOrders({
orders: federation.book,
orders: Object.values(federation.book),
baseFilter: {
currency: fav.currency === 0 ? 1 : fav.currency,
type: fav.type,

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>
);
},

View File

@ -66,8 +66,8 @@ const DepthChart: React.FC<DepthChartProps> = ({
}, [fav.currency]);
useEffect(() => {
if (federation.book.length > 0) {
const enriched = federation.book.map((order) => {
if (Object.values(federation.book).length > 0) {
const enriched = Object.values(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

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

@ -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

@ -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;
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

@ -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;
@ -187,7 +187,7 @@ export class Coordinator {
if (this.isUpdated()) onUpdate(this.shortAlias);
};
this.loadBook(onDataLoad);
// this.loadBook(onDataLoad);
this.loadLimits(onDataLoad);
this.loadInfo(onDataLoad);
};
@ -209,30 +209,30 @@ export class Coordinator {
if (this.url === '') return;
if (this.loadingBook) return;
this.loadingBook = true;
this.book = [];
// this.loadingBook = true;
// this.book = [];
apiClient
.get(this.url, `${this.basePath}/api/book/`)
.then((data) => {
if (!data?.not_found) {
this.book = (data as PublicOrder[]).map((order) => {
order.coordinatorShortAlias = this.shortAlias;
return order;
});
void this.generateAllMakerAvatars(data);
onDataLoad();
} else {
this.book = [];
onDataLoad();
}
})
.catch((e) => {
console.log(e);
})
.finally(() => {
this.loadingBook = false;
});
// apiClient
// .get(this.url, `${this.basePath}/api/book/`)
// .then((data) => {
// if (!data?.not_found) {
// this.book = (data as PublicOrder[]).map((order) => {
// order.coordinatorShortAlias = this.shortAlias;
// return order;
// });
// void this.generateAllMakerAvatars(data);
// onDataLoad();
// } else {
// this.book = [];
// onDataLoad();
// }
// })
// .catch((e) => {
// console.log(e);
// })
// .finally(() => {
// this.loadingBook = false;
// });
};
loadLimits = (onDataLoad: () => void = () => {}): void => {
@ -298,7 +298,7 @@ export class Coordinator {
this.enabled = false;
this.info = undefined;
this.limits = {};
this.book = [];
this.book = {};
};
isUpdated = (): boolean => {

View File

@ -1,3 +1,4 @@
import { SimplePool } from 'nostr-tools';
import {
Coordinator,
type Exchange,
@ -11,6 +12,7 @@ import { systemClient } from '../services/System';
import { getHost } from '../utils';
import { coordinatorDefaultValues } from './Coordinator.model';
import { updateExchangeInfo } from './Exchange.model';
import eventToPublicOrder from '../utils/nostr';
type FederationHooks = 'onFederationUpdate';
@ -33,7 +35,7 @@ export class Federation {
...defaultExchange,
totalCoordinators: Object.keys(this.coordinators).length,
};
this.book = [];
this.book = {};
this.hooks = {
onFederationUpdate: [],
};
@ -59,11 +61,61 @@ export class Federation {
public coordinators: Record<string, Coordinator>;
public exchange: Exchange;
public book: PublicOrder[];
public book: Record<string, PublicOrder>;
public loading: boolean;
public hooks: Record<FederationHooks, Array<() => void>>;
public relayPool: SimplePool = new SimplePool();
connectNostr = (): void => {
this.loading = true;
this.book = {};
const relays = ['ws://satstraoq35jffvkgpfoqld32nzw2siuvowanruindbfojowpwsjdgad.onion/nostr'];
this.exchange.loadingCoordinators = relays.length;
const authors = Object.values(defaultFederation)
.map((f) => f.nostrHexPubkey)
.filter((item) => item !== undefined);
this.relayPool.trustedRelayURLs = new Set<string>(relays);
this.relayPool.subscribeMany(
relays,
[
{
authors,
kinds: [38383],
'#n': ['mainnet'],
},
],
{
onevent: (event) => {
const { dTag, publicOrder } = eventToPublicOrder(event);
if (publicOrder) {
this.book[dTag] = publicOrder;
} else {
delete this.book[dTag];
}
},
oneose: () => {
this.exchange.loadingCoordinators = this.exchange.loadingCoordinators - 1;
this.loading = this.exchange.loadingCoordinators > 0;
this.updateExchange();
this.triggerHook('onFederationUpdate');
},
onclose: () => {
this.exchange.loadingCoordinators = this.exchange.loadingCoordinators - 1;
this.loading = this.exchange.loadingCoordinators > 0;
this.updateExchange();
this.triggerHook('onFederationUpdate');
},
},
);
};
addCoordinator = (
origin: Origin,
settings: Settings,
@ -92,9 +144,12 @@ export class Federation {
};
onCoordinatorSaved = (): void => {
this.book = Object.values(this.coordinators).reduce<PublicOrder[]>((array, coordinator) => {
return [...array, ...coordinator.book];
}, []);
// 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;
@ -126,6 +181,9 @@ export class Federation {
this.exchange.onlineCoordinators = 0;
this.exchange.loadingCoordinators = Object.keys(this.coordinators).length;
this.updateEnabledCoordinators();
this.connectNostr();
for (const coor of Object.values(this.coordinators)) {
void coor.update(() => {
this.exchange.onlineCoordinators = this.exchange.onlineCoordinators + 1;
@ -135,16 +193,16 @@ export class Federation {
};
updateBook = async (): Promise<void> => {
this.loading = true;
this.book = [];
this.triggerHook('onFederationUpdate');
this.exchange.loadingCoordinators = Object.keys(this.coordinators).length;
for (const coor of Object.values(this.coordinators)) {
void coor.updateBook(() => {
this.onCoordinatorSaved();
this.triggerHook('onFederationUpdate');
});
}
// this.loading = true;
// this.book = [];
// this.triggerHook('onFederationUpdate');
// this.exchange.loadingCoordinators = Object.keys(this.coordinators).length;
// for (const coor of Object.values(this.coordinators)) {
// void coor.updateBook(() => {
// this.onCoordinatorSaved();
// this.triggerHook('onFederationUpdate');
// });
// }
};
updateExchange = (): void => {

101
frontend/src/utils/nostr.ts Normal file
View File

@ -0,0 +1,101 @@
import { Event } from 'nostr-tools';
import { 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;
}
});
// price = limitsList[index].price * (1 + premium / 100);
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",

9920
frontend/yarn.lock Normal file

File diff suppressed because it is too large Load Diff