feat(wallet): inject coingecko id for custom tokens (#19489)

* feat(wallet): inject coingecko id for custom tokens

* review(supermassive): add missing return statements

* review(supermassive): remaining fixes

* review(jleonard): move to endpoints + add regex for invalid addresses

* review(jleonard): move address tests to utility funcs + add try..catch

* lint fixes

* skip query when contract address is empty
This commit is contained in:
Anirudha Bose
2023-08-05 06:50:12 +09:00
committed by GitHub
parent 98d661c58a
commit fa66bcb1cd
15 changed files with 472 additions and 24 deletions
@@ -6,6 +6,7 @@
#include "brave/components/brave_wallet/browser/blockchain_list_parser.h"
#include "brave/components/brave_wallet/browser/blockchain_list_schemas.h"
#include <map>
#include <tuple>
#include <utility>
@@ -688,4 +689,56 @@ absl::optional<DappListMap> ParseDappLists(const std::string& json) {
return dapp_lists;
}
absl::optional<CoingeckoIdsMap> ParseCoingeckoIdsMap(const std::string& json) {
// {
// "0x1": {
// "0xb9ef770b6a5e12e45983c5d80545258aa38f3b78": "0chain",
// "0xe41d2489571d322189246dafa5ebde1f4699f498": "0x",
// "0x5a3e6a77ba2f983ec0d371ea3b475f8bc0811ad5":
// "0x0-ai-ai-smart-contract",
// "0xfcdb9e987f9159dab2f507007d5e3d10c510aa70":
// "0x1-tools-ai-multi-tool",
// "0x37268c4f56ebb13dfae9c16d57d17579312d0ee1":
// "0xauto-io-contract-auto-deployer"
// }
// }
absl::optional<base::Value> records_v =
base::JSONReader::Read(json, base::JSON_PARSE_CHROMIUM_EXTENSIONS |
base::JSONParserOptions::JSON_PARSE_RFC);
if (!records_v || !records_v->is_dict()) {
VLOG(1) << "Invalid response, could not parse JSON, JSON is: " << json;
return absl::nullopt;
}
const base::Value::Dict* chain_ids = records_v->GetIfDict();
if (!chain_ids) {
return absl::nullopt;
}
std::map<std::pair<std::string, std::string>, std::string> coingecko_ids_map;
for (const auto chain_id_record : *chain_ids) {
const auto& chain_id = base::ToLowerASCII(chain_id_record.first);
const auto* contract_addresses = chain_id_record.second.GetIfDict();
if (!contract_addresses) {
return absl::nullopt;
}
for (const auto contract_address_record : *contract_addresses) {
const auto& contract_address =
base::ToLowerASCII(contract_address_record.first);
const auto* coingecko_id = contract_address_record.second.GetIfString();
if (!coingecko_id) {
return absl::nullopt;
}
coingecko_ids_map[{chain_id, contract_address}] = *coingecko_id;
}
}
return CoingeckoIdsMap(coingecko_ids_map.begin(), coingecko_ids_map.end());
}
} // namespace brave_wallet
@@ -15,6 +15,9 @@
namespace brave_wallet {
using CoingeckoIdsMap =
base::flat_map<std::pair<std::string, std::string>, std::string>;
using TokenListMap =
base::flat_map<std::string, std::vector<mojom::BlockchainTokenPtr>>;
using ChainList = std::vector<mojom::NetworkInfoPtr>;
@@ -38,7 +41,7 @@ absl::optional<std::vector<mojom::OnRampCurrency>> ParseOnRampCurrencyLists(
std::string GetTokenListKey(mojom::CoinType coin, const std::string& chain_id);
bool ParseChainList(const std::string& json, ChainList* chain_list);
absl::optional<DappListMap> ParseDappLists(const std::string& json);
absl::optional<CoingeckoIdsMap> ParseCoingeckoIdsMap(const std::string& json);
} // namespace brave_wallet
#endif // BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_BLOCKCHAIN_LIST_PARSER_H_
@@ -15,7 +15,7 @@ using testing::ElementsAreArray;
namespace brave_wallet {
TEST(ParseTokenListUnitTest, ParseTokenList) {
TEST(BlockchainListParseUnitTest, ParseTokenList) {
std::string json(R"(
{
"0x06012c8cf97BEaD5deAe237070F9587f8E7A266d": {
@@ -165,7 +165,7 @@ TEST(ParseTokenListUnitTest, GetTokenListKey) {
"solana.0x65");
}
TEST(ParseChainListUnitTest, ParseChainList) {
TEST(BlockchainListParseUnitTest, ParseChainList) {
const std::string chain_list = R"(
[
{
@@ -270,7 +270,7 @@ TEST(ParseChainListUnitTest, ParseChainList) {
EXPECT_FALSE(chain2->is_eip1559);
}
TEST(ParseDappListsUnitTest, ParseDappLists) {
TEST(BlockchainListParseUnitTest, ParseDappLists) {
const std::string dapp_list = R"({
"solana": {
"success": true,
@@ -489,7 +489,7 @@ TEST(ParseDappListsUnitTest, ParseDappLists) {
EXPECT_EQ(poly_dapp_list.size(), 0u);
}
TEST(ParseOnRampTokensListMapUnitTest, ParseOnRampTokensListMap) {
TEST(BlockchainListParseUnitTest, ParseOnRampTokensListMap) {
// Invalid JSON is not parsed
absl::optional<RampTokenListMaps> supported_tokens_list_map =
ParseRampTokenListMaps(R"({)");
@@ -676,7 +676,7 @@ TEST(ParseOnRampTokensListMapUnitTest, ParseOnRampTokensListMap) {
EXPECT_EQ(it->second[1]->coin, mojom::CoinType::ETH);
}
TEST(ParseOffRampTokensListMapUnitTest, ParseOffRampTokensListMap) {
TEST(BlockchainListParseUnitTest, ParseOffRampTokensListMap) {
const std::string supported_tokens_list = R"({
"tokens": [
{
@@ -778,4 +778,38 @@ TEST(ParseOnRampCurrencyListTest, ParseOnRampCurrencyLists) {
mojom::OnRampProvider::kStripe);
}
TEST(BlockchainListParseUnitTest, ParseCoingeckoIdsMap) {
const std::string json = R"({
"0x1": {
"0xb9ef770b6a5e12e45983c5d80545258aa38f3b78": "0chain",
"0xe41d2489571d322189246dafa5ebde1f4699f498": "0x",
"0x5a3e6a77ba2f983ec0d371ea3b475f8bc0811ad5": "0x0-ai-ai-smart-contract",
"0xfcdb9e987f9159dab2f507007d5e3d10c510aa70": "0x1-tools-ai-multi-tool"
}
})";
absl::optional<CoingeckoIdsMap> coingecko_ids_map =
ParseCoingeckoIdsMap(json);
ASSERT_TRUE(coingecko_ids_map);
EXPECT_EQ((*coingecko_ids_map)[std::pair(
"0x1", "0xb9ef770b6a5e12e45983c5d80545258aa38f3b78")],
"0chain");
EXPECT_EQ((*coingecko_ids_map)[std::pair(
"0x1", "0xe41d2489571d322189246dafa5ebde1f4699f498")],
"0x");
EXPECT_EQ((*coingecko_ids_map)[std::pair(
"0x1", "0x5a3e6a77ba2f983ec0d371ea3b475f8bc0811ad5")],
"0x0-ai-ai-smart-contract");
EXPECT_EQ((*coingecko_ids_map)[std::pair(
"0x1", "0xfcdb9e987f9159dab2f507007d5e3d10c510aa70")],
"0x1-tools-ai-multi-tool");
EXPECT_FALSE(coingecko_ids_map->contains({"0x2", "0xdeadbeef"}));
}
} // namespace brave_wallet
@@ -39,6 +39,11 @@ void BlockchainRegistry::Bind(
receivers_.Add(this, std::move(receiver));
}
void BlockchainRegistry::UpdateCoingeckoIdsMap(
CoingeckoIdsMap coingecko_ids_map) {
coingecko_ids_map_ = std::move(coingecko_ids_map);
}
void BlockchainRegistry::UpdateTokenList(TokenListMap token_list_map) {
token_list_map_ = std::move(token_list_map);
}
@@ -274,4 +279,23 @@ void BlockchainRegistry::GetTopDapps(const std::string& chain_id,
std::move(callback).Run(std::move(dapps_copy));
}
absl::optional<std::string> BlockchainRegistry::GetCoingeckoId(
const std::string& chain_id,
const std::string& contract_address) {
const auto& chain_id_lower = base::ToLowerASCII(chain_id);
const auto& contract_address_lower = base::ToLowerASCII(contract_address);
if (!coingecko_ids_map_.contains({chain_id_lower, contract_address_lower})) {
return absl::nullopt;
}
return coingecko_ids_map_[{chain_id_lower, contract_address_lower}];
}
void BlockchainRegistry::GetCoingeckoId(const std::string& chain_id,
const std::string& contract_address,
GetCoingeckoIdCallback callback) {
std::move(callback).Run(GetCoingeckoId(chain_id, contract_address));
}
} // namespace brave_wallet
@@ -33,6 +33,7 @@ class BlockchainRegistry : public mojom::BlockchainRegistry {
mojo::PendingRemote<mojom::BlockchainRegistry> MakeRemote();
void Bind(mojo::PendingReceiver<mojom::BlockchainRegistry> receiver);
void UpdateCoingeckoIdsMap(CoingeckoIdsMap coingecko_ids_map);
void UpdateTokenList(TokenListMap tokens);
void UpdateTokenList(const std::string key,
std::vector<mojom::BlockchainTokenPtr> list);
@@ -46,6 +47,9 @@ class BlockchainRegistry : public mojom::BlockchainRegistry {
mojom::CoinType coin,
const std::string& address);
std::vector<mojom::NetworkInfoPtr> GetPrepopulatedNetworks();
absl::optional<std::string> GetCoingeckoId(
const std::string& chain_id,
const std::string& contract_address);
// BlockchainRegistry interface methods
void GetTokenByAddress(const std::string& chain_id,
@@ -76,11 +80,15 @@ class BlockchainRegistry : public mojom::BlockchainRegistry {
void GetTopDapps(const std::string& chain_id,
mojom::CoinType coin,
GetTopDappsCallback callback) override;
void GetCoingeckoId(const std::string& chain_id,
const std::string& contract_address,
GetCoingeckoIdCallback callback) override;
protected:
std::vector<mojom::BlockchainTokenPtr>* GetTokenListFromChainId(
const std::string& chain_id);
CoingeckoIdsMap coingecko_ids_map_;
TokenListMap token_list_map_;
ChainList chain_list_;
DappListMap dapp_lists_;
@@ -419,6 +419,15 @@ const char dapp_lists_json[] = R"({
}
})";
const char coingecko_ids_map_json[] = R"({
"0xa": {
"0x7f5c764cbc14f9669b88837ca1490cca17c31607": "usd-coin"
},
"0x65": {
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v": "usd-coin"
}
})";
std::vector<std::string> GetChainIds(
const std::vector<mojom::NetworkInfoPtr>& networks) {
std::vector<std::string> result;
@@ -929,4 +938,72 @@ TEST(BlockchainRegistryUnitTest, GetEthTokenListMap) {
}
}
TEST(BlockchainRegistryUnitTest, GetCoingeckoId) {
base::test::TaskEnvironment task_environment;
auto* registry = BlockchainRegistry::GetInstance();
absl::optional<CoingeckoIdsMap> coingecko_ids_map =
ParseCoingeckoIdsMap(coingecko_ids_map_json);
ASSERT_TRUE(coingecko_ids_map);
registry->UpdateCoingeckoIdsMap(std::move(*coingecko_ids_map));
// Chain: ✅
// Contract: ✅
// Result: ✅
EXPECT_EQ(
registry->GetCoingeckoId(mojom::kOptimismMainnetChainId,
"0x7f5c764cbc14f9669b88837ca1490cca17c31607"),
"usd-coin");
// Chain: ✅
// Contract: ❌
// Result: ❌
EXPECT_EQ(
registry->GetCoingeckoId(mojom::kOptimismMainnetChainId, "0xdeadbeef"),
absl::nullopt);
// Chain: ❌
// Contract: ✅
// Result: ❌
EXPECT_EQ(registry->GetCoingeckoId(
"0xdeadbeef", "0x7f5c764cbc14f9669b88837ca1490cca17c31607"),
absl::nullopt);
// Chain: ❌
// Contract: ❌
// Result: ❌
EXPECT_EQ(registry->GetCoingeckoId("0xdeadbeef", "0xcafebabe"),
absl::nullopt);
// Chain: ✅ (wrong case)
// Contract: ✅
// Result: ✅
EXPECT_EQ(registry->GetCoingeckoId(
"0xA", "0x7f5c764cbc14f9669b88837ca1490cca17c31607"),
"usd-coin");
// Chain: ✅
// Contract: ✅ (mixed case EIP-55)
// Result: ✅
EXPECT_EQ(
registry->GetCoingeckoId(mojom::kOptimismMainnetChainId,
"0x7F5c764cBc14f9669B88837ca1490cCa17c31607"),
"usd-coin");
// Chain: ✅
// Contract: ✅ (mixed case Solana)
// Result: ✅
EXPECT_EQ(
registry->GetCoingeckoId(mojom::kSolanaMainnet,
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"),
"usd-coin");
// Chain: ✅
// Contract: ✅ (wrong case Solana)
// Result: ✅
EXPECT_EQ(
registry->GetCoingeckoId(mojom::kSolanaMainnet,
"epjfwdd5aufqssqem2qn1xzybapc8g4weggkzwytdt1v"),
"usd-coin");
}
} // namespace brave_wallet
@@ -1649,6 +1649,15 @@ void BraveWalletService::AddSuggestTokenRequest(
request->token = std::move(token);
}
if (request->token->coingecko_id.empty()) {
absl::optional<std::string> coingecko_id =
BlockchainRegistry::GetInstance()->GetCoingeckoId(
request->token->chain_id, request->token->contract_address);
if (coingecko_id) {
request->token->coingecko_id = *coingecko_id;
}
}
add_suggest_token_requests_[addr] = std::move(request);
add_suggest_token_callbacks_[addr] = std::move(callback);
add_suggest_token_ids_[addr] = std::move(id);
@@ -155,6 +155,39 @@ void OnSanitizedOnRampCurrenciesLists(
std::move(*lists));
}
void OnSanitizedCoingeckoIdsMap(data_decoder::JsonSanitizer::Result result) {
if (!result.has_value()) {
VLOG(1) << "CoingeckoIdsMap JSON validation error:" << result.error();
return;
}
absl::optional<CoingeckoIdsMap> coingecko_ids_map =
ParseCoingeckoIdsMap(*result);
if (!coingecko_ids_map) {
VLOG(1) << "Can't parse coingecko-ids.json";
return;
}
BlockchainRegistry::GetInstance()->UpdateCoingeckoIdsMap(
std::move(*coingecko_ids_map));
}
void HandleParseCoingeckoIdsMap(base::FilePath absolute_install_dir,
const std::string& filename) {
const base::FilePath coingecko_ids_map_json_path =
absolute_install_dir.AppendASCII(filename);
std::string coingecko_ids_map_json;
if (!base::ReadFileToString(coingecko_ids_map_json_path,
&coingecko_ids_map_json)) {
VLOG(1) << "Can't read coingecko ids map file: " << filename;
return;
}
data_decoder::JsonSanitizer::Sanitize(
std::move(coingecko_ids_map_json),
base::BindOnce(&OnSanitizedCoingeckoIdsMap));
}
void HandleParseTokenList(base::FilePath absolute_install_dir,
const std::string& filename,
mojom::CoinType coin_type) {
@@ -231,6 +264,21 @@ void HandleParseOnRampCurrenciesLists(base::FilePath absolute_install_dir,
base::BindOnce(&OnSanitizedOnRampCurrenciesLists));
}
void ParseCoingeckoIdsMapAndUpdateRegistry(const base::FilePath& install_dir) {
// On some platforms (e.g. Mac) we use symlinks for paths. Convert paths to
// absolute paths to avoid unexpected failure. base::MakeAbsoluteFilePath()
// requires IO so it can only be done in this function.
const base::FilePath absolute_install_dir =
base::MakeAbsoluteFilePath(install_dir);
if (absolute_install_dir.empty()) {
LOG(ERROR) << "Failed to get absolute install path.";
return;
}
HandleParseCoingeckoIdsMap(absolute_install_dir, "coingecko-ids.json");
}
void ParseTokenListAndUpdateRegistry(const base::FilePath& install_dir) {
// On some platforms (e.g. Mac) we use symlinks for paths. Convert paths to
// absolute paths to avoid unexpected failure. base::MakeAbsoluteFilePath()
@@ -240,6 +288,7 @@ void ParseTokenListAndUpdateRegistry(const base::FilePath& install_dir) {
if (absolute_install_dir.empty()) {
LOG(ERROR) << "Failed to get absolute install path.";
return;
}
HandleParseTokenList(absolute_install_dir, "contract-map.json",
@@ -259,6 +308,7 @@ void ParseChainListAndUpdateRegistry(const base::FilePath& install_dir) {
if (absolute_install_dir.empty()) {
LOG(ERROR) << "Failed to get absolute install path.";
return;
}
HandleParseChainList(absolute_install_dir, "chainlist.json");
@@ -273,6 +323,7 @@ void ParseDappListsAndUpdateRegistry(const base::FilePath& install_dir) {
if (absolute_install_dir.empty()) {
LOG(ERROR) << "Failed to get absolute install path.";
return;
}
HandleParseDappList(absolute_install_dir, "dapp-lists.json");
@@ -287,6 +338,7 @@ void ParseOnRampListsAndUpdateRegistry(const base::FilePath& install_dir) {
if (absolute_install_dir.empty()) {
LOG(ERROR) << "Failed to get absolute install path.";
return;
}
HandleParseRampTokenLists(absolute_install_dir, "ramp-tokens.json");
@@ -356,6 +408,10 @@ void WalletDataFilesInstallerPolicy::ComponentReady(
const base::FilePath& path,
base::Value::Dict manifest) {
last_installed_wallet_version = version;
sequenced_task_runner_->PostTask(
FROM_HERE, base::BindOnce(&ParseCoingeckoIdsMapAndUpdateRegistry, path));
sequenced_task_runner_->PostTask(
FROM_HERE, base::BindOnce(&ParseTokenListAndUpdateRegistry, path));
@@ -785,6 +785,10 @@ interface BlockchainRegistry {
// Returns lists of top dapps
GetTopDapps(string chain_id, CoinType coin) => (array<Dapp> dapps);
// Returns the Coincecko ID for a given chain id and contract address
GetCoingeckoId(string chain_id, string contract_address)
=> (string? coingecko_id);
};
// Implements the HD wallet, Ledger & Trezor integration, account management,
@@ -46,7 +46,8 @@ export function createWalletApiBase () {
'NFTPinningStatus',
'AutoPinEnabled',
'OnRampAssets',
'OffRampAssets'
'OffRampAssets',
'CoingeckoId'
],
endpoints: ({ mutation, query }) => ({})
})
@@ -101,9 +101,10 @@ import {
signLedgerSolanaTransaction,
signTrezorTransaction
} from '../async/hardware'
import { getAccountBalancesKey } from '../../utils/balance-utils';
import { onRampEndpoints } from './endpoints/on-ramp.endpoints';
import { offRampEndpoints } from './endpoints/off-ramp.endpoints';
import { getAccountBalancesKey } from '../../utils/balance-utils'
import { onRampEndpoints } from './endpoints/on-ramp.endpoints'
import { offRampEndpoints } from './endpoints/off-ramp.endpoints'
import { coingeckoEndpoints } from './endpoints/coingecko-endpoints'
type GetAccountTokenCurrentBalanceArg = {
accountId: BraveWallet.AccountId
@@ -2803,6 +2804,8 @@ export function createWalletApi () {
.injectEndpoints({ endpoints: onRampEndpoints })
// offRamp endpoints
.injectEndpoints({ endpoints: offRampEndpoints })
// coingecko endpoints
.injectEndpoints({ endpoints: coingeckoEndpoints })
}
export type WalletApi = ReturnType<typeof createWalletApi>
@@ -2823,6 +2826,7 @@ export const {
useGetAccountTokenCurrentBalanceQuery,
useGetHardwareAccountDiscoveryBalanceQuery,
useGetAddressByteCodeQuery,
useGetCoingeckoIdQuery,
useGetCombinedTokenBalanceForAllAccountsQuery,
useGetDefaultFiatCurrencyQuery,
useGetERC721MetadataQuery,
@@ -0,0 +1,68 @@
// Copyright (c) 2023 The Brave Authors. All rights reserved.
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
import {
BraveWallet
} from '../../../constants/types'
import { WalletApiEndpointBuilderParams } from '../api-base.slice'
// Utils
import {
isValidEVMAddress,
isValidSolanaAddress
} from '../../../utils/address-utils'
export const coingeckoEndpoints = ({
mutation,
query
}: WalletApiEndpointBuilderParams) => {
return {
getCoingeckoId: query<
string | null,
Pick<BraveWallet.BlockchainToken, 'chainId' | 'contractAddress'>
>({
queryFn: async (
{ chainId, contractAddress },
api,
extraOptions,
baseQuery
) => {
try {
// Ignore invalid EVM and Solana addresses.
//
// EVM => 0x + 40 hex characters
// Solana => 32-44 base58 characters
if (
!isValidEVMAddress(contractAddress) &&
!isValidSolanaAddress(contractAddress)
) {
return {
data: null
}
}
const { blockchainRegistry } = baseQuery(undefined).data
const { coingeckoId } = await blockchainRegistry.getCoingeckoId(
chainId,
contractAddress
)
return {
data: coingeckoId
}
} catch (err) {
console.error(err)
return {
error: 'Unable to query coingeckoId'
}
}
},
providesTags: (res, err, { chainId, contractAddress }) =>
err
? ['CoingeckoId', 'UNKNOWN_ERROR']
: [{ type: 'CoingeckoId', id: `${chainId}-${contractAddress}` }]
})
}
}
@@ -5,6 +5,7 @@
import * as React from 'react'
import { useSelector } from 'react-redux'
import { skipToken } from '@reduxjs/toolkit/query/react'
import Button from '@brave/leo/react/button'
// utils
@@ -27,8 +28,8 @@ import {
useTokenInfo
} from '../../../common/hooks'
import {
useGetNetworksRegistryQuery,
useGetSelectedChainQuery
useGetCoingeckoIdQuery,
useGetNetworksRegistryQuery
} from '../../../common/slices/api.slice'
import {
useGetCombinedTokensListQuery
@@ -74,7 +75,6 @@ export const AddCustomTokenForm = (props: Props) => {
} = props
// queries
const { data: selectedNetwork } = useGetSelectedChainQuery()
const { data: networksRegistry = emptyNetworksRegistry } =
useGetNetworksRegistryQuery()
@@ -86,7 +86,8 @@ export const AddCustomTokenForm = (props: Props) => {
const [tokenName, setTokenName] = React.useState<string>('')
const [tokenSymbol, setTokenSymbol] = React.useState<string>('')
const [tokenDecimals, setTokenDecimals] = React.useState<string>('')
const [coingeckoID, setCoingeckoID] = React.useState<string>('')
const [customCoingeckoId, setCustomCoingeckoId] =
React.useState<string | undefined>(undefined)
const [iconURL, setIconURL] = React.useState<string>('')
const [customAssetsNetwork, setCustomAssetsNetwork] = React.useState<BraveWallet.NetworkInfo>()
@@ -107,12 +108,28 @@ export const AddCustomTokenForm = (props: Props) => {
} = useTokenInfo(
getBlockchainTokenInfo,
combinedTokensList,
customAssetsNetwork || selectedNetwork
customAssetsNetwork
)
const {
onAddCustomAsset
} = useAssetManagement()
const { data: matchedCoingeckoId } = useGetCoingeckoIdQuery(
customAssetsNetwork && tokenContractAddress
? {
chainId: customAssetsNetwork.chainId,
contractAddress: tokenContractAddress
}
: skipToken
)
// If user has customized the coingecko id, use that even if it's an empty
// string.
const coingeckoId = customCoingeckoId ?? (
foundTokenInfoByContractAddress?.coingeckoId ||
tokenContractAddress ? matchedCoingeckoId || '': ''
)
// Handle Form Input Changes
const handleTokenNameChanged = React.useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
setHasError(false)
@@ -136,7 +153,7 @@ export const AddCustomTokenForm = (props: Props) => {
const handleCoingeckoIDChanged = React.useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
setHasError(false)
setCoingeckoID(event.target.value)
setCustomCoingeckoId(event.target.value)
}, [])
const handleIconURLChanged = React.useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
@@ -150,7 +167,7 @@ export const AddCustomTokenForm = (props: Props) => {
onChangeContractAddress('')
setTokenSymbol('')
setTokenDecimals('')
setCoingeckoID('')
setCustomCoingeckoId(undefined)
setIconURL('')
}, [onChangeContractAddress])
@@ -162,7 +179,7 @@ export const AddCustomTokenForm = (props: Props) => {
onNftAssetFound(foundTokenInfoByContractAddress.contractAddress)
}
let foundToken = { ...foundTokenInfoByContractAddress }
foundToken.coingeckoId = coingeckoID !== '' ? coingeckoID : foundTokenInfoByContractAddress.coingeckoId
foundToken.coingeckoId = coingeckoId
foundToken.logo = foundToken.logo ? foundToken.logo : iconURL
foundToken.chainId = customAssetsNetwork.chainId
onAddCustomAsset(foundToken)
@@ -180,14 +197,26 @@ export const AddCustomTokenForm = (props: Props) => {
tokenId: '',
logo: iconURL,
visible: true,
coingeckoId: coingeckoID,
coingeckoId,
chainId: customAssetsNetwork.chainId,
coin: customAssetsNetwork.coin
}
onAddCustomAsset(newToken)
}
onHideForm()
}, [tokenContractAddress, foundTokenInfoByContractAddress, customAssetsNetwork, iconURL, tokenDecimals, tokenName, tokenSymbol, coingeckoID, onAddCustomAsset, onHideForm, onNftAssetFound])
}, [
tokenContractAddress,
foundTokenInfoByContractAddress,
customAssetsNetwork,
iconURL,
tokenDecimals,
tokenName,
tokenSymbol,
coingeckoId,
onAddCustomAsset,
onHideForm,
onNftAssetFound
])
const onToggleShowAdvancedFields = () => setShowAdvancedFields(prev => !prev)
@@ -204,7 +233,12 @@ export const AddCustomTokenForm = (props: Props) => {
const onSelectCustomNetwork = React.useCallback((network: BraveWallet.NetworkInfo) => {
setCustomAssetsNetwork(network)
onHideNetworkDropDown()
}, [onHideNetworkDropDown])
setCustomCoingeckoId(undefined)
}, [
setCustomAssetsNetwork,
onHideNetworkDropDown,
setCustomCoingeckoId
])
const onClickCancel = React.useCallback(() => {
resetInputFields()
@@ -375,7 +409,7 @@ export const AddCustomTokenForm = (props: Props) => {
{getLocale('braveWalletWatchListCoingeckoId')}
</InputLabel>
<Input
value={coingeckoID}
value={coingeckoId}
onChange={handleCoingeckoIDChanged}
/>
</>
@@ -2,8 +2,18 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// you can obtain one at https://mozilla.org/MPL/2.0/.
import { isValidAddress, isValidFilAddress } from './address-utils'
import { mockAddresses, mockFilAddresses, mockFilInvalilAddresses } from '../common/constants/mocks'
import {
isValidAddress,
isValidEVMAddress,
isValidSolanaAddress,
isValidFilAddress
} from './address-utils'
import {
mockAddresses,
mockFilAddresses,
mockFilInvalilAddresses,
mockSolanaAccount
} from '../common/constants/mocks'
const validAdresses = mockAddresses.map((addr: string) => [addr, true])
const invalidAddresses = [
@@ -14,6 +24,7 @@ const invalidAddresses = [
const validFilAdresses = mockFilAddresses.map((addr: string) => [addr, true])
const validFilInvalidAdresses = mockFilInvalilAddresses.map((addr: string) => [addr, false])
const validSolanaAddress = mockSolanaAccount.address
describe('Address Utils', () => {
describe('isValidAddress', () => {
@@ -46,4 +57,55 @@ describe('Address Utils', () => {
expect(isValid).toBe(isValidFilAddress(address))
})
})
describe('isValidEVMAddress', () => {
it.each(validAdresses)(
'should return true if address is valid',
(address: string, isValid: boolean) => {
expect(isValid).toBe(isValidEVMAddress(address))
}
)
it.each(invalidAddresses)(
'should return false if address is invalid',
(address: string, isValid: boolean) => {
expect(isValid).toBe(isValidEVMAddress(address))
}
)
it('should return false if address length is invalid', () => {
expect(isValidEVMAddress('0xdeadbeef')).toBe(false)
expect(isValidEVMAddress(mockAddresses[1] + "0")).toBe(false)
})
it('should return false if address does not start with 0x', () => {
const testAddress = mockAddresses[1].substring(3) // exclude 0x
expect(isValidEVMAddress(testAddress)).toBe(false)
})
it('should return false if address contains invalid characters', () => {
expect(
isValidEVMAddress('0xdeadbeefdeadbeefdeadbeefdeadbeefdeadzzzz')
).toBe(false)
})
})
describe('isValidSolanaAddress', () => {
it('should return true if address is valid', () => {
expect(isValidSolanaAddress(validSolanaAddress)).toBe(true)
})
it('should return false if address length is invalid', () => {
expect(
isValidSolanaAddress(validSolanaAddress.substring(0, 31))
).toBe(false)
expect(isValidSolanaAddress(validSolanaAddress + "4")).toBe(false)
})
it('should return false if address contains invalid characters', () => {
expect(
isValidSolanaAddress(validSolanaAddress.substring(0, 31) + '0')
).toBe(false)
})
})
})
@@ -20,6 +20,9 @@ export function isValidFilAddress (value: string): boolean {
return (value.length === 41 || value.length === 86 || value.length === 44)
}
/**
* @deprecated Use isValidEVMAddress instead
*/
export function isValidAddress (value: string, length: number = 20): boolean {
if (!value.match(/^0x[0-9A-Fa-f]*$/)) {
return false
@@ -32,6 +35,14 @@ export function isValidAddress (value: string, length: number = 20): boolean {
return true
}
export function isValidEVMAddress (value: string): boolean {
return /^0x[a-fA-F0-9]{40}$/.test(value)
}
export function isValidSolanaAddress (value: string): boolean {
return /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(value)
}
export const suggestNewAccountName = (
accounts: BraveWallet.AccountInfo[],
network: BraveWallet.NetworkInfo