Delete all outdated typescript code (#16752)

This commit is contained in:
Daniel 2021-12-22 19:22:21 +03:00 committed by GitHub
parent 1aad5824aa
commit f49f43f09d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 26 additions and 22397 deletions

View File

@ -1,9 +0,0 @@
# don't ever lint node_modules
node_modules
# don't lint build output (make sure it's set to your correct build folder name)
dist
# don't lint nyc coverage output
coverage
.eslintrc.js
jest.config.js

View File

@ -1,12 +0,0 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
plugins: [
'@typescript-eslint',
],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
],
};

View File

@ -37,9 +37,6 @@ jobs:
if: github.repository_owner == 'trustwallet'
run: go run ./cmd/main.go --config=./.github/assets.config.yaml --script=fixer
# - name: Debug
# run: echo "GITHUB_REF " $GITHUB_REF " GITHUB_BASE_REF " $GITHUB_BASE_REF
- name: Show fix result (diff)
run: |
git status

View File

@ -25,4 +25,16 @@ endif
lint: lint-install
@echo " > Running golint"
bin/golangci-lint run --timeout=2m
bin/golangci-lint run --timeout=2m
check:
go run ./cmd/main.go --script=checker
fix:
go run ./cmd/main.go --script=fixer
update-auto:
go run ./cmd/main.go --script=updater-auto
update-manual:
go run ./cmd/main.go --script=updater-manual

View File

@ -3,6 +3,7 @@
![Check](https://github.com/trustwallet/assets/workflows/Check/badge.svg)
## Overview
Trust Wallet token repository is a comprehensive, up-to-date collection of information about several thousands (!) of crypto tokens.
[Trust Wallet](https://trustwallet.com) uses token logos from this source, alongside a number of other projects.
@ -18,7 +19,7 @@ Such a large collection can be maintained only through a community effort, so _f
Please note that __brand new tokens are not accepted__,
the projects have to be sound, with information available, and __non-minimal circulation__
(for limit details see https://developer.trustwallet.com/assets/requirements).
(for limit details see <https://developer.trustwallet.com/assets/requirements>).
### Assets App
@ -31,6 +32,7 @@ Details of the repository structure and contribution guidelines are listed on th
Here is a quick starter summary for the most common use case.
**Adding an ERC20 token checklist**:
- [ ] Make sure your smartcontract has more than 10000 address holders, otherwise you will be rejected
- [ ] Fork the Github repository
- [ ] Create folder with name of token smartcontact address in [_checksum format_](https://piyolab.github.io/sushiether/RunScrapboxCode/?web3=1.0.0-beta.33&code=https://scrapbox.io/api/code/sushiether/web3.js_-_Ethereum_のアドレスをチェックサム付きアドレスに変換する/demo.js) `blockchains/ethereum/assets/<token_smartcontract_address>/`.
@ -42,7 +44,7 @@ Here is a quick starter summary for the most common use case.
## Documentation
For details, see the [Developers site](https://developer.trustwallet.com:
For details, see the [Developers site](https://developer.trustwallet.com):
- [Contribution guidelines](https://developer.trustwallet.com/assets/repository_details)
@ -52,21 +54,18 @@ For details, see the [Developers site](https://developer.trustwallet.com:
There are several scripts available for maintainers:
- `npm run check` -- Execute validation checks; also used in continuous integration.
- `npm run check-sanity` -- Strict subset of checks
- `npm run fix` -- Perform automatic fixes where possible
- `npm run fix-sanity` -- Stricter subset
- `npm run updateAuto` -- Run automatic updates from external sources, executed regularly (GitHub action)
- `npm run update` -- Run manual updates from external sources, for manual use.
- `npm test` -- Run script unit tests
- `npm lint` -- Run Lint static code check
- `make check` -- Execute validation checks; also used in continuous integration.
- `make fix` -- Perform automatic fixes where possible
- `make update-auto` -- Run automatic updates from external sources, executed regularly (GitHub action)
- `make update-manual` -- Run manual updates from external sources, for manual use.
## On Checks
This repo contains a set of scripts for verification of all the information. Implemented as Typescript scripts, available through `npm run check`, and executed in CI build; checks the whole repo.
This repo contains a set of scripts for verification of all the information. Implemented as Golang scripts, available through `make check`, and executed in CI build; checks the whole repo.
There are similar check logic implemented:
- in assets-management app; for checking changed token files in PRs, or when creating a PR. Implemented as a Typescript library, checks diffs, can be run from browser environment.
- in merge-fee-bot, which runs as a GitHub app shows result in PR comment. Also uses library, but executes in a non-browser environment.
- in assets-management app; for checking changed token files in PRs, or when creating a PR. Checks diffs, can be run from browser environment.
- in merge-fee-bot, which runs as a GitHub app shows result in PR comment. Executes in a non-browser environment.
## Trading pair maintenance
@ -77,6 +76,7 @@ Minimal limit values for trading pair inclusion are set in the `config.ts` file.
There are also options for force-include and force-exclude in the config.
## Disclaimer
Trust Wallet team allows anyone to submit new assets to this repository. However, this does not mean that we are in direct partnership with all of the projects.
Trust Wallet team will reject projects that are deemed as scam or fraudulent after careful review.

View File

@ -1,13 +0,0 @@
module.exports = {
"roots": [
"<rootDir>/test"
],
testMatch: [
"**/__tests__/**/*.+(ts|tsx|js)",
"**/?(*.)+(spec|test).+(ts|tsx|js)"
],
"transform": {
"^.+\\.(ts|tsx)?$": "ts-jest"
},
"setupFilesAfterEnv": ["jest-expect-message"]
}

18729
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,72 +0,0 @@
{
"name": "assets",
"version": "1.0.0",
"description": "Assets consumed by Trust Wallet",
"main": "index.js",
"scripts": {
"test": "jest",
"check": "ts-node ./script/entrypoint/check",
"check-sanity": "ts-node ./script/entrypoint/check-sanity",
"fix": "ts-node ./script/entrypoint/fix",
"fix-sanity": "ts-node ./script/entrypoint/fix-sanity",
"updateAuto": "ts-node ./script/entrypoint/updateAuto",
"update": "ts-node ./script/entrypoint/updateManual",
"lint": "npx eslint . --ext .js,.jsx,.ts,.tsx",
"lint:fix": "npx eslint . --ext .js,.jsx,.ts,.tsx --fix"
},
"repository": {
"type": "git",
"url": "git+https://github.com/trustwallet/assets.git"
},
"keywords": [
"ERC20 token images",
"TRC-10 token images",
"TRC-20 token images",
"BEP-2 token images"
],
"engines": {
"node": ">=10.0"
},
"author": "Trust Wallet",
"license": "MIT",
"bugs": {
"url": "https://github.com/trustwallet/assets/issues"
},
"homepage": "https://github.com/trustwallet/assets#readme",
"devDependencies": {
"@trustwallet/wallet-core": "^2.6.12",
"@types/jest": "^25.2.3",
"@types/jest-expect-message": "^1.0.3",
"@types/node": "^13.13.52",
"@typescript-eslint/eslint-plugin": "^4.29.0",
"@typescript-eslint/parser": "^4.29.0",
"axios": "^0.21.1",
"bignumber.js": "^9.0.0",
"bip44-constants": "^8.0.103",
"bluebird": "^3.7.2",
"chalk": "^4.1.2",
"eslint": "^7.32.0",
"ethereum-checksum-address": "0.0.6",
"eztz-lib": "^0.1.2",
"image-size": "^0.8.3",
"is-image": "^3.0.0",
"jest": "^26.6.3",
"jest-expect-message": "^1.0.2",
"meow": "^10.1.1",
"nested-property": "^2.0.2",
"sharp": "^0.28.3",
"tinify": "^1.6.0-beta.2",
"ts-jest": "^26.5.6",
"ts-node": "^9.1.1",
"typescript": "^4.3.5"
},
"dependencies": {
"codecov": "^3.8.3",
"jsondiffpatch": "^0.4.1"
},
"jest": {
"setupFilesAfterEnv": [
"jest-expect-message"
]
}
}

View File

@ -1,253 +0,0 @@
import axios from "axios";
import * as bluebird from "bluebird";
import * as fs from "fs";
import * as path from "path";
import * as chalk from 'chalk';
import * as config from "../config";
import { ActionInterface, CheckStepInterface } from "../generic/interface";
import { Binance } from "../generic/blockchains";
import { readDirSync } from "../generic/filesystem";
import { readJsonFile, writeJsonFile } from "../generic/json";
import { TokenItem, Pair, createTokensList, writeToFileWithUpdate } from "../generic/tokenlists";
import {
getChainAssetLogoPath,
getChainAssetsPath,
getChainAssetInfoPath,
getChainTokenlistPath
} from "../generic/repo-structure";
import { CoinType } from "@trustwallet/wallet-core";
import { toSatoshis } from "../generic/numbers";
import { assetIdSymbol, logoURI, tokenType } from "../generic/asset";
import { TokenType } from "../generic/tokentype";
import { explorerUrl } from "../generic/asset-infos";
const binanceChain = "binance";
const binanceUrlTokenAssets = config.binanceUrlTokenAssets;
let cachedAssets = [];
export class BinanceTokenInfo {
asset: string
name: string
assetImg: string
mappedAsset: string
decimals: number
}
async function retrieveBep2AssetList(): Promise<BinanceTokenInfo[]> {
console.log(`Retrieving token asset infos from: ${binanceUrlTokenAssets}`);
const { assetInfoList } = await axios.get(binanceUrlTokenAssets)
.then(r => r.data)
.catch(function (error) {
console.log(JSON.stringify(error))
});
console.log(`Retrieved ${assetInfoList.length} token asset infos`);
return assetInfoList
}
async function retrieveAssets(): Promise<unknown[]> {
// cache results because of rate limit, used more than once
if (cachedAssets.length == 0) {
console.log(`Retrieving token infos`);
const bep2assets = await axios.get(`${config.binanceDexURL}/v1/tokens?limit=1000`);
const bep8assets = await axios.get(`${config.binanceDexURL}/v1/mini/tokens?limit=1000`);
cachedAssets = bep2assets.data.concat(bep8assets.data);
}
console.log(`Using ${cachedAssets.length} assets`);
return cachedAssets;
}
export async function retrieveAssetSymbols(): Promise<string[]> {
const assets = await retrieveAssets();
const symbols = assets.map(({ symbol }) => symbol);
return symbols;
}
async function fetchImage(url) {
return axios.get(url, { responseType: "stream" })
.then(r => r.data)
.catch(err => {
throw `Error fetchImage: ${url} ${err.message}`;
});
}
/// Return: array with images to fetch; {asset, assetImg}
export function findImagesToFetch(assetInfoList: BinanceTokenInfo[]): BinanceTokenInfo[] {
const toFetch: BinanceTokenInfo[] = [];
console.log(`Checking for asset images to be fetched`);
assetInfoList.forEach((tokenInfo) => {
process.stdout.write(`.${tokenInfo.asset} `);
// pick assets only if img and decimals is present
if (tokenInfo.assetImg && tokenInfo.decimals) {
const imagePath = getChainAssetLogoPath(binanceChain, tokenInfo.asset);
if (!fs.existsSync(imagePath)) {
console.log(chalk.red(`Missing image: ${tokenInfo.asset}`));
toFetch.push(tokenInfo);
}
}
});
console.log();
console.log(`${toFetch.length} asset image(s) to be fetched`);
return toFetch;
}
async function createInfoJson(tokenInfo: BinanceTokenInfo): Promise<void> {
//console.log(tokenInfo);
const info = {
name: tokenInfo.name,
type: "BEP2",
symbol: tokenInfo.mappedAsset,
decimals: tokenInfo.decimals,
website: '',
description: '-',
explorer: explorerUrl(binanceChain, tokenInfo.asset),
status: 'active',
id: tokenInfo.asset
};
const infoPath = getChainAssetInfoPath(binanceChain, tokenInfo.asset);
writeJsonFile(infoPath, info);
}
async function fetchMissingImages(toFetch: BinanceTokenInfo[]): Promise<string[]> {
console.log(`Attempting to fetch ${toFetch.length} asset image(s)`);
const fetchedAssets: string[] = [];
await bluebird.each(toFetch, async (tokenInfo) => {
if (tokenInfo && tokenInfo.asset && tokenInfo.assetImg) {
const imagePath = getChainAssetLogoPath(binanceChain, tokenInfo.asset);
fs.mkdir(path.dirname(imagePath), err => {
if (err && err.code != `EEXIST`) throw err;
});
const buffer = await fetchImage(tokenInfo.assetImg);
await buffer.pipe(fs.createWriteStream(imagePath));
await createInfoJson(tokenInfo);
fetchedAssets.push(tokenInfo.asset)
console.log(`Token ${tokenInfo.asset} ${tokenInfo.mappedAsset}: Fetched image, created info.json (${tokenInfo.assetImg})`)
}
});
console.log();
return fetchedAssets;
}
export class BinanceAction implements ActionInterface {
getName(): string { return "Binance chain"; }
getSanityChecks(): CheckStepInterface[] {
return [
{
getName: () => { return "Binance chain; assets must exist on chain"},
check: async () => {
const errors = [];
const tokenSymbols = await retrieveAssetSymbols();
const assets = readDirSync(getChainAssetsPath(Binance));
assets.forEach(asset => {
if (!(tokenSymbols.indexOf(asset) >= 0)) {
errors.push(`Asset ${asset} missing on chain`);
}
});
console.log(` ${assets.length} assets checked.`);
return [errors, []];
}
},
];
}
async updateAuto(): Promise<void> {
// retrieve missing token images; BEP2 (bep8 not supported)
const bep2InfoList = await retrieveBep2AssetList();
if (bep2InfoList.length < 5) {
console.log(`ERROR: No Binance token info is returned! ${bep2InfoList.length}`);
return;
}
const toFetch = findImagesToFetch(bep2InfoList);
const fetchedAssets = await fetchMissingImages(toFetch);
if (fetchedAssets.length > 0) {
console.log(`Fetched ${fetchedAssets.length} asset(s):`);
fetchedAssets.forEach(asset => console.log(` ${asset}`));
}
// binance chain list
const tokenList = await generateBinanceTokensList();
if (tokenList.length < 5) {
console.log(`ERROR: no token pair info available from Binance DEX! ${tokenList.length}`);
return;
}
const list = createTokensList("BNB", tokenList,
"2020-10-03T12:37:57.000+00:00", // use constants here to prevent changing time every time
0, 1, 0);
if (tokenList.length > 0) {
writeToFileWithUpdate(getChainTokenlistPath(Binance), list);
}
}
}
class BinanceMarket {
base_asset_symbol: string
quote_asset_symbol: string
lot_size: string
tick_size: string
}
async function generateBinanceTokensList(): Promise<TokenItem[]> {
const decimals = CoinType.decimals(CoinType.binance)
const BNBSymbol = CoinType.symbol(CoinType.binance)
const markets: BinanceMarket[] = await axios.get(`${config.binanceDexURL}/v1/markets?limit=10000`)
.then(r => r.data)
.catch(function (error) {
console.log(JSON.stringify(error))
});
if (markets.length < 5) {
console.log(`ERROR: No markets info is returned from Binance DEX! ${markets.length}`);
return [];
}
const tokens = await axios.get(`${config.binanceDexURL}/v1/tokens?limit=10000`)
.then(r => r.data)
.catch(function (error) {
console.log(JSON.stringify(error))
});
if (tokens.length < 5) {
console.log(`ERROR: No tokens info is returned from Binance DEX! ${tokens.length}`);
return [];
}
const tokensMap = Object.assign({}, ...tokens.map(s => ({[s.symbol]: s})));
const pairsMap = {}
const pairsList = new Set();
markets.forEach(market => {
const key = market.quote_asset_symbol
function pair(market: BinanceMarket): Pair {
return new Pair(
assetIdSymbol(market.base_asset_symbol, BNBSymbol, CoinType.binance),
toSatoshis(market.lot_size, decimals),
toSatoshis(market.tick_size, decimals)
)
}
if (pairsMap[key]) {
const newList = pairsMap[key]
newList.push(pair(market))
pairsMap[key] = newList
} else {
pairsMap[key] = [
pair(market)
]
}
pairsList.add(market.base_asset_symbol)
pairsList.add(market.quote_asset_symbol)
})
const list = <string[]>Array.from(pairsList.values())
return <TokenItem[]>list.map(item => {
const token = tokensMap[item]
return new TokenItem (
assetIdSymbol(token.symbol, BNBSymbol, CoinType.binance),
tokenType(token.symbol, BNBSymbol, TokenType.BEP2),
token.symbol,
token.name,
token.original_symbol,
decimals,
logoURI(token.symbol, 'binance', BNBSymbol),
pairsMap[token.symbol] || []
)
});
}

View File

@ -1,34 +0,0 @@
import { Cosmos } from "../generic/blockchains";
import { getChainValidatorsAssets } from "../generic/repo-structure";
import { ActionInterface, CheckStepInterface } from "../generic/interface";
import { isLowerCase } from "../generic/types";
export class CosmosAction implements ActionInterface {
getName(): string { return "Cosmos chain"; }
getSanityChecks(): CheckStepInterface[] {
return [
{
getName: () => { return "Cosmos validator assets must have correct format"},
check: async () => {
const errors: string[] = [];
const assets = getChainValidatorsAssets(Cosmos);
const prefix = "cosmosvaloper1";
const expLength = 52;
assets.forEach(addr => {
if (!(addr.startsWith(prefix))) {
errors.push(`Address ${addr} should start with '${prefix}'`);
}
if (addr.length != expLength) {
errors.push(`Address ${addr} should have length ${expLength}`);
}
if (!isLowerCase(addr)) {
errors.push(`Address ${addr} should be in lowercase`);
}
});
return [errors, []];
}
},
];
}
}

View File

@ -1,89 +0,0 @@
import { ActionInterface, CheckStepInterface } from "../generic/interface";
import {
checkTradingPair,
getTradingPairs,
PairInfo,
primaryTokenIndex,
TokenInfo
} from "../generic/subgraph";
import { Ethereum } from "../generic/blockchains";
import {
parseForceList,
rebuildTokenlist,
TokenItem
} from "../generic/tokenlists";
import { toChecksum } from "../generic/eth-address";
import { assetID, logoURI } from "../generic/asset";
import * as config from "../config"
const PrimaryTokens: string[] = ["WETH", "ETH"];
// Retrieve trading pairs from Uniswap
async function retrieveUniswapPairs(): Promise<PairInfo[]> {
console.log(`Retrieving pairs from Uniswap, limit liquidity USD ${config.Uniswap_MinLiquidity} volume ${config.Uniswap_MinVol24} txcount ${config.Uniswap_MinTxCount24}`);
console.log(` forceIncludeList: ${config.Uniswap_ForceInclude}`);
const includeList = parseForceList(config.Uniswap_ForceInclude);
const pairs = await getTradingPairs(config.Uniswap_TradingPairsUrl, config.Uniswap_TradingPairsQuery);
const filtered: PairInfo[] = [];
pairs.forEach(x => {
try {
if (typeof(x) === "object") {
const pairInfo = x as PairInfo;
if (pairInfo) {
if (checkTradingPair(pairInfo, config.Uniswap_MinLiquidity, config.Uniswap_MinVol24, config.Uniswap_MinTxCount24, PrimaryTokens, includeList)) {
filtered.push(pairInfo);
}
}
}
} catch (err) {
console.log("Exception:", err);
}
});
console.log("Retrieved & filtered", filtered.length, "pairs:");
filtered.forEach(p => {
console.log(`pair: ${p.token0.symbol} -- ${p.token1.symbol} \t USD ${Math.round(p.reserveUSD)} ${Math.round(p.volumeUSD)} ${p.txCount}`);
});
return filtered;
}
function tokenInfoFromSubgraphToken(token: TokenInfo): TokenItem {
const idChecksum = toChecksum(token.id);
return new TokenItem(
assetID(60, idChecksum),
"ERC20",
idChecksum, token.name, token.symbol, parseInt(token.decimals.toString()),
logoURI(idChecksum, "ethereum", "--"),
[]);
}
// Retrieve trading pairs from PancakeSwap
async function generateTokenlist(): Promise<void> {
// note: if [] is returned here for some reason, all pairs will be *removed*. In case of error (e.g. timeout) it should throw
const tradingPairs = await retrieveUniswapPairs();
// convert
const pairs2: [TokenItem, TokenItem][] = [];
tradingPairs.forEach(p => {
let tokenItem0 = tokenInfoFromSubgraphToken(p.token0);
let tokenItem1 = tokenInfoFromSubgraphToken(p.token1);
if (primaryTokenIndex(p, PrimaryTokens) == 2) {
// reverse
const tmp = tokenItem1; tokenItem1 = tokenItem0; tokenItem0 = tmp;
}
pairs2.push([tokenItem0, tokenItem1]);
});
await rebuildTokenlist(Ethereum, pairs2, "Ethereum", config.Uniswap_ForceExclude);
}
export class EthereumAction implements ActionInterface {
getName(): string { return "Ethereum"; }
getSanityChecks(): CheckStepInterface[] { return []; }
async updateManual(): Promise<void> {
await generateTokenlist();
}
}

View File

@ -1,34 +0,0 @@
import { Kava } from "../generic/blockchains";
import { getChainValidatorsAssets } from "../generic/repo-structure";
import { ActionInterface, CheckStepInterface } from "../generic/interface";
import { isLowerCase } from "../generic/types";
export class KavaAction implements ActionInterface {
getName(): string { return "Kava chain"; }
getSanityChecks(): CheckStepInterface[] {
return [
{
getName: () => { return "Kava validator assets must have correct format"},
check: async () => {
const errors: string[] = [];
const assets = getChainValidatorsAssets(Kava);
const prefix = "kavavaloper1";
const expLength = 50;
assets.forEach(addr => {
if (!(addr.startsWith(prefix))) {
errors.push(`Address ${addr} should start with '${prefix}'`);
}
if (addr.length != expLength) {
errors.push(`Address ${addr} should have length ${expLength}`);
}
if (!isLowerCase(addr)) {
errors.push(`Address ${addr} should be in lowercase`);
}
});
return [errors, []];
}
},
];
}
}

View File

@ -1,121 +0,0 @@
import { ActionInterface, CheckStepInterface } from "../generic/interface";
import {
checkTradingPair,
getTradingPairsBitquery,
PairInfoBitquery,
PairInfo,
primaryTokenIndex,
TokenInfo,
TokenInfoBitquery
} from "../generic/subgraph";
import { Polygon } from "../generic/blockchains";
import {
parseForceList,
rebuildTokenlist,
TokenItem
} from "../generic/tokenlists";
import { toChecksum } from "../generic/eth-address";
import { assetID, logoURI } from "../generic/asset";
import * as config from "../config"
const PrimaryTokens: string[] = ["WMATIC", "MATIC", "WETH", "USDC", "USDT", "DAI"];
function convertTokenInfo(token: TokenInfoBitquery): TokenInfo {
return {
id: token.address,
symbol: token.symbol,
name: token.name,
decimals: token.decimals
} as TokenInfo;
}
function convertPairInfo(pair: PairInfoBitquery): PairInfo {
return {
id: '?',
reserveUSD: 1,
volumeUSD: pair.tradeAmount,
txCount: pair.trade,
token0: convertTokenInfo(pair.buyCurrency),
token1: convertTokenInfo(pair.sellCurrency)
} as PairInfo;
}
// Return the date 1,5 days ago in the form 2021-10-06
function dateOfYesterday() {
const now = new Date();
const yesterday = new Date(now.getTime() - 36*3600*1000);
return yesterday.toISOString().substr(0, 10);
}
async function retrievePolygonSwapPairs(): Promise<PairInfo[]> {
console.log(`Retrieving pairs from Polygon, limit volume ${config.PolygonSwap_MinVol24} txcount ${config.PolygonSwap_MinTxCount24}`);
console.log(` forceIncludeList: ${config.PolygonSwap_ForceInclude}`);
const includeList = parseForceList(config.PolygonSwap_ForceInclude);
const query = config.PolygonSwap_TradingPairsQuery;
const date = dateOfYesterday();
const queryReplaced = query.replace("$DATE$", date);
const pairs = await getTradingPairsBitquery(config.PolygonSwap_TradingPairsUrl, queryReplaced);
const filtered: PairInfo[] = [];
pairs.forEach(x => {
try {
if (typeof(x) === "object") {
const pairInfo = convertPairInfo(x as PairInfoBitquery);
if (pairInfo) {
if (checkTradingPair(pairInfo, 0, config.PolygonSwap_MinVol24, config.PolygonSwap_MinTxCount24, PrimaryTokens, includeList)) {
filtered.push(pairInfo);
}
}
}
} catch (err) {
console.log("Exception:", err);
}
});
console.log("Retrieved & filtered", filtered.length, "pairs:");
filtered.forEach(p => {
console.log(`pair: ${p.token0.symbol} -- ${p.token1.symbol} \t USD ${Math.round(p.reserveUSD)} ${Math.round(p.volumeUSD)} ${p.txCount}`);
});
return filtered;
}
function tokenInfoFromSubgraphToken(token: TokenInfo): TokenItem {
const idChecksum = toChecksum(token.id);
return new TokenItem(
assetID(966, idChecksum),
"POLYGON",
idChecksum, token.name, token.symbol, parseInt(token.decimals.toString()),
logoURI(idChecksum, "polygon", "--"),
[]);
}
// Retrieve trading pairs from Polygon
async function generateTokenlist(): Promise<void> {
// note: if [] is returned here for some reason, all pairs will be *removed*. In case of error (e.g. timeout) it should throw
const tradingPairs = await retrievePolygonSwapPairs();
// convert
const pairs2: [TokenItem, TokenItem][] = [];
tradingPairs.forEach(p => {
let tokenItem0 = tokenInfoFromSubgraphToken(p.token0);
let tokenItem1 = tokenInfoFromSubgraphToken(p.token1);
if (primaryTokenIndex(p, PrimaryTokens) == 2) {
// reverse
const tmp = tokenItem1; tokenItem1 = tokenItem0; tokenItem0 = tmp;
}
pairs2.push([tokenItem0, tokenItem1]);
});
await rebuildTokenlist(Polygon, pairs2, "Polygon", config.PolygonSwap_ForceExclude);
}
export class PolygonAction implements ActionInterface {
getName(): string { return "Polygon"; }
getSanityChecks(): CheckStepInterface[] { return []; }
async updateManual(): Promise<void> {
await generateTokenlist();
}
}

View File

@ -1,88 +0,0 @@
import { ActionInterface, CheckStepInterface } from "../generic/interface";
import {
checkTradingPair,
getTradingPairs,
PairInfo,
primaryTokenIndex,
TokenInfo
} from "../generic/subgraph";
import { SmartChain } from "../generic/blockchains";
import {
parseForceList,
rebuildTokenlist,
TokenItem
} from "../generic/tokenlists";
import { toChecksum } from "../generic/eth-address";
import { assetID, logoURI } from "../generic/asset";
import * as config from "../config"
const PrimaryTokens: string[] = ["WBNB", "BNB"];
async function retrievePancakeSwapPairs(): Promise<PairInfo[]> {
console.log(`Retrieving pairs from PancakeSwap, limit liquidity USD ${config.PancakeSwap_MinLiquidity} volume ${config.PancakeSwap_MinVol24} txcount ${config.PancakeSwap_MinTxCount24}`);
console.log(` forceIncludeList: ${config.PancakeSwap_ForceInclude}`);
const includeList = parseForceList(config.PancakeSwap_ForceInclude);
const pairs = await getTradingPairs(config.PancakeSwap_TradingPairsUrl, config.PancakeSwap_TradingPairsQuery);
const filtered: PairInfo[] = [];
pairs.forEach(x => {
try {
if (typeof(x) === "object") {
const pairInfo = x as PairInfo;
if (pairInfo) {
if (checkTradingPair(pairInfo, config.PancakeSwap_MinLiquidity, config.PancakeSwap_MinVol24, config.PancakeSwap_MinTxCount24, PrimaryTokens, includeList)) {
filtered.push(pairInfo);
}
}
}
} catch (err) {
console.log("Exception:", err);
}
});
console.log("Retrieved & filtered", filtered.length, "pairs:");
filtered.forEach(p => {
console.log(`pair: ${p.token0.symbol} -- ${p.token1.symbol} \t USD ${Math.round(p.reserveUSD)} ${Math.round(p.volumeUSD)} ${p.txCount}`);
});
return filtered;
}
function tokenInfoFromSubgraphToken(token: TokenInfo): TokenItem {
const idChecksum = toChecksum(token.id);
return new TokenItem(
assetID(20000714, idChecksum),
"BEP20",
idChecksum, token.name, token.symbol, parseInt(token.decimals.toString()),
logoURI(idChecksum, "smartchain", "--"),
[]);
}
// Retrieve trading pairs from PancakeSwap
async function generateTokenlist(): Promise<void> {
// note: if [] is returned here for some reason, all pairs will be *removed*. In case of error (e.g. timeout) it should throw
const tradingPairs = await retrievePancakeSwapPairs();
// convert
const pairs2: [TokenItem, TokenItem][] = [];
tradingPairs.forEach(p => {
let tokenItem0 = tokenInfoFromSubgraphToken(p.token0);
let tokenItem1 = tokenInfoFromSubgraphToken(p.token1);
if (primaryTokenIndex(p, PrimaryTokens) == 2) {
// reverse
const tmp = tokenItem1; tokenItem1 = tokenItem0; tokenItem0 = tmp;
}
pairs2.push([tokenItem0, tokenItem1]);
});
await rebuildTokenlist(SmartChain, pairs2, "Smart Chain", config.PancakeSwap_ForceExclude);
}
export class SmartchainAction implements ActionInterface {
getName(): string { return "Binance Smartchain"; }
getSanityChecks(): CheckStepInterface[] { return []; }
async updateManual(): Promise<void> {
await generateTokenlist();
}
}

View File

@ -1,34 +0,0 @@
import { Terra } from "../generic/blockchains";
import { getChainValidatorsAssets } from "../generic/repo-structure";
import { ActionInterface, CheckStepInterface } from "../generic/interface";
import { isLowerCase } from "../generic/types";
export class TerraAction implements ActionInterface {
getName(): string { return "Terra chain"; }
getSanityChecks(): CheckStepInterface[] {
return [
{
getName: () => { return "Terra validator assets must have correct format"},
check: async () => {
const errors: string[] = [];
const assets = getChainValidatorsAssets(Terra);
const prefix = "terravaloper1";
const expLength = 51;
assets.forEach(addr => {
if (!(addr.startsWith(prefix))) {
errors.push(`Address ${addr} should start with '${prefix}'`);
}
if (addr.length != expLength) {
errors.push(`Address ${addr} should have length ${expLength}`);
}
if (!isLowerCase(addr)) {
errors.push(`Address ${addr} should be in lowercase`);
}
});
return [errors, []];
}
},
];
}
}

View File

@ -1,26 +0,0 @@
import * as eztz from "eztz-lib";
import { getChainValidatorsAssets } from "../generic/repo-structure";
import { Tezos } from "../generic/blockchains";
import { ActionInterface, CheckStepInterface } from "../generic/interface";
export class TezosAction implements ActionInterface {
getName(): string { return "Tezos"; }
getSanityChecks(): CheckStepInterface[] {
return [
{
getName: () => { return "Tezos validator assets must have correct format"},
check: async () => {
const errors: string[] = [];
const assets = getChainValidatorsAssets(Tezos);
assets.forEach(addr => {
if (!(eztz.crypto.checkAddress(addr))) {
errors.push(`Address ${addr} must be valid Tezos address'`);
}
});
return [errors, []];
}
},
];
}
}

View File

@ -1,54 +0,0 @@
import { ActionInterface, CheckStepInterface } from "../generic/interface";
import { getChainAssetsPath } from "../generic/repo-structure";
import { Tron } from "../generic/blockchains";
import { readDirSync } from "../generic/filesystem";
import { getChainValidatorsAssets } from "../generic/repo-structure";
import { isLowerCase, isUpperCase } from "../generic/types";
import * as bluebird from "bluebird";
export function isTRC10(str: string): boolean {
return (/^\d+$/.test(str));
}
export function isTRC20(address: string): boolean {
return address.length == 34 &&
address.startsWith("T") &&
isLowerCase(address) == false &&
isUpperCase(address) == false;
}
export class TronAction implements ActionInterface {
getName(): string { return "Tron chain"; }
getSanityChecks(): CheckStepInterface[] {
return [
{
getName: () => { return "Tron assets should be TRC10 or TRC20, logo of correct size"; },
check: async () => {
const errors: string[] = [];
const path = getChainAssetsPath(Tron);
const assets = readDirSync(path);
await bluebird.each(assets, async (asset) => {
if (!isTRC10(asset) && !isTRC20(asset)) {
errors.push(`Asset ${asset} at path '${path}' is not TRC10 nor TRC20`);
}
});
return [errors, []];
}
},
{
getName: () => { return "Tron validator assets must have correct format"},
check: async () => {
const errors: string[] = [];
const assets = getChainValidatorsAssets(Tron);
assets.forEach(addr => {
if (!(isTRC20(addr))) {
errors.push(`Address ${addr} should be TRC20 address'`);
}
});
return [errors, []];
}
}
];
}
}

View File

@ -1,33 +0,0 @@
import { Waves } from "../generic/blockchains";
import { getChainValidatorsAssets } from "../generic/repo-structure";
import { ActionInterface, CheckStepInterface } from "../generic/interface";
import { isLowerCase, isUpperCase } from "../generic/types";
export function isWavesAddress(address: string): boolean {
return address.length == 35 &&
address.startsWith("3P") &&
isLowerCase(address) == false &&
isUpperCase(address) == false;
}
export class WavesAction implements ActionInterface {
getName(): string { return "Waves chain"; }
getSanityChecks(): CheckStepInterface[] {
return [
{
getName: () => { return "Waves validator assets must have correct format"},
check: async () => {
const errors: string[] = [];
const assets = getChainValidatorsAssets(Waves);
assets.forEach(addr => {
if (!(isWavesAddress(addr))) {
errors.push(`Address ${addr} should be a Waves address'`);
}
});
return [errors, []];
}
},
];
}
}

View File

@ -1,71 +0,0 @@
export const imageMaxLogoWidth = 512;
export const imageMaxLogoHeight = 512;
export const imageMinLogoWidth = 64;
export const imageMinLogoHeight = 64;
export const imageMaxLogoSizeKb = 100;
export const foldersRootdirAllowedFiles: string[] = [".github", "blockchains", "dapps", "media", "node_modules", "script-old", "script", "test", ".gitignore", "azure-pipelines.yml", "jest.config.js", "LICENSE", "package-lock.json", "package.json", "README.md", ".git", ".eslintignore", ".eslintrc.js", "cmd", "go.mod", "go.sum"];
export const binanceUrlTokenAssets = "https://explorer.binance.org/api/v1/assets?page=1&rows=1000";
export const binanceDexURL = 'https://dex-atlantic.binance.org/api'
export const assetsURL = 'https://assets.trustwalletapp.com'
// Force include & exclude config: list of token symbols, or symbol pairs (e.g. ["Cake", "DAI-WBNB"]).
export const PancakeSwap_ForceInclude: string[] = ["Cake", "DAI", "ETH", "TWT", "VAI", "USDT", "BLINK", "BTCB", "ALPHA", "INJ", "CTK", "UNI", "XVS", "BUSD", "HARD", "BIFI", "FRONT"];
export const PancakeSwap_ForceExclude: string[] = [];
export const PancakeSwap_TradingPairsUrl = "https://api.bscgraph.org/subgraphs/name/cakeswap";
export const PancakeSwap_TradingPairsQuery = `
query pairs {
pairs(first: 300, orderBy: reserveUSD, orderDirection: desc) {
id reserveUSD volumeUSD txCount __typename
token0 {
id symbol name decimals __typename
}
token1 {
id symbol name decimals __typename
}
}
}
`;
export const PancakeSwap_MinLiquidity = 1000000;
export const PancakeSwap_MinVol24 = 500000;
export const PancakeSwap_MinTxCount24 = 288;
// Force include & exclude config: list of token symbols, or symbol pairs (e.g. ["BAT", "YFI-WETH"]).
export const Uniswap_ForceInclude: string[] = ["TUSD", "STAKE", "YFI", "BAT", "MANA", "1INCH", "REP", "KP3R", "UNI", "WBTC", "HEX", "CREAM", "SLP", "REN", "XOR", "Link", "sUSD", "HEGIC", "RLC", "DAI", "SUSHI", "FYZ", "DYT", "AAVE", "LEND", "UBT", "DIA", "RSR", "SXP", "OCEAN", "MKR", "USDC", "CEL", "BAL", "BAND", "COMP", "SNX", "OMG", "AMPL", "USDT", "KNC", "ZRX", "AXS", "ENJ", "STMX", "DPX", "FTT", "DPI", "PAX"];
export const Uniswap_ForceExclude: string[] = ["STARL", "UFO"];
export const Uniswap_TradingPairsUrl = "https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v2"; // see https://thegraph.com/explorer/subgraph/uniswap/uniswap-v2
export const Uniswap_TradingPairsQuery = `
query pairs {
pairs(first: 800, orderBy: reserveUSD, orderDirection: desc) {
id reserveUSD trackedReserveETH volumeUSD txCount untrackedVolumeUSD __typename
token0 {
id symbol name decimals __typename
}
token1 {
id symbol name decimals __typename
}
}
}
`;
export const Uniswap_MinLiquidity = 2000000;
export const Uniswap_MinVol24 = 1000000;
export const Uniswap_MinTxCount24 = 480;
// Force include & exclude config: list of token symbols, or symbol pairs (e.g. ["Cake", "DAI-WBNB"]).
export const PolygonSwap_ForceInclude: string[] = [];
export const PolygonSwap_ForceExclude: string[] = [];
export const PolygonSwap_TradingPairsUrl = "https://graphql.bitquery.io";
export const PolygonSwap_TradingPairsQuery = `
{
ethereum(network: matic) {
dexTrades(date: {is: "$DATE$"}) {
sellCurrency {address symbol name decimals}
buyCurrency {address symbol name decimals}
trade: count
tradeAmount(in: USD)
}
}
}
`;
//export const PolygonSwap_MinLiquidity = 1000000;
export const PolygonSwap_MinVol24 = 500000;
export const PolygonSwap_MinTxCount24 = 288;

View File

@ -1,14 +0,0 @@
import { sanityCheckAll } from "../generic/update-all";
export async function main(): Promise<void> {
try {
// warnings ignored
const [errors] = await sanityCheckAll();
process.exit(errors.length);
} catch(err) {
console.error(err);
process.exit(1);
}
}
main();

View File

@ -1,31 +0,0 @@
import { sanityCheckAll, consistencyCheckAll } from "../generic/update-all";
export async function main(): Promise<void> {
let returnCode = 0;
try {
// warnings ignored
const [errors1] = await sanityCheckAll();
if (errors1.length > 0) {
returnCode = errors1.length;
}
} catch(err) {
console.error(err);
returnCode = 1;
}
try {
// warnings ignored
const [errors1] = await consistencyCheckAll();
if (errors1.length > 0) {
returnCode = errors1.length;
}
} catch(err) {
console.error(err);
returnCode = 1;
}
process.exit(returnCode);
}
main();

View File

@ -1,12 +0,0 @@
import { sanityFixAll } from "../generic/update-all";
export async function main(): Promise<void> {
try {
await sanityFixAll();
} catch(err) {
console.error(err);
process.exit(1);
}
}
main();

View File

@ -1,19 +0,0 @@
import { sanityFixAll, consistencyFixAll } from "../generic/update-all";
export async function main(): Promise<void> {
try {
await sanityFixAll();
} catch(err) {
console.error(err);
process.exit(1);
}
try {
await consistencyFixAll();
} catch(err) {
console.error(err);
process.exit(1);
}
}
main();

View File

@ -1,12 +0,0 @@
import { updateAutoAll } from "../generic/update-all";
export async function main(): Promise<void> {
try {
await updateAutoAll();
} catch(err) {
console.error(err);
process.exit(1);
}
}
main();

View File

@ -1,12 +0,0 @@
import { updateManualAll } from "../generic/update-all";
export async function main(): Promise<void> {
try {
await updateManualAll();
} catch(err) {
console.error(err);
process.exit(1);
}
}
main();

View File

@ -1,492 +0,0 @@
import {
allChains,
getChainAssetsList,
getChainAssetsPath,
getChainAssetInfoPath,
getChainInfoPath,
getChainCoinInfoPath
} 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 requiredKeysCoin = ["name", "type", "symbol", "decimals", "description", "website", "explorer", "status"];
const requiredKeysToken = [...requiredKeysCoin, "id"];
// Supported keys in links, and their mandatory prefix
const linksKeys = {
//"explorer": "",
"github": "https://github.com/",
"whitepaper": "",
"twitter": "https://twitter.com/",
"telegram": "https://t.me/",
"telegram_news": "https://t.me/", // read-only announcement channel
"medium": "", // url contains 'medium.com'
"discord": "https://discord.com/",
"reddit": "https://reddit.com/",
"facebook": "https://facebook.com/",
"youtube": "https://youtube.com/",
"coinmarketcap": "https://coinmarketcap.com/",
"coingecko": "https://coingecko.com/",
"blog": "", // blog, other than medium
"forum": "", // community site
"docs": "",
"source_code": "" // other than github
};
const linksKeysString = Object.keys(linksKeys).reduce(function (agg, item) { return agg + item + ","; }, '');
const linksMediumContains = 'medium.com';
function isAssetInfoHasAllKeys(info: unknown, path: string, isCoin: boolean): [boolean, string] {
const infoKeys = Object.keys(info);
const requiredKeys = isCoin ? requiredKeysCoin : requiredKeysToken;
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, isCoin: boolean, 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'] !== ""
;
if (!isKeys1CorrectType) {
return [`Field missing or invalid; name '${info['name']}' type '${info['type']}' symbol '${info['symbol']}' decimals '${info['decimals']}' ${path}`, "", fixedInfo];
}
if (!isCoin) {
const isIdKeyCorrectType = typeof info['id'] === "string" && info['id'] !== "";
if (!isIdKeyCorrectType) {
return [`Field 'id' missing or invalid, '${info['id']}' ${path}`, "", fixedInfo];
}
}
// type
if (!isCoin) { // token
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 [`Type should be ALLCAPS '${info['type'].toUpperCase()}' instead of '${info['type']}' '${chain}' ${path}`, "", fixedInfo];
}
// fix
if (!fixedInfo) { fixedInfo = info; }
fixedInfo['type'] = info['type'].toUpperCase();
}
} else { // coin
const expectedType = 'coin';
if (info['type'] !== expectedType) {
if (checkOnly) {
return [`Incorrect value for type '${info['type']}', expected '${expectedType}' '${chain}' ${path}`, "", fixedInfo];
}
// fix
if (!fixedInfo) { fixedInfo = info; }
fixedInfo['type'] = expectedType;
}
}
if (!isCoin) {
// 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;
}
}
// extra checks on decimals
if (info['decimals'] > 30 || info['decimals'] < 0) {
return [`Incorrect value for decimals '${info['decimals']}' '${chain}' ${path}`, "", fixedInfo];
}
if (info['type'] === 'BEP2' && info['decimals'] != 8) {
return [`Incorrect value for decimals, BEP2 tokens have 8 decimals. '${info['decimals']}' '${chain}' ${path}`, "", fixedInfo];
}
// 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];
}
// return error, warning
function isInfoLinksValid(links: unknown, path: string, address: string, chain: string): [string, string] {
if (!Array.isArray(links)) {
return [`Links must be an array '${JSON.stringify(links)}' '${path}' '${address}' '${chain}'`, ""];
}
for (let idx = 0; idx < links.length; idx++) {
const f = links[idx];
const fname = f['name'];
if (!fname) {
return [`Field name missing '${JSON.stringify(f)}'`, ""];
}
const furl = f['url'];
if (!fname) {
return [`Field url missing '${JSON.stringify(f)}'`, ""];
}
// Check there are no other fields
for (const f2 in f) {
if (f2 !== 'name' && f2 !== 'url') {
return [`Invalid field '${f2}' in links '${JSON.stringify(f)}', path ${path}`, ""];
}
}
if (!Object.prototype.hasOwnProperty.call(linksKeys, fname)) {
return [`Not supported field in links '${fname}'. Supported keys: ${linksKeysString}`, ""];
}
const prefix = linksKeys[fname];
if (prefix) {
if (!furl.startsWith(prefix)) {
return [`Links field '${fname}': '${furl}' must start with '${prefix}'. Supported keys: ${linksKeysString}`, ""];
}
}
if (!furl.startsWith('https://')) {
return [`Links field '${fname}': '${furl}' must start with 'https://'. Supported keys: ${linksKeysString}`, ""];
}
// special handling for medium
if (fname === 'medium') {
if (!furl.includes(linksMediumContains)) {
return [`Links field '${fname}': '${furl}' must include '${linksMediumContains}'. Supported keys: ${linksKeysString}`, ""];
}
}
}
return ["", ""];
}
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 "EOS": return "eos";
case "GO20": return "gochain";
case "KAVA": return "kava";
case "NEP5": return "neo";
case "NRC20": return "nuls";
case "VET": return "vechain";
case "ONTOLOGY": return "ontology";
case "THETA": return "theta";
case "TOMO": return "tomochain";
case "XDAI": return "xdai";
case "WAVES": return "waves";
case "POA": return "poa";
case "POLYGON": return "polygon";
case "OPTIMISM": return "optimism";
case "AVALANCHE": return "avalanchec";
case "ARBITRUM": return "arbitrum";
case "FANTOM": return "fantom";
case "TERRA": return "terra";
case "RONIN": return "ronin";
case "CELO": return "celo";
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.eos).toLowerCase():
return `https://bloks.io/account/${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.theta).toLowerCase():
return 'https://explorer.thetatoken.org/';
case CoinType.name(CoinType.thundertoken).toLowerCase():
case "thundertoken":
return `https://viewblock.io/thundercore/address/${contract}`;
case CoinType.name(CoinType.classic).toLowerCase():
case "classic":
return `https://blockscout.com/etc/mainnet/tokens/${contract}`;
case CoinType.name(CoinType.vechain).toLowerCase():
case "vechain":
return `https://explore.vechain.org/accounts/${contract}`;
case CoinType.name(CoinType.waves).toLowerCase():
return `https://wavesexplorer.com/assets/${contract}`;
case "xdai":
return `https://blockscout.com/xdai/mainnet/tokens/${contract}`;
case CoinType.name(CoinType.poa).toLowerCase():
case "poa":
return `https://blockscout.com/poa/core/tokens/${contract}`;
case CoinType.name(CoinType.polygon).toLowerCase():
case "polygon":
return `https://polygonscan.com/token/${contract}`;
case "optimism":
return `https://optimistic.etherscan.io/address/${contract}`;
case "avalanchec":
return `https://snowtrace.io/address/${contract}`
case "arbitrum":
return `https://arbiscan.io/token/${contract}`
case "fantom":
return `https://ftmscan.com/token/${contract}`
case "terra":
return `https://finder.terra.money/columbus-4/${contract}`
case "ronin":
return `https://explorer.roninchain.com/token/ronin:${contract}`
case "celo":
return `https://explorer.bitquery.io/celo_rc1/token/${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, isCoin: boolean, address: string, errors: string[], warnings: string[], checkOnly: boolean): void {
const assetInfoPath = isCoin ? getChainCoinInfoPath(chain) : 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, isCoin);
if (!hasAllKeys) {
console.log(msg1);
errors.push(msg1);
}
const [err2, warn2, fixedInfo2] = isAssetInfoValid(info, assetInfoPath, address, chain, isCoin, checkOnly);
if (err2) {
errors.push(err2);
}
if (warn2) {
warnings.push(warn2);
}
if (fixedInfo2 && !checkOnly) {
info = fixedInfo2;
fixedInfo = fixedInfo2;
}
if (Object.prototype.hasOwnProperty.call(info, 'links') && info['links']) {
const [err3, warn3] = isInfoLinksValid(info['links'], assetInfoPath, address, chain);
if (err3) {
errors.push(err3);
}
if (warn3) {
warnings.push(warn3);
}
}
// Fields moved to links section:
['socials', 'source_code', 'whitepaper', 'white_paper'].forEach(f => {
if (Object.prototype.hasOwnProperty.call(info, f)) {
errors.push(`Field ${f} is no longer used, use 'links' section instead. (${chain} ${address})`);
}
});
if (!isCoin) {
const explorerExpected = explorerUrl(chain, address);
const hasExplorer = Object.prototype.hasOwnProperty.call(info, 'explorer');
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 error
errors.push(`Incorrect explorer, ${explorerActual} instead of ${explorerExpected} (${explorersAlt.join(', ')})`);
}
}
}
}
} else {
// fix: simply replace with expected (case-only deviation is accepted)
if (explorerActualLower !== explorerExpectedLower) {
if (!fixedInfo) { fixedInfo = info; }
fixedInfo['explorer'] = explorerExpected;
}
}
}
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[] = [];
// tokens info.json's
allChains.forEach(chain => {
if (isPathExistsSync(getChainAssetsPath(chain))) {
steps.push(
{
getName: () => { return `Token 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, false, address, errors, warnings, true);
});
return [errors, warnings];
}
}
);
}
});
// coin info.json
steps.push(
{
getName: () => { return `Coin info.json's`; },
check: async () => {
const errors: string[] = [];
const warnings: string[] = [];
allChains.forEach(chain => {
if (isPathExistsSync(getChainInfoPath(chain))) {
isAssetInfoOK(chain, true, '../info', 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, false, address, errors, warnings, false);
});
}
if (isPathExistsSync(getChainInfoPath(chain))) {
const errors: string[] = [];
const warnings: string[] = [];
isAssetInfoOK(chain, true, '[COIN]', errors, warnings, false);
}
});
}
}

View File

@ -1,61 +0,0 @@
import axios from "axios";
import { TokenType } from "../generic/tokentype";
import * as config from "../config";
// Asset ID from coin symbol (diff between native and token coins)
export function assetIdSymbol(tokenId: string, nativeCoinId: string, coinType: number): string {
if (tokenId == nativeCoinId) {
return assetID(coinType)
}
return assetID(coinType, tokenId)
}
// Asset ID from coin number and ID
export function assetID(coinType: number, tokenId = ``): string {
if (tokenId.length > 0) {
return `c${coinType}_t${tokenId}`
}
return `c${coinType}`
}
// Token type from token symbol (diff between native and token coins)
export function tokenType(symbol: string, nativeCoinSymbol: string, tokenType: string): string {
if (symbol == nativeCoinSymbol) {
return TokenType.COIN
}
return tokenType;
}
// Github logo URL for coin.
export function logoURI(id: string, githubChainFolder: string, nativeCoinSymbol: string): string {
if (id == nativeCoinSymbol) {
return `${config.assetsURL}/blockchains/${githubChainFolder}/info/logo.png`
}
return `${config.assetsURL}/blockchains/${githubChainFolder}/assets/${id}/logo.png`
}
// Token info from TW api
// e.g. {"name":"Binance-Peg Cosmos","symbol":"ATOM","type":"BEP20","decimals":18,"asset_id":"c20000714_t0x0Eb3..."}
export class TokenTwInfo {
name: string;
symbol: string;
type: string;
decimals: number;
asset_id: string;
}
export async function tokenInfoFromTwApi(assetID: string): Promise<TokenTwInfo> {
try {
const apiUrl = `https://api.trustwallet.com/v1/assets/${assetID}`;
const result = await axios.get(apiUrl).then(r => r.data);
if (!result || !result.asset_id) {
console.log("Unexpected result", result);
return undefined;
}
const info = result as TokenTwInfo;
return info;
} catch (err) {
console.log("Exception:", err);
return undefined;
}
}

View File

@ -1,60 +0,0 @@
import { CoinType } from "@trustwallet/wallet-core";
export const getChainName = (id: CoinType): string => CoinType.id(id); // 60 => ethereum
export const Binance = getChainName(CoinType.binance);
export const Callisto = getChainName(CoinType.callisto);
export const Classic = getChainName(CoinType.classic);
export const Cosmos = getChainName(CoinType.cosmos);
export const EOS = getChainName(CoinType.eos);
export const Ethereum = getChainName(CoinType.ethereum);
export const GoChain = getChainName(CoinType.gochain);
export const IoTeX = getChainName(CoinType.iotex);
export const NEO = getChainName(CoinType.neo);
export const NULS = getChainName(CoinType.nuls);
export const Ontology = getChainName(CoinType.ontology);
export const POA = getChainName(CoinType.poa);
export const Tezos = getChainName(CoinType.tezos);
export const ThunderCore = getChainName(CoinType.thundertoken);
export const Terra = getChainName(CoinType.terra);
export const Theta = getChainName(CoinType.theta);
export const TomoChain = getChainName(CoinType.tomochain);
export const Tron = getChainName(CoinType.tron);
export const Kava = getChainName(CoinType.kava);
export const Vechain = getChainName(CoinType.vechain);
export const Wanchain = getChainName(CoinType.wanchain);
export const Waves = getChainName(CoinType.waves);
export const Solana = getChainName(CoinType.solana);
export const SmartChain = getChainName(CoinType.smartchain);
export const Polygon = getChainName(CoinType.polygon);
export const Optimism = "optimism";
export const xDAI = "xdai";
export const Avalanche = "avalanchec";
export const Arbitrum = "arbitrum";
export const Fantom = "fantom";
export const ethForkChains = [
Ethereum,
Classic,
POA,
TomoChain,
GoChain,
Wanchain,
ThunderCore,
SmartChain,
Polygon,
Optimism,
xDAI,
Avalanche,
Arbitrum,
Fantom,
];
export const stakingChains = [
Tezos,
Cosmos,
IoTeX,
Tron,
Waves,
Kava,
Terra,
Binance
];

View File

@ -1,42 +0,0 @@
import { toChecksumAddress, checkAddressChecksum } from "ethereum-checksum-address";
import { reverseCase } from "./types";
export const isChecksumEthereum = (address: string): boolean => checkAddressChecksum(address);
export const toChecksumEthereum = (address: string): string => toChecksumAddress(address);
export function toChecksum(address: string, chain = "ethereum"): string {
const checksumEthereum = toChecksumEthereum(address);
// special handling for Wanchain
if (chain.toLowerCase() === "wanchain") {
const checksumWanchain = reverseCase(checksumEthereum).replace("X", "x");
return checksumWanchain;
}
return checksumEthereum;
}
export function isChecksum(address: string, chain = "ethereum"): boolean {
// special handling for Wanchain
if (chain.toLowerCase() === "wanchain") {
const addressEthereum = reverseCase(address).replace("X", "x");
return isChecksumEthereum(addressEthereum);
}
return isChecksumEthereum(address);
}
export function isEthereumAddress(address: string): boolean {
if (!(address.length == 40 || (address.length == 42 && address.substring(0, 2) === '0x'))) {
return false;
}
try {
const check1 = toChecksum(address);
if (toChecksum(check1) !== check1) {
return false;
}
return true;
} catch (error) {
return false;
}
}

View File

@ -1,86 +0,0 @@
import { ethForkChains } from "../generic/blockchains";
import {
getChainAssetsPath,
getChainAssetsList,
getChainAssetPath,
getChainAssetFilesList,
logoName,
logoExtension,
logoFullName,
} from "../generic/repo-structure";
import {
getFileName,
getFileExt,
gitMove,
readDirSync,
isPathExistsSync,
} from "../generic/filesystem";
import { toChecksum } from "../generic/eth-address";
import { ActionInterface, CheckStepInterface } from "../generic/interface";
import * as bluebird from "bluebird";
function checkAddressChecksum(assetsFolderPath: string, address: string, chain: string) {
const checksumAddress = toChecksum(address, chain);
if (checksumAddress !== address) {
gitMove(assetsFolderPath, address, checksumAddress);
console.log(`Renamed to checksum format ${checksumAddress}`);
}
}
async function checkAddressChecksums() {
console.log(`Checking for checksum formats ...`);
await bluebird.each(ethForkChains, async (chain) => {
const assetsPath = getChainAssetsPath(chain);
await bluebird.each(readDirSync(assetsPath), async (address) => {
await bluebird.each(getChainAssetFilesList(chain, address), async (file) => {
if (getFileName(file) == logoName && getFileExt(file) !== logoExtension) {
console.log(`Renaming incorrect asset logo extension ${file} ...`);
gitMove(getChainAssetPath(chain, address), file, logoFullName);
}
});
checkAddressChecksum(assetsPath, address, chain);
});
});
}
export class EthForks implements ActionInterface {
getName(): string { return "Ethereum forks"; }
getSanityChecks(): CheckStepInterface[] {
const steps: CheckStepInterface[] = [];
ethForkChains.forEach(chain => {
steps.push(
{
getName: () => { return `Folder structure for chain ${chain} (ethereum fork)`;},
check: async () => {
const errors: string[] = [];
const assetsFolder = getChainAssetsPath(chain);
if (!isPathExistsSync(assetsFolder)) {
console.log(` Found 0 assets for chain ${chain}`);
return [errors, []];
}
const assetsList = getChainAssetsList(chain);
console.log(` Found ${assetsList.length} assets for chain ${chain}`);
await bluebird.each(assetsList, async (address) => {
const assetPath = `${assetsFolder}/${address}`;
if (!isPathExistsSync(assetPath)) {
errors.push(`Expect directory at path: ${assetPath}`);
}
const inChecksum = toChecksum(address, chain);
if (address !== inChecksum) {
errors.push(`Expect asset at path ${assetPath} in checksum: '${inChecksum}'`);
}
});
return [errors, []];
}
}
);
});
return steps;
}
async sanityFix(): Promise<void> {
await checkAddressChecksums();
}
}

View File

@ -1,43 +0,0 @@
import * as fs from "fs";
import * as path from "path";
import { execSync } from "child_process";
export const getFileName = (name: string): string => path.basename(name, path.extname(name))
export const getFileExt = (name: string): string => name.slice((Math.max(0, name.lastIndexOf(".")) || Infinity) + 1)
export const readFileSync = (path: string): string => fs.readFileSync(path, 'utf8');
export const writeFileSync = (path: string, data: unknown): void => fs.writeFileSync(path, data);
export const readDirSync = (path: string): string[] => fs.readdirSync(path);
export const isPathExistsSync = (path: string): boolean => fs.existsSync(path);
export const getFileSizeInKilobyte = (path: string): number => fs.statSync(path).size / 1000;
function execRename(command: string, cwd: string) {
console.log(`Running command ${command}`);
execSync(command, {encoding: "utf-8", cwd: cwd});
}
function gitMoveCommand(oldName: string, newName: string): string {
return `git mv ${oldName} ${newName}-temp && git mv ${newName}-temp ${newName}`;
}
export function gitMove(path: string, oldName: string, newName: string): void {
console.log(`Renaming file or folder at path ${path}: ${oldName} => ${newName}`);
execRename(gitMoveCommand(oldName, newName), path);
}
export function findFiles(base: string, ext: string, files: string[] = [], result: string[] = []): string[] {
files = fs.readdirSync(base) || files;
result = result || result;
files.forEach(file => {
const newbase = path.join(base, file);
if (fs.statSync(newbase).isDirectory()) {
result = findFiles(newbase, ext, fs.readdirSync(newbase), result);
} else {
if (file.substr(-1*(ext.length+1)) == '.' + ext) {
result.push(newbase);
}
}
});
return result;
}

View File

@ -1,174 +0,0 @@
import {
readDirSync,
isPathExistsSync
} from "../generic/filesystem";
import { CheckStepInterface, ActionInterface } from "../generic/interface";
import {
allChains,
dappsPath,
getChainLogoPath,
getChainAssetInfoPath,
getChainAssetsPath,
getChainAssetPath,
getChainAssetLogoPath,
assetFolderAllowedFiles,
getChainFolderFilesList,
chainFolderAllowedFiles,
rootDirAllowedFiles
} from "../generic/repo-structure";
import { isLogoOK } from "../generic/image";
import { isLowerCase } from "../generic/types";
import { readJsonFile } from "../generic/json";
import * as bluebird from "bluebird";
export class FoldersFiles implements ActionInterface {
getName(): string { return "Folders and Files"; }
getSanityChecks(): CheckStepInterface[] {
return [
{
getName: () => { return "Repository root dir"},
check: async () => {
const errors: string[] = [];
const dirActualFiles = readDirSync(".");
dirActualFiles.forEach(file => {
if (!(rootDirAllowedFiles.indexOf(file) >= 0)) {
errors.push(`File "${file}" should not be in root or added to predifined list`);
}
});
return [errors, []];
}
},
{
getName: () => { return "Chain folders are lowercase, contain only predefined list of files"},
check: async () => {
const errors: string[] = [];
allChains.forEach(chain => {
if (!isLowerCase(chain)) {
errors.push(`Chain folder must be in lowercase "${chain}"`);
}
getChainFolderFilesList(chain).forEach(file => {
if (!(chainFolderAllowedFiles.indexOf(file) >= 0)) {
errors.push(`File '${file}' not allowed in chain folder: ${chain}`);
}
});
});
return [errors, []];
}
},
{
getName: () => { return "Chain folders have logo, and correct size"},
check: async () => {
const errors: string[] = [];
await bluebird.each(allChains, async (chain) => {
const chainLogoPath = getChainLogoPath(chain);
if (!isPathExistsSync(chainLogoPath)) {
errors.push(`File missing at path "${chainLogoPath}"`);
}
const [isOk, error1] = await isLogoOK(chainLogoPath);
if (!isOk) {
errors.push(error1);
}
});
return [errors, []];
}
},
{
getName: () => { return "Asset folders contain logo and info"},
check: async () => {
const errors: string[] = [];
const warnings: string[] = [];
allChains.forEach(chain => {
const assetsPath = getChainAssetsPath(chain);
if (isPathExistsSync(assetsPath)) {
readDirSync(assetsPath).forEach(address => {
const logoFullPath = getChainAssetLogoPath(chain, address);
const logoExists = isPathExistsSync(logoFullPath);
const infoFullPath = getChainAssetInfoPath(chain, address);
const infoExists = isPathExistsSync(infoFullPath);
// Assets should have a logo and an info file. Exceptions:
// - status=spam tokens may have no logo
// - on some chains some valid tokens have no info (should be fixed)
if (!infoExists || !logoExists) {
if (!infoExists && logoExists) {
const msg = `Missing info file for asset '${chain}/${address}' -- ${infoFullPath}`;
// enforce that info must be present
console.log(msg);
errors.push(msg);
}
if (!logoExists && infoExists) {
// logo must be present, with some exceptions
const info: unknown = readJsonFile(infoFullPath);
if (!info['status'] || !(info['status'] == 'spam' || info['status'] == 'abandoned')) {
const msg = `Missing logo file for non-spam/non-abandoned asset '${chain}/${address}' -- ${logoFullPath}`;
console.log(msg);
errors.push(msg);
}
}
}
});
}
});
return [errors, warnings];
}
},
/*
{
getName: () => { return "Asset folders contain info.json"},
check: async () => {
const warnings: string[] = [];
allChains.forEach(chain => {
const assetsPath = getChainAssetsPath(chain);
if (isPathExistsSync(assetsPath)) {
readDirSync(assetsPath).forEach(address => {
const infoFullPath = getChainAssetInfoPath(chain, address);
if (!isPathExistsSync(infoFullPath)) {
warnings.push(`Missing info file for asset '${chain}/${address}' -- ${infoFullPath}`);
}
});
}
});
return [[], warnings];
}
},
*/
{
getName: () => { return "Asset folders contain only predefined set of files"},
check: async () => {
const errors: string[] = [];
allChains.forEach(chain => {
const assetsPath = getChainAssetsPath(chain);
if (isPathExistsSync(assetsPath)) {
readDirSync(assetsPath).forEach(address => {
const assetFiles = getChainAssetPath(chain, address);
readDirSync(assetFiles).forEach(assetFolderFile => {
if (!(assetFolderAllowedFiles.indexOf(assetFolderFile) >= 0)) {
errors.push(`File '${assetFolderFile}' not allowed at this path: ${assetsPath}`);
}
});
});
}
});
return [errors, []];
}
},
{
getName: () => { return "Dapps folders contain only .png files, with all lowercase names"},
check: async () => {
const errors: string[] = [];
if (isPathExistsSync(dappsPath)) {
readDirSync(dappsPath).forEach(filename => {
if (!filename.endsWith('.png')) {
errors.push(`File '${filename}' has invalid extension; ${dappsPath}`);
}
if (filename.toLowerCase() != filename) {
errors.push(`File '${filename}' is not all-lowercase; ${dappsPath}`);
}
});
}
return [errors, []];
}
}
];
}
}

View File

@ -1,123 +0,0 @@
import * as sharp from "sharp";
import * as tinify from "tinify";
import * as image_size from "image-size";
import {
writeFileSync,
getFileSizeInKilobyte
} from "./filesystem";
import * as chalk from 'chalk';
import * as config from "../config";
export const minLogoWidth = config.imageMinLogoWidth;
export const minLogoHeight = config.imageMinLogoHeight;
export const maxLogoWidth = config.imageMaxLogoWidth;
export const maxLogoHeight = config.imageMaxLogoHeight;
export const maxLogoSizeInKilobyte = config.imageMaxLogoSizeKb;
export function isDimensionTooLarge(width: number, height: number): boolean {
return (width > maxLogoWidth) || (height > maxLogoHeight);
}
export function isDimensionOK(width: number, height: number): boolean {
return (width <= maxLogoWidth) && (height <= maxLogoHeight) &&
(width >= minLogoWidth) && (height >= minLogoHeight);
}
export function calculateTargetSize(srcWidth: number, srcHeight: number, targetWidth: number, targetHeight: number): {width: number, height: number} {
if (srcWidth == 0 || srcHeight == 0) {
return {width: targetWidth, height: targetHeight};
}
const ratio = Math.min(targetWidth / srcWidth, targetHeight / srcHeight);
return {
width: Math.round(srcWidth * ratio),
height: Math.round(srcHeight * ratio)
};
}
// check logo dimensions (pixel) and size (kilobytes)
export async function isLogoOK(path: string): Promise<[boolean, string]> {
let [isOK, msg] = await isLogoDimensionOK(path);
if (!isOK) {
return [false, msg];
}
[isOK, msg] = await isLogoSizeOK(path);
if (!isOK) {
return [false, msg];
}
return [true, ""];
}
const getImageDimensions = (path: string) => image_size.imageSize(path);
async function isLogoDimensionOK(path: string): Promise<[boolean, string]> {
const { width, height } = getImageDimensions(path)
if (isDimensionOK(width, height)) {
return [true, ""];
}
return [false, `Image at path ${path} must have dimensions: min: ${minLogoWidth}x${minLogoHeight} and max: ${maxLogoWidth}x${maxLogoHeight} instead ${width}x${height}`];
}
async function compressTinyPNG(path: string) {
console.log(`Compressing image via tinypng at path ${path}`);
const source = await tinify.fromFile(path);
await source.toFile(path);
}
async function isLogoSizeOK(path: string): Promise<[boolean, string]> {
const sizeKilobyte = getFileSizeInKilobyte(path);
if (sizeKilobyte > maxLogoSizeInKilobyte) {
return [false, `Logo ${path} is too large, ${sizeKilobyte} kB instead of ${maxLogoSizeInKilobyte}`];
}
return [true, ''];
}
// return if image if too large, and if image has been updated
export async function checkResizeIfTooLarge(path: string, checkOnly: boolean): Promise<[boolean, boolean]> {
let tooLarge = false;
let updated = false;
const { width: srcWidth, height: srcHeight } = getImageDimensions(path);
if (!isDimensionOK(srcWidth, srcHeight)) {
tooLarge = true; // may be too small as well
console.log(`Wrong image dimensions, ${srcWidth}x${srcHeight}, ${path}`);
}
if (isDimensionTooLarge(srcWidth, srcHeight)) {
tooLarge = true;
console.log(`Image too large, ${srcWidth}x${srcHeight}, ${path}`);
if (!checkOnly) {
// resize
const { width, height } = calculateTargetSize(srcWidth, srcHeight, maxLogoWidth, maxLogoHeight);
console.log(`Resizing image at ${path} from ${srcWidth}x${srcHeight} => ${width}x${height}`)
await sharp(path).resize(width, height).toBuffer()
.then(data => {
writeFileSync(path, data);
updated = true;
})
.catch(e => {
console.log(chalk.red(e.message));
});
}
}
// If file size > max limit, compress with tinypng
const sizeKilobyte = getFileSizeInKilobyte(path);
if (sizeKilobyte > maxLogoSizeInKilobyte) {
tooLarge = true;
console.log(`Image too big, ${sizeKilobyte} kb, ${path}`);
if (!checkOnly) {
console.log(`Resizing image at path ${path} from ${sizeKilobyte} kB`);
await compressTinyPNG(path)
.then(() => {
updated = true;
console.log(`Resized image at path ${path} from ${sizeKilobyte} kB => ${getFileSizeInKilobyte(path)} kB`);
})
.catch(e => {
console.log(chalk.red(e.message));
});
}
}
return [tooLarge, updated];
}

View File

@ -1,26 +0,0 @@
// A single check step
export interface CheckStepInterface {
getName(): string;
// return [errors, warnings]
check(): Promise<[string[], string[]]>;
}
// An action for a check, fix, or update, or a combination.
export interface ActionInterface {
getName(): string;
// return check steps for sanity check (0, 1, or more)
getSanityChecks(): CheckStepInterface[];
// return check steps for consistenct check (0, 1, or more)
getConsistencyChecks?(): CheckStepInterface[];
sanityFix?(): Promise<void>;
consistencyFix?(): Promise<void>;
updateAuto?(): Promise<void>; // For regular automatic updates (from external source)
updateManual?(): Promise<void>; // For occasional manual updates (from external source)
}
export enum FixCheckMode {
CheckSanityOnly = 1,
CheckAll,
FixSanityOnly,
FixAll
}

View File

@ -1,51 +0,0 @@
import { chainsPath } from "../generic/repo-structure";
import { findFiles } from "../generic/filesystem";
import { ActionInterface, CheckStepInterface } from "../generic/interface";
import { formatJsonFile, isValidJSON } from "../generic/json";
import * as bluebird from "bluebird";
async function formatInfos(): Promise<void> {
console.log(`Formatting json files...`);
const files = [
...findFiles(chainsPath, 'json'),
];
let count1 = 0;
let count2 = 0;
await bluebird.each(files, async (file) => {
if (formatJsonFile(file)) {
++count2;
}
++count1;
});
console.log(`Formatted ${count2} json files (total ${count1})`);
}
export class JsonAction implements ActionInterface {
getName(): string { return "Json files"; }
getSanityChecks(): CheckStepInterface[] {
return [
{
getName: () => { return "Check all JSON files to have valid content"},
check: async () => {
const errors: string[] = [];
const files = [
...findFiles(chainsPath, 'json'),
];
await bluebird.each(files, async file => {
if (!isValidJSON(file)) {
errors.push(`${file} path contains invalid JSON`);
}
});
return [errors, []];
}
},
];
}
async sanityFix(): Promise<void> {
await formatInfos();
}
}

View File

@ -1,48 +0,0 @@
import {
readFileSync,
writeFileSync
} from "./filesystem";
import { sortElements } from "./types";
export function isValidJSON(path: string): boolean {
try {
const rawdata = readFileSync(path);
JSON.parse(rawdata);
return true;
} catch {
return false;
}
}
export function formatJson(content: unknown): string {
return JSON.stringify(content, null, 4);
}
export function formatSortJson(content: unknown[]): string {
return JSON.stringify(sortElements(content), null, 4);
}
// Return if updated
export function formatJsonFile(filename: string): boolean {
const origText: string = readFileSync(filename);
const newText: string = formatJson(JSON.parse(origText));
if (newText == origText) {
return false;
}
writeFileSync(filename, newText);
console.log(`Formatted json file ${filename}`);
return true;
}
export function formatSortJsonFile(filename: string): void {
writeFileSync(filename, formatSortJson(JSON.parse(readFileSync(filename))));
console.log(`Formatted json file ${filename}`);
}
export function readJsonFile(path: string): unknown {
return JSON.parse(readFileSync(path));
}
export function writeJsonFile(path: string, data: unknown): void {
writeFileSync(path, JSON.stringify(data, null, 4));
}

View File

@ -1,123 +0,0 @@
import * as bluebird from "bluebird";
import {
allChains,
getChainLogoPath,
getChainAssetsPath,
getChainAssetLogoPath,
getChainValidatorsListPath,
getChainValidatorAssetLogoPath,
dappsPath
} from "../generic/repo-structure";
import {
readDirSync,
readFileSync,
isPathExistsSync
} from "../generic/filesystem";
import { checkResizeIfTooLarge } from "../generic/image";
import { ActionInterface, CheckStepInterface } from "../generic/interface";
// return name of large logo, or empty
async function checkDownsize(chains: string[], checkOnly: boolean): Promise<string[]> {
console.log(`Checking all logos for size ...`);
let totalCountChecked = 0;
let totalCountTooLarge = 0;
let totalCountUpdated = 0;
const largePaths: string[] = [];
// Check asset logos, under given chains
await bluebird.map(chains, async chain => {
let countChecked = 0;
let countTooLarge = 0;
let countUpdated = 0;
const path = getChainLogoPath(chain);
countChecked++;
const [tooLarge, updated] = await checkResizeIfTooLarge(path, checkOnly);
if (tooLarge) { largePaths.push(path); }
countTooLarge += tooLarge ? 1 : 0;
countUpdated += updated ? 1 : 0;
// Check and resize if needed chain assets
const assetsPath = getChainAssetsPath(chain);
if (isPathExistsSync(assetsPath)) {
await bluebird.mapSeries(readDirSync(assetsPath), async asset => {
const path = getChainAssetLogoPath(chain, asset);
if (isPathExistsSync(path)) {
countChecked++;
const [tooLarge, updated] = await checkResizeIfTooLarge(path, checkOnly);
if (tooLarge) { largePaths.push(path); }
countTooLarge += tooLarge ? 1 : 0;
countUpdated += updated ? 1 : 0;
}
})
}
// Check and resize if needed chain validators image
const chainValidatorsList = getChainValidatorsListPath(chain);
if (isPathExistsSync(chainValidatorsList)) {
const validatorsList = JSON.parse(readFileSync(getChainValidatorsListPath(chain)));
await bluebird.mapSeries(validatorsList, async ({ id }) => {
const path = getChainValidatorAssetLogoPath(chain, id);
countChecked++;
const [tooLarge, updated] = await checkResizeIfTooLarge(path, checkOnly);
if (tooLarge) { largePaths.push(path); }
countTooLarge += tooLarge ? 1 : 0;
countUpdated += updated ? 1 : 0;
});
}
totalCountChecked += countChecked;
totalCountTooLarge += countTooLarge;
totalCountUpdated += countUpdated;
if (countTooLarge > 0 || countUpdated > 0) {
console.log(`Checking logos on chain ${chain} completed, ${countChecked} checked, ${countTooLarge} too large, ${largePaths}, ${countUpdated} logos updated`);
}
});
// Check dapps logos
if (isPathExistsSync(dappsPath)) {
let countChecked = 0;
let countTooLarge = 0;
let countUpdated = 0;
await bluebird.mapSeries(readDirSync(dappsPath), async filename => {
const path = dappsPath + `/` + filename;
countChecked++;
const [tooLarge, updated] = await checkResizeIfTooLarge(path, checkOnly);
if (tooLarge) { largePaths.push(path); }
countTooLarge += tooLarge ? 1 : 0;
countUpdated += updated ? 1 : 0;
});
totalCountChecked += countChecked;
totalCountTooLarge += countTooLarge;
totalCountUpdated += countUpdated;
if (countTooLarge > 0 || countUpdated > 0) {
console.log(`Checking dapps logos completed, ${countChecked} checked, ${countTooLarge} too large, ${largePaths}, ${countUpdated} logos updated`);
}
}
console.log(`Checking logos completed, ${totalCountChecked} logos checked, ${totalCountTooLarge} too large, ${totalCountUpdated} logos updated`);
return largePaths;
}
export class LogoSize implements ActionInterface {
getName(): string { return "Logo sizes"; }
getSanityChecks(): CheckStepInterface[] {
return [
{
getName: () => { return "Check that logos are not too large"},
check: async () => {
const largePaths = await checkDownsize(allChains, true);
const errors: string[] = largePaths.map(p => `Logo too large: ${p}`);
return [errors, []];
}
},
];
}
async sanityFix(): Promise<void> {
await checkDownsize(allChains, false);
}
}

View File

@ -1,9 +0,0 @@
import BigNumber from "bignumber.js";
export function toSatoshis(value: string, decimals: number): string {
return new BigNumber(value).multipliedBy(new BigNumber(10).exponentiatedBy(decimals)).toFixed()
}
export function fromSatoshis(value: string, decimals: number): string {
return new BigNumber(value).dividedBy(new BigNumber(10).exponentiatedBy(decimals)).toFixed()
}

View File

@ -1,52 +0,0 @@
import * as path from "path";
import {
isPathExistsSync,
readDirSync
} from "./filesystem";
import * as config from "../config";
export const logoName = `logo`;
export const infoName = `info`;
export const listName = `list`
export const logoExtension = "png";
export const jsonExtension = "json";
export const logoFullName = `${logoName}.${logoExtension}`;
export const infoFullName = `${infoName}.${jsonExtension}`;
const tokenList = `tokenlist.${jsonExtension}`;
export const validatorsList = `${listName}.${jsonExtension}`
export const assetFolderAllowedFiles = [logoFullName, infoFullName];
export const chainFolderAllowedFiles = [
"assets",
tokenList,
"validators",
infoName
]
export const chainsPath: string = path.join(process.cwd(), '/blockchains');
export const getChainPath = (chain: string): string => `${chainsPath}/${chain}`;
export const allChains = readDirSync(chainsPath);
export const getChainInfoPath = (chain: string): string => `${getChainPath(chain)}/info`;
export const getChainLogoPath = (chain: string): string => `${getChainInfoPath(chain)}/${logoFullName}`;
export const getChainCoinInfoPath = (chain: string): string => `${getChainInfoPath(chain)}/${infoFullName}`;
export const getChainAssetsPath = (chain: string): string => `${getChainPath(chain)}/assets`;
export const getChainAssetPath = (chain: string, asset: string): string => `${getChainAssetsPath(chain)}/${asset}`;
export const getChainAssetLogoPath = (chain: string, asset: string): string => `${getChainAssetPath(chain, asset)}/${logoFullName}`;
export const getChainAssetInfoPath = (chain: string, asset: string): string => `${getChainAssetPath(chain, asset)}/${infoFullName}`;
export const getChainTokenlistPath = (chain: string): string => `${getChainPath(chain)}/${tokenList}`;
export const pricingFolderPath = path.join(process.cwd(), '/pricing');
export const getChainValidatorsPath = (chain: string): string => `${getChainPath(chain)}/validators`;
export const getChainValidatorsListPath = (chain: string): string => `${getChainValidatorsPath(chain)}/${validatorsList}`;
export const getChainValidatorsAssetsPath = (chain: string): string => `${getChainValidatorsPath(chain)}/assets`
export const getChainValidatorAssetLogoPath = (chain: string, asset: string): string => `${getChainValidatorsAssetsPath(chain)}/${asset}/${logoFullName}`
export const isChainAssetInfoExistSync = (chain: string, address: string): boolean => isPathExistsSync(getChainAssetInfoPath(chain, address));
export const getChainFolderFilesList = (chain: string): string[] => readDirSync(getChainPath(chain))
export const getChainAssetsList = (chain: string): string[] => readDirSync(getChainAssetsPath(chain));
export const getChainAssetFilesList = (chain: string, address: string): string[] => readDirSync(getChainAssetPath(chain, address));
export const getChainValidatorsAssets = (chain: string): string[] => readDirSync(getChainValidatorsAssetsPath(chain));
export const dappsPath: string = path.join(process.cwd(), '/dapps');
export const rootDirAllowedFiles = config.foldersRootdirAllowedFiles;

View File

@ -1,16 +0,0 @@
// see https://developer.trustwallet.com/add_new_asset
const StatusValues: string[] = [
'active',
'spam',
'abandoned'
];
export function isValidStatusValue(value: string): boolean {
if (!value) {
return false;
}
if (!StatusValues.find(e => e === value)) {
return false;
}
return true;
}

View File

@ -1,124 +0,0 @@
// Interfacing with TheGraph subgraph APIs
import axios from "axios";
import { ForceListPair, matchPairToForceList, TokenItem } from "./tokenlists";
export interface TokenInfo {
id: string;
symbol: string;
name: string;
decimals: number;
}
export interface PairInfo {
id: string;
reserveUSD: number;
volumeUSD: number;
txCount: number;
token0: TokenInfo;
token1: TokenInfo;
}
export interface TokenInfoBitquery {
address: string;
symbol: string;
name: string;
decimals: number;
}
export interface PairInfoBitquery {
sellCurrency: TokenInfoBitquery;
buyCurrency: TokenInfoBitquery;
trade: number; // trade count
tradeAmount: number; // trade volume usd
}
export async function getTradingPairs(apiUrl: string, subgraphQuery: string): Promise<unknown[]> {
// compact the query string
const compactQuery = subgraphQuery.replace(/(?:\r\n|\r|\n)/g, ' ').replace(/\s[\s]+/g, ' ');
const postData = '{"operationName":"pairs", "variables":{}, "query":"' + compactQuery + '"}';
console.log(`Retrieving trading pair infos from: ${apiUrl}`);
try {
const result = await axios.post(apiUrl, postData).then(r => r.data);
if (!result || !result.data || !result.data.pairs) {
throw `Unexpected result: ${JSON.stringify(result)}`;
}
const pairs = result.data.pairs;
console.log(`Retrieved ${pairs.length} trading pair infos`);
return pairs;
} catch (err) {
console.log("Exception from graph api:", err, apiUrl, JSON.stringify(postData));
throw err;
}
}
export async function getTradingPairsBitquery(apiUrl: string, subgraphQuery: string): Promise<unknown[]> {
// compact the query string
const compactQuery = subgraphQuery.replace(/(?:\r\n|\r|\n)/g, ' ').replace(/\s[\s]+/g, ' ');
const postData = {"query": compactQuery, "variables":{}};
console.log(`Retrieving trading pair infos from: ${apiUrl}`);
try {
const result = await axios.post(apiUrl, postData).then(r => r.data);
if (!result || !result.data || !result.data.ethereum || !result.data.ethereum.dexTrades) {
throw `Unexpected result: ${JSON.stringify(result)}`;
}
const pairs = result.data.ethereum.dexTrades;
console.log(`Retrieved ${pairs.length} trading pair infos`);
return pairs;
} catch (err) {
console.log("Exception from graph api:", err, apiUrl, JSON.stringify(postData));
throw err;
}
}
function isTokenPrimary(token: TokenInfo, primaryTokens: string[]): boolean {
return (primaryTokens.find(t => (t === token.symbol.toUpperCase())) != undefined);
}
// check which token of the pair is the primary token, 1st, or 2nd, or 0 for none
export function primaryTokenIndex(pair: PairInfo, primaryTokens: string[]): number {
if (isTokenPrimary(pair.token0, primaryTokens)) {
return 1;
}
if (isTokenPrimary(pair.token1, primaryTokens)) {
return 2;
}
return 0;
}
function tokenItemFromInfo(tokenInfo: TokenInfo): TokenItem {
return new TokenItem(tokenInfo.id, '', tokenInfo.id, tokenInfo.name, tokenInfo.symbol, tokenInfo.decimals, '', []);
}
// Verify a trading pair, whether we support the tokens, has enough liquidity, etc.
export function checkTradingPair(pair: PairInfo, minLiquidity: number, minVol24: number, minTxCount24: number, primaryTokens: string[], forceIncludeList: ForceListPair[]): boolean {
if (!pair.id || !pair.reserveUSD || !pair.volumeUSD || !pair.txCount || !pair.token0 || !pair.token1) {
return false;
}
if (primaryTokenIndex(pair, primaryTokens) == 0) {
console.log("pair with no primary coin:", pair.token0.symbol, "--", pair.token1.symbol);
return false;
}
if (matchPairToForceList(tokenItemFromInfo(pair.token0), tokenItemFromInfo(pair.token1), forceIncludeList)) {
console.log("pair included due to FORCE INCLUDE:", pair.token0.symbol, "--", pair.token1.symbol, " ", Math.round(pair.reserveUSD));
return true;
}
if (pair.reserveUSD < minLiquidity) {
console.log("pair with low liquidity:", pair.token0.symbol, "--", pair.token1.symbol, " ", Math.round(pair.reserveUSD));
return false;
}
if (pair.volumeUSD < minVol24) {
console.log("pair with low volume:", pair.token0.symbol, "--", pair.token1.symbol, " ", Math.round(pair.volumeUSD));
return false;
}
if (pair.txCount < minTxCount24) {
console.log("pair with low tx count:", pair.token0.symbol, "--", pair.token1.symbol, " ", pair.txCount);
return false;
}
//console.log("pair:", pair.token0.symbol, "--", pair.token1.symbol, " ", pair.reserveUSD);
return true;
}

View File

@ -1,18 +0,0 @@
import { TagValues } from "../tags-config";
export function isValidTagValue(value: string): boolean {
//console.log(`isValidTagValue ${value}`);
if (!value) {
return false;
}
const tag = TagValues.find(t => t.id === value);
if (!tag) {
return false;
}
//console.log(`TAG ${tag.name}`);
return true;
}
export function isValidTagValues(values: string[]): boolean {
return values.reduce((accum: boolean, value: string) => accum && isValidTagValue(value), true);
}

View File

@ -1,343 +0,0 @@
// Handling of tokenlist.json files, tokens and trading pairs.
import { readJsonFile, writeJsonFile } from "../generic/json";
import { diff } from "jsondiffpatch";
import { tokenInfoFromTwApi, TokenTwInfo } from "../generic/asset";
import {
getChainAssetLogoPath,
getChainTokenlistPath,
} from "../generic/repo-structure";
import * as bluebird from "bluebird";
import { isPathExistsSync } from "../generic/filesystem";
class Version {
major: number
minor: number
patch: number
constructor(major: number, minor: number, patch: number) {
this.major = major
this.minor = minor
this.patch = patch
}
}
export class List {
name: string
logoURI: string
timestamp: string
tokens: TokenItem[]
pairs: Pair[]
version: Version
constructor(name: string, logoURI: string, timestamp: string, tokens: TokenItem[], version: Version) {
this.name = name
this.logoURI = logoURI
this.timestamp = timestamp;
this.tokens = tokens
this.version = version
}
}
export class TokenItem {
asset: string;
type: string;
address: string;
name: string;
symbol: string;
decimals: number;
logoURI: string;
pairs: Pair[];
constructor(asset: string, type: string, address: string, name: string, symbol: string, decimals: number, logoURI: string, pairs: Pair[]) {
this.asset = asset
this.type = type
this.address = address
this.name = name;
this.symbol = symbol
this.decimals = decimals
this.logoURI = logoURI
this.pairs = pairs
}
}
export class Pair {
base: string;
lotSize?: string;
tickSize?: string;
constructor(base: string, lotSize?: string, tickSize?: string) {
this.base = base
this.lotSize = lotSize
this.tickSize = tickSize
}
}
///// 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;
}
/////
export function createTokensList(titleCoin: string, tokens: TokenItem[], time: string, versionMajor: number, versionMinor = 1, versionPatch = 0): List {
if (!time) {
time = (new Date()).toISOString();
}
const list = new List(
`Trust Wallet: ${titleCoin}`,
"https://trustwallet.com/assets/images/favicon.png",
time,
tokens,
new Version(versionMajor, versionMinor, versionPatch)
);
sort(list);
return list;
}
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}.`);
}
// 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;
}
if (listOld !== undefined) {
list.version = listOld.version; // take over
list.timestamp = listOld.timestamp; // take over
const diffs = diffTokenlist(list, listOld);
if (diffs != undefined) {
//console.log("List has Changed", JSON.stringify(diffs));
list.version = new Version(list.version.major + 1, 0, 0);
list.timestamp = (new Date()).toISOString();
console.log(`Version and timestamp updated, ${list.version.major}.${list.version.minor}.${list.version.patch} timestamp ${list.timestamp}`);
}
}
writeToFile(filename, list);
}
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));
}
function checkTokenExists(id: string, chainName: string): boolean {
const logoPath = getChainAssetLogoPath(chainName, id);
if (!isPathExistsSync(logoPath)) {
//console.log("logo file missing", logoPath);
return false;
}
return true;
}
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);
}
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));
});
}
function removeAllPairs(list: List) {
// remove all pairs
list.pairs = [];
list.tokens.forEach(t => t.pairs = []);
}
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;
}
export async function rebuildTokenlist(chainName: string, pairs: [TokenItem, TokenItem][], listName: string, forceExcludeList: string[]): Promise<void> {
// sanity check, prevent deletion of many pairs
if (!pairs || pairs.length < 5) {
console.log(`Warning: Only ${pairs.length} pairs returned, ignoring`);
return;
}
const excludeList = parseForceList(forceExcludeList);
// filter out pairs with missing and excluded tokens
// prepare phase
const pairs2: [TokenItem, TokenItem][] = [];
pairs.forEach(p => {
if (!checkTokenExists(p[0].address, chainName)) {
console.log("pair with unsupported 1st coin:", p[0].symbol, "--", p[1].symbol);
return;
}
if (!checkTokenExists(p[1].address, chainName)) {
console.log("pair with unsupported 2nd coin:", p[0].symbol, "--", p[1].symbol);
return;
}
if (matchPairToForceList(p[0], p[1], excludeList)) {
console.log("pair excluded due to FORCE EXCLUDE:", p[0].symbol, "--", p[1].symbol);
return;
}
pairs2.push(p);
});
const filteredCount: number = pairs.length - pairs2.length;
console.log(`${filteredCount} unsupported tokens filtered out, ${pairs2.length} pairs`);
const tokenlistFile = getChainTokenlistPath(chainName);
const json = readJsonFile(tokenlistFile);
const list: List = json as List;
console.log(`Tokenlist original: ${list.tokens.length} tokens`);
removeAllPairs(list);
await bluebird.each(pairs2, async (p) => {
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);
}

View File

@ -1,5 +0,0 @@
export enum TokenType {
COIN = 'coin',
BEP2 = 'BEP2',
ERC20 = 'ERC20'
}

View File

@ -1,86 +0,0 @@
export const isLowerCase = (str: string): boolean => str.toLowerCase() === str;
export const isUpperCase = (str: string): boolean => str.toUpperCase() === str;
// Sort: treat numbers as number, strings as case-insensitive
export function sortElements (arr: unknown[]): unknown[] {
arr.sort((a, b) => {
if (typeof a === "number" && typeof b == "number") {
// numerical comparison
return a - b;
}
if ((typeof a === 'string' || a instanceof String) && (typeof b === 'string' || b instanceof String)) {
if (!isNaN(Number(a)) && !isNaN(Number(b))) {
// numerical comparison
return Number(a) - Number(b);
}
return a.toLowerCase() > b.toLowerCase() ? 1 : -1;
}
return 0;
});
return arr;
}
export function makeUnique(arr: string[]): string[] {
return Array.from(new Set(arr));
}
// Remove from set A elements of set B.
export function arrayDiff(a: string[], b: string[]): string[] {
const setB = new Set(b);
return a.filter(e => !setB.has(e));
}
// Remove from set A elements of set B, case insensitive
export function arrayDiffNocase(a: string[], b: string[]): string[] {
const setB = new Set(b.map(e => e.toLowerCase()));
return a.filter(e => !setB.has(e.toLowerCase()));
}
export function findDuplicates(list: string[]): string[] {
const m = new Map<string, number>();
const duplicates: string[] = [];
list.forEach(val => {
if (m.has(val.toLowerCase())) {
duplicates.push(val);
} else {
m.set(val.toLowerCase(), 0);
}
});
return makeUnique(duplicates);
}
// Check that two lists have no common elements, and no duplicates in either.
// Do a single check: checking for duplicates in the concatenated list.
export function findCommonElementsOrDuplicates(list1: string[], list2: string[]): string[] {
return findDuplicates(list1.concat(list2));
}
// Compare two arrays, order does not matter
export function arrayEqual(a1: string[], a2: string[]): boolean {
if (a1.length != a2.length) {
return false;
}
if (!(arrayDiff(a1, a2).length == 0)) {
return false;
}
if (!(arrayDiff(a2, a1).length == 0)) {
return false;
}
return true;
}
export function reverseCase(s: string): string {
const n = s.length;
let out = "";
for (let i = 0; i < n; ++i) {
const c = s[i];
if (isLowerCase(c)) {
out += c.toUpperCase();
} else if (isUpperCase(s[i])) {
out += c.toLowerCase();
} else {
out += c;
}
}
return out;
}

View File

@ -1,235 +0,0 @@
import { BinanceAction } from "../blockchain/binance";
import { SmartchainAction } from "../blockchain/smartchain";
import { EthereumAction } from "../blockchain/ethereum";
import { CosmosAction } from "../blockchain/cosmos";
import { AssetInfos } from "../generic/asset-infos";
import { EthForks } from "../generic/eth-forks";
import { FoldersFiles } from "../generic/folders-and-files";
import { JsonAction } from "../generic/json-format";
import { KavaAction } from "../blockchain/kava";
import { LogoSize } from "../generic/logo-size";
import { TerraAction } from "../blockchain/terra";
import { TezosAction } from "../blockchain/tezos";
import { TronAction } from "../blockchain/tron";
import { Validators } from "../generic/validators";
import { WavesAction } from "../blockchain/waves";
import { PolygonAction } from "../blockchain/polygon";
import { ActionInterface, CheckStepInterface } from "../generic/interface";
import * as chalk from 'chalk';
import * as bluebird from "bluebird";
const actionList: ActionInterface[] = [
new FoldersFiles(),
new AssetInfos(),
new EthForks(),
new LogoSize(),
new Validators(),
new JsonAction(),
// chains:
new BinanceAction(),
new SmartchainAction(),
new EthereumAction(),
new CosmosAction(),
new KavaAction(),
new PolygonAction(),
new TerraAction(),
new TezosAction(),
new TronAction(),
new WavesAction()
];
const maxErrosFromOneCheck = 5;
const markerError = chalk.red('XXX');
const markerWarning = chalk.yellow('!!');
const markerOK = chalk.green('✓');
async function checkStepList(steps: CheckStepInterface[]): Promise<[string[], string[]]> {
const errorsAll: string[] = [];
const warningsAll: string[] = [];
await bluebird.each(steps, async (step) => {
try {
//console.log(` Running check step '${step.getName()}'...`);
const [errors, warnings] = await step.check();
if (errors && errors.length > 0) {
console.log(`- ${markerError} '${step.getName()}': ${errors.length} errors`);
let cnt = 0;
errors.forEach(err => {
if (cnt < maxErrosFromOneCheck) {
console.log(` ${markerError} '${err}'`);
errorsAll.push(err);
} else if (cnt == maxErrosFromOneCheck) {
console.log(` ${markerError} ${errors.length} errors in total, omitting rest ...`);
}
cnt++;
});
}
if (warnings && warnings.length > 0) {
console.log(`- ${markerWarning} '${step.getName()}': ${warnings.length} warnings`);
let cnt = 0;
warnings.forEach(warn => {
if (cnt < maxErrosFromOneCheck) {
console.log(` ${markerWarning} '${warn}'`);
warningsAll.push(warn);
} else if (cnt == maxErrosFromOneCheck) {
console.log(` ${markerWarning} ${warnings.length} warnings in total, omitting rest ...`);
}
cnt++;
});
}
if (errors.length == 0 && warnings.length == 0) {
console.log(`- ${markerOK} '${step.getName()}' OK`);
}
} catch (error) {
console.log(`- ${markerError} '${step.getName()}': Caught error: ${error.message}`);
errorsAll.push(`${step.getName()}: Exception: ${error.message}`);
}
});
return [errorsAll, warningsAll];
}
async function sanityCheckByActionList(actions: ActionInterface[]): Promise<[string[], string[]]> {
console.log("Running sanity checks...");
const errors: string[] = [];
const warnings: string[] = [];
await bluebird.each(actions, async (action) => {
try {
if (action.getSanityChecks) {
const steps = action.getSanityChecks();
if (steps && steps.length > 0) {
console.log(` Action '${action.getName()}' has ${steps.length} check steps`);
const [errors1, warnings1] = await checkStepList(steps);
if (errors1.length > 0) {
errors1.forEach(e => errors.push(e));
}
if (warnings1.length > 0) {
warnings1.forEach(w => warnings.push(w));
}
if (errors1.length == 0 && warnings1.length == 0) {
console.log(`- ${markerOK} Action '${action.getName()}' OK, all ${steps.length} steps`);
}
}
}
} catch (error) {
console.log(`- ${markerError} '${action.getName()}' Caught error: ${error.message}`);
errors.push(`${action.getName()}: Exception: ${error.message}`);
}
});
console.log(`All sanity checks done, found ${errors.length} errors, ${warnings.length} warnings`);
return [errors, warnings];
}
async function consistencyCheckByActionList(actions: ActionInterface[]): Promise<[string[], string[]]> {
console.log("Running consistency checks...");
const errors: string[] = [];
const warnings: string[] = [];
await bluebird.each(actions, async (action) => {
try {
if (action.getConsistencyChecks) {
const steps = action.getConsistencyChecks();
if (steps && steps.length > 0) {
console.log(` Action '${action.getName()}' has ${steps.length} check steps`);
const [errors1, warnings1] = await checkStepList(steps);
if (errors1.length > 0) {
errors1.forEach(e => errors.push(e));
}
if (warnings1.length > 0) {
warnings1.forEach(w => warnings.push(w));
}
if (errors1.length == 0 && warnings1.length == 0) {
console.log(`- ${markerOK} Action '${action.getName()}' OK, all ${steps.length} steps`);
}
}
}
} catch (error) {
console.log(`- ${markerError} '${action.getName()}' Caught error: ${error.message}`);
errors.push(`${action.getName()}: Exception: ${error.message}`);
}
});
console.log(`All consistency checks done, found ${errors.length} errors, ${warnings.length} warnings`);
return [errors, warnings];
}
async function sanityFixByList(actions: ActionInterface[]) {
console.log("Running sanity fixes...");
await bluebird.each(actions, async (action) => {
try {
if (action.sanityFix) {
console.log(`Sanity fix '${action.getName()}':`);
await action.sanityFix();
}
} catch (error) {
console.log(`Caught error: ${error.message}`);
}
});
console.log("All sanity fixes done.");
}
async function consistencyFixByList(actions: ActionInterface[]) {
console.log("Running consistency fixes...");
await bluebird.each(actions, async (action) => {
try {
if (action.consistencyFix) {
console.log(`Consistency fix '${action.getName()}':`);
await action.consistencyFix();
}
} catch (error) {
console.log(`Caught error: ${error.message}`);
}
});
console.log("All consistency fixes done.");
}
async function updateAutoByList(actions: ActionInterface[]) {
console.log("Running auto updates (using external data sources) ...");
await bluebird.each(actions, async (action) => {
try {
if (action.updateAuto) {
console.log(`Auto update '${action.getName()}':`);
await action.updateAuto();
}
} catch (error) {
console.log(`Caught error: ${error.message}`);
}
});
console.log("All auto updates done.");
}
async function updateManualByList(actions: ActionInterface[]) {
console.log("Running manual updates (using external data sources) ...");
await bluebird.each(actions, async (action) => {
try {
if (action.updateManual) {
console.log(`Manual update '${action.getName()}':`);
await action.updateManual();
}
} catch (error) {
console.log(`Caught error: ${error.message}`);
}
});
console.log("All manual updates done.");
}
export async function sanityCheckAll(): Promise<[string[], string[]]> {
return await sanityCheckByActionList(actionList);
}
export async function consistencyCheckAll(): Promise<[string[], string[]]> {
return await consistencyCheckByActionList(actionList);
}
export async function sanityFixAll(): Promise<void> {
await sanityFixByList(actionList);
}
export async function consistencyFixAll(): Promise<void> {
await consistencyFixByList(actionList);
}
export async function updateAutoAll(): Promise<void> {
await updateAutoByList(actionList);
}
export async function updateManualAll(): Promise<void> {
await updateManualByList(actionList);
}

View File

@ -1,26 +0,0 @@
export interface ValidatorModel {
id: string,
name: string,
description: string,
website: string,
staking: Staking
payout: Payout
status: ValidatorStatus
}
interface Staking {
freeSpace: number,
minDelegation: number
openForDelegation: boolean
}
interface Payout {
commission: number // in %
payoutDelay: number // in cycles
payoutPeriod: number
}
interface ValidatorStatus {
disabled: boolean;
note: string;
}

View File

@ -1,100 +0,0 @@
import { stakingChains } from "../generic/blockchains";
import {
getChainValidatorsListPath,
getChainValidatorAssetLogoPath,
getChainValidatorsAssets
} from "../generic/repo-structure";
import { isPathExistsSync } from "../generic/filesystem";
import { formatSortJsonFile, readJsonFile } from "../generic/json";
import { ActionInterface, CheckStepInterface } from "../generic/interface";
import { isValidJSON } from "../generic/json";
import { ValidatorModel } from "../generic/validator-models";
import { isLogoOK } from "../generic/image";
import * as bluebird from "bluebird";
function formatValidators() {
stakingChains.forEach(chain => {
const validatorsPath = getChainValidatorsListPath(chain);
formatSortJsonFile(validatorsPath);
})
}
function getChainValidatorsList(chain: string): ValidatorModel[] {
return readJsonFile(getChainValidatorsListPath(chain)) as ValidatorModel[];
}
function isValidatorHasAllKeys(val: ValidatorModel): boolean {
return typeof val.id === "string"
&& typeof val.name === "string"
&& typeof val.description === "string"
&& typeof val.website === "string";
}
export class Validators implements ActionInterface {
getName(): string { return "Validators"; }
getSanityChecks(): CheckStepInterface[] {
const steps: CheckStepInterface[] = [
{
getName: () => { return "Make sure tests added for new staking chain"},
check: async (): Promise<[string[], string[]]> => {
if (stakingChains.length != 8) {
return [[`Wrong number of staking chains ${stakingChains.length}`], []];
}
return [[], []];
}
},
];
stakingChains.forEach(chain => {
steps.push(
{
getName: () => { return `Make sure chain ${chain} has valid list file, has logo`},
check: async (): Promise<[string[], string[]]> => {
const validatorsListPath = getChainValidatorsListPath(chain);
if (!isValidJSON(validatorsListPath)) {
return [[`Not valid Json file at path ${validatorsListPath}`], []];
}
const errors: string[] = [];
const validatorsList = getChainValidatorsList(chain);
const chainValidatorsAssetsList = getChainValidatorsAssets(chain);
await bluebird.each(validatorsList, async (val: ValidatorModel) => {
if (!isValidatorHasAllKeys(val)) {
errors.push(`Some key and/or type missing for validator ${JSON.stringify(val)}`);
}
const id = val.id;
const path = getChainValidatorAssetLogoPath(chain, id);
if (!isPathExistsSync(path)) {
errors.push(`Chain ${chain} asset ${id} logo must be present at path ${path}`);
}
const [isOk, logoMsg] = await isLogoOK(path);
if (!isOk) {
errors.push(logoMsg);
}
// Make sure validator has corresponding logo
if (!(chainValidatorsAssetsList.indexOf(id) >= 0)) {
errors.push(`Expecting image asset for validator ${id} on chain ${chain}`);
}
});
// Make sure validator asset logo has corresponding info
chainValidatorsAssetsList.forEach(valAssetLogoID => {
if (validatorsList.filter(v => v.id === valAssetLogoID).length != 1) {
errors.push(`Expect validator logo ${valAssetLogoID} to have info`);
}
});
return [errors, []];
}
}
);
});
return steps;
}
async sanityFix(): Promise<void> {
formatValidators();
}
}

View File

@ -1,68 +0,0 @@
export interface TagDescription {
id: string;
name: string;
description: string;
}
export const TagValues: TagDescription[] = [
{
id: "stablecoin",
name: "Stablecoin",
description: "Tokens that are fixed to an external asset, e.g. the US dollar."
},
{
id: "wrapped",
name: "Wrapped",
description: "Tokens that are wrapped or peg representation of digital assets. Excluded stablecoins"
},
{
id: "synthetics",
name: "Synthetics",
description: "Synthetic assets created to track the value of another asset"
},
{
id: "nft",
name: "NFT",
description: "Non-fungible tokens or tokens associated with the NFT ecosystem."
},
{
id: "governance",
name: "Governance",
description: "Tokens that used to participate in the governance process of the project."
},
{
id: "defi",
name: "DeFi",
description: "Tokens that are used for variety of decentralized financial applications."
},
{
id: "staking",
name: "Staking",
description: "Tokens that are used for staking to receive rewards."
},
{
id: "staking-native",
name: "Staking Native",
description: "Coins/Blockchains that are used for staking to secure the network to receive rewards."
},
{
id: "privacy",
name: "Privacy",
description: "Privacy tokens."
},
{
id: "nsfw",
name: "NSFW",
description: "Content Not suitable for work."
},
{
id: "binance-peg",
name: "Binance-Peg",
description: "Binance-Peg tokens."
},
{
id: "deflationary",
name: "Deflationary",
description: "Tokens that are deflationary or use mechanism to burn a token on transfer/swap."
}
];

View File

@ -1,13 +0,0 @@
import {
assetID
} from "../script/generic/asset";
describe("Test eth-address helpers", () => {
test(`Test coin`, () => {
expect(assetID(714)).toEqual('c714');
expect(assetID(714, '')).toEqual('c714');
});
test(`Test token`, () => {
expect(assetID(714, 'TWT-8C2')).toEqual('c714_tTWT-8C2');
});
});

View File

@ -1,166 +0,0 @@
import {
findDuplicates,
findCommonElementsOrDuplicates,
} from "../script/generic/types";
import {
isChecksum,
toChecksum,
isEthereumAddress
} from "../script/generic/eth-address";
import {
isDimensionTooLarge,
isDimensionOK,
calculateTargetSize
} from "../script/generic/image";
import {
sortElements,
makeUnique,
arrayDiff,
arrayDiffNocase,
arrayEqual,
reverseCase
} from "../script/generic/types";
import {
BinanceTokenInfo,
findImagesToFetch
} from "../script/blockchain/binance";
import { isValidStatusValue } from "../script/generic/status-values";
import { isValidTagValue, isValidTagValues } from "../script/generic/tag-values";
describe("Test eth-address helpers", () => {
test(`Test isChecksum`, () => {
expect(isChecksum("0x7Bb09bC8aDE747178e95B1D035ecBeEBbB18cFee", "ethereum"), `checksum`).toBe(true);
expect(isChecksum("0x7bb09bc8ade747178e95b1d035ecbeebbb18cfee", "ethereum"), `lowercase`).toBe(false);
expect(isChecksum("0x7Bb09bC8aDE747178e95B1D035ecBe", "ethereum"), `too short`).toBe(false);
expect(isChecksum("7Bb09bC8aDE747178e95B1D035ecBeEBbB18cFee", "ethereum"), `checksum, no prefix`).toBe(true);
expect(isChecksum("7bb09bc8ade747178e95b1d035ecbeebbb18cfee", "ethereum"), `lowercase, no prefix`).toBe(false);
expect(isChecksum("0x7Bb09bC8aDE747178e95B1D035ecBeEBbB18cFee", "wanchain"), `wanchain wrong checksum`).toBe(false);
expect(isChecksum("0x7bb09bc8ade747178e95b1d035ecbeebbb18cfee", "wanchain"), `wanchain lowercase`).toBe(false);
expect(isChecksum("0x7bB09Bc8Ade747178E95b1d035ECbEebBb18CfEE", "wanchain"), `wanchain checksum`).toBe(true);
});
test(`Test toChecksum`, () => {
expect(toChecksum("0x7bb09bc8ade747178e95b1d035ecbeebbb18cfee", "ethereum"), `from lowercase`).toEqual("0x7Bb09bC8aDE747178e95B1D035ecBeEBbB18cFee");
expect(toChecksum("0x7Bb09bC8aDE747178e95B1D035ecBeEBbB18cFee", "ethereum"), `from checksum`).toEqual("0x7Bb09bC8aDE747178e95B1D035ecBeEBbB18cFee");
expect(toChecksum("7bb09bc8ade747178e95b1d035ecbeebbb18cfee", "ethereum"), `from lowercase, no prefix`).toEqual("0x7Bb09bC8aDE747178e95B1D035ecBeEBbB18cFee");
expect(toChecksum("0x7bb09bc8ade747178e95b1d035ecbeebbb18cfee", "wanchain"), `wanchain, from lowercase`).toEqual("0x7bB09Bc8Ade747178E95b1d035ECbEebBb18CfEE");
});
test(`Test isEthereumAddress`, () => {
expect(isEthereumAddress("0x7bb09bc8ade747178e95b1d035ecbeebbb18cfee"), `valid, lowercase`).toBe(true);
expect(isEthereumAddress("0x7Bb09bC8aDE747178e95B1D035ecBeEBbB18cFee"), `valid, checksum`).toBe(true);
expect(isEthereumAddress("b09bc8ade747178e95b1d035ecbeebbb18cfee"), `invalid, short`).toBe(false);
expect(isEthereumAddress("7bb09bc8ade747178e95b1d035ecbeebbb18cfee"), `valid, no prefix`).toBe(true);
expect(isEthereumAddress("0x7bb09bc8qde747178e95b1d035ecbeebbb18cfee"), `invalid, length ok, invalid char`).toBe(false);
});
});
describe("Test image helpers", () => {
test(`Test isDimensionTooLarge`, () => {
expect(isDimensionTooLarge(256, 256), `256x256`).toBe(false);
expect(isDimensionTooLarge(64, 64), `64x64`).toBe(false);
expect(isDimensionTooLarge(800, 800), `800x800`).toBe(true);
expect(isDimensionTooLarge(256, 800), `256x800`).toBe(true);
expect(isDimensionTooLarge(800, 256), `800x256`).toBe(true);
});
test(`Test isDimensionOK`, () => {
expect(isDimensionOK(256, 256), `256x256`).toBe(true);
expect(isDimensionOK(64, 64), `64x64`).toBe(true);
expect(isDimensionOK(800, 800), `800x800`).toBe(false);
expect(isDimensionOK(256, 800), `256x800`).toBe(false);
expect(isDimensionOK(800, 256), `800x256`).toBe(false);
expect(isDimensionOK(60, 60), `60x60`).toBe(false);
expect(isDimensionOK(64, 60), `64x60`).toBe(false);
expect(isDimensionOK(60, 64), `60x64`).toBe(false);
});
test(`Test calculateReducedSize`, () => {
expect(calculateTargetSize(256, 256, 512, 512), `small 1.0`).toEqual({width: 512, height: 512});
expect(calculateTargetSize(800, 800, 512, 512), `large 1.0`).toEqual({width: 512, height: 512});
expect(calculateTargetSize(200, 100, 512, 512), `small 2.0`).toEqual({width: 512, height: 256});
expect(calculateTargetSize(100, 200, 512, 512), `small 0.5`).toEqual({width: 256, height: 512});
expect(calculateTargetSize(1200, 600, 512, 512), `small 2.0`).toEqual({width: 512, height: 256});
expect(calculateTargetSize(600, 1200, 512, 512), `small 0.5`).toEqual({width: 256, height: 512});
expect(calculateTargetSize(256, 0, 512, 512), `zero`).toEqual({width: 512, height: 512});
});
});
describe("Test type helpers", () => {
test(`Test sortElements`, () => {
expect(sortElements(["c", "a", "b"]), `3 elems`).toEqual(["a", "b", "c"]);
expect(sortElements(["C", "a", "b"]), `mixed case`).toEqual(["a", "b", "C"]);
expect(sortElements(["1", "2", "11"]), `numerical string`).toEqual(["1", "2", "11"]);
expect(sortElements([1, 2, 11]), `numerical`).toEqual([1, 2, 11]);
expect(sortElements(["C", "a", "1", "b", "2", "11"]), `complex`).toEqual(["1", "2", "11", "a", "b", "C"]);
});
test(`Test makeUnique`, () => {
expect(makeUnique(["a", "b", "c", "b"]), `4 elems with 1 duplicate`).toEqual(["a", "b", "c"]);
});
test(`Test arrayDiff`, () => {
expect(arrayDiff(["a", "b", "c"], ["c"]), `4 elems with 1 duplicate`).toEqual(["a", "b"]);
expect(arrayDiff(["a", "b", "c"], ["d"]), `4 elems with 0 duplicate`).toEqual(["a", "b", "c"]);
expect(arrayDiff(["a", "B", "c"], ["C"]), `4 elems with 0 duplicate`).toEqual(["a", "B", "c"]);
expect(arrayDiffNocase(["a", "B", "c"], ["C"]), `4 elems with 0 duplicate`).toEqual(["a", "B"]);
});
test(`Test findDuplicates`, () => {
expect(findDuplicates(["a", "bb", "ccc"]), `No duplicates`).toEqual([]);
expect(findDuplicates(["a", "bb", "ccc", "bb"]), `One double duplicate`).toEqual(["bb"]);
expect(findDuplicates([]), `Empty array`).toEqual([]);
expect(findDuplicates(["a"]), `One element`).toEqual([]);
expect(findDuplicates(["a", "bb", "ccc", "bb", "d", "bb"]), `One triple duplicate`).toEqual(["bb"]);
expect(findDuplicates(["a", "bb", "ccc", "bb", "a"]), `Two double duplicates`).toEqual(["bb", "a"]);
});
test(`Test findCommonElementsOrDuplicates`, () => {
expect(findCommonElementsOrDuplicates(["a", "bb", "ccc"], ["1", "22", "333"]), `No intersection or duplicates`).toEqual([]);
expect(findCommonElementsOrDuplicates(["a", "bb", "ccc"], ["1", "bb", "333"]), `Common element`).toEqual(["bb"]);
expect(findCommonElementsOrDuplicates(["a", "bb", "ccc", "bb"], ["1", "22", "333"]), `Duplicate in first`).toEqual(["bb"]);
expect(findCommonElementsOrDuplicates(["a", "bb", "ccc"], ["1", "22", "333", "22"]), `Duplicate in second`).toEqual(["22"]);
expect(findCommonElementsOrDuplicates(["a", "bb", "ccc", "1", "bb"], ["1", "22", "333", "22"]), `Intersection and duplicates`).toEqual(["bb", "1", "22"]);
expect(findCommonElementsOrDuplicates([], []), `Empty lists`).toEqual([]);
});
test(`Test arrayEqual`, () => {
expect(arrayEqual(["a", "b", "c"], ["a", "b", "c"]), `equal`).toBe(true);
expect(arrayEqual(["a", "b", "c", "d"], ["a", "b", "c"]), `length mismatch`).toBe(false);
expect(arrayEqual(["a", "b", "c"], ["a", "b", "b"]), `length mismatch`).toBe(false);
expect(arrayEqual(["a", "b", "b"], ["a", "b", "c"]), `length mismatch`).toBe(false);
});
test(`Test reverseCase`, () => {
expect(reverseCase("abCDef12+-"), `mixed`).toEqual("ABcdEF12+-");
expect(reverseCase("ABcdEF12+-"), `mixed`).toEqual("abCDef12+-");
});
});
describe("Test blockchain binance", () => {
test(`Test findImagesToFetch`, () => {
const infoA1: BinanceTokenInfo = {asset: "A1-11", name: "A 1", mappedAsset: "A1", assetImg: "imgurl1", decimals: 8};
const infoA2: BinanceTokenInfo = {asset: "A2-12", name: "A 2", mappedAsset: "A2", assetImg: "imgurl2", decimals: 8};
const assetsInfoListNonexisting: BinanceTokenInfo[] = [infoA1, infoA2];
const assetsInfoListExisting: BinanceTokenInfo[] = [
{asset: "BUSD-BD1", name: "Binance USD", mappedAsset: "BUSD", assetImg: "imgurlBUSD", decimals: 8},
{asset: "ETH-1C9", name: "Binance Ethereum", mappedAsset: "BETH", assetImg: "imgurlETH", decimals: 8}
];
expect(findImagesToFetch(assetsInfoListNonexisting), `2 nonexisting`).toEqual(assetsInfoListNonexisting);
expect(findImagesToFetch(assetsInfoListExisting), `2 existing`).toEqual([]);
expect(findImagesToFetch([]), `empty`).toEqual([]);
});
});
describe("Test status, tag values", () => {
test(`Test status-values`, () => {
expect(isValidStatusValue("active")).toEqual(true);
expect(isValidStatusValue("abandoned")).toEqual(true);
expect(isValidStatusValue("invalidvalue")).toEqual(false);
expect(isValidStatusValue("ACTIVE")).toEqual(false);
expect(isValidStatusValue("")).toEqual(false);
});
test(`Test tag-values`, () => {
expect(isValidTagValue("defi")).toEqual(true);
expect(isValidTagValue("staking")).toEqual(true);
expect(isValidStatusValue("invalidvalue")).toEqual(false);
expect(isValidStatusValue("STAKING")).toEqual(false);
expect(isValidStatusValue("")).toEqual(false);
expect(isValidTagValues(["defi"])).toEqual(true);
expect(isValidTagValues(["staking"])).toEqual(true);
expect(isValidTagValues(["defi", "staking"])).toEqual(true);
expect(isValidTagValues(["invalid"])).toEqual(false);
expect(isValidTagValues(["defi", "invalid"])).toEqual(false);
});
});

View File

@ -1,18 +0,0 @@
import {
toSatoshis,
fromSatoshis
} from "../script/generic/numbers";
describe("Test eth-address helpers", () => {
test(`Test to satoshis`, () => {
expect(toSatoshis('43523.423423432112321234', 18)).toEqual('43523423423432112321234');
expect(toSatoshis('0.123', 3)).toEqual('123');
expect(toSatoshis('0.00000001', 8)).toEqual('1');
});
test(`Test from Satoshis`, () => {
expect(fromSatoshis('123', 3)).toEqual('0.123');
expect(fromSatoshis('1234', 3)).toEqual('1.234');
expect(fromSatoshis('43523423423432112321234', 18)).toEqual('43523.423423432112321234');
expect(fromSatoshis('1', 8)).toEqual('0.00000001');
});
});