diff --git a/.github/workflows/fix-dryrun.yml b/.github/workflows/fix-dryrun.yml index a42ef0fbf..01ff854ca 100644 --- a/.github/workflows/fix-dryrun.yml +++ b/.github/workflows/fix-dryrun.yml @@ -1,25 +1,27 @@ -name: Fix (Dry run) -on: - pull_request: - branches: [ master ] +#Disabled, we have 'check' job, 'fix' job do invisible changes which is not convenient to debug -jobs: - fix-dryrun: - runs-on: ubuntu-latest - steps: - - name: Set up Go - uses: actions/setup-go@v4 - with: - go-version: 1.18 - - - name: Check out code - uses: actions/checkout@v3 - - - name: Run fix - run: make fix - - - name: Show fix result (diff) - run: | - git config core.ignorecase false - git status - git diff +#name: Fix (Dry run) +#on: +# pull_request: +# branches: [ master ] +# +#jobs: +# fix-dryrun: +# runs-on: ubuntu-latest +# steps: +# - name: Set up Go +# uses: actions/setup-go@v4 +# with: +# go-version: 1.18 +# +# - name: Check out code +# uses: actions/checkout@v3 +# +# - name: Run fix +# run: make fix +# +# - name: Show fix result (diff) +# run: | +# git config core.ignorecase false +# git status +# git diff diff --git a/.github/workflows/fix.yml b/.github/workflows/fix.yml index 8dec678a5..3d80ad089 100644 --- a/.github/workflows/fix.yml +++ b/.github/workflows/fix.yml @@ -1,46 +1,48 @@ -name: Fix All +#Disabled, we have 'check' job, 'fix' job do invisible changes which is not convenient to debug -on: - push: - branches: - - '*' - workflow_dispatch: - -jobs: - fix-all: - runs-on: ubuntu-latest - - if: github.repository_owner == 'trustwallet' && github.event.repository.fork == false - - steps: - - name: Checkout (trustwallet repo, secret token) - uses: actions/checkout@v3 - with: - # Use trust-ci fine-grained PAT to checkout and later commit the code - # Do not use for forked repos - token: ${{ secrets.CI_GITHUB_TOKEN }} - ref: ${{ github.ref }} - - - name: Set up Go - uses: actions/setup-go@v4 - with: - go-version: 1.18 - - - name: Run fix - run: make fix - - - name: Show fix result (diff) - run: | - git status - git diff - - - name: Run check - run: make check - - - name: Commit changes - if: success() - uses: stefanzweifel/git-auto-commit-action@v4.16.0 - with: - commit_user_name: trust-ci - commit_user_email: "117647528+trust-ci@users.noreply.github.com" - commit_message: Auto fix all (sanity and consistency) +#name: Fix All +# +#on: +# push: +# branches: +# - '*' +# workflow_dispatch: +# +#jobs: +# fix-all: +# runs-on: ubuntu-latest +# +# if: github.repository_owner == 'trustwallet' && github.event.repository.fork == false +# +# steps: +# - name: Checkout (trustwallet repo, secret token) +# uses: actions/checkout@v3 +# with: +# # Use trust-ci fine-grained PAT to checkout and later commit the code +# # Do not use for forked repos +# token: ${{ secrets.CI_GITHUB_TOKEN }} +# ref: ${{ github.ref }} +# +# - name: Set up Go +# uses: actions/setup-go@v4 +# with: +# go-version: 1.18 +# +# - name: Run fix +# run: make fix +# +# - name: Show fix result (diff) +# run: | +# git status +# git diff +# +# - name: Run check +# run: make check +# +# - name: Commit changes +# if: success() +# uses: stefanzweifel/git-auto-commit-action@v4.16.0 +# with: +# commit_user_name: trust-ci +# commit_user_email: "117647528+trust-ci@users.noreply.github.com" +# commit_message: Auto fix all (sanity and consistency) diff --git a/.golangci.yml b/.golangci.yml index 6633927eb..e8bdcae7d 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -192,7 +192,6 @@ linters: - deadcode - depguard - dogsled - - dupl - errcheck - exportloopref - exhaustive diff --git a/blockchains/nativeinjective/assets/inj1d5vz0uzwlpfvgwrwulxg6syy82axa58y4fuszd/info.json b/blockchains/nativeinjective/assets/inj1d5vz0uzwlpfvgwrwulxg6syy82axa58y4fuszd/info.json new file mode 100644 index 000000000..9d30b30ff --- /dev/null +++ b/blockchains/nativeinjective/assets/inj1d5vz0uzwlpfvgwrwulxg6syy82axa58y4fuszd/info.json @@ -0,0 +1,29 @@ +{ + "name": "Arbitrum", + "type": "INJECTIVE", + "symbol": "ARB", + "decimals": 8, + "website": "https://arbitrum.foundation/", + "description": "$ARB tokens can be used to vote on Arbitrum DAO governance proposals, allowing $ARB holders to collectively shape the future of Arbitrum protocols and chains. Token holders can also delegate their voting power to delegates.", + "explorer": "https://explorer.injective.network/asset/?tokenType=tokenFactory&denom=factory/inj14ejqjyq8um4p3xfqj74yld5waqljf88f9eneuk/inj1d5vz0uzwlpfvgwrwulxg6syy82axa58y4fuszd", + "status": "active", + "id": "inj1d5vz0uzwlpfvgwrwulxg6syy82axa58y4fuszd", + "links": [ + { + "name": "twitter", + "url": "https://twitter.com/arbitrum" + }, + { + "name": "whitepaper", + "url": "https://docs.arbitrum.foundation/deployment-addresses" + }, + { + "name": "discord", + "url": "https://discord.com/arbitrum" + }, + { + "name": "coinmarketcap", + "url": "https://coinmarketcap.com/ru/currencies/arbitrum-iou/" + } + ] +} \ No newline at end of file diff --git a/blockchains/nativeinjective/assets/inj1d5vz0uzwlpfvgwrwulxg6syy82axa58y4fuszd/logo.png b/blockchains/nativeinjective/assets/inj1d5vz0uzwlpfvgwrwulxg6syy82axa58y4fuszd/logo.png new file mode 100644 index 000000000..5402ed6bc Binary files /dev/null and b/blockchains/nativeinjective/assets/inj1d5vz0uzwlpfvgwrwulxg6syy82axa58y4fuszd/logo.png differ diff --git a/blockchains/nativeinjective/assets/inj1maeyvxfamtn8lfyxpjca8kuvauuf2qeu6gtxm3/info.json b/blockchains/nativeinjective/assets/inj1maeyvxfamtn8lfyxpjca8kuvauuf2qeu6gtxm3/info.json new file mode 100644 index 000000000..407407953 --- /dev/null +++ b/blockchains/nativeinjective/assets/inj1maeyvxfamtn8lfyxpjca8kuvauuf2qeu6gtxm3/info.json @@ -0,0 +1,21 @@ +{ + "name": "Talis", + "website": "https://injective.talis.art", + "description": "In the dynamic landscape of NFTs and cryptocurrency, platforms that offer innovative solutions and tools play a crucial role in shaping the ecosystem.", + "explorer": "https://explorer.injective.network/asset/?tokenType=tokenFactory&denom=factory/inj1maeyvxfamtn8lfyxpjca8kuvauuf2qeu6gtxm3/Talis", + "type": "INJECTIVE", + "symbol": "TALIS", + "decimals": 6, + "status": "active", + "id": "inj1maeyvxfamtn8lfyxpjca8kuvauuf2qeu6gtxm3", + "links": [ + { + "name": "twitter", + "url": "https://twitter.com/ProtocolTalis" + }, + { + "name": "telegram", + "url": "https://t.me/talisprotocol" + } + ] +} \ No newline at end of file diff --git a/blockchains/nativeinjective/assets/inj1maeyvxfamtn8lfyxpjca8kuvauuf2qeu6gtxm3/logo.png b/blockchains/nativeinjective/assets/inj1maeyvxfamtn8lfyxpjca8kuvauuf2qeu6gtxm3/logo.png new file mode 100644 index 000000000..4dc87c4c3 Binary files /dev/null and b/blockchains/nativeinjective/assets/inj1maeyvxfamtn8lfyxpjca8kuvauuf2qeu6gtxm3/logo.png differ diff --git a/blockchains/nativeinjective/assets/inj1q6kpxy6ar5lkxqudjvryarrrttmakwsvzkvcyh/info.json b/blockchains/nativeinjective/assets/inj1q6kpxy6ar5lkxqudjvryarrrttmakwsvzkvcyh/info.json new file mode 100644 index 000000000..28789d5cb --- /dev/null +++ b/blockchains/nativeinjective/assets/inj1q6kpxy6ar5lkxqudjvryarrrttmakwsvzkvcyh/info.json @@ -0,0 +1,40 @@ +{ + "name": "Chiliz", + "website": "https://chiliz.com", + "description": "Chiliz, powering Socios.com, aims to give sports and esports fans the ability to crowd-manage their favorite teams, games, leagues, and events.", + "explorer": "https://explorer.injective.network/asset/?tokenType=tokenFactory&denom=factory/inj14ejqjyq8um4p3xfqj74yld5waqljf88f9eneuk/inj1q6kpxy6ar5lkxqudjvryarrrttmakwsvzkvcyh", + "type": "INJECTIVE", + "symbol": "CHZ", + "decimals": 8, + "status": "active", + "id": "inj1q6kpxy6ar5lkxqudjvryarrrttmakwsvzkvcyh", + "tags": [ + "nft" + ], + "links": [ + { + "name": "telegram", + "url": "https://t.me/chiliz_io" + }, + { + "name": "twitter", + "url": "https://twitter.com/chiliZ/" + }, + { + "name": "facebook", + "url": "https://facebook.com/chiliZdotcom/" + }, + { + "name": "blog", + "url": "https://medium.com/chiliz" + }, + { + "name": "whitepaper", + "url": "https://chiliz.com/docs/CHZ_whitepaper.pdf" + }, + { + "name": "coingecko", + "url": "https://coingecko.com/en/coins/chiliz/" + } + ] +} \ No newline at end of file diff --git a/blockchains/nativeinjective/assets/inj1q6kpxy6ar5lkxqudjvryarrrttmakwsvzkvcyh/logo.png b/blockchains/nativeinjective/assets/inj1q6kpxy6ar5lkxqudjvryarrrttmakwsvzkvcyh/logo.png new file mode 100644 index 000000000..7db7a4585 Binary files /dev/null and b/blockchains/nativeinjective/assets/inj1q6kpxy6ar5lkxqudjvryarrrttmakwsvzkvcyh/logo.png differ diff --git a/blockchains/nativeinjective/assets/inj1q6zlut7gtkzknkk773jecujwsdkgq882akqksk/info.json b/blockchains/nativeinjective/assets/inj1q6zlut7gtkzknkk773jecujwsdkgq882akqksk/info.json new file mode 100644 index 000000000..5c36e623c --- /dev/null +++ b/blockchains/nativeinjective/assets/inj1q6zlut7gtkzknkk773jecujwsdkgq882akqksk/info.json @@ -0,0 +1,37 @@ +{ + "name": "USD Coin", + "website": "https://centre.io/usdc", + "description": "USDC is a fully collateralized US dollar stablecoin, an Ethereum powered coin and is the brainchild of CENTRE, an open source project bootstrapped by contributions from Circle and Coinbase.", + "explorer": "https://explorer.injective.network/asset/?tokenType=tokenFactory&denom=factory/inj14ejqjyq8um4p3xfqj74yld5waqljf88f9eneuk/inj1q6zlut7gtkzknkk773jecujwsdkgq882akqksk", + "type": "INJECTIVE", + "symbol": "USDCET", + "decimals": 6, + "status": "active", + "id": "inj1q6zlut7gtkzknkk773jecujwsdkgq882akqksk", + "tags": [ + "defi", + "stablecoin" + ], + "links": [ + { + "name": "github", + "url": "https://github.com/centrehq" + }, + { + "name": "medium", + "url": "https://medium.com/centre-blog" + }, + { + "name": "whitepaper", + "url": "https://centre.io/pdfs/centre-whitepaper.pdf" + }, + { + "name": "coinmarketcap", + "url": "https://coinmarketcap.com/currencies/usd-coin/" + }, + { + "name": "coingecko", + "url": "https://coingecko.com/en/coins/usd-coin/" + } + ] +} \ No newline at end of file diff --git a/blockchains/nativeinjective/assets/inj1q6zlut7gtkzknkk773jecujwsdkgq882akqksk/logo.png b/blockchains/nativeinjective/assets/inj1q6zlut7gtkzknkk773jecujwsdkgq882akqksk/logo.png new file mode 100644 index 000000000..c4def0472 Binary files /dev/null and b/blockchains/nativeinjective/assets/inj1q6zlut7gtkzknkk773jecujwsdkgq882akqksk/logo.png differ diff --git a/blockchains/nativeinjective/assets/inj1sthrn5ep8ls5vzz8f9gp89khhmedahhdkqa8z3/info.json b/blockchains/nativeinjective/assets/inj1sthrn5ep8ls5vzz8f9gp89khhmedahhdkqa8z3/info.json new file mode 100644 index 000000000..2402cacd3 --- /dev/null +++ b/blockchains/nativeinjective/assets/inj1sthrn5ep8ls5vzz8f9gp89khhmedahhdkqa8z3/info.json @@ -0,0 +1,32 @@ +{ + "name": "Solana", + "website": "https://solana.com/", + "description": "Solana is the worlds most performant blockchain in the world at 710k transactions per second. 710k TPS is achieved by encoding the passage of time as data.", + "explorer": "https://explorer.injective.network/asset/?tokenType=tokenFactory&denom=factory/inj14ejqjyq8um4p3xfqj74yld5waqljf88f9eneuk/inj1sthrn5ep8ls5vzz8f9gp89khhmedahhdkqa8z3", + "symbol": "SOL", + "type": "INJECTIVE", + "decimals": 8, + "status": "active", + "id": "inj1sthrn5ep8ls5vzz8f9gp89khhmedahhdkqa8z3", + "tags": [ + "staking-native" + ], + "links": [ + { + "name": "github", + "url": "https://github.com/solana-labs" + }, + { + "name": "twitter", + "url": "https://twitter.com/solana" + }, + { + "name": "reddit", + "url": "https://reddit.com/solana" + }, + { + "name": "whitepaper", + "url": "https://github.com/solana-labs/whitepaper" + } + ] +} \ No newline at end of file diff --git a/blockchains/nativeinjective/assets/inj1sthrn5ep8ls5vzz8f9gp89khhmedahhdkqa8z3/logo.png b/blockchains/nativeinjective/assets/inj1sthrn5ep8ls5vzz8f9gp89khhmedahhdkqa8z3/logo.png new file mode 100644 index 000000000..1d045ec77 Binary files /dev/null and b/blockchains/nativeinjective/assets/inj1sthrn5ep8ls5vzz8f9gp89khhmedahhdkqa8z3/logo.png differ diff --git a/go.mod b/go.mod index a7bc820d1..4ad6da675 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/spf13/cobra v1.7.0 github.com/trustwallet/assets-go-libs v0.3.7 github.com/trustwallet/go-libs v0.7.3 - github.com/trustwallet/go-primitives v0.1.16 + github.com/trustwallet/go-primitives v0.1.17 ) require ( diff --git a/go.sum b/go.sum index 0d272e5dd..6722c2a98 100644 --- a/go.sum +++ b/go.sum @@ -264,6 +264,14 @@ github.com/trustwallet/go-primitives v0.1.16-0.20240109124104-7cbe07e453d4 h1:sY github.com/trustwallet/go-primitives v0.1.16-0.20240109124104-7cbe07e453d4/go.mod h1:GN9WNIOvtz9j77FSYLEqC7apbMlzQnFLNO9LOCMy5Mc= github.com/trustwallet/go-primitives v0.1.16 h1:vpHVXlAYYK0o7GpWYadbauY+H4GOJ0bKVp/k6gZbH6I= github.com/trustwallet/go-primitives v0.1.16/go.mod h1:GN9WNIOvtz9j77FSYLEqC7apbMlzQnFLNO9LOCMy5Mc= +github.com/trustwallet/go-primitives v0.1.17-0.20240112104809-d94379365003 h1:0XkIncqfu2hBCkkhMrVw7hx/lraoyZ+G1BENh+CFtVA= +github.com/trustwallet/go-primitives v0.1.17-0.20240112104809-d94379365003/go.mod h1:GN9WNIOvtz9j77FSYLEqC7apbMlzQnFLNO9LOCMy5Mc= +github.com/trustwallet/go-primitives v0.1.17-0.20240112110133-ea920360531b h1:akbAfXaD987dDl5nbQbCiBSRP65pIWZdg9SNqY/pkfY= +github.com/trustwallet/go-primitives v0.1.17-0.20240112110133-ea920360531b/go.mod h1:GN9WNIOvtz9j77FSYLEqC7apbMlzQnFLNO9LOCMy5Mc= +github.com/trustwallet/go-primitives v0.1.17-0.20240112111413-aca39bd29cc2 h1:3tt6/BPEtZWpQlagvqEoGz9Z53Jx0vrU+uPtxT/lCWg= +github.com/trustwallet/go-primitives v0.1.17-0.20240112111413-aca39bd29cc2/go.mod h1:GN9WNIOvtz9j77FSYLEqC7apbMlzQnFLNO9LOCMy5Mc= +github.com/trustwallet/go-primitives v0.1.17 h1:X0vVRbJZoDcrQJTQ2vi5gubPfXEn43M9mpxkfRg/Fok= +github.com/trustwallet/go-primitives v0.1.17/go.mod h1:GN9WNIOvtz9j77FSYLEqC7apbMlzQnFLNO9LOCMy5Mc= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/internal/info/asset.go b/internal/info/asset.go new file mode 100644 index 000000000..8dffd8a7b --- /dev/null +++ b/internal/info/asset.go @@ -0,0 +1,56 @@ +package info + +import ( + "github.com/trustwallet/assets-go-libs/validation" + "github.com/trustwallet/go-primitives/coin" +) + +func ValidateAsset(a AssetModel, chain coin.Coin, addr string) error { + if err := ValidateAssetRequiredKeys(a); err != nil { + return err + } + + // All fields validated for nil and can be safety used. + compErr := validation.NewErrComposite() + if err := ValidateAssetType(*a.Type, chain); err != nil { + compErr.Append(err) + } + + if err := ValidateAssetID(*a.ID, addr); err != nil { + compErr.Append(err) + } + + if err := ValidateDecimals(*a.Decimals); err != nil { + compErr.Append(err) + } + + if err := ValidateAssetDecimalsAccordingType(*a.Type, *a.Decimals); err != nil { + compErr.Append(err) + } + + if err := ValidateStatus(*a.Status); err != nil { + compErr.Append(err) + } + + if err := ValidateDescription(*a.Description); err != nil { + compErr.Append(err) + } + + if err := ValidateDescriptionWebsite(*a.Description, *a.Website); err != nil { + compErr.Append(err) + } + + if err := ValidateExplorer(*a.Explorer, *a.Name, chain, addr, *a.Type); err != nil { + compErr.Append(err) + } + + if err := ValidateLinks(a.Links); err != nil { + compErr.Append(err) + } + + if compErr.Len() > 0 { + return compErr + } + + return nil +} diff --git a/internal/info/coin.go b/internal/info/coin.go new file mode 100644 index 000000000..623ea2470 --- /dev/null +++ b/internal/info/coin.go @@ -0,0 +1,47 @@ +package info + +import ( + "github.com/trustwallet/assets-go-libs/validation" +) + +func ValidateCoin(c CoinModel, allowedTags []string) error { + if err := ValidateCoinRequiredKeys(c); err != nil { + return err + } + + // All fields validated for nil and can be safety used. + compErr := validation.NewErrComposite() + if err := ValidateCoinType(*c.Type); err != nil { + compErr.Append(err) + } + + if err := ValidateDecimals(*c.Decimals); err != nil { + compErr.Append(err) + } + + if err := ValidateStatus(*c.Status); err != nil { + compErr.Append(err) + } + + if err := ValidateTags(c.Tags, allowedTags); err != nil { + compErr.Append(err) + } + + if err := ValidateDescription(*c.Description); err != nil { + compErr.Append(err) + } + + if err := ValidateDescriptionWebsite(*c.Description, *c.Website); err != nil { + compErr.Append(err) + } + + if err := ValidateLinks(c.Links); err != nil { + compErr.Append(err) + } + + if compErr.Len() > 0 { + return compErr + } + + return nil +} diff --git a/internal/info/external/erc20.go b/internal/info/external/erc20.go new file mode 100644 index 000000000..adf5981af --- /dev/null +++ b/internal/info/external/erc20.go @@ -0,0 +1,35 @@ +package external + +import ( + "fmt" + "strconv" + + "github.com/trustwallet/assets-go-libs/http" +) + +const ethAPIURL = "https://api.ethplorer.io/getTokenInfo/%s?apiKey=freekey" + +type TokenInfoERC20 struct { + Decimals string `json:"decimals"` + HoldersCount int `json:"holdersCount"` +} + +func GetTokenInfoForERC20(tokenID string) (*TokenInfo, error) { + url := fmt.Sprintf(ethAPIURL, tokenID) + + var result TokenInfoERC20 + err := http.GetHTTPResponse(url, &result) + if err != nil { + return nil, err + } + + decimals, err := strconv.Atoi(result.Decimals) + if err != nil { + return nil, err + } + + return &TokenInfo{ + Decimals: decimals, + HoldersCount: result.HoldersCount, + }, nil +} diff --git a/internal/info/external/external.go b/internal/info/external/external.go new file mode 100644 index 000000000..727015884 --- /dev/null +++ b/internal/info/external/external.go @@ -0,0 +1,85 @@ +package external + +import ( + "fmt" + "regexp" + "strconv" + "strings" + + "github.com/trustwallet/assets-go-libs/http" +) + +var ( + holdersRegexp = regexp.MustCompile(`(\d+)\saddresses`) + decimalsRegexp = regexp.MustCompile(`(\d+)\s+<\/div>`) + symbolRegexp = regexp.MustCompile(`(\w+)<\/b>\s 1 { + symbol = match[1] + } + + match = decimalsRegexp.FindStringSubmatch(pageContent) + if len(match) > 1 { + decimals, err = strconv.Atoi(match[1]) + if err != nil { + return nil, err + } + } + + match = holdersRegexp.FindStringSubmatch(pageContent) + if len(match) > 1 { + holders, err = strconv.Atoi(match[1]) + if err != nil { + return nil, err + } + } + + return &TokenInfo{ + Symbol: symbol, + Decimals: decimals, + HoldersCount: holders, + }, nil +} diff --git a/internal/info/external/spl.go b/internal/info/external/spl.go new file mode 100644 index 000000000..a95addf68 --- /dev/null +++ b/internal/info/external/spl.go @@ -0,0 +1,37 @@ +package external + +import ( + "fmt" + + "github.com/trustwallet/assets-go-libs/http" +) + +const splAPIURL = "https://public-api.solscan.io/token/holders?tokenAddress=%s" + +type TokenInfoSPL struct { + Data []Data `json:"data"` + HoldersCount int `json:"total"` +} + +type Data struct { + Decimals int `json:"decimals"` +} + +func GetTokenInfoForSPL(tokenID string) (*TokenInfo, error) { + url := fmt.Sprintf(splAPIURL, tokenID) + + var result TokenInfoSPL + err := http.GetHTTPResponse(url, &result) + if err != nil { + return nil, err + } + + if len(result.Data) == 0 { + return nil, fmt.Errorf("failed to get token info for SPL token") + } + + return &TokenInfo{ + Decimals: result.Data[0].Decimals, + HoldersCount: result.HoldersCount, + }, nil +} diff --git a/internal/info/external/trc10.go b/internal/info/external/trc10.go new file mode 100644 index 000000000..9a585f8d2 --- /dev/null +++ b/internal/info/external/trc10.go @@ -0,0 +1,38 @@ +package external + +import ( + "errors" + "fmt" + + "github.com/trustwallet/assets-go-libs/http" +) + +const trc10APIURL = "https://apilist.tronscan.io/api/token?id=%s" + +type TRC10TokensResponse struct { + Data []struct { + Symbol string `json:"abbr"` + Decimals int `json:"precision"` + HoldersCount int `json:"nrOfTokenHolders"` + } `json:"data"` +} + +func GetTokenInfoForTRC10(tokenID string) (*TokenInfo, error) { + url := fmt.Sprintf(trc10APIURL, tokenID) + + var res TRC10TokensResponse + err := http.GetHTTPResponse(url, &res) + if err != nil { + return nil, err + } + + if len(res.Data) == 0 { + return nil, errors.New("not found") + } + + return &TokenInfo{ + Symbol: res.Data[0].Symbol, + Decimals: res.Data[0].Decimals, + HoldersCount: res.Data[0].HoldersCount, + }, nil +} diff --git a/internal/info/external/trc20.go b/internal/info/external/trc20.go new file mode 100644 index 000000000..3f4c22885 --- /dev/null +++ b/internal/info/external/trc20.go @@ -0,0 +1,38 @@ +package external + +import ( + "errors" + "fmt" + + "github.com/trustwallet/assets-go-libs/http" +) + +const trc20APIURL = "https://apilist.tronscan.io/api/token_trc20?contract=%s" + +type TRC20TokensResponse struct { + TRC20Tokens []struct { + Symbol string `json:"symbol"` + Decimals int `json:"decimals"` + HoldersCount int `json:"holders_count"` + } `json:"trc20_tokens"` +} + +func GetTokenInfoForTRC20(tokenID string) (*TokenInfo, error) { + url := fmt.Sprintf(trc20APIURL, tokenID) + + var res TRC20TokensResponse + err := http.GetHTTPResponse(url, &res) + if err != nil { + return nil, err + } + + if len(res.TRC20Tokens) == 0 { + return nil, errors.New("not found") + } + + return &TokenInfo{ + Symbol: res.TRC20Tokens[0].Symbol, + Decimals: res.TRC20Tokens[0].Decimals, + HoldersCount: res.TRC20Tokens[0].HoldersCount, + }, nil +} diff --git a/internal/info/fields_validators.go b/internal/info/fields_validators.go new file mode 100644 index 000000000..0d87b3239 --- /dev/null +++ b/internal/info/fields_validators.go @@ -0,0 +1,263 @@ +package info + +import ( + "fmt" + "strings" + + str "github.com/trustwallet/assets-go-libs/strings" + "github.com/trustwallet/assets-go-libs/validation" + "github.com/trustwallet/go-primitives/coin" + "github.com/trustwallet/go-primitives/types" +) + +// Asset info specific validators. + +func ValidateAssetRequiredKeys(a AssetModel) error { + var fields []string + if a.Name != nil && !isEmpty(*a.Name) { + fields = append(fields, "name") + } + if a.Symbol != nil && !isEmpty(*a.Symbol) { + fields = append(fields, "symbol") + } + if a.Type != nil && !isEmpty(*a.Type) { + fields = append(fields, "type") + } + if a.Decimals != nil { + fields = append(fields, "decimals") + } + if a.Description != nil && !isEmpty(*a.Description) { + fields = append(fields, "description") + } + if a.Website != nil { + fields = append(fields, "website") + } + if a.Explorer != nil && !isEmpty(*a.Explorer) { + fields = append(fields, "explorer") + } + if a.Status != nil && !isEmpty(*a.Status) { + fields = append(fields, "status") + } + if a.ID != nil && !isEmpty(*a.ID) { + fields = append(fields, "id") + } + + if len(fields) != len(requiredAssetFields) { + return fmt.Errorf("%w: %s", validation.ErrMissingField, + strings.Join(str.Difference(requiredAssetFields, fields), ", ")) + } + + return nil +} + +func ValidateAssetType(assetType string, chain coin.Coin) error { + chainFromType, err := types.GetChainFromAssetType(assetType) + if err != nil { + return fmt.Errorf("failed to get chain from asset type: %w", err) + } + + if chainFromType != chain { + return fmt.Errorf("%w: asset type field", validation.ErrInvalidField) + } + + if strings.ToUpper(assetType) != assetType { + return fmt.Errorf("%w: asset type should be ALLCAPS", validation.ErrInvalidField) + } + + return nil +} + +func ValidateAssetID(id, address string) error { + if id != address { + if !strings.EqualFold(id, address) { + return fmt.Errorf("%w: invalid id field", validation.ErrInvalidField) + } + + return fmt.Errorf("%w: invalid case for id field", validation.ErrInvalidField) + } + + return nil +} + +func ValidateAssetDecimalsAccordingType(assetType string, decimals int) error { + if assetType == "BEP2" && decimals != 8 { + return fmt.Errorf("%w: invalid decimals field, BEP2 tokens have 8 decimals", validation.ErrInvalidField) + } + + return nil +} + +// CoinModel info specific validators. + +func ValidateCoinRequiredKeys(c CoinModel) error { + var fields []string + if c.Name != nil && !isEmpty(*c.Name) { + fields = append(fields, "name") + } + if c.Symbol != nil && !isEmpty(*c.Symbol) { + fields = append(fields, "symbol") + } + if c.Type != nil && !isEmpty(*c.Type) { + fields = append(fields, "type") + } + if c.Decimals != nil { + fields = append(fields, "decimals") + } + if c.Description != nil && !isEmpty(*c.Description) { + fields = append(fields, "description") + } + if c.Website != nil && !isEmpty(*c.Website) { + fields = append(fields, "website") + } + if c.Explorer != nil && !isEmpty(*c.Explorer) { + fields = append(fields, "explorer") + } + if c.Status != nil && !isEmpty(*c.Status) { + fields = append(fields, "status") + } + + if len(fields) != len(requiredCoinFields) { + return fmt.Errorf("%w: %s", validation.ErrMissingField, + strings.Join(str.Difference(requiredCoinFields, fields), ", ")) + } + + return nil +} + +func ValidateLinks(links []Link) error { + if len(links) < 2 { + return nil + } + + for _, l := range links { + if l.Name == nil || l.URL == nil { + return fmt.Errorf("%w: missing required fields links.url and links.name", validation.ErrMissingField) + } + + if !linkNameAllowed(*l.Name) { + return fmt.Errorf("invalid value for links.name filed, allowed only: %s", + strings.Join(supportedLinkNames(), ", ")) + } + + prefix := allowedLinkKeys[*l.Name] + if prefix != "" { + if !strings.HasPrefix(*l.URL, prefix) { + return fmt.Errorf("invalid value '%s' for %s link url, allowed only with prefix: %s", + *l.URL, *l.Name, prefix) + } + } + + if !strings.HasPrefix(*l.URL, "https://") { + return fmt.Errorf("invalid value for links.url field, allowed only with https:// prefix") + } + + if *l.Name == "medium" { + if !strings.Contains(*l.URL, "medium.com") { + return fmt.Errorf("invalid value for links.url field, should contain medium.com") + } + } + } + + return nil +} + +func ValidateCoinType(assetType string) error { + if assetType != "coin" { + return fmt.Errorf("%w: only \"coin\" type allowed for coin field", validation.ErrInvalidField) + } + + return nil +} + +func ValidateTags(tags, allowedTags []string) error { + for _, t := range tags { + if !str.Contains(t, allowedTags) { + return fmt.Errorf("%w: tag '%s' not allowed", validation.ErrInvalidField, t) + } + } + + return nil +} + +// Both infos can be validated by this validators. + +func ValidateDecimals(decimals int) error { + if decimals > 30 || decimals < 0 { + return fmt.Errorf("%w: decimals field", validation.ErrInvalidField) + } + + return nil +} + +func ValidateStatus(status string) error { + for _, f := range allowedStatusValues { + if f == status { + return nil + } + } + + return fmt.Errorf("%w: allowed status field values: %s", validation.ErrInvalidField, + strings.Join(allowedStatusValues, ", ")) +} + +func ValidateDescription(description string) error { + if len(description) > 600 { + return fmt.Errorf("%w: invalid length for description field", validation.ErrInvalidField) + } + + for _, ch := range whiteSpaceCharacters { + if strings.Contains(description, ch) { + return fmt.Errorf("%w: description contains not allowed characters (new line, double space)", + validation.ErrInvalidField) + } + } + + return nil +} + +func ValidateDescriptionWebsite(description, website string) error { + if description != "-" && website == "" { + return fmt.Errorf("%w: website field", validation.ErrMissingField) + } + + return nil +} + +func ValidateExplorer(explorer, name string, chain coin.Coin, addr, tokenType string) error { + // explorer url is composed with values outside info.json. + if chain == coin.Nativeinjective() { + return nil + } + + explorerExpected, err := coin.GetCoinExploreURL(chain, addr, tokenType) + if err != nil { + explorerExpected = "" + } + + explorerActual := explorer + + if !strings.EqualFold(explorerActual, explorerExpected) { + explorerAlt := explorerURLAlternatives(chain.Handle, name) + if len(explorerAlt) == 0 { + return nil + } + + var matchCount int + + for _, e := range explorerAlt { + if strings.EqualFold(e, explorerActual) { + matchCount++ + } + } + + if matchCount == 0 { + return fmt.Errorf("invalid value for explorer field, %s insted of %s", explorerActual, explorerExpected) + } + } + + return nil +} + +func isEmpty(field string) bool { + return field == "" +} diff --git a/internal/info/model.go b/internal/info/model.go new file mode 100644 index 000000000..0a5d237e9 --- /dev/null +++ b/internal/info/model.go @@ -0,0 +1,55 @@ +package info + +type ( + CoinModel struct { + Name *string `json:"name,omitempty"` + Website *string `json:"website,omitempty"` + Description *string `json:"description,omitempty"` + Explorer *string `json:"explorer,omitempty"` + Research string `json:"research,omitempty"` + Symbol *string `json:"symbol,omitempty"` + Type *string `json:"type,omitempty"` + Decimals *int `json:"decimals,omitempty"` + Status *string `json:"status,omitempty"` + Tags []string `json:"tags,omitempty"` + Links []Link `json:"links,omitempty"` + } + + AssetModel struct { + Name *string `json:"name,omitempty"` + Symbol *string `json:"symbol,omitempty"` + Type *string `json:"type,omitempty"` + Decimals *int `json:"decimals,omitempty"` + Description *string `json:"description,omitempty"` + Website *string `json:"website,omitempty"` + Explorer *string `json:"explorer,omitempty"` + Research string `json:"research,omitempty"` + Status *string `json:"status,omitempty"` + ID *string `json:"id,omitempty"` + Links []Link `json:"links,omitempty"` + ShortDesc *string `json:"short_desc,omitempty"` + Audit *string `json:"audit,omitempty"` + AuditReport *string `json:"audit_report,omitempty"` + Tags []string `json:"tags,omitempty"` + Code *string `json:"code,omitempty"` + Ticker *string `json:"ticker,omitempty"` + ExplorerEth *string `json:"explorer-ETH,omitempty"` + Address *string `json:"address,omitempty"` + Twitter *string `json:"twitter,omitempty"` + CoinMarketcap *string `json:"coinmarketcap,omitempty"` + DataSource *string `json:"data_source,omitempty"` + } + + Link struct { + Name *string `json:"name,omitempty"` + URL *string `json:"url,omitempty"` + } +) + +func (a *AssetModel) GetStatus() string { + if a.Status == nil { + return "" + } + + return *a.Status +} diff --git a/internal/info/values.go b/internal/info/values.go new file mode 100644 index 000000000..9463c1082 --- /dev/null +++ b/internal/info/values.go @@ -0,0 +1,79 @@ +package info + +import ( + "fmt" + "strings" + + "github.com/trustwallet/go-primitives/coin" +) + +//nolint:gochecknoglobals +var ( + requiredCoinFields = []string{ + "name", "type", "symbol", "decimals", + "description", "website", "explorer", "status", + } + + requiredAssetFields = []string{ + "name", "type", "symbol", "decimals", + "description", "website", "explorer", "status", "id", + } + + allowedStatusValues = []string{"active", "spam", "abandoned"} + + allowedLinkKeys = map[string]string{ + "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. + } + + whiteSpaceCharacters = []string{"\n", " "} +) + +func explorerURLAlternatives(chain, name string) []string { + var altUrls []string + + if name != "" { + nameNorm := strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll( + strings.ToLower(name), " ", ""), ")", ""), "(", "") + + if strings.ToLower(chain) == coin.Coins[coin.ETHEREUM].Name { + altUrls = append(altUrls, fmt.Sprintf("https://etherscan.io/token/%s", nameNorm)) + } + + altUrls = append(altUrls, fmt.Sprintf("https://explorer.%s.io", nameNorm)) + altUrls = append(altUrls, fmt.Sprintf("https://scan.%s.io", nameNorm)) + } + + return altUrls +} + +func linkNameAllowed(str string) bool { + if _, exists := allowedLinkKeys[str]; !exists { + return false + } + + return true +} + +func supportedLinkNames() []string { + names := make([]string, 0, len(allowedLinkKeys)) + for k := range allowedLinkKeys { + names = append(names, k) + } + + return names +} diff --git a/internal/processor/validators.go b/internal/processor/validators.go index c01f15902..61842118c 100644 --- a/internal/processor/validators.go +++ b/internal/processor/validators.go @@ -8,10 +8,10 @@ import ( "github.com/trustwallet/assets-go-libs/file" "github.com/trustwallet/assets-go-libs/path" "github.com/trustwallet/assets-go-libs/validation" - "github.com/trustwallet/assets-go-libs/validation/info" "github.com/trustwallet/assets-go-libs/validation/list" "github.com/trustwallet/assets-go-libs/validation/tokenlist" "github.com/trustwallet/assets/internal/config" + "github.com/trustwallet/assets/internal/info" "github.com/trustwallet/go-primitives/coin" )