trustwallet-assets/script/generic/asset-infos.ts
Adam R 63c1dcaf1a
[Internal] Add check for tags field (array) (#5960)
* Add check for tags field (array)

* Lint fix

* Using reduce() instead of forEach() for checking all values

Co-authored-by: Catenocrypt <catenocrypt@users.noreply.github.com>
2021-03-18 00:27:21 +01:00

299 lines
12 KiB
TypeScript

import {
allChains,
getChainAssetsList,
getChainAssetsPath,
getChainAssetInfoPath
} from "./repo-structure";
import { isPathExistsSync } from "./filesystem";
import { arrayDiff } from "./types";
import { isValidJSON, readJsonFile, writeJsonFile } from "../generic/json";
import { ActionInterface, CheckStepInterface } from "../generic/interface";
import { CoinType } from "@trustwallet/wallet-core";
import { isValidStatusValue } from "../generic/status-values";
import { isValidTagValues } from "../generic/tag-values";
import * as bluebird from "bluebird";
const requiredKeys = ["name", "type", "symbol", "decimals", "description", "website", "explorer", "status", "id"];
function isAssetInfoHasAllKeys(info: unknown, path: string): [boolean, string] {
const infoKeys = Object.keys(info);
const hasAllKeys = requiredKeys.every(k => Object.prototype.hasOwnProperty.call(info, k));
return [hasAllKeys, `Info at path '${path}' missing next key(s): ${arrayDiff(requiredKeys, infoKeys)}`];
}
// return error, warning, and fixed into if applicable
function isAssetInfoValid(info: unknown, path: string, address: string, chain: string, checkOnly: boolean): [string, string, unknown?] {
let fixedInfo: unknown|null = null;
const isKeys1CorrectType =
typeof info['name'] === "string" && info['name'] !== "" &&
typeof info['type'] === "string" && info['type'] !== "" &&
typeof info['symbol'] === "string" && info['symbol'] !== "" &&
typeof info['decimals'] === "number" && //(info['description'] === "-" || info['decimals'] !== 0) &&
typeof info['status'] === "string" && info['status'] !== "" &&
typeof info['id'] === "string" && info['id'] !== ""
;
if (!isKeys1CorrectType) {
return [`Field missing or invalid; name '${info['name']}' type '${info['type']}' symbol '${info['symbol']}' decimals '${info['decimals']}' id '${info['id']}' ${path}`, "", fixedInfo];
}
// type
if (chainFromAssetType(info['type'].toUpperCase()) !== chain ) {
return [`Incorrect value for type '${info['type']}' '${chain}' ${path}`, "", fixedInfo];
}
if (info['type'] !== info['type'].toUpperCase()) {
// type is correct value, but casing is wrong, fix
if (checkOnly) {
return ["", `Wrong casing for type '${info['type']}' '${chain}' ${path}`, fixedInfo];
}
// fix
if (!fixedInfo) { fixedInfo = info; }
fixedInfo['type'] = info['type'].toUpperCase();
}
// id, should match address
if (info['id'] != address) {
if (checkOnly) {
if (info['id'].toUpperCase() != address.toUpperCase()) {
return [`Incorrect value for id '${info['id']}' '${chain}' ${path}`, "", fixedInfo];
}
// is is correct value, but casing is wrong
return ["", `Wrong casing for id '${info['id']}' '${chain}' ${path}`, fixedInfo];
}
// fix
if (!fixedInfo) { fixedInfo = info; }
fixedInfo['id'] = address;
}
// status
if (!isValidStatusValue(info['status'])) {
return [`Invalid value for status field, '${info['status']}'`, "", fixedInfo];
}
// tags
if (info['tags']) {
if (!isValidTagValues(info['tags'])) {
return [`Invalid tags, '${info['tags']}'`, "", fixedInfo];
}
}
const isKeys2CorrectType =
typeof info['description'] === "string" && info['description'] !== "" &&
// website should be set (exception description='-' marks empty infos)
typeof info['website'] === "string" && (info['description'] === "-" || info['website'] !== "") &&
typeof info['explorer'] === "string" && info['explorer'] != "";
if (!isKeys2CorrectType) {
return [`Check keys2 '${info['description']}' '${info['website']}' '${info['explorer']}' ${path}`, "", fixedInfo];
}
if (info['description'].length > 500) {
const msg = `Description too long, ${info['description'].length}, ${path}`;
return [msg, "", fixedInfo];
}
return ["", "", fixedInfo];
}
export function chainFromAssetType(type: string): string {
switch (type) {
case "ERC20": return "ethereum";
case "BEP2": return "binance";
case "BEP20": return "smartchain";
case "ETC20": return "classic";
case "TRC10":
case "TRC20":
return "tron";
case "WAN20": return "wanchain";
case "TRC21": return "tomochain";
case "TT20": return "thundertoken";
case "SPL": return "solana";
case "GO20": return "gochain";
case "KAVA": return "kava";
case "NEP5": return "neo";
case "NRC20": return "nuls";
case "VET": return "vechain";
case "ONTOLOGY": return "ontology";
default: return "";
}
}
export function explorerUrl(chain: string, contract: string): string {
if (contract) {
switch (chain.toLowerCase()) {
case CoinType.name(CoinType.ethereum).toLowerCase():
return `https://etherscan.io/token/${contract}`;
case CoinType.name(CoinType.tron).toLowerCase():
if (contract.startsWith("10")) {
// trc10
return `https://tronscan.io/#/token/${contract}`;
}
// trc20
return `https://tronscan.io/#/token20/${contract}`;
case CoinType.name(CoinType.binance).toLowerCase():
return `https://explorer.binance.org/asset/${contract}`;
case CoinType.name(CoinType.smartchain).toLowerCase():
case "smartchain":
return `https://bscscan.com/token/${contract}`;
case CoinType.name(CoinType.neo).toLowerCase():
return `https://neo.tokenview.com/en/token/0x${contract}`;
case CoinType.name(CoinType.nuls).toLowerCase():
return `https://nulscan.io/token/info?contractAddress=${contract}`;
case CoinType.name(CoinType.wanchain).toLowerCase():
return `https://www.wanscan.org/token/${contract}`;
case CoinType.name(CoinType.solana).toLowerCase():
return `https://explorer.solana.com/address/${contract}`;
case CoinType.name(CoinType.tomochain).toLowerCase():
return `https://scan.tomochain.com/address/${contract}`;
case CoinType.name(CoinType.kava).toLowerCase():
return "https://www.mintscan.io/kava";
case CoinType.name(CoinType.ontology).toLowerCase():
return "https://explorer.ont.io";
case CoinType.name(CoinType.gochain).toLowerCase():
return `https://explorer.gochain.io/addr/${contract}`;
case CoinType.name(CoinType.thundertoken).toLowerCase():
case "thundertoken":
return `https://scan.thundercore.com/`;
case CoinType.name(CoinType.classic).toLowerCase():
case "classic":
return `https://blockscout.com/etc/mainnet/tokens/${contract}`;
}
}
return "";
}
function explorerUrlAlternatives(chain: string, contract: string, name: string): string[] {
const altUrls: string[] = [];
if (name) {
const nameNorm = name.toLowerCase().replace(' ', '').replace(')', '').replace('(', '');
if (chain.toLowerCase() == CoinType.name(CoinType.ethereum)) {
altUrls.push(`https://etherscan.io/token/${nameNorm}`);
}
altUrls.push(`https://explorer.${nameNorm}.io`);
altUrls.push(`https://scan.${nameNorm}.io`);
}
return altUrls;
}
// Check the an assets's info.json; for errors/warning. Also does fixes in certain cases
function isAssetInfoOK(chain: string, address: string, errors: string[], warnings: string[], checkOnly: boolean): void {
const assetInfoPath = getChainAssetInfoPath(chain, address);
if (!isPathExistsSync(assetInfoPath)) {
// Info file doesn't exist, no need to check
return;
}
if (!isValidJSON(assetInfoPath)) {
console.log(`JSON at path: '${assetInfoPath}' is invalid`);
errors.push(`JSON at path: '${assetInfoPath}' is invalid`);
return;
}
let info: unknown = readJsonFile(assetInfoPath);
let fixedInfo: unknown|null = null;
const [hasAllKeys, msg1] = isAssetInfoHasAllKeys(info, assetInfoPath);
if (!hasAllKeys) {
console.log(msg1);
errors.push(msg1);
}
const [err2, warn2, fixedInfo2] = isAssetInfoValid(info, assetInfoPath, address, chain, checkOnly);
if (err2) {
errors.push(err2);
}
if (warn2) {
warnings.push(warn2);
}
if (fixedInfo2 && !checkOnly) {
info = fixedInfo2;
fixedInfo = fixedInfo2;
}
const hasExplorer = Object.prototype.hasOwnProperty.call(info, 'explorer');
if (!hasExplorer) {
errors.push(`Missing explorer key`);
} else {
const explorerActual = info['explorer'];
const explorerActualLower = explorerActual.toLowerCase();
const explorerExpected = explorerUrl(chain, address);
if (explorerActualLower != explorerExpected.toLowerCase() && explorerExpected) {
// doesn't match, check for alternatives
const explorersAlt = explorerUrlAlternatives(chain, address, info['name']);
if (explorersAlt && explorersAlt.length > 0) {
let matchCount = 0;
explorersAlt.forEach(exp => { if (exp.toLowerCase() == explorerActualLower) { ++matchCount; }});
if (matchCount == 0) {
// none matches, this is warning/error
if (chain.toLowerCase() == CoinType.name(CoinType.ethereum) || chain.toLowerCase() == CoinType.name(CoinType.smartchain)) {
errors.push(`Incorrect explorer, ${explorerActual} instead of ${explorerExpected} (${explorersAlt.join(', ')})`);
} else {
warnings.push(`Unexpected explorer, ${explorerActual} instead of ${explorerExpected} (${explorersAlt.join(', ')})`);
}
}
}
}
}
if (fixedInfo && !checkOnly) {
writeJsonFile(assetInfoPath, fixedInfo);
console.log(`Done fixes to info.json, ${assetInfoPath}`);
}
}
export class AssetInfos implements ActionInterface {
getName(): string { return "Asset Infos"; }
getSanityChecks(): CheckStepInterface[] {
const steps: CheckStepInterface[] = [];
allChains.forEach(chain => {
// only if there is no assets subfolder
if (isPathExistsSync(getChainAssetsPath(chain))) {
steps.push(
{
getName: () => { return `Info.json's for chain ${chain}`;},
check: async () => {
const errors: string[] = [];
const warnings: string[] = [];
const assetsList = getChainAssetsList(chain);
//console.log(` Found ${assetsList.length} assets for chain ${chain}`);
await bluebird.each(assetsList, async (address) => {
isAssetInfoOK(chain, address, errors, warnings, true);
});
return [errors, warnings];
}
}
);
}
});
return steps;
}
async consistencyFix(): Promise<void> {
bluebird.each(allChains, async chain => {
// only if there is no assets subfolder
if (isPathExistsSync(getChainAssetsPath(chain))) {
const errors: string[] = [];
const warnings: string[] = [];
const assetsList = getChainAssetsList(chain);
await bluebird.each(assetsList, async (address) => {
isAssetInfoOK(chain, address, errors, warnings, false);
});
}
});
}
}