trustwallet-assets/script/generic/asset-infos.ts

160 lines
6.2 KiB
TypeScript
Raw Normal View History

import {
allChains,
getChainAssetsList,
getChainAssetsPath,
getChainAssetInfoPath
} from "./repo-structure";
import {
readFileSync,
isPathExistsSync
} from "./filesystem";
import { arrayDiff } from "./types";
import { isValidJSON } from "../generic/json";
import { ActionInterface, CheckStepInterface } from "../generic/interface";
import { CoinType } from "@trustwallet/wallet-core";
import * as bluebird from "bluebird";
const requiredKeys = ["explorer", "name", "website", "short_description"];
function isAssetInfoHasAllKeys(info: unknown, path: string): [boolean, string] {
const infoKeys = Object.keys(info);
const hasAllKeys = requiredKeys.every(k => Object.prototype.hasOwnProperty.call(info, k));
if (!hasAllKeys) {
return [false, `Info at path '${path}' missing next key(s): ${arrayDiff(requiredKeys, infoKeys)}`];
}
const isKeysCorrentType =
typeof info['explorer'] === "string" && info['explorer'] != ""
&& typeof info['name'] === "string" && info['name'] != ""
&& typeof info['website'] === "string"
&& typeof info['short_description'] === "string";
return [isKeysCorrentType, `Check keys '${requiredKeys}' vs. '${infoKeys}'`];
}
function explorerUrl(chain: string, contract: string): string {
if (contract) {
switch (chain.toLowerCase()) {
case CoinType.name(CoinType.ethereum):
return `https://etherscan.io/token/${contract}`;
case CoinType.name(CoinType.tron):
if (contract.startsWith("10")) {
// trc10
return `https://tronscan.io/#/token/${contract}`;
}
// trc20
return `https://tronscan.io/#/token20/${contract}`;
case CoinType.name(CoinType.binance):
return `https://explorer.binance.org/asset/${contract}`;
case CoinType.name(CoinType.smartchain):
return `https://bscscan.com/token/${contract}`;
case CoinType.name(CoinType.neo):
return `https://neo.tokenview.com/en/token/0x${contract}`;
case CoinType.name(CoinType.nuls):
return `https://nulscan.io/token/info?contractAddress=${contract}`;
case CoinType.name(CoinType.wanchain):
return `https://www.wanscan.org/token/${contract}`;
case CoinType.name(CoinType.solana):
return `https://explorer.solana.com/address/${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;
}
function isAssetInfoOK(chain: string, address: string, errors: string[], warnings: string[]): 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;
}
const info = JSON.parse(readFileSync(assetInfoPath));
const [hasAllKeys, msg] = isAssetInfoHasAllKeys(info, assetInfoPath);
if (!hasAllKeys) {
console.log(msg);
errors.push(msg);
}
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(', ')})`);
}
}
}
}
}
}
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);
});
return [errors, warnings];
}
}
);
}
});
return steps;
}
}