2023-10-27 11:00:53 +00:00
|
|
|
import {
|
|
|
|
type Robot,
|
|
|
|
type LimitList,
|
|
|
|
type PublicOrder,
|
|
|
|
type Settings,
|
|
|
|
type Order,
|
|
|
|
type Garage,
|
|
|
|
} from '.';
|
2023-10-27 10:01:59 +00:00
|
|
|
import { apiClient } from '../services/api';
|
2023-11-02 14:15:18 +00:00
|
|
|
import { validateTokenEntropy } from '../utils';
|
2023-10-27 10:01:59 +00:00
|
|
|
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';
|
2023-10-27 10:01:59 +00:00
|
|
|
|
|
|
|
export interface Contact {
|
|
|
|
nostr?: string | undefined;
|
|
|
|
pgp?: string | undefined;
|
|
|
|
fingerprint?: string | undefined;
|
|
|
|
email?: string | undefined;
|
|
|
|
telegram?: string | undefined;
|
|
|
|
reddit?: string | undefined;
|
|
|
|
matrix?: string | undefined;
|
2023-11-03 12:13:59 +00:00
|
|
|
simplex?: string | undefined;
|
2023-10-27 10:01:59 +00:00
|
|
|
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;
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface getEndpointProps {
|
|
|
|
coordinator: Coordinator;
|
|
|
|
network: 'mainnet' | 'testnet';
|
|
|
|
origin: Origin;
|
|
|
|
selfHosted: boolean;
|
|
|
|
hostUrl: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
export class Coordinator {
|
|
|
|
constructor(value: any) {
|
|
|
|
this.longAlias = value.longAlias;
|
|
|
|
this.shortAlias = value.shortAlias;
|
|
|
|
this.description = value.description;
|
|
|
|
this.motto = value.motto;
|
|
|
|
this.color = value.color;
|
|
|
|
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 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,
|
2023-12-31 14:59:46 +00:00
|
|
|
onUpdate: (shortAlias: string) => void = () => {},
|
2023-10-27 10:01:59 +00:00
|
|
|
): Promise<void> => {
|
2023-11-02 14:15:18 +00:00
|
|
|
if (this.enabled !== true) return;
|
2023-12-31 14:59:46 +00:00
|
|
|
void this.updateUrl(settings, origin, hostUrl, onUpdate);
|
2023-12-30 16:45:45 +00:00
|
|
|
};
|
2023-10-27 10:01:59 +00:00
|
|
|
|
2023-12-31 14:59:46 +00:00
|
|
|
updateUrl = async (
|
|
|
|
settings: Settings,
|
|
|
|
origin: Origin,
|
|
|
|
hostUrl: string,
|
|
|
|
onUpdate: (shortAlias: string) => void = () => {},
|
|
|
|
): Promise<void> => {
|
2023-10-27 10:01:59 +00:00
|
|
|
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 = '';
|
|
|
|
}
|
2023-12-31 14:59:46 +00:00
|
|
|
void this.update(() => {
|
|
|
|
onUpdate(this.shortAlias);
|
|
|
|
});
|
2023-10-27 10:01:59 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
update = async (onUpdate: (shortAlias: string) => void = () => {}): Promise<void> => {
|
2023-11-02 14:15:18 +00:00
|
|
|
const onDataLoad = (): void => {
|
2023-10-27 10:01:59 +00:00
|
|
|
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;
|
2023-12-30 16:45:45 +00:00
|
|
|
if (this.url === '') return;
|
2023-10-27 10:01:59 +00:00
|
|
|
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) {
|
2023-10-27 10:01:59 +00:00
|
|
|
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);
|
2023-10-27 10:01:59 +00:00
|
|
|
onDataLoad();
|
2024-01-13 12:43:17 +00:00
|
|
|
} else {
|
|
|
|
this.book = [];
|
|
|
|
onDataLoad();
|
2023-10-27 10:01:59 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
.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;
|
2023-12-30 16:45:45 +00:00
|
|
|
if (this.url === '') return;
|
2023-10-27 10:01:59 +00:00
|
|
|
if (this.loadingLimits) return;
|
|
|
|
|
|
|
|
this.loadingLimits = true;
|
|
|
|
|
|
|
|
apiClient
|
|
|
|
.get(this.url, `${this.basePath}/api/limits/`)
|
|
|
|
.then((data) => {
|
2023-11-20 14:45:58 +00:00
|
|
|
if (data !== null) {
|
2023-10-27 10:01:59 +00:00
|
|
|
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;
|
2023-12-30 16:45:45 +00:00
|
|
|
if (this.url === '') return;
|
2023-10-27 10:01:59 +00:00
|
|
|
if (this.loadingInfo) return;
|
|
|
|
|
|
|
|
this.loadingInfo = true;
|
|
|
|
|
|
|
|
apiClient
|
|
|
|
.get(this.url, `${this.basePath}/api/info/`)
|
|
|
|
.then((data) => {
|
2023-11-20 14:45:58 +00:00
|
|
|
if (data !== null) {
|
|
|
|
this.info = data as Info;
|
|
|
|
onDataLoad();
|
|
|
|
}
|
2023-10-27 10:01:59 +00:00
|
|
|
})
|
|
|
|
.catch((e) => {
|
|
|
|
console.log(e);
|
|
|
|
})
|
|
|
|
.finally(() => {
|
|
|
|
this.loadingInfo = false;
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2023-11-02 14:15:18 +00:00
|
|
|
enable = (onEnabled: () => void = () => {}): void => {
|
2023-10-27 10:01:59 +00:00
|
|
|
this.enabled = true;
|
2023-11-02 14:15:18 +00:00
|
|
|
void this.update(() => {
|
2023-11-01 12:18:00 +00:00
|
|
|
onEnabled();
|
|
|
|
});
|
2023-10-27 10:01:59 +00:00
|
|
|
};
|
|
|
|
|
2023-11-02 14:15:18 +00:00
|
|
|
disable = (): void => {
|
2023-10-27 10:01:59 +00:00
|
|
|
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-10-27 10:01:59 +00:00
|
|
|
};
|
|
|
|
|
2023-11-02 14:15:18 +00:00
|
|
|
getBaseUrl = (): string => {
|
2023-10-27 10:01:59 +00:00
|
|
|
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> => {
|
2024-01-06 12:33:57 +00:00
|
|
|
if (!this.enabled || !token) return null;
|
2023-10-27 10:01:59 +00:00
|
|
|
|
2023-11-21 17:36:59 +00:00
|
|
|
const robot = garage?.getSlot(token)?.getRobot() ?? null;
|
2024-01-06 12:33:57 +00:00
|
|
|
const authHeaders = robot?.getAuthHeaders();
|
2023-11-21 17:36:59 +00:00
|
|
|
|
2024-01-06 12:33:57 +00:00
|
|
|
if (!authHeaders) return null;
|
2023-10-27 10:01:59 +00:00
|
|
|
|
2024-01-06 12:33:57 +00:00
|
|
|
const { hasEnoughEntropy, bitsEntropy, shannonEntropy } = validateTokenEntropy(token);
|
2023-10-27 10:01:59 +00:00
|
|
|
|
|
|
|
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);
|
|
|
|
});
|
|
|
|
|
2023-11-21 17:36:59 +00:00
|
|
|
garage.upsertRobot(token, this.shortAlias, {
|
|
|
|
...newAttributes,
|
|
|
|
tokenSHA256: authHeaders.tokenSHA256,
|
|
|
|
loading: false,
|
|
|
|
bitsEntropy,
|
|
|
|
shannonEntropy,
|
|
|
|
shortAlias: this.shortAlias,
|
|
|
|
});
|
2023-10-27 10:01:59 +00:00
|
|
|
|
2023-11-21 17:36:59 +00:00
|
|
|
return garage.getSlot(this.shortAlias)?.getRobot() ?? null;
|
2023-10-27 10:01:59 +00:00
|
|
|
};
|
|
|
|
|
2024-01-06 12:33:57 +00:00
|
|
|
fetchOrder = async (orderId: number, robot: Robot, token: string): Promise<Order | null> => {
|
2023-12-22 12:58:59 +00:00
|
|
|
if (!this.enabled) return null;
|
2024-01-06 12:33:57 +00:00
|
|
|
if (!token) return null;
|
2023-10-27 10:01:59 +00:00
|
|
|
|
|
|
|
const authHeaders = robot.getAuthHeaders();
|
2024-01-06 12:33:57 +00:00
|
|
|
if (!authHeaders) return null;
|
2023-10-27 10:01:59 +00:00
|
|
|
|
|
|
|
return await apiClient
|
|
|
|
.get(this.url, `${this.basePath}/api/order/?order_id=${orderId}`, authHeaders)
|
|
|
|
.then((data) => {
|
2024-01-06 12:33:57 +00:00
|
|
|
console.log('data', data);
|
2023-11-15 12:59:54 +00:00
|
|
|
const order: Order = {
|
|
|
|
...defaultOrder,
|
|
|
|
...data,
|
|
|
|
shortAlias: this.shortAlias,
|
|
|
|
};
|
|
|
|
return order;
|
2023-10-27 10:01:59 +00:00
|
|
|
})
|
|
|
|
.catch((e) => {
|
|
|
|
console.log(e);
|
|
|
|
return null;
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
fetchReward = async (
|
|
|
|
signedInvoice: string,
|
|
|
|
garage: Garage,
|
2023-11-21 17:36:59 +00:00
|
|
|
index: string,
|
2023-10-27 10:01:59 +00:00
|
|
|
): 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
|
|
|
|
2024-01-06 12:33:57 +00:00
|
|
|
const slot = garage.getSlot(index);
|
|
|
|
const robot = slot?.getRobot();
|
2023-10-27 10:01:59 +00:00
|
|
|
|
2024-01-06 12:33:57 +00:00
|
|
|
if (!slot?.token || !robot?.encPrivKey) return null;
|
2023-10-27 10:01:59 +00:00
|
|
|
|
|
|
|
const data = await apiClient.post(
|
|
|
|
this.url,
|
|
|
|
`${this.basePath}`,
|
|
|
|
{
|
|
|
|
invoice: signedInvoice,
|
|
|
|
},
|
|
|
|
{ tokenSHA256: robot.tokenSHA256 },
|
|
|
|
);
|
2024-01-06 12:33:57 +00:00
|
|
|
garage.upsertRobot(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
|
|
|
});
|
2023-10-27 10:01: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
|
|
|
|
2024-01-06 12:33:57 +00:00
|
|
|
const slot = garage.getSlot(index);
|
|
|
|
const robot = slot?.getRobot();
|
2023-10-27 10:01:59 +00:00
|
|
|
|
2024-01-06 12:33:57 +00:00
|
|
|
if (!(slot?.token != null) || !(robot?.encPrivKey != null)) return null;
|
2023-10-27 10:01:59 +00:00
|
|
|
|
|
|
|
await apiClient.post(
|
|
|
|
this.url,
|
|
|
|
`${this.basePath}/api/stealth/`,
|
|
|
|
{ wantsStealth },
|
|
|
|
{ tokenSHA256: robot.tokenSHA256 },
|
|
|
|
);
|
|
|
|
|
2024-01-06 12:33:57 +00:00
|
|
|
garage.upsertRobot(slot?.token, this.shortAlias, {
|
2023-11-21 17:36:59 +00:00
|
|
|
stealthInvoices: wantsStealth,
|
|
|
|
});
|
2023-10-27 10:01:59 +00:00
|
|
|
|
|
|
|
return null;
|
|
|
|
};
|
2022-10-30 19:13:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export default Coordinator;
|