mirror of
https://github.com/Instadapp/trustwallet-assets.git
synced 2024-07-29 22:37:31 +00:00
Delete all outdated typescript code (#16752)
This commit is contained in:
parent
1aad5824aa
commit
f49f43f09d
|
@ -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
|
12
.eslintrc.js
12
.eslintrc.js
|
@ -1,12 +0,0 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: [
|
||||
'@typescript-eslint',
|
||||
],
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
],
|
||||
};
|
||||
|
3
.github/workflows/fix.yml
vendored
3
.github/workflows/fix.yml
vendored
|
@ -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
|
||||
|
|
14
Makefile
14
Makefile
|
@ -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
|
26
README.md
26
README.md
|
@ -3,6 +3,7 @@
|
|||

|
||||
|
||||
## 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.
|
||||
|
|
|
@ -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
18729
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
72
package.json
72
package.json
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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] || []
|
||||
)
|
||||
});
|
||||
}
|
|
@ -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, []];
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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, []];
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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, []];
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
|
@ -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, []];
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
|
@ -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, []];
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
|
@ -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, []];
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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();
|
|
@ -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();
|
|
@ -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();
|
|
@ -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();
|
|
@ -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();
|
|
@ -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();
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
];
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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, []];
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
|
@ -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];
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
export enum TokenType {
|
||||
COIN = 'coin',
|
||||
BEP2 = 'BEP2',
|
||||
ERC20 = 'ERC20'
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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."
|
||||
}
|
||||
];
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user