robosats/frontend/src/models/Coordinator.model.ts

439 lines
12 KiB
TypeScript
Raw Normal View History

2023-10-27 11:00:53 +00:00
import {
type Robot,
type LimitList,
type PublicOrder,
type Settings,
type Order,
type Garage,
} from '.';
import { apiClient } from '../services/api';
2023-11-02 14:15:18 +00:00
import { validateTokenEntropy } from '../utils';
import { compareUpdateLimit } from './Limit.model';
2023-11-15 12:59:54 +00:00
import { defaultOrder } from './Order.model';
2023-12-02 11:31:21 +00:00
import { robohash } from '../components/RobotAvatar/RobohashGenerator';
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 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) {
const established = new Date(value.established);
this.longAlias = value.longAlias;
this.shortAlias = value.shortAlias;
this.description = value.description;
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 = '';
}
// These properties are loaded from federation.json
public longAlias: string;
public shortAlias: string;
public enabled?: boolean = true;
public description: string;
public motto: string;
public color: string;
public size_limit: number;
public established: Date;
public policies: Record<string, string> = {};
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: PublicOrder[] = [];
public loadingBook: boolean = false;
public info?: Info | undefined = undefined;
public loadingInfo: boolean = false;
public limits: LimitList = {};
public loadingLimits: boolean = false;
public loadingRobot: boolean = true;
start = async (
origin: Origin,
settings: Settings,
hostUrl: string,
onUpdate: (shortAlias: string) => void = () => {},
): Promise<void> => {
2023-11-02 14:15:18 +00:00
if (this.enabled !== true) return;
void this.updateUrl(settings, origin, hostUrl, onUpdate);
};
updateUrl = async (
settings: Settings,
origin: Origin,
hostUrl: string,
onUpdate: (shortAlias: string) => void = () => {},
): Promise<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 = '';
}
void this.update(() => {
onUpdate(this.shortAlias);
});
};
update = async (onUpdate: (shortAlias: string) => void = () => {}): Promise<void> => {
2023-11-02 14:15:18 +00:00
const onDataLoad = (): void => {
if (this.isUpdated()) onUpdate(this.shortAlias);
};
this.loadBook(onDataLoad);
this.loadLimits(onDataLoad);
this.loadInfo(onDataLoad);
};
2023-12-22 12:58:59 +00:00
updateBook = async (onUpdate: (shortAlias: string) => void = () => {}): Promise<void> => {
this.loadBook(() => {
onUpdate(this.shortAlias);
});
};
generateAllMakerAvatars = async (data: [PublicOrder]): Promise<void> => {
2023-12-02 11:31:21 +00:00
for (const order of data) {
2023-12-22 12:58:59 +00:00
void robohash.generate(order.maker_hash_id, 'small');
2023-12-02 11:31:21 +00:00
}
};
2023-11-02 14:15:18 +00:00
loadBook = (onDataLoad: () => void = () => {}): void => {
2023-12-22 12:58:59 +00:00
if (!this.enabled) return;
if (this.url === '') return;
if (this.loadingBook) return;
this.loadingBook = true;
apiClient
.get(this.url, `${this.basePath}/api/book/`)
.then((data) => {
2023-12-22 12:58:59 +00:00
if (!data?.not_found) {
this.book = (data as PublicOrder[]).map((order) => {
order.coordinatorShortAlias = this.shortAlias;
return order;
});
2023-12-22 12:58:59 +00:00
void this.generateAllMakerAvatars(data);
onDataLoad();
} else {
this.book = [];
onDataLoad();
}
})
.catch((e) => {
console.log(e);
})
.finally(() => {
this.loadingBook = false;
});
};
2023-11-02 14:15:18 +00:00
loadLimits = (onDataLoad: () => void = () => {}): void => {
2023-12-22 12:58:59 +00:00
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;
});
};
2023-11-02 14:15:18 +00:00
loadInfo = (onDataLoad: () => void = () => {}): void => {
2023-12-22 12:58:59 +00:00
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;
});
};
2023-11-02 14:15:18 +00:00
enable = (onEnabled: () => void = () => {}): void => {
this.enabled = true;
2023-11-02 14:15:18 +00:00
void this.update(() => {
2023-11-01 12:18:00 +00:00
onEnabled();
});
};
2023-11-02 14:15:18 +00:00
disable = (): void => {
this.enabled = false;
this.info = undefined;
this.limits = {};
this.book = [];
};
2023-11-02 14:15:18 +00:00
isUpdated = (): boolean => {
2023-10-27 11:00:53 +00:00
return !((this.loadingBook === this.loadingInfo) === this.loadingLimits);
};
2023-11-02 14:15:18 +00:00
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: '' };
}
};
2024-01-14 16:56:03 +00:00
fetchRobot = async (garage: Garage, token: string): Promise<Robot | null> => {
if (!this.enabled || !token) return null;
2023-11-21 17:36:59 +00:00
const robot = garage?.getSlot(token)?.getRobot() ?? null;
const authHeaders = robot?.getAuthHeaders();
2023-11-21 17:36:59 +00:00
if (!authHeaders) return null;
const { hasEnoughEntropy, bitsEntropy, shannonEntropy } = validateTokenEntropy(token);
if (!hasEnoughEntropy) return null;
const newAttributes = await apiClient
.get(this.url, `${this.basePath}/api/robot/`, authHeaders)
.then((data: any) => {
return {
nickname: data.nickname,
activeOrderId: data.active_order_id ?? null,
lastOrderId: data.last_order_id ?? null,
earnedRewards: data.earned_rewards ?? 0,
stealthInvoices: data.wants_stealth,
tgEnabled: data.tg_enabled,
tgBotName: data.tg_bot_name,
tgToken: data.tg_token,
found: data?.found,
last_login: data.last_login,
pubKey: data.public_key,
encPrivKey: data.encrypted_private_key,
};
})
.catch((e) => {
console.log(e);
});
2024-01-23 10:37:04 +00:00
garage.updateRobot(token, this.shortAlias, {
2023-11-21 17:36:59 +00:00
...newAttributes,
tokenSHA256: authHeaders.tokenSHA256,
loading: false,
bitsEntropy,
shannonEntropy,
shortAlias: this.shortAlias,
});
2023-11-21 17:36:59 +00:00
return garage.getSlot(this.shortAlias)?.getRobot() ?? null;
};
fetchOrder = async (orderId: number, robot: Robot, token: string): Promise<Order | null> => {
2023-12-22 12:58:59 +00:00
if (!this.enabled) return null;
if (!token) return null;
const authHeaders = robot.getAuthHeaders();
if (!authHeaders) return null;
return await apiClient
.get(this.url, `${this.basePath}/api/order/?order_id=${orderId}`, authHeaders)
.then((data) => {
console.log('data', data);
2023-11-15 12:59:54 +00:00
const order: Order = {
...defaultOrder,
...data,
shortAlias: this.shortAlias,
};
return order;
})
.catch((e) => {
console.log(e);
return null;
});
};
fetchReward = async (
signedInvoice: string,
garage: Garage,
2023-11-21 17:36:59 +00:00
index: string,
): Promise<null | {
bad_invoice?: string;
successful_withdrawal?: boolean;
}> => {
2023-12-22 12:58:59 +00:00
if (!this.enabled) return null;
2023-11-21 17:36:59 +00:00
const slot = garage.getSlot(index);
const robot = slot?.getRobot();
if (!slot?.token || !robot?.encPrivKey) return null;
const data = await apiClient.post(
this.url,
2024-01-29 20:05:20 +00:00
`${this.basePath}/api/reward/`,
{
invoice: signedInvoice,
},
{ tokenSHA256: robot.tokenSHA256 },
);
2024-01-23 10:37:04 +00:00
garage.updateRobot(slot?.token, this.shortAlias, {
2023-11-02 14:15:18 +00:00
earnedRewards: data?.successful_withdrawal === true ? 0 : robot.earnedRewards,
2023-11-21 17:36:59 +00:00
});
return data ?? {};
};
2023-11-21 17:36:59 +00:00
fetchStealth = async (wantsStealth: boolean, garage: Garage, index: string): Promise<null> => {
2023-12-22 12:58:59 +00:00
if (!this.enabled) return null;
2023-11-21 17:36:59 +00:00
const slot = garage.getSlot(index);
const robot = slot?.getRobot();
if (!(slot?.token != null) || !(robot?.encPrivKey != null)) return null;
await apiClient.post(
this.url,
`${this.basePath}/api/stealth/`,
{ wantsStealth },
{ tokenSHA256: robot.tokenSHA256 },
);
2024-01-23 10:37:04 +00:00
garage.updateRobot(slot?.token, this.shortAlias, {
2023-11-21 17:36:59 +00:00
stealthInvoices: wantsStealth,
});
return null;
};
Add Nav Bar, Settings Page, large refactor (#308) commit a5b63aed93e084fae19d9e444e06238a52f24f3a Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Sun Oct 30 10:46:05 2022 -0700 Small fixes commit d64adfc2bf9b9c31dca47ab113c06a1268c347c6 Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Sun Oct 30 06:02:06 2022 -0700 wip work on federation settings commit ca35d6b3d2776812b07109e197d2e1d46f9f4e81 Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Sun Oct 30 04:05:33 2022 -0700 Refactor confirmation Dialogs commit c660a5b0d1345d4996efb10cb8999987689bede9 Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Sat Oct 29 13:36:59 2022 -0700 refactor login (clean separation robot/info. Style navbar. commit b9dc7f7c95a683e3aca024ec6d7857176b4e3a25 Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Fri Oct 28 09:54:38 2022 -0700 Add size slider and settings widget commit 20b2b3dcd6838b129741705f1c65d445271e231d Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Fri Oct 28 05:41:48 2022 -0700 Add show more and Dialogs commit da8b70091b5f28139cdec1a8895f4563d64d8e88 Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Thu Oct 27 16:26:07 2022 -0700 Add sliding pages commit 6dd90aa1182a7a5e0f0189d1467ba474b68c28c2 Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Thu Oct 27 06:34:58 2022 -0700 Add settings forms commit d3d0f3ee1a52bbf1829714050cc798d2542af8f6 Author: Reckless_Satoshi <reckless.satoshi@protonmail.com> Date: Wed Oct 26 04:16:06 2022 -0700 Refactor utils
2022-10-30 19:13:01 +00:00
}
export default Coordinator;