This commit is contained in:
KoalaSat 2024-10-02 13:26:12 +00:00 committed by GitHub
commit 02b9800129
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
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": "Nati come gruppo di informatici con esperienze diverse, scoperti i bitcoin a fine 2013 ci siamo entusiasmati e dedicati al bitcoin, alla blockchain ed alle criptovalute in generale, in particolare aiutando, informando e seguendo nel percorso di alfabetizzazione nel mondo della valuta digitale tutte le Aziende ed i privati che negli ultimi anni ci hanno dato la loro fiducia.",
"motto": "Le Tue Guide NON Virtuali su Bitcoin, Blockchain e Crypto. In Veneto.",
"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