2021-02-01 15:45:55 +00:00
|
|
|
// Handling of tokenlist.json files, tokens and trading pairs.
|
2021-01-29 06:45:43 +00:00
|
|
|
|
2021-01-25 14:23:03 +00:00
|
|
|
import { readJsonFile, writeJsonFile } from "../generic/json";
|
|
|
|
import { diff } from "jsondiffpatch";
|
2021-01-29 06:45:43 +00:00
|
|
|
import { tokenInfoFromTwApi, TokenTwInfo } from "../generic/asset";
|
|
|
|
import {
|
2021-01-29 07:26:38 +00:00
|
|
|
getChainAssetLogoPath,
|
|
|
|
getChainAllowlistPath,
|
|
|
|
getChainTokenlistPath,
|
2021-01-29 06:45:43 +00:00
|
|
|
} from "../generic/repo-structure";
|
|
|
|
import * as bluebird from "bluebird";
|
2021-01-29 07:26:38 +00:00
|
|
|
import { isPathExistsSync } from "../generic/filesystem";
|
2021-01-23 00:31:03 +00:00
|
|
|
|
2020-10-14 01:20:42 +00:00
|
|
|
class Version {
|
|
|
|
major: number
|
|
|
|
minor: number
|
|
|
|
patch: number
|
|
|
|
|
|
|
|
constructor(major: number, minor: number, patch: number) {
|
|
|
|
this.major = major
|
|
|
|
this.minor = minor
|
|
|
|
this.patch = patch
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-25 14:23:03 +00:00
|
|
|
export class List {
|
2020-10-14 01:20:42 +00:00
|
|
|
name: string
|
|
|
|
logoURI: string
|
|
|
|
timestamp: string
|
2021-01-23 00:04:56 +00:00
|
|
|
tokens: TokenItem[]
|
|
|
|
pairs: Pair[]
|
2020-10-14 01:20:42 +00:00
|
|
|
version: Version
|
|
|
|
|
2021-01-23 00:04:56 +00:00
|
|
|
constructor(name: string, logoURI: string, timestamp: string, tokens: TokenItem[], version: Version) {
|
2020-10-14 01:20:42 +00:00
|
|
|
this.name = name
|
|
|
|
this.logoURI = logoURI
|
|
|
|
this.timestamp = timestamp;
|
|
|
|
this.tokens = tokens
|
|
|
|
this.version = version
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-23 00:04:56 +00:00
|
|
|
export class TokenItem {
|
2020-10-14 01:20:42 +00:00
|
|
|
asset: string;
|
2020-10-23 04:10:51 +00:00
|
|
|
type: string;
|
2020-10-14 01:20:42 +00:00
|
|
|
address: string;
|
|
|
|
name: string;
|
|
|
|
symbol: string;
|
|
|
|
decimals: number;
|
|
|
|
logoURI: string;
|
2021-01-23 00:04:56 +00:00
|
|
|
pairs: Pair[];
|
2020-10-14 01:20:42 +00:00
|
|
|
|
2021-01-23 00:04:56 +00:00
|
|
|
constructor(asset: string, type: string, address: string, name: string, symbol: string, decimals: number, logoURI: string, pairs: Pair[]) {
|
2020-10-14 01:20:42 +00:00
|
|
|
this.asset = asset
|
2020-10-23 04:10:51 +00:00
|
|
|
this.type = type
|
2020-10-14 01:20:42 +00:00
|
|
|
this.address = address
|
|
|
|
this.name = name;
|
|
|
|
this.symbol = symbol
|
|
|
|
this.decimals = decimals
|
|
|
|
this.logoURI = logoURI
|
|
|
|
this.pairs = pairs
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-23 00:04:56 +00:00
|
|
|
export class Pair {
|
2020-10-14 01:20:42 +00:00
|
|
|
base: string;
|
2021-01-25 14:23:03 +00:00
|
|
|
lotSize?: string;
|
|
|
|
tickSize?: string;
|
2020-10-14 01:20:42 +00:00
|
|
|
|
2021-01-25 14:23:03 +00:00
|
|
|
constructor(base: string, lotSize?: string, tickSize?: string) {
|
2020-10-14 01:20:42 +00:00
|
|
|
this.base = base
|
|
|
|
this.lotSize = lotSize
|
|
|
|
this.tickSize = tickSize
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-01 15:45:55 +00:00
|
|
|
///// Exclude/Include list token/pair matching
|
|
|
|
|
|
|
|
// A token or pair in the force exclude/include list
|
|
|
|
export class ForceListPair {
|
|
|
|
token1: string;
|
|
|
|
// second is optional, if empty --> token only, if set --> pair
|
|
|
|
token2: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function parseForceListEntry(rawForceListEntry: string): ForceListPair {
|
|
|
|
const pair: ForceListPair = new ForceListPair();
|
|
|
|
const tokens: string[] = rawForceListEntry.split("-");
|
|
|
|
pair.token1 = tokens[0];
|
|
|
|
pair.token2 = "";
|
|
|
|
if (tokens.length >= 2) {
|
|
|
|
pair.token2 = tokens[1];
|
|
|
|
}
|
|
|
|
return pair;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function parseForceList(rawForceList: string[]): ForceListPair[] {
|
|
|
|
return rawForceList.map(e => parseForceListEntry(e));
|
|
|
|
}
|
|
|
|
|
|
|
|
export function matchTokenToForceListEntry(token: TokenItem, forceListEntry: string): boolean {
|
|
|
|
if (forceListEntry.toLowerCase() === token.symbol.toLowerCase() ||
|
|
|
|
forceListEntry.toLowerCase() === token.asset.toLowerCase() ||
|
|
|
|
forceListEntry.toLowerCase() === token.name.toLowerCase()) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function matchPairToForceListEntry(token1: TokenItem, token2: TokenItem, forceListEntry: ForceListPair): boolean {
|
|
|
|
if (!forceListEntry.token2) {
|
|
|
|
// entry is token only
|
|
|
|
if (matchTokenToForceListEntry(token1, forceListEntry.token1) ||
|
|
|
|
(token2 && matchTokenToForceListEntry(token2, forceListEntry.token1))) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
// entry is pair
|
|
|
|
if (!token2) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if (matchTokenToForceListEntry(token1, forceListEntry.token1) && matchTokenToForceListEntry(token2, forceListEntry.token2)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
// reverse
|
|
|
|
if (matchTokenToForceListEntry(token1, forceListEntry.token2) && matchTokenToForceListEntry(token2, forceListEntry.token1)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function matchTokenToForceList(token: TokenItem, forceList: ForceListPair[]): boolean {
|
|
|
|
let matched = false;
|
|
|
|
forceList.forEach(e => {
|
|
|
|
if (matchTokenToForceListEntry(token, e.token1)) {
|
|
|
|
matched = true;
|
|
|
|
}
|
|
|
|
if (matchTokenToForceListEntry(token, e.token2)) {
|
|
|
|
matched = true;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return matched;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function matchPairToForceList(token1: TokenItem, token2: TokenItem, forceList: ForceListPair[]): boolean {
|
|
|
|
let matched = false;
|
|
|
|
forceList.forEach(p => {
|
|
|
|
if (matchPairToForceListEntry(token1, token2, p)) {
|
|
|
|
matched = true;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return matched;
|
|
|
|
}
|
|
|
|
|
|
|
|
/////
|
|
|
|
|
2021-01-29 06:45:43 +00:00
|
|
|
export function createTokensList(titleCoin: string, tokens: TokenItem[], time: string, versionMajor: number, versionMinor = 1, versionPatch = 0): List {
|
2021-01-25 14:23:03 +00:00
|
|
|
if (!time) {
|
|
|
|
time = (new Date()).toISOString();
|
|
|
|
}
|
|
|
|
const list = new List(
|
2021-01-23 00:04:56 +00:00
|
|
|
`Trust Wallet: ${titleCoin}`,
|
2020-10-14 01:20:42 +00:00
|
|
|
"https://trustwallet.com/assets/images/favicon.png",
|
2021-01-25 14:23:03 +00:00
|
|
|
time,
|
|
|
|
tokens,
|
|
|
|
new Version(versionMajor, versionMinor, versionPatch)
|
|
|
|
);
|
|
|
|
sort(list);
|
|
|
|
return list;
|
2020-10-14 01:20:42 +00:00
|
|
|
}
|
2021-01-23 00:31:03 +00:00
|
|
|
|
|
|
|
function totalPairs(list: List): number {
|
|
|
|
let c = 0;
|
|
|
|
list.tokens.forEach(t => c += (t.pairs || []).length);
|
|
|
|
return c;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function writeToFile(filename: string, list: List): void {
|
|
|
|
writeJsonFile(filename, list);
|
|
|
|
console.log(`Tokenlist: list with ${list.tokens.length} tokens and ${totalPairs(list)} pairs written to ${filename}.`);
|
|
|
|
}
|
2021-01-25 14:23:03 +00:00
|
|
|
|
|
|
|
// Write out to file, updating version+timestamp if there was change
|
|
|
|
export function writeToFileWithUpdate(filename: string, list: List): void {
|
|
|
|
let listOld: List = undefined;
|
|
|
|
try {
|
|
|
|
listOld = readJsonFile(filename) as List;
|
|
|
|
} catch (err) {
|
|
|
|
listOld = undefined;
|
|
|
|
}
|
2021-01-29 06:45:43 +00:00
|
|
|
if (listOld !== undefined) {
|
2021-01-25 14:35:52 +00:00
|
|
|
list.version = listOld.version; // take over
|
2021-01-29 06:45:43 +00:00
|
|
|
list.timestamp = listOld.timestamp; // take over
|
2021-01-25 14:23:03 +00:00
|
|
|
const diffs = diffTokenlist(list, listOld);
|
|
|
|
if (diffs != undefined) {
|
|
|
|
//console.log("List has Changed", JSON.stringify(diffs));
|
2021-01-25 14:35:52 +00:00
|
|
|
list.version = new Version(list.version.major + 1, 0, 0);
|
2021-01-29 06:45:43 +00:00
|
|
|
list.timestamp = (new Date()).toISOString();
|
|
|
|
console.log(`Version and timestamp updated, ${list.version.major}.${list.version.minor}.${list.version.patch} timestamp ${list.timestamp}`);
|
2021-01-25 14:23:03 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
writeToFile(filename, list);
|
|
|
|
}
|
|
|
|
|
2021-01-29 06:45:43 +00:00
|
|
|
async function addTokenIfNeeded(token: TokenItem, list: List): Promise<void> {
|
|
|
|
if (list.tokens.map(t => t.address.toLowerCase()).includes(token.address.toLowerCase())) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
token = await updateTokenInfo(token);
|
|
|
|
list.tokens.push(token);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update/fix token info, with properties retrieved from TW API
|
|
|
|
async function updateTokenInfo(token: TokenItem): Promise<TokenItem> {
|
|
|
|
const tokenInfo: TokenTwInfo = await tokenInfoFromTwApi(token.asset);
|
|
|
|
if (tokenInfo) {
|
|
|
|
if (token.name && token.name != tokenInfo.name) {
|
|
|
|
console.log(`Token name adjusted: '${token.name}' -> '${tokenInfo.name}'`);
|
|
|
|
token.name = tokenInfo.name;
|
|
|
|
}
|
|
|
|
if (token.symbol && token.symbol != tokenInfo.symbol) {
|
|
|
|
console.log(`Token symbol adjusted: '${token.symbol}' -> '${tokenInfo.symbol}'`);
|
|
|
|
token.symbol = tokenInfo.symbol;
|
|
|
|
}
|
|
|
|
if (token.decimals && token.decimals != tokenInfo.decimals) {
|
|
|
|
console.log(`Token decimals adjusted: '${token.decimals}' -> '${tokenInfo.decimals}'`);
|
|
|
|
token.decimals = parseInt(tokenInfo.decimals.toString());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return token;
|
|
|
|
}
|
|
|
|
|
|
|
|
function addPairToToken(pairToken: TokenItem, token: TokenItem, list: List): void {
|
|
|
|
const tokenInList = list.tokens.find(t => t.address === token.address);
|
|
|
|
if (!tokenInList) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (!tokenInList.pairs) {
|
|
|
|
tokenInList.pairs = [];
|
|
|
|
}
|
|
|
|
if (tokenInList.pairs.map(p => p.base).includes(pairToken.asset)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
tokenInList.pairs.push(new Pair(pairToken.asset));
|
|
|
|
}
|
|
|
|
|
2021-01-29 07:26:38 +00:00
|
|
|
function checkTokenExists(id: string, chainName: string, tokenAllowlist: string[]): boolean {
|
|
|
|
const logoPath = getChainAssetLogoPath(chainName, id);
|
|
|
|
if (!isPathExistsSync(logoPath)) {
|
|
|
|
//console.log("logo file missing", logoPath);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if (tokenAllowlist.find(t => (id.toLowerCase() === t.toLowerCase())) === undefined) {
|
|
|
|
//console.log(`Token not found in allowlist, ${id}`);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2021-01-29 06:45:43 +00:00
|
|
|
export async function addPairIfNeeded(token0: TokenItem, token1: TokenItem, list: List): Promise<void> {
|
|
|
|
await addTokenIfNeeded(token0, list);
|
|
|
|
await addTokenIfNeeded(token1, list);
|
|
|
|
addPairToToken(token1, token0, list);
|
|
|
|
// reverse direction not needed addPairToToken(token0, token1, list);
|
|
|
|
}
|
|
|
|
|
2021-01-25 14:23:03 +00:00
|
|
|
function sort(list: List) {
|
|
|
|
list.tokens.sort((t1, t2) => {
|
|
|
|
const t1pairs = (t1.pairs || []).length;
|
|
|
|
const t2pairs = (t2.pairs || []).length;
|
|
|
|
if (t1pairs != t2pairs) { return t2pairs - t1pairs; }
|
|
|
|
return t1.address.localeCompare(t2.address);
|
|
|
|
});
|
|
|
|
list.tokens.forEach(t => {
|
|
|
|
t.pairs.sort((p1, p2) => p1.base.localeCompare(p2.base));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-02-01 15:45:55 +00:00
|
|
|
function removeAllPairs(list: List) {
|
|
|
|
// remove all pairs
|
|
|
|
list.pairs = [];
|
|
|
|
list.tokens.forEach(t => t.pairs = []);
|
|
|
|
}
|
|
|
|
|
2021-01-25 14:23:03 +00:00
|
|
|
function clearUnimportantFields(list: List) {
|
|
|
|
list.timestamp = "";
|
|
|
|
list.version = new Version(0, 0, 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
export function diffTokenlist(listOrig1: List, listOrig2: List): unknown {
|
|
|
|
// deep copy, to avoid changes
|
|
|
|
const list1 = JSON.parse(JSON.stringify(listOrig1));
|
|
|
|
const list2 = JSON.parse(JSON.stringify(listOrig2));
|
|
|
|
clearUnimportantFields(list1);
|
|
|
|
clearUnimportantFields(list2);
|
|
|
|
sort(list1);
|
|
|
|
sort(list2);
|
|
|
|
// compare
|
|
|
|
const diffs = diff(list1, list2);
|
|
|
|
return diffs;
|
|
|
|
}
|
2021-01-29 06:45:43 +00:00
|
|
|
|
2021-02-01 15:45:55 +00:00
|
|
|
export async function rebuildTokenlist(chainName: string, pairs: [TokenItem, TokenItem][], listName: string, forceExcludeList: string[]): Promise<void> {
|
2021-01-29 06:45:43 +00:00
|
|
|
// sanity check, prevent deletion of many pairs
|
|
|
|
if (!pairs || pairs.length < 5) {
|
|
|
|
console.log(`Warning: Only ${pairs.length} pairs returned, ignoring`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-02-01 15:45:55 +00:00
|
|
|
const excludeList = parseForceList(forceExcludeList);
|
|
|
|
// filter out pairs with missing and excluded tokens
|
2021-01-29 07:26:38 +00:00
|
|
|
// prepare phase, read allowlist
|
|
|
|
const allowlist: string[] = readJsonFile(getChainAllowlistPath(chainName)) as string[];
|
|
|
|
const pairs2: [TokenItem, TokenItem][] = [];
|
|
|
|
pairs.forEach(p => {
|
|
|
|
if (!checkTokenExists(p[0].address, chainName, allowlist)) {
|
|
|
|
console.log("pair with unsupported 1st coin:", p[0].symbol, "--", p[1].symbol);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (!checkTokenExists(p[1].address, chainName, allowlist)) {
|
|
|
|
console.log("pair with unsupported 2nd coin:", p[0].symbol, "--", p[1].symbol);
|
|
|
|
return;
|
|
|
|
}
|
2021-02-01 15:45:55 +00:00
|
|
|
if (matchPairToForceList(p[0], p[1], excludeList)) {
|
|
|
|
console.log("pair excluded due to FORCE EXCLUDE:", p[0].symbol, "--", p[1].symbol);
|
|
|
|
return;
|
|
|
|
}
|
2021-01-29 07:26:38 +00:00
|
|
|
pairs2.push(p);
|
|
|
|
});
|
|
|
|
const filteredCount: number = pairs.length - pairs2.length;
|
|
|
|
console.log(`${filteredCount} unsupported tokens filtered out, ${pairs2.length} pairs`);
|
|
|
|
|
2021-01-29 06:45:43 +00:00
|
|
|
const tokenlistFile = getChainTokenlistPath(chainName);
|
2021-02-01 15:45:55 +00:00
|
|
|
const json = readJsonFile(tokenlistFile);
|
2021-01-29 06:45:43 +00:00
|
|
|
const list: List = json as List;
|
2021-02-01 15:45:55 +00:00
|
|
|
console.log(`Tokenlist original: ${list.tokens.length} tokens`);
|
|
|
|
removeAllPairs(list);
|
2021-01-29 06:45:43 +00:00
|
|
|
|
2021-01-29 07:26:38 +00:00
|
|
|
await bluebird.each(pairs2, async (p) => {
|
2021-01-29 06:45:43 +00:00
|
|
|
await addPairIfNeeded(p[0], p[1], list);
|
|
|
|
});
|
|
|
|
console.log(`Tokenlist updated: ${list.tokens.length} tokens`);
|
|
|
|
|
|
|
|
const newList = createTokensList(listName, list.tokens,
|
|
|
|
"2021-01-29T01:02:03.000+00:00", // use constant here to prevent changing time every time
|
|
|
|
0, 1, 0);
|
|
|
|
writeToFileWithUpdate(tokenlistFile, newList);
|
|
|
|
}
|