2023-10-27 10:01:59 +00:00
|
|
|
import { sha256 } from 'js-sha256';
|
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';
|
|
|
|
import { hexToBase91, validateTokenEntropy } from '../utils';
|
|
|
|
import { compareUpdateLimit } from './Limit.model';
|
|
|
|
import { signCleartextMessage } from '../pgp';
|
|
|
|
|
|
|
|
export interface Contact {
|
|
|
|
nostr?: string | undefined;
|
|
|
|
pgp?: string | undefined;
|
|
|
|
fingerprint?: string | undefined;
|
|
|
|
email?: string | undefined;
|
|
|
|
telegram?: string | undefined;
|
|
|
|
reddit?: string | undefined;
|
|
|
|
matrix?: 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;
|
|
|
|
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 robot?: Robot | undefined = undefined;
|
|
|
|
public loadingRobot: boolean = true;
|
|
|
|
|
|
|
|
start = async (
|
|
|
|
origin: Origin,
|
|
|
|
settings: Settings,
|
|
|
|
hostUrl: string,
|
|
|
|
onStarted: (shortAlias: string) => void = () => {},
|
|
|
|
): Promise<void> => {
|
|
|
|
if (!this.enabled) return;
|
|
|
|
|
|
|
|
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 = '';
|
|
|
|
}
|
|
|
|
|
|
|
|
this.update(() => {
|
|
|
|
onStarted(this.shortAlias);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
update = async (onUpdate: (shortAlias: string) => void = () => {}): Promise<void> => {
|
|
|
|
const onDataLoad = () => {
|
|
|
|
if (this.isUpdated()) onUpdate(this.shortAlias);
|
|
|
|
};
|
|
|
|
|
|
|
|
this.loadBook(onDataLoad);
|
|
|
|
this.loadLimits(onDataLoad);
|
|
|
|
this.loadInfo(onDataLoad);
|
|
|
|
};
|
|
|
|
|
|
|
|
loadBook = (onDataLoad: () => void = () => {}) => {
|
|
|
|
if (this.loadingBook) return;
|
|
|
|
|
|
|
|
this.loadingBook = true;
|
|
|
|
|
|
|
|
apiClient
|
|
|
|
.get(this.url, `${this.basePath}/api/book/`)
|
|
|
|
.then((data) => {
|
|
|
|
if (data.not_found === undefined) {
|
|
|
|
this.book = (data as PublicOrder[]).map((order) => {
|
|
|
|
order.coordinatorShortAlias = this.shortAlias;
|
|
|
|
return order;
|
|
|
|
});
|
|
|
|
onDataLoad();
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.catch((e) => {
|
|
|
|
console.log(e);
|
|
|
|
})
|
|
|
|
.finally(() => {
|
|
|
|
this.loadingBook = false;
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
loadLimits = (onDataLoad: () => void = () => {}) => {
|
|
|
|
if (this.loadingLimits) return;
|
|
|
|
|
|
|
|
this.loadingLimits = true;
|
|
|
|
|
|
|
|
apiClient
|
|
|
|
.get(this.url, `${this.basePath}/api/limits/`)
|
|
|
|
.then((data) => {
|
2023-10-27 11:00:53 +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;
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
loadInfo = (onDataLoad: () => void = () => {}) => {
|
|
|
|
if (this.loadingInfo) return;
|
|
|
|
|
|
|
|
this.loadingInfo = true;
|
|
|
|
|
|
|
|
apiClient
|
|
|
|
.get(this.url, `${this.basePath}/api/info/`)
|
|
|
|
.then((data) => {
|
|
|
|
this.info = data as Info;
|
|
|
|
onDataLoad();
|
|
|
|
})
|
|
|
|
.catch((e) => {
|
|
|
|
console.log(e);
|
|
|
|
})
|
|
|
|
.finally(() => {
|
|
|
|
this.loadingInfo = false;
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2023-11-01 12:18:00 +00:00
|
|
|
enable = (onEnabled: () => void = () => {}) => {
|
2023-10-27 10:01:59 +00:00
|
|
|
this.enabled = true;
|
2023-11-01 12:18:00 +00:00
|
|
|
this.update(() => {
|
|
|
|
onEnabled();
|
|
|
|
});
|
2023-10-27 10:01:59 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
disable = () => {
|
|
|
|
this.enabled = false;
|
|
|
|
this.info = undefined;
|
|
|
|
this.limits = {};
|
|
|
|
this.book = [];
|
|
|
|
};
|
|
|
|
|
|
|
|
isUpdated = () => {
|
2023-10-27 11:00:53 +00:00
|
|
|
return !((this.loadingBook === this.loadingInfo) === this.loadingLimits);
|
2023-10-27 10:01:59 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
getBaseUrl = () => {
|
|
|
|
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: '' };
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
fecthRobot = async (garage: Garage, index: number): Promise<Robot | null> => {
|
|
|
|
const robot = garage?.getRobot(index);
|
|
|
|
|
|
|
|
if (!robot?.token) return null;
|
|
|
|
|
|
|
|
const authHeaders = robot.getAuthHeaders();
|
|
|
|
|
|
|
|
if (authHeaders === null) return null;
|
|
|
|
|
|
|
|
const { hasEnoughEntropy, bitsEntropy, shannonEntropy } = validateTokenEntropy(robot.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,
|
|
|
|
copiedToken: Boolean(data.found),
|
|
|
|
};
|
|
|
|
})
|
|
|
|
.catch((e) => {
|
|
|
|
console.log(e);
|
|
|
|
});
|
|
|
|
|
|
|
|
garage.updateRobot(
|
|
|
|
{
|
|
|
|
...newAttributes,
|
|
|
|
tokenSHA256: authHeaders.tokenSHA256,
|
|
|
|
loading: false,
|
|
|
|
bitsEntropy,
|
|
|
|
shannonEntropy,
|
|
|
|
},
|
|
|
|
index,
|
|
|
|
);
|
|
|
|
|
|
|
|
return garage.getRobot(index);
|
|
|
|
};
|
|
|
|
|
|
|
|
fetchOrder = async (orderId: number, robot: Robot): Promise<Order | null> => {
|
|
|
|
if (!robot.token) return null;
|
|
|
|
|
|
|
|
const authHeaders = robot.getAuthHeaders();
|
|
|
|
|
|
|
|
if (authHeaders === null) return null;
|
|
|
|
|
|
|
|
return await apiClient
|
|
|
|
.get(this.url, `${this.basePath}/api/order/?order_id=${orderId}`, authHeaders)
|
|
|
|
.then((data) => {
|
|
|
|
return data as Order;
|
|
|
|
})
|
|
|
|
.catch((e) => {
|
|
|
|
console.log(e);
|
|
|
|
return null;
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
fetchReward = async (
|
|
|
|
signedInvoice: string,
|
|
|
|
garage: Garage,
|
|
|
|
index: number,
|
|
|
|
): Promise<null | {
|
|
|
|
bad_invoice?: string;
|
|
|
|
successful_withdrawal?: boolean;
|
|
|
|
}> => {
|
|
|
|
const robot = garage.getRobot(index);
|
|
|
|
|
|
|
|
if (!robot?.token || !robot.encPrivKey) return null;
|
|
|
|
|
|
|
|
const data = await apiClient.post(
|
|
|
|
this.url,
|
|
|
|
`${this.basePath}`,
|
|
|
|
{
|
|
|
|
invoice: signedInvoice,
|
|
|
|
},
|
|
|
|
{ tokenSHA256: robot.tokenSHA256 },
|
|
|
|
);
|
|
|
|
const newRobot = {
|
|
|
|
...robot,
|
|
|
|
earnedRewards: data?.successful_withdrawal ? 0 : robot.earnedRewards,
|
|
|
|
};
|
|
|
|
garage.updateRobot(newRobot, index);
|
|
|
|
|
|
|
|
return data ?? {};
|
|
|
|
};
|
|
|
|
|
|
|
|
fetchStealth = async (wantsStealth: boolean, garage: Garage, index: number): Promise<null> => {
|
|
|
|
const robot = garage?.getRobot(index);
|
|
|
|
|
|
|
|
if (!robot?.token || !robot.encPrivKey) return null;
|
|
|
|
|
|
|
|
await apiClient.post(
|
|
|
|
this.url,
|
|
|
|
`${this.basePath}/api/stealth/`,
|
|
|
|
{ wantsStealth },
|
|
|
|
{ tokenSHA256: robot.tokenSHA256 },
|
|
|
|
);
|
|
|
|
|
|
|
|
garage.updateRobot(
|
|
|
|
{
|
|
|
|
stealthInvoices: wantsStealth,
|
|
|
|
},
|
|
|
|
index,
|
|
|
|
);
|
|
|
|
|
|
|
|
return null;
|
|
|
|
};
|
2022-10-30 19:13:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export default Coordinator;
|