import { type LimitList, type PublicOrder, type Settings } from '.'; import { roboidentitiesClient } from '../services/Roboidentities/Web'; import { apiClient } from '../services/api'; import { compareUpdateLimit } from './Limit.model'; export interface Contact { nostr?: string | undefined; pgp?: string | undefined; fingerprint?: string | undefined; email?: string | undefined; telegram?: string | undefined; reddit?: string | undefined; matrix?: string | undefined; simplex?: string | undefined; twitter?: string | undefined; website?: string | undefined; } export interface Version { major: number; minor: number; patch: number; } export interface Badges { isFounder?: boolean | undefined; donatesToDevFund: number; hasGoodOpSec?: boolean | undefined; robotsLove?: boolean | undefined; hasLargeLimits?: boolean | undefined; } export interface Info { num_public_buy_orders: number; num_public_sell_orders: number; book_liquidity: number; active_robots_today: number; last_day_nonkyc_btc_premium: number; last_day_volume: number; lifetime_volume: number; lnd_version?: string; cln_version?: string; robosats_running_commit_hash: string; alternative_site: string; alternative_name: string; node_alias: string; node_id: string; version: Version; maker_fee: number; taker_fee: number; bond_size: number; min_order_size: number; max_order_size: number; swap_enabled: boolean; max_swap: number; current_swap_fee_rate: number; network: 'mainnet' | 'testnet' | undefined; openUpdateClient: boolean; notice_severity: 'none' | 'warning' | 'error' | 'success' | 'info'; notice_message: string; loading: boolean; } export type Origin = 'onion' | 'i2p' | 'clearnet'; export const coordinatorDefaultValues = { longAlias: '', shortAlias: '', description: '', motto: '', color: '#000', size_limit: 21 * 100000000, established: new Date(), policies: {}, contact: { email: '', telegram: '', simplex: '', matrix: '', website: '', nostr: '', pgp: '', fingerprint: '', }, badges: { isFounder: false, donatesToDevFund: 0, hasGoodOpSec: false, robotsLove: false, hasLargeLimits: false, }, mainnet: undefined, testnet: undefined, mainnetNodesPubkeys: '', testnetNodesPubkeys: '', federated: true, }; export interface Origins { clearnet: Origin | undefined; onion: Origin | undefined; i2p: Origin | undefined; } function calculateSizeLimit(inputDate: Date): number { const now = new Date(); const numDifficultyAdjustments = Math.ceil( (now.getTime() - inputDate.getTime()) / (1000 * 60 * 60 * 24 * 14), ); let value = 250000; for (let i = 1; i < numDifficultyAdjustments; i++) { value *= 1.3; if (i >= 12) { // after 12 difficulty adjustments (6 weeks) limit becomes 21 BTC (mature coordinator) return 21 * 100000000; } } return value; } export class Coordinator { constructor(value: any, origin: Origin, settings: Settings, hostUrl: string) { const established = new Date(value.established); this.longAlias = value.longAlias; this.shortAlias = value.shortAlias; this.description = value.description; this.federated = value.federated; this.motto = value.motto; this.color = value.color; this.size_limit = value.badges.isFounder ? 21 * 100000000 : calculateSizeLimit(established); this.established = established; this.policies = value.policies; this.contact = value.contact; this.badges = value.badges; this.mainnet = value.mainnet; this.testnet = value.testnet; this.mainnetNodesPubkeys = value.mainnetNodesPubkeys; this.testnetNodesPubkeys = value.testnetNodesPubkeys; this.url = ''; this.basePath = ''; this.updateUrl(origin, settings, hostUrl); } // These properties are loaded from federation.json public longAlias: string; public shortAlias: string; public federated: boolean; public enabled?: boolean = true; public description: string; public motto: string; public color: string; public size_limit: number; public established: Date; public policies: Record = {}; public contact: Contact | undefined; public badges: Badges; public mainnet: Origins; public testnet: Origins; public mainnetNodesPubkeys: string[] | undefined; public testnetNodesPubkeys: string[] | undefined; public url: string; public basePath: string; // These properties are fetched from coordinator API public book: Record = {}; public loadingBook: boolean = false; public info?: Info | undefined = undefined; public loadingInfo: boolean = false; public limits: LimitList = {}; public loadingLimits: boolean = false; updateUrl = (origin: Origin, settings: Settings, hostUrl: string): void => { if (settings.selfhostedClient && this.shortAlias !== 'local') { this.url = hostUrl; this.basePath = `/${settings.network}/${this.shortAlias}`; } else { this.url = String(this[settings.network][origin]); this.basePath = ''; } }; update = async (onUpdate: (shortAlias: string) => void = () => {}): Promise => { 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 => { this.loadBook(() => { onUpdate(this.shortAlias); }); }; generateAllMakerAvatars = async (data: [PublicOrder]): Promise => { for (const order of data) { void roboidentitiesClient.generateRobohash(order.maker_hash_id, 'small'); } }; loadBook = (onDataLoad: () => void = () => {}): void => { if (!this.enabled) return; if (this.url === '') return; if (this.loadingBook) return; // 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; // }); }; loadLimits = (onDataLoad: () => void = () => {}): void => { if (!this.enabled) return; if (this.url === '') return; if (this.loadingLimits) return; this.loadingLimits = true; apiClient .get(this.url, `${this.basePath}/api/limits/`) .then((data) => { if (data !== null) { const newLimits = data as LimitList; for (const currency in this.limits) { newLimits[currency] = compareUpdateLimit(this.limits[currency], newLimits[currency]); } this.limits = newLimits; onDataLoad(); } }) .catch((e) => { console.log(e); }) .finally(() => { this.loadingLimits = false; }); }; loadInfo = (onDataLoad: () => void = () => {}): void => { if (!this.enabled) return; if (this.url === '') return; if (this.loadingInfo) return; this.loadingInfo = true; apiClient .get(this.url, `${this.basePath}/api/info/`) .then((data) => { if (data !== null) { this.info = data as Info; onDataLoad(); } }) .catch((e) => { console.log(e); }) .finally(() => { this.loadingInfo = false; }); }; enable = (onEnabled: () => void = () => {}): void => { this.enabled = true; void this.update(() => { onEnabled(); }); }; disable = (): void => { this.enabled = false; this.info = undefined; this.limits = {}; this.book = {}; }; isUpdated = (): boolean => { return !((this.loadingBook === this.loadingInfo) === this.loadingLimits); }; getBaseUrl = (): string => { return this.url + this.basePath; }; getEndpoint = ( network: 'mainnet' | 'testnet', origin: Origin, selfHosted: boolean, hostUrl: string, ): { url: string; basePath: string } => { if (selfHosted && this.shortAlias !== 'local') { return { url: hostUrl, basePath: `/${network}/${this.shortAlias}` }; } else { return { url: String(this[network][origin]), basePath: '' }; } }; } export default Coordinator;