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:
@@ -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}` }]
|
||||
})
|
||||
}
|
||||
}
|
||||
+46
-12
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user