2020-10-19 22:51:47 +00:00
import {
allChains ,
getChainAssetsList ,
getChainAssetsPath ,
getChainAssetInfoPath
} from "./repo-structure" ;
2021-03-01 14:46:10 +00:00
import { isPathExistsSync } from "./filesystem" ;
2020-10-19 22:51:47 +00:00
import { arrayDiff } from "./types" ;
2021-03-01 14:46:10 +00:00
import { isValidJSON , readJsonFile , writeJsonFile } from "../generic/json" ;
2020-10-19 22:51:47 +00:00
import { ActionInterface , CheckStepInterface } from "../generic/interface" ;
2020-12-07 10:56:44 +00:00
import { CoinType } from "@trustwallet/wallet-core" ;
2021-03-17 10:51:03 +00:00
import { isValidStatusValue } from "../generic/status-values" ;
2021-03-17 23:27:21 +00:00
import { isValidTagValues } from "../generic/tag-values" ;
2020-10-19 22:51:47 +00:00
import * as bluebird from "bluebird" ;
2021-02-08 10:38:26 +00:00
const requiredKeys = [ "name" , "type" , "symbol" , "decimals" , "description" , "website" , "explorer" , "status" , "id" ] ;
2020-10-19 22:51:47 +00:00
2020-11-05 22:51:08 +00:00
function isAssetInfoHasAllKeys ( info : unknown , path : string ) : [ boolean , string ] {
2020-10-19 22:51:47 +00:00
const infoKeys = Object . keys ( info ) ;
const hasAllKeys = requiredKeys . every ( k = > Object . prototype . hasOwnProperty . call ( info , k ) ) ;
2021-02-04 16:47:14 +00:00
return [ hasAllKeys , ` Info at path ' ${ path } ' missing next key(s): ${ arrayDiff ( requiredKeys , infoKeys ) } ` ] ;
}
2020-10-19 22:51:47 +00:00
2021-03-01 14:46:10 +00:00
// 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 ;
2021-02-04 16:47:14 +00:00
const isKeys1CorrectType =
typeof info [ 'name' ] === "string" && info [ 'name' ] !== "" &&
typeof info [ 'type' ] === "string" && info [ 'type' ] !== "" &&
typeof info [ 'symbol' ] === "string" && info [ 'symbol' ] !== "" &&
2021-02-08 10:38:26 +00:00
typeof info [ 'decimals' ] === "number" && //(info['description'] === "-" || info['decimals'] !== 0) &&
2021-03-12 18:01:29 +00:00
typeof info [ 'status' ] === "string" && info [ 'status' ] !== "" &&
typeof info [ 'id' ] === "string" && info [ 'id' ] !== ""
2021-02-05 10:59:21 +00:00
;
2021-02-04 16:47:14 +00:00
if ( ! isKeys1CorrectType ) {
2021-03-12 18:01:29 +00:00
return [ ` Field missing or invalid; name ' ${ info [ 'name' ] } ' type ' ${ info [ 'type' ] } ' symbol ' ${ info [ 'symbol' ] } ' decimals ' ${ info [ 'decimals' ] } ' id ' ${ info [ 'id' ] } ' ${ path } ` , "" , fixedInfo ] ;
2021-02-04 16:47:14 +00:00
}
2021-02-05 10:59:21 +00:00
2021-03-01 14:46:10 +00:00
// type
if ( chainFromAssetType ( info [ 'type' ] . toUpperCase ( ) ) !== chain ) {
2021-03-17 10:51:03 +00:00
return [ ` Incorrect value for type ' ${ info [ 'type' ] } ' ' ${ chain } ' ${ path } ` , "" , fixedInfo ] ;
2021-03-01 14:46:10 +00:00
}
if ( info [ 'type' ] !== info [ 'type' ] . toUpperCase ( ) ) {
// type is correct value, but casing is wrong, fix
if ( checkOnly ) {
2021-04-20 14:05:09 +00:00
return [ ` Type should be ALLCAPS ' ${ info [ 'type' ] . toUpperCase ( ) } ' instead of ' ${ info [ 'type' ] } ' ' ${ chain } ' ${ path } ` , "" , fixedInfo ] ;
2021-03-01 14:46:10 +00:00
}
// fix
if ( ! fixedInfo ) { fixedInfo = info ; }
fixedInfo [ 'type' ] = info [ 'type' ] . toUpperCase ( ) ;
2021-02-05 10:59:21 +00:00
}
2021-03-12 18:01:29 +00:00
// id, should match address
if ( info [ 'id' ] != address ) {
if ( checkOnly ) {
if ( info [ 'id' ] . toUpperCase ( ) != address . toUpperCase ( ) ) {
2021-03-17 10:51:03 +00:00
return [ ` Incorrect value for id ' ${ info [ 'id' ] } ' ' ${ chain } ' ${ path } ` , "" , fixedInfo ] ;
2021-03-12 18:01:29 +00:00
}
// is is correct value, but casing is wrong
2021-03-25 13:28:56 +00:00
return [ ` Wrong casing for id ' ${ info [ 'id' ] } ' ' ${ chain } ' ${ path } ` , "" , fixedInfo ] ;
2021-03-12 18:01:29 +00:00
}
// fix
if ( ! fixedInfo ) { fixedInfo = info ; }
fixedInfo [ 'id' ] = address ;
}
2021-03-17 10:51:03 +00:00
// status
if ( ! isValidStatusValue ( info [ 'status' ] ) ) {
2021-03-17 23:27:21 +00:00
return [ ` Invalid value for status field, ' ${ info [ 'status' ] } ' ` , "" , fixedInfo ] ;
}
// tags
if ( info [ 'tags' ] ) {
if ( ! isValidTagValues ( info [ 'tags' ] ) ) {
return [ ` Invalid tags, ' ${ info [ 'tags' ] } ' ` , "" , fixedInfo ] ;
}
2021-03-17 10:51:03 +00:00
}
2021-02-04 16:47:14 +00:00
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 ) {
2021-03-01 14:46:10 +00:00
return [ ` Check keys2 ' ${ info [ 'description' ] } ' ' ${ info [ 'website' ] } ' ' ${ info [ 'explorer' ] } ' ${ path } ` , "" , fixedInfo ] ;
2021-02-04 16:47:14 +00:00
}
if ( info [ 'description' ] . length > 500 ) {
const msg = ` Description too long, ${ info [ 'description' ] . length } , ${ path } ` ;
2021-03-01 14:46:10 +00:00
return [ msg , "" , fixedInfo ] ;
2021-02-04 16:47:14 +00:00
}
2021-03-01 14:46:10 +00:00
return [ "" , "" , fixedInfo ] ;
2020-10-19 22:51:47 +00:00
}
2021-02-08 10:23:34 +00:00
export function chainFromAssetType ( type : string ) : string {
2021-03-01 14:46:10 +00:00
switch ( type ) {
2021-02-08 10:23:34 +00:00
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" ;
2021-05-20 12:33:44 +00:00
case "EOS" : return "eos" ;
2021-02-08 10:23:34 +00:00
case "GO20" : return "gochain" ;
case "KAVA" : return "kava" ;
case "NEP5" : return "neo" ;
case "NRC20" : return "nuls" ;
case "VET" : return "vechain" ;
2021-02-09 22:43:12 +00:00
case "ONTOLOGY" : return "ontology" ;
2021-05-20 12:33:44 +00:00
case "THETA" : return "theta" ;
case "TOMO" : return "tomochain" ;
case "XDAI" : return "xdai" ;
2021-06-03 14:21:12 +00:00
case "WAVES" : return "waves" ;
2021-03-01 14:46:10 +00:00
default : return "" ;
2021-02-08 10:23:34 +00:00
}
}
2021-02-01 11:12:30 +00:00
export function explorerUrl ( chain : string , contract : string ) : string {
2020-10-19 23:22:00 +00:00
if ( contract ) {
switch ( chain . toLowerCase ( ) ) {
2021-02-03 22:42:15 +00:00
case CoinType . name ( CoinType . ethereum ) . toLowerCase ( ) :
2020-10-19 23:22:00 +00:00
return ` https://etherscan.io/token/ ${ contract } ` ;
2021-02-03 22:42:15 +00:00
case CoinType . name ( CoinType . tron ) . toLowerCase ( ) :
2020-10-19 23:22:00 +00:00
if ( contract . startsWith ( "10" ) ) {
// trc10
return ` https://tronscan.io/#/token/ ${ contract } ` ;
}
// trc20
return ` https://tronscan.io/#/token20/ ${ contract } ` ;
2021-02-03 22:42:15 +00:00
case CoinType . name ( CoinType . binance ) . toLowerCase ( ) :
2020-10-19 23:22:00 +00:00
return ` https://explorer.binance.org/asset/ ${ contract } ` ;
2021-02-03 22:42:15 +00:00
case CoinType . name ( CoinType . smartchain ) . toLowerCase ( ) :
2021-02-04 16:47:14 +00:00
case "smartchain" :
2020-10-19 23:22:00 +00:00
return ` https://bscscan.com/token/ ${ contract } ` ;
2021-05-20 12:33:44 +00:00
case CoinType . name ( CoinType . eos ) . toLowerCase ( ) :
return ` https://bloks.io/account/ ${ contract } ` ;
2021-02-03 22:42:15 +00:00
case CoinType . name ( CoinType . neo ) . toLowerCase ( ) :
2020-10-22 14:41:12 +00:00
return ` https://neo.tokenview.com/en/token/0x ${ contract } ` ;
2020-10-19 23:22:00 +00:00
2021-02-03 22:42:15 +00:00
case CoinType . name ( CoinType . nuls ) . toLowerCase ( ) :
2020-10-19 23:22:00 +00:00
return ` https://nulscan.io/token/info?contractAddress= ${ contract } ` ;
2021-02-03 22:42:15 +00:00
case CoinType . name ( CoinType . wanchain ) . toLowerCase ( ) :
2020-10-19 23:22:00 +00:00
return ` https://www.wanscan.org/token/ ${ contract } ` ;
2020-12-07 10:56:44 +00:00
2021-02-03 22:42:15 +00:00
case CoinType . name ( CoinType . solana ) . toLowerCase ( ) :
2020-12-07 10:56:44 +00:00
return ` https://explorer.solana.com/address/ ${ contract } ` ;
2021-02-03 22:42:15 +00:00
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 ( ) :
2021-05-10 13:08:10 +00:00
return ` https://explorer.gochain.io/addr/ ${ contract } ` ;
2021-02-04 16:47:14 +00:00
2021-05-20 12:33:44 +00:00
case CoinType . name ( CoinType . theta ) . toLowerCase ( ) :
return 'https://explorer.thetatoken.org/' ;
2021-02-04 16:47:14 +00:00
case CoinType . name ( CoinType . thundertoken ) . toLowerCase ( ) :
case "thundertoken" :
2021-05-10 13:08:10 +00:00
return ` https://viewblock.io/thundercore/address/ ${ contract } ` ;
2021-02-08 10:08:45 +00:00
case CoinType . name ( CoinType . classic ) . toLowerCase ( ) :
case "classic" :
2021-05-10 13:08:10 +00:00
return ` https://blockscout.com/etc/mainnet/tokens/ ${ contract } ` ;
case CoinType . name ( CoinType . vechain ) . toLowerCase ( ) :
case "vechain" :
return ` https://explore.vechain.org/accounts/ ${ contract } ` ;
2021-05-20 12:33:44 +00:00
2021-06-03 14:21:12 +00:00
case CoinType . name ( CoinType . waves ) . toLowerCase ( ) :
return ` https://wavesexplorer.com/assets/ ${ contract } ` ;
2021-05-20 12:33:44 +00:00
case "xdai" :
return ` https://blockscout.com/xdai/mainnet/tokens/ ${ contract } ` ;
2020-10-19 23:22:00 +00:00
}
}
return "" ;
}
2020-10-22 14:41:12 +00:00
function explorerUrlAlternatives ( chain : string , contract : string , name : string ) : string [ ] {
const altUrls : string [ ] = [ ] ;
if ( name ) {
const nameNorm = name . toLowerCase ( ) . replace ( ' ' , '' ) . replace ( ')' , '' ) . replace ( '(' , '' ) ;
2020-12-07 10:56:44 +00:00
if ( chain . toLowerCase ( ) == CoinType . name ( CoinType . ethereum ) ) {
2020-10-22 14:41:12 +00:00
altUrls . push ( ` https://etherscan.io/token/ ${ nameNorm } ` ) ;
}
altUrls . push ( ` https://explorer. ${ nameNorm } .io ` ) ;
altUrls . push ( ` https://scan. ${ nameNorm } .io ` ) ;
}
return altUrls ;
}
2021-03-01 14:46:10 +00:00
// 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 {
2020-10-19 22:51:47 +00:00
const assetInfoPath = getChainAssetInfoPath ( chain , address ) ;
if ( ! isPathExistsSync ( assetInfoPath ) ) {
2020-10-19 23:22:00 +00:00
// Info file doesn't exist, no need to check
return ;
2020-10-19 22:51:47 +00:00
}
if ( ! isValidJSON ( assetInfoPath ) ) {
console . log ( ` JSON at path: ' ${ assetInfoPath } ' is invalid ` ) ;
2020-10-19 23:22:00 +00:00
errors . push ( ` JSON at path: ' ${ assetInfoPath } ' is invalid ` ) ;
return ;
2020-10-19 22:51:47 +00:00
}
2021-03-01 14:46:10 +00:00
let info : unknown = readJsonFile ( assetInfoPath ) ;
let fixedInfo : unknown | null = null ;
2021-02-04 16:47:14 +00:00
const [ hasAllKeys , msg1 ] = isAssetInfoHasAllKeys ( info , assetInfoPath ) ;
2020-10-19 22:51:47 +00:00
if ( ! hasAllKeys ) {
2021-02-04 16:47:14 +00:00
console . log ( msg1 ) ;
errors . push ( msg1 ) ;
2020-10-19 22:51:47 +00:00
}
2021-03-01 14:46:10 +00:00
const [ err2 , warn2 , fixedInfo2 ] = isAssetInfoValid ( info , assetInfoPath , address , chain , checkOnly ) ;
2021-02-04 16:47:14 +00:00
if ( err2 ) {
errors . push ( err2 ) ;
}
if ( warn2 ) {
warnings . push ( warn2 ) ;
2021-02-03 22:42:15 +00:00
}
2021-03-01 14:46:10 +00:00
if ( fixedInfo2 && ! checkOnly ) {
info = fixedInfo2 ;
fixedInfo = fixedInfo2 ;
}
2021-02-03 22:42:15 +00:00
2021-05-10 13:08:10 +00:00
const explorerExpected = explorerUrl ( chain , address ) ;
2020-10-19 23:22:00 +00:00
const hasExplorer = Object . prototype . hasOwnProperty . call ( info , 'explorer' ) ;
2021-05-10 13:08:10 +00:00
const explorerActual = info [ 'explorer' ] || '' ;
const explorerActualLower = explorerActual . toLowerCase ( ) ;
const explorerExpectedLower = explorerExpected . toLowerCase ( ) ;
if ( checkOnly ) {
if ( ! hasExplorer ) {
errors . push ( ` Missing explorer key ` ) ;
} else {
if ( explorerActualLower !== explorerExpectedLower && 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 ( ', ' ) } ) ` ) ;
}
2020-10-27 15:09:28 +00:00
}
2020-10-22 14:41:12 +00:00
}
}
2020-10-19 23:22:00 +00:00
}
2021-05-10 13:08:10 +00:00
} else {
// fix: simply replace with expected (case-only deviation is accepted)
if ( explorerActualLower !== explorerExpectedLower ) {
if ( ! fixedInfo ) { fixedInfo = info ; }
fixedInfo [ 'explorer' ] = explorerExpected ;
}
2020-10-19 23:22:00 +00:00
}
2021-03-01 14:46:10 +00:00
if ( fixedInfo && ! checkOnly ) {
writeJsonFile ( assetInfoPath , fixedInfo ) ;
console . log ( ` Done fixes to info.json, ${ assetInfoPath } ` ) ;
}
2020-10-19 22:51:47 +00:00
}
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 [ ] = [ ] ;
2020-10-19 23:22:00 +00:00
const warnings : string [ ] = [ ] ;
2020-10-19 22:51:47 +00:00
const assetsList = getChainAssetsList ( chain ) ;
//console.log(` Found ${assetsList.length} assets for chain ${chain}`);
await bluebird . each ( assetsList , async ( address ) = > {
2021-03-01 14:46:10 +00:00
isAssetInfoOK ( chain , address , errors , warnings , true ) ;
2020-10-19 22:51:47 +00:00
} ) ;
2020-10-19 23:22:00 +00:00
return [ errors , warnings ] ;
2020-10-19 22:51:47 +00:00
}
}
) ;
}
} ) ;
return steps ;
}
2021-03-01 14:46:10 +00:00
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 ) ;
} ) ;
}
} ) ;
}
2020-10-19 22:51:47 +00:00
}