From a3085fbe71f63de90dfc273f74fdf4e3a97a6c24 Mon Sep 17 00:00:00 2001 From: nvonpentz <12549658+nvonpentz@users.noreply.github.com> Date: Fri, 11 Nov 2022 11:05:37 -0500 Subject: [PATCH 01/17] Add backend support for Solana NFT metadata fetching --- .../brave_wallet/browser/json_rpc_service.cc | 266 ++++++++++++++---- .../brave_wallet/browser/json_rpc_service.h | 35 ++- .../solana_instruction_data_decoder.cc | 37 +++ .../browser/solana_instruction_data_decoder.h | 1 + ...olana_instruction_data_decoder_unittest.cc | 27 ++ .../brave_wallet/browser/solana_keyring.cc | 24 ++ .../brave_wallet/browser/solana_keyring.h | 3 + .../browser/solana_keyring_unittest.cc | 13 + .../brave_wallet/common/brave_wallet.mojom | 3 + 9 files changed, 349 insertions(+), 60 deletions(-) diff --git a/components/brave_wallet/browser/json_rpc_service.cc b/components/brave_wallet/browser/json_rpc_service.cc index ab53d5674b6a..164bb96bbcc0 100644 --- a/components/brave_wallet/browser/json_rpc_service.cc +++ b/components/brave_wallet/browser/json_rpc_service.cc @@ -31,6 +31,7 @@ #include "brave/components/brave_wallet/browser/json_rpc_requests_helper.h" #include "brave/components/brave_wallet/browser/json_rpc_response_parser.h" #include "brave/components/brave_wallet/browser/pref_names.h" +#include "brave/components/brave_wallet/browser/solana_instruction_data_decoder.h" #include "brave/components/brave_wallet/browser/solana_keyring.h" #include "brave/components/brave_wallet/browser/solana_requests.h" #include "brave/components/brave_wallet/browser/solana_response_parser.h" @@ -2085,62 +2086,28 @@ void JsonRpcService::OnGetTokenUri(GetTokenMetadataCallback callback, return; } - // Obtain JSON from the URL depending on the scheme. - // IPFS, HTTPS, and data URIs are supported. - // IPFS and HTTPS URIs require an additional request to fetch the metadata. - std::string metadata_json; - std::string scheme = url.scheme(); -#if BUILDFLAG(ENABLE_IPFS) - if (scheme != url::kDataScheme && scheme != url::kHttpsScheme && - scheme != ipfs::kIPFSScheme) { -#else - if (scheme != url::kDataScheme && scheme != url::kHttpsScheme) { -#endif - std::move(callback).Run( - "", mojom::ProviderError::kMethodNotSupported, - l10n_util::GetStringUTF8(IDS_WALLET_METHOD_NOT_SUPPORTED_ERROR)); - return; - } - - if (scheme == url::kDataScheme) { - if (!eth::ParseDataURIAndExtractJSON(url, &metadata_json)) { - std::move(callback).Run( - "", mojom::ProviderError::kParsingError, - l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); - return; - } - - // Sanitize JSON - data_decoder::JsonSanitizer::Sanitize( - std::move(metadata_json), - base::BindOnce(&JsonRpcService::OnSanitizeTokenMetadata, - weak_ptr_factory_.GetWeakPtr(), std::move(callback))); - return; - } - -#if BUILDFLAG(ENABLE_IPFS) - if (scheme == ipfs::kIPFSScheme && - !ipfs::TranslateIPFSURI(url, &url, ipfs::GetDefaultNFTIPFSGateway(prefs_), - false)) { - std::move(callback).Run("", mojom::ProviderError::kParsingError, - l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); - return; - } -#endif - - auto internal_callback = - base::BindOnce(&JsonRpcService::OnGetTokenMetadataPayload, - weak_ptr_factory_.GetWeakPtr(), std::move(callback)); - api_request_helper_->Request("GET", url, "", "", true, - std::move(internal_callback)); + FetchTokenMetadata( + url, mojom::CoinType::ETH, + base::BindOnce(&JsonRpcService::CompleteGetTokenMetadataEth, + weak_ptr_factory_.GetWeakPtr(), std::move(callback))); } void JsonRpcService::OnSanitizeTokenMetadata( - GetTokenMetadataCallback callback, + mojom::CoinType coin, + GetTokenMetadataIntermediateCallback callback, data_decoder::JsonSanitizer::Result result) { + mojom::ProviderErrorUnionPtr error; if (result.error) { + if (coin == mojom::CoinType::SOL) { + error = mojom::ProviderErrorUnion::NewSolanaProviderError( + mojom::SolanaProviderError::kParsingError); + } else { + error = mojom::ProviderErrorUnion::NewProviderError( + mojom::ProviderError::kParsingError); + } + VLOG(1) << "Data URI JSON validation error:" << *result.error; - std::move(callback).Run("", mojom::ProviderError::kParsingError, + std::move(callback).Run("", std::move(error), l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); return; } @@ -2150,28 +2117,77 @@ void JsonRpcService::OnSanitizeTokenMetadata( metadata_json = result.value.value(); } - std::move(callback).Run(metadata_json, mojom::ProviderError::kSuccess, ""); + if (coin == mojom::CoinType::SOL) { + error = mojom::ProviderErrorUnion::NewSolanaProviderError( + mojom::SolanaProviderError::kSuccess); + } else { + error = mojom::ProviderErrorUnion::NewProviderError( + mojom::ProviderError::kSuccess); + } + std::move(callback).Run(metadata_json, std::move(error), ""); } void JsonRpcService::OnGetTokenMetadataPayload( - GetTokenMetadataCallback callback, + mojom::CoinType coin, + GetTokenMetadataIntermediateCallback callback, APIRequestResult api_request_result) { + mojom::ProviderErrorUnionPtr error; if (!api_request_result.Is2XXResponseCode()) { + if (coin == mojom::CoinType::SOL) { + error = mojom::ProviderErrorUnion::NewSolanaProviderError( + mojom::SolanaProviderError::kInternalError); + } else { + error = mojom::ProviderErrorUnion::NewProviderError( + mojom::ProviderError::kInternalError); + } std::move(callback).Run( - "", mojom::ProviderError::kInternalError, + "", std::move(error), l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); return; } // Invalid JSON becomes an empty string after sanitization if (api_request_result.body().empty()) { - std::move(callback).Run("", mojom::ProviderError::kParsingError, + if (coin == mojom::CoinType::SOL) { + error = mojom::ProviderErrorUnion::NewSolanaProviderError( + mojom::SolanaProviderError::kParsingError); + } else { + error = mojom::ProviderErrorUnion::NewProviderError( + mojom::ProviderError::kParsingError); + } + std::move(callback).Run("", std::move(error), l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); return; } - std::move(callback).Run(api_request_result.body(), - mojom::ProviderError::kSuccess, ""); + if (coin == mojom::CoinType::SOL) { + error = mojom::ProviderErrorUnion::NewSolanaProviderError( + mojom::SolanaProviderError::kSuccess); + } else { + error = mojom::ProviderErrorUnion::NewProviderError( + mojom::ProviderError::kSuccess); + } + + std::move(callback).Run(api_request_result.body(), std::move(error), ""); +} + +void JsonRpcService::CompleteGetTokenMetadataEth( + GetTokenMetadataCallback callback, + const std::string& response, + mojom::ProviderErrorUnionPtr error, + const std::string& error_message) { + DCHECK(error && error->is_provider_error()); + std::move(callback).Run(response, error->get_provider_error(), error_message); +} + +void JsonRpcService::CompleteGetTokenMetadataSol( + GetMetaplexMetadataCallback callback, + const std::string& response, + mojom::ProviderErrorUnionPtr error, + const std::string& error_message) { + DCHECK(error && error->is_solana_provider_error()); + std::move(callback).Run(response, error->get_solana_provider_error(), + error_message); } void JsonRpcService::GetERC1155TokenBalance( @@ -2527,6 +2543,144 @@ void JsonRpcService::OnGetSPLTokenAccountBalance( std::move(callback).Run(amount, decimals, ui_amount_string, mojom::SolanaProviderError::kSuccess, ""); } + +void JsonRpcService::GetMetaplexMetadata(const std::string& nft_account_address, + GetMetaplexMetadataCallback callback) { + // Derive metadata PDA for the NFT accounts + absl::optional associated_metadata_account = + SolanaKeyring::GetAssociatedMetadataAccount(nft_account_address); + if (!associated_metadata_account) { + std::move(callback).Run( + "", mojom::SolanaProviderError::kInternalError, + l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); + return; + } + + auto account_info_metadata_callback = + base::BindOnce(&JsonRpcService::OnGetSolanaAccountInfoMetaplex, + weak_ptr_factory_.GetWeakPtr(), std::move(callback)); + + // Call getAccountInfo for the metadata PDA + auto account_info_response_callback = base::BindOnce( + &JsonRpcService::OnGetSolanaAccountInfo, weak_ptr_factory_.GetWeakPtr(), + std::move(account_info_metadata_callback)); + RequestInternal(solana::getAccountInfo(*associated_metadata_account), true, + network_urls_[mojom::CoinType::SOL], + std::move(account_info_response_callback)); +} + +void JsonRpcService::OnGetSolanaAccountInfoMetaplex( + GetMetaplexMetadataCallback callback, + absl::optional account_info, + mojom::SolanaProviderError error, + const std::string& error_message) { + if (error != mojom::SolanaProviderError::kSuccess || !account_info) { + std::move(callback).Run( + "", mojom::SolanaProviderError::kInternalError, + l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); + return; + } + + absl::optional> metadata = + base::Base64Decode((*account_info).data); + if (!metadata) { + std::move(callback).Run( + "", mojom::SolanaProviderError::kInternalError, + l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); + } + + absl::optional url = + solana_ins_data_decoder::DecodeMetadataUri(*metadata); + if (!url) { + std::move(callback).Run( + "", mojom::SolanaProviderError::kInternalError, + l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); + } + + FetchTokenMetadata( + *url, mojom::CoinType::SOL, + base::BindOnce(&JsonRpcService::CompleteGetTokenMetadataSol, + weak_ptr_factory_.GetWeakPtr(), std::move(callback))); +} + +void JsonRpcService::FetchTokenMetadata( + GURL url, + mojom::CoinType coin, + GetTokenMetadataIntermediateCallback callback) { + mojom::ProviderErrorUnionPtr error; + // Obtain JSON from the URL depending on the scheme. + // IPFS, HTTPS, and data URIs are supported. + // IPFS and HTTPS URIs require an additional request to fetch the metadata. + std::string metadata_json; + std::string scheme = url.scheme(); +#if BUILDFLAG(ENABLE_IPFS) + if (scheme != url::kDataScheme && scheme != url::kHttpsScheme && + scheme != ipfs::kIPFSScheme) { +#else + if (scheme != url::kDataScheme && scheme != url::kHttpsScheme) { +#endif + if (coin == mojom::CoinType::SOL) { + error = mojom::ProviderErrorUnion::NewSolanaProviderError( + mojom::SolanaProviderError::kInternalError); + } else { + error = mojom::ProviderErrorUnion::NewProviderError( + mojom::ProviderError::kMethodNotSupported); + } + std::move(callback).Run( + "", std::move(error), + l10n_util::GetStringUTF8(IDS_WALLET_METHOD_NOT_SUPPORTED_ERROR)); + return; + } + + if (scheme == url::kDataScheme) { + if (coin == mojom::CoinType::SOL) { + error = mojom::ProviderErrorUnion::NewSolanaProviderError( + mojom::SolanaProviderError::kParsingError); + } else { + error = mojom::ProviderErrorUnion::NewProviderError( + mojom::ProviderError::kParsingError); + } + + if (!eth::ParseDataURIAndExtractJSON(url, &metadata_json)) { + std::move(callback).Run( + "", std::move(error), + l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); + return; + } + + // Sanitize JSON + data_decoder::JsonSanitizer::Sanitize( + std::move(metadata_json), + base::BindOnce(&JsonRpcService::OnSanitizeTokenMetadata, + weak_ptr_factory_.GetWeakPtr(), coin, + std::move(callback))); + return; + } + +#if BUILDFLAG(ENABLE_IPFS) + if (scheme == ipfs::kIPFSScheme && + !ipfs::TranslateIPFSURI(url, &url, ipfs::GetDefaultNFTIPFSGateway(prefs_), + false)) { + if (coin == mojom::CoinType::SOL) { + error = mojom::ProviderErrorUnion::NewSolanaProviderError( + mojom::SolanaProviderError::kParsingError); + } else { + error = mojom::ProviderErrorUnion::NewProviderError( + mojom::ProviderError::kParsingError); + } + std::move(callback).Run("", std::move(error), + l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); + return; + } +#endif + + auto internal_callback = + base::BindOnce(&JsonRpcService::OnGetTokenMetadataPayload, + weak_ptr_factory_.GetWeakPtr(), coin, std::move(callback)); + api_request_helper_->Request("GET", url, "", "", true, + std::move(internal_callback)); +} + void JsonRpcService::SendFilecoinTransaction( const std::string& signed_tx, SendFilecoinTransactionCallback callback) { diff --git a/components/brave_wallet/browser/json_rpc_service.h b/components/brave_wallet/browser/json_rpc_service.h index a695c3248a17..a5f284209e03 100644 --- a/components/brave_wallet/browser/json_rpc_service.h +++ b/components/brave_wallet/browser/json_rpc_service.h @@ -86,11 +86,15 @@ class JsonRpcService : public KeyedService, public mojom::JsonRpcService { const std::vector>& reward, mojom::ProviderError error, const std::string& error_message)>; - using EthGetLogsCallback = base::OnceCallback& logs, mojom::ProviderError error, const std::string& error_message)>; + using GetTokenMetadataIntermediateCallback = + base::OnceCallback; + void GetBlockNumber(GetBlockNumberCallback callback); void GetFeeHistory(GetFeeHistoryCallback callback); @@ -365,6 +369,17 @@ class JsonRpcService : public KeyedService, public mojom::JsonRpcService { const std::string& token_mint_address, const std::string& chain_id, GetSPLTokenAccountBalanceCallback callback) override; + void GetMetaplexMetadata(const std::string& nft_account_address, + GetMetaplexMetadataCallback callback) override; + void FetchTokenMetadata(GURL url, + mojom::CoinType coin, + GetTokenMetadataIntermediateCallback callback); + void OnGetSolanaAccountInfoMetaplex( + GetMetaplexMetadataCallback callback, + absl::optional account_info, + mojom::SolanaProviderError error, + const std::string& error_message); + using SendSolanaTransactionCallback = base::OnceCallback* DecodeInstructionType( } // namespace +// Expects a the bytes of a Borsh encoded Metadata struct (see +// https://docs.rs/spl-token-metadata/latest/spl_token_metadata/state/struct.Metadata.html) +// and returns the URI string in of the nested Data struct (see +// https://docs.rs/spl-token-metadata/latest/spl_token_metadata/state/struct.Data.html) +// as a GURL. +absl::optional DecodeMetadataUri(const std::vector data) { + size_t offset = 0; + offset = offset + /* Skip first byte for metadata.key */ 1 + + /* Skip next 32 bytes for `metadata.update_authority` */ 32 + + /* Skip next 32 bytes for `metadata.mint` */ 32; + + // Skip next field, metdata.data.name, a string + // whose length is represented by a leading 32 bit integer + auto length = DecodeUint32(data, offset); + if (!length) { + return absl::nullopt; + } + offset += static_cast(*length); + + // Skip next field, `metdata.data.symbol`, a string + // whose length is represented by a leading 32 bit integer + length = DecodeUint32(data, offset); + if (!length) { + return absl::nullopt; + } + offset += static_cast(*length); + + // Parse next field, metadata.data.uri, a string + length = DecodeUint32(data, offset); + if (!length) { + return absl::nullopt; + } + std::string uri = + std::string(reinterpret_cast(&data[offset]), *length); + return GURL(uri); +} + absl::optional Decode( const std::vector& data, const std::string& program_id) { diff --git a/components/brave_wallet/browser/solana_instruction_data_decoder.h b/components/brave_wallet/browser/solana_instruction_data_decoder.h index 38b112841d57..9abb0ef89e61 100644 --- a/components/brave_wallet/browser/solana_instruction_data_decoder.h +++ b/components/brave_wallet/browser/solana_instruction_data_decoder.h @@ -15,6 +15,7 @@ namespace brave_wallet::solana_ins_data_decoder { +absl::optional DecodeMetadataUri(const std::vector data); absl::optional Decode( const std::vector& data, const std::string& program_id); diff --git a/components/brave_wallet/browser/solana_instruction_data_decoder_unittest.cc b/components/brave_wallet/browser/solana_instruction_data_decoder_unittest.cc index c243578cde36..045111f9fe2a 100644 --- a/components/brave_wallet/browser/solana_instruction_data_decoder_unittest.cc +++ b/components/brave_wallet/browser/solana_instruction_data_decoder_unittest.cc @@ -9,6 +9,7 @@ #include #include +#include "base/base64.h" #include "brave/components/brave_wallet/common/brave_wallet.mojom.h" #include "brave/components/brave_wallet/common/brave_wallet_constants.h" #include "testing/gtest/include/gtest/gtest.h" @@ -383,4 +384,30 @@ TEST_F(SolanaInstructionDecoderTest, Decode_InitializeMint2) { {{"decimals", "9"}, {"mint_authority", kPubkey1}}, true); } +TEST_F(SolanaInstructionDecoderTest, DecodeMetadataUri) { + auto uri_encoded = base::Base64Decode( + "BGUN5hJf2zSue3S0I/fCq16UREt5NxP6mQdaq4cdGPs3Q8PG/" + "R6KFUSgce78Nwk9Frvkd9bMbvTIKCRSDy88nZQgAAAAU1BFQ0lBTCBTQVVDRQAAAAAAAAAAA" + "AAAAAAAAAAAAAAKAAAAAAAAAAAAAAAAAMgAAABodHRwczovL2JhZmtyZWlmNHd4NTR3anI3c" + "GdmdWczd2xhdHIzbmZudHNmd25ndjZldXNlYmJxdWV6cnhlbmo2Y2s0LmlwZnMuZHdlYi5sa" + "W5rP2V4dD0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAOgDAQIAAABlDeYSX9s0rnt0tCP3wqtelERLeTcT+" + "pkHWquHHRj7NwFiDUmu+U8sXOOZQXL36xmknL+Zzd/" + "z3uw2G0ERMo8Eth4BAgABAf8BAAEBoivvbAzLh2kD2cSu6IQIqGQDGeoh/" + "UEDizyp6mLT1tUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="); + ASSERT_TRUE(uri_encoded); + auto uri = DecodeMetadataUri(*uri_encoded); + ASSERT_TRUE(uri); + EXPECT_EQ((*uri).spec(), + "https://" + "bafkreif4wx54wjr7pgfug3wlatr3nfntsfwngv6eusebbquezrxenj6ck4.ipfs." + "dweb.link/?ext="); + ASSERT_FALSE(DecodeMetadataUri({})); +} + } // namespace brave_wallet::solana_ins_data_decoder diff --git a/components/brave_wallet/browser/solana_keyring.cc b/components/brave_wallet/browser/solana_keyring.cc index 07d241c296de..dedbcd117fca 100644 --- a/components/brave_wallet/browser/solana_keyring.cc +++ b/components/brave_wallet/browser/solana_keyring.cc @@ -165,4 +165,28 @@ absl::optional SolanaKeyring::GetAssociatedTokenAccount( mojom::kSolanaAssociatedTokenProgramId); } +// static +absl::optional SolanaKeyring::GetAssociatedMetadataAccount( + const std::string& nft_account_address) { + std::vector> seeds; + const std::string& metadata_seed_constant = "metadata"; + std::vector metaplex_seed_constant_bytes( + metadata_seed_constant.begin(), metadata_seed_constant.end()); + std::vector metadata_program_id_bytes; + std::vector nft_account_address_bytes; + + if (!Base58Decode(mojom::kSolanaMetadataProgramId, &metadata_program_id_bytes, + kSolanaPubkeySize) || + !Base58Decode(nft_account_address, &nft_account_address_bytes, + kSolanaPubkeySize)) { + return absl::nullopt; + } + + seeds.push_back(std::move(metaplex_seed_constant_bytes)); + seeds.push_back(std::move(metadata_program_id_bytes)); + seeds.push_back(std::move(nft_account_address_bytes)); + + return FindProgramDerivedAddress(seeds, mojom::kSolanaMetadataProgramId); +} + } // namespace brave_wallet diff --git a/components/brave_wallet/browser/solana_keyring.h b/components/brave_wallet/browser/solana_keyring.h index 22de3bda81a5..9e1a698370dc 100644 --- a/components/brave_wallet/browser/solana_keyring.h +++ b/components/brave_wallet/browser/solana_keyring.h @@ -39,6 +39,9 @@ class SolanaKeyring : public HDKeyring { const std::string& spl_token_mint_address, const std::string& wallet_address); + static absl::optional GetAssociatedMetadataAccount( + const std::string& nft_account_address); + private: std::string GetAddressInternal(HDKeyBase* hd_key) const override; }; diff --git a/components/brave_wallet/browser/solana_keyring_unittest.cc b/components/brave_wallet/browser/solana_keyring_unittest.cc index b1f1437cb2d2..56f9a9d109ea 100644 --- a/components/brave_wallet/browser/solana_keyring_unittest.cc +++ b/components/brave_wallet/browser/solana_keyring_unittest.cc @@ -237,4 +237,17 @@ TEST(SolanaKeyringUnitTest, GetAssociatedTokenAccount) { EXPECT_EQ(*addr, "3bHK4cYoW94angdFWJeDBQcAuSq3mtYEdVaqkm1xXKcy"); } +TEST(SolanaKeyringUnitTest, GetAssociatedMetadataAccount) { + auto addr = SolanaKeyring::GetAssociatedMetadataAccount( + "5ZXToo7froykjvjnpHtTLYr9u2tW3USMwPg3sNkiaQVh"); + ASSERT_TRUE(addr); + EXPECT_EQ(*addr, "6L255rMB19d544HLNumpvbdTKkTgiQ3fgMszzX6F9VAL"); + + addr = SolanaKeyring::GetAssociatedMetadataAccount( + "8q5qbP8xu1TgDWYXokwFjgTqoSNe6W3Ljj3phwqhDKqe"); + ASSERT_TRUE(addr); + + EXPECT_EQ(*addr, "586XgHr69ZhbUkkGJsQqGt16mf7jpFS6uhnvCAwb68Qq"); +} + } // namespace brave_wallet diff --git a/components/brave_wallet/common/brave_wallet.mojom b/components/brave_wallet/common/brave_wallet.mojom index 9b523566198d..fdbfb4fd4acb 100644 --- a/components/brave_wallet/common/brave_wallet.mojom +++ b/components/brave_wallet/common/brave_wallet.mojom @@ -798,6 +798,7 @@ const string kSolanaSystemProgramId = "11111111111111111111111111111111"; const string kSolanaTokenProgramId = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"; const string kSolanaAssociatedTokenProgramId = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"; const string kSolanaSysvarRentProgramId = "SysvarRent111111111111111111111111111111111"; +const string kSolanaMetadataProgramId = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"; // It is required to keep these chain IDs lowercase ASCII const string kMainnetChainId = "0x1"; @@ -973,6 +974,8 @@ interface JsonRpcService { GetSPLTokenAccountBalance(string wallet_address, string token_mint_address, string chain_id) => (string amount, uint8 decimals, string uiAmountString, SolanaProviderError error, string error_message); + // Returns the metadata json associated with the NFT account address + GetMetaplexMetadata(string nft_account_address) => (string response, SolanaProviderError error, string error_message); }; enum TransactionStatus { From d501f73f7860def73ff4f21e6e9dab8cc2f2d138 Mon Sep 17 00:00:00 2001 From: nvonpentz <12549658+nvonpentz@users.noreply.github.com> Date: Mon, 14 Nov 2022 12:51:25 -0500 Subject: [PATCH 02/17] Address feedback: Separate some decode functions from solana_instruction_decoder to new solana_data_decoder_utils --- components/brave_wallet/browser/BUILD.gn | 2 + .../brave_wallet/browser/json_rpc_service.cc | 5 +- .../browser/solana_data_decoder_utils.cc | 204 ++++++++++++++ .../browser/solana_data_decoder_utils.h | 56 ++++ .../solana_data_decoder_utils_unittest.cc | 45 ++++ .../solana_instruction_data_decoder.cc | 251 +++--------------- .../browser/solana_instruction_data_decoder.h | 1 - ...olana_instruction_data_decoder_unittest.cc | 27 -- components/brave_wallet/browser/test/BUILD.gn | 1 + 9 files changed, 346 insertions(+), 246 deletions(-) create mode 100644 components/brave_wallet/browser/solana_data_decoder_utils.cc create mode 100644 components/brave_wallet/browser/solana_data_decoder_utils.h create mode 100644 components/brave_wallet/browser/solana_data_decoder_utils_unittest.cc diff --git a/components/brave_wallet/browser/BUILD.gn b/components/brave_wallet/browser/BUILD.gn index 43442db2c3b0..849a53f7b5c7 100644 --- a/components/brave_wallet/browser/BUILD.gn +++ b/components/brave_wallet/browser/BUILD.gn @@ -101,6 +101,8 @@ static_library("browser") { "solana_account_meta.h", "solana_block_tracker.cc", "solana_block_tracker.h", + "solana_data_decoder_utils.cc", + "solana_data_decoder_utils.h", "solana_instruction.cc", "solana_instruction.h", "solana_instruction_builder.cc", diff --git a/components/brave_wallet/browser/json_rpc_service.cc b/components/brave_wallet/browser/json_rpc_service.cc index 164bb96bbcc0..2fd6da3c56ac 100644 --- a/components/brave_wallet/browser/json_rpc_service.cc +++ b/components/brave_wallet/browser/json_rpc_service.cc @@ -31,7 +31,7 @@ #include "brave/components/brave_wallet/browser/json_rpc_requests_helper.h" #include "brave/components/brave_wallet/browser/json_rpc_response_parser.h" #include "brave/components/brave_wallet/browser/pref_names.h" -#include "brave/components/brave_wallet/browser/solana_instruction_data_decoder.h" +#include "brave/components/brave_wallet/browser/solana_data_decoder_utils.h" #include "brave/components/brave_wallet/browser/solana_keyring.h" #include "brave/components/brave_wallet/browser/solana_requests.h" #include "brave/components/brave_wallet/browser/solana_response_parser.h" @@ -2589,8 +2589,7 @@ void JsonRpcService::OnGetSolanaAccountInfoMetaplex( l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); } - absl::optional url = - solana_ins_data_decoder::DecodeMetadataUri(*metadata); + absl::optional url = DecodeMetadataUri(*metadata); if (!url) { std::move(callback).Run( "", mojom::SolanaProviderError::kInternalError, diff --git a/components/brave_wallet/browser/solana_data_decoder_utils.cc b/components/brave_wallet/browser/solana_data_decoder_utils.cc new file mode 100644 index 000000000000..4c2bd131f1f8 --- /dev/null +++ b/components/brave_wallet/browser/solana_data_decoder_utils.cc @@ -0,0 +1,204 @@ +/* Copyright (c) 2022 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 http://mozilla.org/MPL/2.0/. */ + +#include "brave/components/brave_wallet/browser/solana_instruction_data_decoder.h" + +#include +#include + +#include "base/containers/flat_map.h" +#include "base/containers/span.h" +#include "base/no_destructor.h" +#include "base/notreached.h" +#include "base/sys_byteorder.h" +#include "brave/components/brave_wallet/common/brave_wallet_constants.h" +#include "brave/components/brave_wallet/common/solana_utils.h" +#include "build/build_config.h" +#include "components/grit/brave_components_strings.h" +#include "ui/base/l10n/l10n_util.h" + +namespace brave_wallet { + +constexpr uint8_t kAuthorityTypeMax = 3; +constexpr size_t kMaxStringSize32Bit = 4294967291u; + +absl::optional DecodeUint8(const std::vector& input, + size_t& offset) { + if (offset >= input.size() || input.size() - offset < sizeof(uint8_t)) { + return absl::nullopt; + } + + offset += sizeof(uint8_t); + return input[offset - sizeof(uint8_t)]; +} + +absl::optional DecodeUint8String(const std::vector& input, + size_t& offset) { + auto ret = DecodeUint8(input, offset); + if (!ret) + return absl::nullopt; + return base::NumberToString(*ret); +} + +absl::optional DecodeAuthorityTypeString( + const std::vector& input, + size_t& offset) { + auto ret = DecodeUint8(input, offset); + if (ret && *ret <= kAuthorityTypeMax) + return base::NumberToString(*ret); + return absl::nullopt; +} + +absl::optional DecodeUint32(const std::vector& input, + size_t& offset) { + if (offset >= input.size() || input.size() - offset < sizeof(uint32_t)) { + return absl::nullopt; + } + + // Read bytes in little endian order. + base::span s = + base::make_span(input.begin() + offset, sizeof(uint32_t)); + uint32_t uint32_le = *reinterpret_cast(s.data()); + + offset += sizeof(uint32_t); + +#if defined(ARCH_CPU_LITTLE_ENDIAN) + return uint32_le; +#else + return base::ByteSwap(uint32_le); +#endif +} + +absl::optional DecodeUint32String( + const std::vector& input, + size_t& offset) { + auto ret = DecodeUint32(input, offset); + if (!ret) + return absl::nullopt; + return base::NumberToString(*ret); +} + +absl::optional DecodeUint64(const std::vector& input, + size_t& offset) { + if (offset >= input.size() || input.size() - offset < sizeof(uint64_t)) { + return absl::nullopt; + } + + // Read bytes in little endian order. + base::span s = + base::make_span(input.begin() + offset, sizeof(uint64_t)); + uint64_t uint64_le = *reinterpret_cast(s.data()); + + offset += sizeof(uint64_t); + +#if defined(ARCH_CPU_LITTLE_ENDIAN) + return uint64_le; +#else + return base::ByteSwap(uint64_le); +#endif +} + +absl::optional DecodeUint64String( + const std::vector& input, + size_t& offset) { + auto ret = DecodeUint64(input, offset); + if (!ret) + return absl::nullopt; + return base::NumberToString(*ret); +} + +absl::optional DecodePublicKey(const std::vector& input, + size_t& offset) { + if (offset >= input.size() || input.size() - offset < kSolanaPubkeySize) + return absl::nullopt; + + offset += kSolanaPubkeySize; + return Base58Encode(std::vector( + input.begin() + offset - kSolanaPubkeySize, input.begin() + offset)); +} + +absl::optional DecodeOptionalPublicKey( + const std::vector& input, + size_t& offset) { + if (offset == input.size()) { + return absl::nullopt; + } + + // First byte is 0 or 1 to indicate if public key is passed. + // And the rest bytes are the actual public key. + if (input[offset] == 0) { + offset++; + return ""; // No public key is passed. + } else if (input[offset] == 1) { + offset++; + return DecodePublicKey(input, offset); + } else { + return absl::nullopt; + } +} + +// bincode::serialize uses two u32 together for the string length and a byte +// array for the actual strings. The first u32 represents the lower bytes of +// the length, the second represents the upper bytes. The upper bytes will have +// non-zero value only when the length exceeds the maximum of u32. +// We currently cap the length here to be the max size of std::string +// on 32 bit systems, it's safe to do so because currently we don't expect any +// valid cases would have strings larger than it. +absl::optional DecodeString(const std::vector& input, + size_t& offset) { + auto len_lower = DecodeUint32(input, offset); + if (!len_lower || *len_lower > kMaxStringSize32Bit) + return absl::nullopt; + auto len_upper = DecodeUint32(input, offset); + if (!len_upper || *len_upper != 0) { // Non-zero means len exceeds u32 max. + return absl::nullopt; + } + + if (offset + *len_lower > input.size()) + return absl::nullopt; + + offset += *len_lower; + return std::string(reinterpret_cast(&input[offset - *len_lower]), + *len_lower); +} + +// Expects a the bytes of a Borsh encoded Metadata struct (see +// https://docs.rs/spl-token-metadata/latest/spl_token_metadata/state/struct.Metadata.html) +// and returns the URI string in of the nested Data struct (see +// https://docs.rs/spl-token-metadata/latest/spl_token_metadata/state/struct.Data.html) +// as a GURL. +absl::optional DecodeMetadataUri(const std::vector data) { + size_t offset = 0; + offset = offset + /* Skip first byte for metadata.key */ 1 + + /* Skip next 32 bytes for `metadata.update_authority` */ 32 + + /* Skip next 32 bytes for `metadata.mint` */ 32; + + // Skip next field, metdata.data.name, a string + // whose length is represented by a leading 32 bit integer + auto length = DecodeUint32(data, offset); + if (!length) { + return absl::nullopt; + } + offset += static_cast(*length); + + // Skip next field, `metdata.data.symbol`, a string + // whose length is represented by a leading 32 bit integer + length = DecodeUint32(data, offset); + if (!length) { + return absl::nullopt; + } + offset += static_cast(*length); + + // Parse next field, metadata.data.uri, a string + length = DecodeUint32(data, offset); + if (!length) { + return absl::nullopt; + } + std::string uri = + std::string(reinterpret_cast(&data[offset]), *length); + return GURL(uri); +} + +} // namespace brave_wallet diff --git a/components/brave_wallet/browser/solana_data_decoder_utils.h b/components/brave_wallet/browser/solana_data_decoder_utils.h new file mode 100644 index 000000000000..017d216dc195 --- /dev/null +++ b/components/brave_wallet/browser/solana_data_decoder_utils.h @@ -0,0 +1,56 @@ +/* Copyright (c) 2022 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_SOLANA_DATA_DECODER_UTILS_H_ +#define BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_SOLANA_DATA_DECODER_UTILS_H_ + +#include +#include + +#include "brave/components/brave_wallet/browser/solana_instruction_decoded_data.h" +#include "brave/components/brave_wallet/common/brave_wallet.mojom.h" +#include "third_party/abseil-cpp/absl/types/optional.h" + +namespace brave_wallet { + +absl::optional DecodeUint8(const std::vector& input, + size_t& offset); +absl::optional DecodeUint8String(const std::vector& input, + size_t& offset); +absl::optional DecodeAuthorityTypeString( + const std::vector& input, + size_t& offset); + +absl::optional DecodeUint32(const std::vector& input, + size_t& offset); + +absl::optional DecodeUint32String( + const std::vector& input, + size_t& offset); + +absl::optional DecodeMetadataUri(const std::vector data); + +absl::optional DecodeUint64(const std::vector& input, + size_t& offset); + +absl::optional DecodeUint64String( + const std::vector& input, + size_t& offset); + +absl::optional DecodePublicKey(const std::vector& input, + size_t& offset); + +absl::optional DecodeOptionalPublicKey( + const std::vector& input, + size_t& offset); + +absl::optional DecodeString(const std::vector& input, + size_t& offset); + +absl::optional DecodeMetadataUri(const std::vector data); + +} // namespace brave_wallet + +#endif // BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_SOLANA_DATA_DECODER_UTILS_H_ diff --git a/components/brave_wallet/browser/solana_data_decoder_utils_unittest.cc b/components/brave_wallet/browser/solana_data_decoder_utils_unittest.cc new file mode 100644 index 000000000000..ffa18bd92da6 --- /dev/null +++ b/components/brave_wallet/browser/solana_data_decoder_utils_unittest.cc @@ -0,0 +1,45 @@ +/* Copyright (c) 2022 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 http://mozilla.org/MPL/2.0/. */ + +#include "brave/components/brave_wallet/browser/solana_data_decoder_utils.h" + +#include "base/base64.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace brave_wallet { + +class SolanaDataDecoderUtilsTest : public testing::Test { + public: + SolanaDataDecoderUtilsTest() = default; + ~SolanaDataDecoderUtilsTest() override = default; +}; + +TEST_F(SolanaDataDecoderUtilsTest, DecodeMetadataUri) { + auto uri_encoded = base::Base64Decode( + "BGUN5hJf2zSue3S0I/fCq16UREt5NxP6mQdaq4cdGPs3Q8PG/" + "R6KFUSgce78Nwk9Frvkd9bMbvTIKCRSDy88nZQgAAAAU1BFQ0lBTCBTQVVDRQAAAAAAAAAAA" + "AAAAAAAAAAAAAAKAAAAAAAAAAAAAAAAAMgAAABodHRwczovL2JhZmtyZWlmNHd4NTR3anI3c" + "GdmdWczd2xhdHIzbmZudHNmd25ndjZldXNlYmJxdWV6cnhlbmo2Y2s0LmlwZnMuZHdlYi5sa" + "W5rP2V4dD0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAOgDAQIAAABlDeYSX9s0rnt0tCP3wqtelERLeTcT+" + "pkHWquHHRj7NwFiDUmu+U8sXOOZQXL36xmknL+Zzd/" + "z3uw2G0ERMo8Eth4BAgABAf8BAAEBoivvbAzLh2kD2cSu6IQIqGQDGeoh/" + "UEDizyp6mLT1tUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="); + ASSERT_TRUE(uri_encoded); + auto uri = DecodeMetadataUri(*uri_encoded); + ASSERT_TRUE(uri); + EXPECT_EQ((*uri).spec(), + "https://" + "bafkreif4wx54wjr7pgfug3wlatr3nfntsfwngv6eusebbquezrxenj6ck4.ipfs." + "dweb.link/?ext="); + ASSERT_FALSE(DecodeMetadataUri({})); +} + +} // namespace brave_wallet diff --git a/components/brave_wallet/browser/solana_instruction_data_decoder.cc b/components/brave_wallet/browser/solana_instruction_data_decoder.cc index e3b647644e40..0d0ee424564f 100644 --- a/components/brave_wallet/browser/solana_instruction_data_decoder.cc +++ b/components/brave_wallet/browser/solana_instruction_data_decoder.cc @@ -13,6 +13,7 @@ #include "base/no_destructor.h" #include "base/notreached.h" #include "base/sys_byteorder.h" +#include "brave/components/brave_wallet/browser/solana_data_decoder_utils.h" #include "brave/components/brave_wallet/common/brave_wallet_constants.h" #include "brave/components/brave_wallet/common/solana_utils.h" #include "build/build_config.h" @@ -33,9 +34,6 @@ enum class ParamTypes { kAuthorityType, }; -constexpr uint8_t kAuthorityTypeMax = 3; -constexpr size_t kMaxStringSize32Bit = 4294967291u; - // Tuple of param name, localized name, and type. using ParamNameTypeTuple = std::tuple; @@ -599,145 +597,53 @@ GetTokenInstructionAccountParams() { return *params; } -absl::optional DecodeUint8(const std::vector& input, - size_t& offset) { - if (offset >= input.size() || input.size() - offset < sizeof(uint8_t)) { - return absl::nullopt; - } - - offset += sizeof(uint8_t); - return input[offset - sizeof(uint8_t)]; -} - -absl::optional DecodeUint8String(const std::vector& input, - size_t& offset) { - auto ret = DecodeUint8(input, offset); - if (!ret) - return absl::nullopt; - return base::NumberToString(*ret); -} - -absl::optional DecodeAuthorityTypeString( - const std::vector& input, - size_t& offset) { - auto ret = DecodeUint8(input, offset); - if (ret && *ret <= kAuthorityTypeMax) - return base::NumberToString(*ret); - return absl::nullopt; -} - -absl::optional DecodeUint32(const std::vector& input, - size_t& offset) { - if (offset >= input.size() || input.size() - offset < sizeof(uint32_t)) { - return absl::nullopt; - } - - // Read bytes in little endian order. - base::span s = - base::make_span(input.begin() + offset, sizeof(uint32_t)); - uint32_t uint32_le = *reinterpret_cast(s.data()); - - offset += sizeof(uint32_t); - -#if defined(ARCH_CPU_LITTLE_ENDIAN) - return uint32_le; -#else - return base::ByteSwap(uint32_le); -#endif -} - -absl::optional DecodeUint32String( - const std::vector& input, +absl::optional DecodeSystemInstructionType( + const std::vector& data, size_t& offset) { - auto ret = DecodeUint32(input, offset); - if (!ret) - return absl::nullopt; - return base::NumberToString(*ret); -} - -absl::optional DecodeUint64(const std::vector& input, - size_t& offset) { - if (offset >= input.size() || input.size() - offset < sizeof(uint64_t)) { + auto ins_type = DecodeUint32(data, offset); + if (!ins_type || *ins_type > static_cast( + mojom::SolanaSystemInstruction::kMaxValue)) return absl::nullopt; - } - - // Read bytes in little endian order. - base::span s = - base::make_span(input.begin() + offset, sizeof(uint64_t)); - uint64_t uint64_le = *reinterpret_cast(s.data()); - - offset += sizeof(uint64_t); - -#if defined(ARCH_CPU_LITTLE_ENDIAN) - return uint64_le; -#else - return base::ByteSwap(uint64_le); -#endif + return static_cast(*ins_type); } -absl::optional DecodeUint64String( - const std::vector& input, +absl::optional DecodeTokenInstructionType( + const std::vector& data, size_t& offset) { - auto ret = DecodeUint64(input, offset); - if (!ret) - return absl::nullopt; - return base::NumberToString(*ret); -} - -absl::optional DecodePublicKey(const std::vector& input, - size_t& offset) { - if (offset >= input.size() || input.size() - offset < kSolanaPubkeySize) + auto ins_type = DecodeUint8(data, offset); + if (!ins_type || *ins_type > static_cast( + mojom::SolanaTokenInstruction::kMaxValue)) return absl::nullopt; - - offset += kSolanaPubkeySize; - return Base58Encode(std::vector( - input.begin() + offset - kSolanaPubkeySize, input.begin() + offset)); + return static_cast(*ins_type); } -absl::optional DecodeOptionalPublicKey( - const std::vector& input, - size_t& offset) { - if (offset == input.size()) { - return absl::nullopt; +const std::vector* DecodeInstructionType( + const std::string& program_id, + const std::vector& data, + size_t& offset, + SolanaInstructionDecodedData& decoded_data) { + if (program_id == mojom::kSolanaSystemProgramId) { + if (auto ins_type = DecodeSystemInstructionType(data, offset)) { + auto* ret = &GetSystemInstructionParams().at(*ins_type); + decoded_data.sys_ins_type = std::move(ins_type); + decoded_data.account_params = + GetSystemInstructionAccountParams().at(*ins_type); + return ret; + } + } else if (program_id == mojom::kSolanaTokenProgramId) { + if (auto ins_type = DecodeTokenInstructionType(data, offset)) { + auto* ret = &GetTokenInstructionParams().at(*ins_type); + decoded_data.token_ins_type = std::move(ins_type); + decoded_data.account_params = + GetTokenInstructionAccountParams().at(*ins_type); + return ret; + } } - // First byte is 0 or 1 to indicate if public key is passed. - // And the rest bytes are the actual public key. - if (input[offset] == 0) { - offset++; - return ""; // No public key is passed. - } else if (input[offset] == 1) { - offset++; - return DecodePublicKey(input, offset); - } else { - return absl::nullopt; - } + return nullptr; } -// bincode::serialize uses two u32 together for the string length and a byte -// array for the actual strings. The first u32 represents the lower bytes of -// the length, the second represents the upper bytes. The upper bytes will have -// non-zero value only when the length exceeds the maximum of u32. -// We currently cap the length here to be the max size of std::string -// on 32 bit systems, it's safe to do so because currently we don't expect any -// valid cases would have strings larger than it. -absl::optional DecodeString(const std::vector& input, - size_t& offset) { - auto len_lower = DecodeUint32(input, offset); - if (!len_lower || *len_lower > kMaxStringSize32Bit) - return absl::nullopt; - auto len_upper = DecodeUint32(input, offset); - if (!len_upper || *len_upper != 0) { // Non-zero means len exceeds u32 max. - return absl::nullopt; - } - - if (offset + *len_lower > input.size()) - return absl::nullopt; - - offset += *len_lower; - return std::string(reinterpret_cast(&input[offset - *len_lower]), - *len_lower); -} +} // namespace bool DecodeParamType(const ParamNameTypeTuple& name_type_tuple, const std::vector data, @@ -783,91 +689,6 @@ bool DecodeParamType(const ParamNameTypeTuple& name_type_tuple, return true; } -absl::optional DecodeSystemInstructionType( - const std::vector& data, - size_t& offset) { - auto ins_type = DecodeUint32(data, offset); - if (!ins_type || *ins_type > static_cast( - mojom::SolanaSystemInstruction::kMaxValue)) - return absl::nullopt; - return static_cast(*ins_type); -} - -absl::optional DecodeTokenInstructionType( - const std::vector& data, - size_t& offset) { - auto ins_type = DecodeUint8(data, offset); - if (!ins_type || *ins_type > static_cast( - mojom::SolanaTokenInstruction::kMaxValue)) - return absl::nullopt; - return static_cast(*ins_type); -} - -const std::vector* DecodeInstructionType( - const std::string& program_id, - const std::vector& data, - size_t& offset, - SolanaInstructionDecodedData& decoded_data) { - if (program_id == mojom::kSolanaSystemProgramId) { - if (auto ins_type = DecodeSystemInstructionType(data, offset)) { - auto* ret = &GetSystemInstructionParams().at(*ins_type); - decoded_data.sys_ins_type = std::move(ins_type); - decoded_data.account_params = - GetSystemInstructionAccountParams().at(*ins_type); - return ret; - } - } else if (program_id == mojom::kSolanaTokenProgramId) { - if (auto ins_type = DecodeTokenInstructionType(data, offset)) { - auto* ret = &GetTokenInstructionParams().at(*ins_type); - decoded_data.token_ins_type = std::move(ins_type); - decoded_data.account_params = - GetTokenInstructionAccountParams().at(*ins_type); - return ret; - } - } - - return nullptr; -} - -} // namespace - -// Expects a the bytes of a Borsh encoded Metadata struct (see -// https://docs.rs/spl-token-metadata/latest/spl_token_metadata/state/struct.Metadata.html) -// and returns the URI string in of the nested Data struct (see -// https://docs.rs/spl-token-metadata/latest/spl_token_metadata/state/struct.Data.html) -// as a GURL. -absl::optional DecodeMetadataUri(const std::vector data) { - size_t offset = 0; - offset = offset + /* Skip first byte for metadata.key */ 1 + - /* Skip next 32 bytes for `metadata.update_authority` */ 32 + - /* Skip next 32 bytes for `metadata.mint` */ 32; - - // Skip next field, metdata.data.name, a string - // whose length is represented by a leading 32 bit integer - auto length = DecodeUint32(data, offset); - if (!length) { - return absl::nullopt; - } - offset += static_cast(*length); - - // Skip next field, `metdata.data.symbol`, a string - // whose length is represented by a leading 32 bit integer - length = DecodeUint32(data, offset); - if (!length) { - return absl::nullopt; - } - offset += static_cast(*length); - - // Parse next field, metadata.data.uri, a string - length = DecodeUint32(data, offset); - if (!length) { - return absl::nullopt; - } - std::string uri = - std::string(reinterpret_cast(&data[offset]), *length); - return GURL(uri); -} - absl::optional Decode( const std::vector& data, const std::string& program_id) { diff --git a/components/brave_wallet/browser/solana_instruction_data_decoder.h b/components/brave_wallet/browser/solana_instruction_data_decoder.h index 9abb0ef89e61..38b112841d57 100644 --- a/components/brave_wallet/browser/solana_instruction_data_decoder.h +++ b/components/brave_wallet/browser/solana_instruction_data_decoder.h @@ -15,7 +15,6 @@ namespace brave_wallet::solana_ins_data_decoder { -absl::optional DecodeMetadataUri(const std::vector data); absl::optional Decode( const std::vector& data, const std::string& program_id); diff --git a/components/brave_wallet/browser/solana_instruction_data_decoder_unittest.cc b/components/brave_wallet/browser/solana_instruction_data_decoder_unittest.cc index 045111f9fe2a..c243578cde36 100644 --- a/components/brave_wallet/browser/solana_instruction_data_decoder_unittest.cc +++ b/components/brave_wallet/browser/solana_instruction_data_decoder_unittest.cc @@ -9,7 +9,6 @@ #include #include -#include "base/base64.h" #include "brave/components/brave_wallet/common/brave_wallet.mojom.h" #include "brave/components/brave_wallet/common/brave_wallet_constants.h" #include "testing/gtest/include/gtest/gtest.h" @@ -384,30 +383,4 @@ TEST_F(SolanaInstructionDecoderTest, Decode_InitializeMint2) { {{"decimals", "9"}, {"mint_authority", kPubkey1}}, true); } -TEST_F(SolanaInstructionDecoderTest, DecodeMetadataUri) { - auto uri_encoded = base::Base64Decode( - "BGUN5hJf2zSue3S0I/fCq16UREt5NxP6mQdaq4cdGPs3Q8PG/" - "R6KFUSgce78Nwk9Frvkd9bMbvTIKCRSDy88nZQgAAAAU1BFQ0lBTCBTQVVDRQAAAAAAAAAAA" - "AAAAAAAAAAAAAAKAAAAAAAAAAAAAAAAAMgAAABodHRwczovL2JhZmtyZWlmNHd4NTR3anI3c" - "GdmdWczd2xhdHIzbmZudHNmd25ndjZldXNlYmJxdWV6cnhlbmo2Y2s0LmlwZnMuZHdlYi5sa" - "W5rP2V4dD0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - "AAAAAAAAAAAAAAAAOgDAQIAAABlDeYSX9s0rnt0tCP3wqtelERLeTcT+" - "pkHWquHHRj7NwFiDUmu+U8sXOOZQXL36xmknL+Zzd/" - "z3uw2G0ERMo8Eth4BAgABAf8BAAEBoivvbAzLh2kD2cSu6IQIqGQDGeoh/" - "UEDizyp6mLT1tUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="); - ASSERT_TRUE(uri_encoded); - auto uri = DecodeMetadataUri(*uri_encoded); - ASSERT_TRUE(uri); - EXPECT_EQ((*uri).spec(), - "https://" - "bafkreif4wx54wjr7pgfug3wlatr3nfntsfwngv6eusebbquezrxenj6ck4.ipfs." - "dweb.link/?ext="); - ASSERT_FALSE(DecodeMetadataUri({})); -} - } // namespace brave_wallet::solana_ins_data_decoder diff --git a/components/brave_wallet/browser/test/BUILD.gn b/components/brave_wallet/browser/test/BUILD.gn index 8619002c08bc..460ee1d2830e 100644 --- a/components/brave_wallet/browser/test/BUILD.gn +++ b/components/brave_wallet/browser/test/BUILD.gn @@ -52,6 +52,7 @@ source_set("brave_wallet_unit_tests") { "//brave/components/brave_wallet/browser/sns_resolver_task_unittest.cc", "//brave/components/brave_wallet/browser/solana_account_meta_unittest.cc", "//brave/components/brave_wallet/browser/solana_block_tracker_unittest.cc", + "//brave/components/brave_wallet/browser/solana_data_decoder_utils_unittest.cc", "//brave/components/brave_wallet/browser/solana_instruction_builder_unittest.cc", "//brave/components/brave_wallet/browser/solana_instruction_data_decoder_unittest.cc", "//brave/components/brave_wallet/browser/solana_instruction_decoded_data_unittest.cc", From 090459e75e7dd6bd4c79d6865b95abeceeb32de7 Mon Sep 17 00:00:00 2001 From: nvonpentz <12549658+nvonpentz@users.noreply.github.com> Date: Mon, 14 Nov 2022 18:04:26 -0500 Subject: [PATCH 03/17] Add unit tests for JsonRpcService::GetMetaplexMetadata Also: * Add missing return JsonRpcService::GetMetaplexMetadata * Add addition test to solana data decoder utils unittest --- .../brave_wallet/browser/json_rpc_service.cc | 21 +- .../browser/json_rpc_service_unittest.cc | 231 ++++++++++++++++++ .../solana_data_decoder_utils_unittest.cc | 22 +- 3 files changed, 261 insertions(+), 13 deletions(-) diff --git a/components/brave_wallet/browser/json_rpc_service.cc b/components/brave_wallet/browser/json_rpc_service.cc index 2fd6da3c56ac..57952df5a3f7 100644 --- a/components/brave_wallet/browser/json_rpc_service.cc +++ b/components/brave_wallet/browser/json_rpc_service.cc @@ -2566,7 +2566,8 @@ void JsonRpcService::GetMetaplexMetadata(const std::string& nft_account_address, std::move(account_info_metadata_callback)); RequestInternal(solana::getAccountInfo(*associated_metadata_account), true, network_urls_[mojom::CoinType::SOL], - std::move(account_info_response_callback)); + std::move(account_info_response_callback), + solana::ConverterForGetAccountInfo()); } void JsonRpcService::OnGetSolanaAccountInfoMetaplex( @@ -2575,25 +2576,23 @@ void JsonRpcService::OnGetSolanaAccountInfoMetaplex( mojom::SolanaProviderError error, const std::string& error_message) { if (error != mojom::SolanaProviderError::kSuccess || !account_info) { - std::move(callback).Run( - "", mojom::SolanaProviderError::kInternalError, - l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); + std::move(callback).Run("", std::move(error), error_message); return; } absl::optional> metadata = base::Base64Decode((*account_info).data); if (!metadata) { - std::move(callback).Run( - "", mojom::SolanaProviderError::kInternalError, - l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); + std::move(callback).Run("", mojom::SolanaProviderError::kParsingError, + l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); + return; } absl::optional url = DecodeMetadataUri(*metadata); - if (!url) { - std::move(callback).Run( - "", mojom::SolanaProviderError::kInternalError, - l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); + if (!url || !url.value().is_valid()) { + std::move(callback).Run("", mojom::SolanaProviderError::kParsingError, + l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); + return; } FetchTokenMetadata( diff --git a/components/brave_wallet/browser/json_rpc_service_unittest.cc b/components/brave_wallet/browser/json_rpc_service_unittest.cc index 1e4ecbeafee0..266035a1ebea 100644 --- a/components/brave_wallet/browser/json_rpc_service_unittest.cc +++ b/components/brave_wallet/browser/json_rpc_service_unittest.cc @@ -923,6 +923,25 @@ class JsonRpcServiceUnitTest : public testing::Test { })); } + void SetMetaplexMetadataInterceptor( + const GURL& expected_rpc_url, + const std::string& get_account_info_response, + const GURL& expected_metadata_url, + const std::string& metadata_response) { + auto network_url = + GetNetwork(mojom::kLocalhostChainId, mojom::CoinType::SOL); + ASSERT_TRUE(expected_rpc_url.is_valid()); + ASSERT_TRUE(expected_metadata_url.is_valid()); + url_loader_factory_.SetInterceptor(base::BindLambdaForTesting( + [&, expected_rpc_url, get_account_info_response, expected_metadata_url, + metadata_response](const network::ResourceRequest& request) { + url_loader_factory_.AddResponse(expected_rpc_url.spec(), + get_account_info_response); + url_loader_factory_.AddResponse(expected_metadata_url.spec(), + metadata_response); + })); + } + void SetInterceptor(const GURL& expected_url, const std::string& expected_method, const std::string& expected_cache_header, @@ -1398,6 +1417,24 @@ class JsonRpcServiceUnitTest : public testing::Test { loop.Run(); } + void TestGetMetaplexMetadata(const std::string& nft_account_address, + const std::string& expected_response, + mojom::SolanaProviderError expected_error, + const std::string& expected_error_message) { + base::RunLoop loop; + json_rpc_service_->GetMetaplexMetadata( + nft_account_address, + base::BindLambdaForTesting([&](const std::string& response, + mojom::SolanaProviderError error, + const std::string& error_message) { + EXPECT_EQ(response, expected_response); + EXPECT_EQ(error, expected_error); + EXPECT_EQ(error_message, expected_error_message); + loop.Quit(); + })); + loop.Run(); + } + protected: std::unique_ptr json_rpc_service_; network::TestURLLoaderFactory url_loader_factory_; @@ -5850,4 +5887,198 @@ TEST_F(JsonRpcServiceUnitTest, EthGetLogs) { std::move(expected_logs), mojom::ProviderError::kSuccess, ""); } +TEST_F(JsonRpcServiceUnitTest, GetMetaplexMetadata) { + // Valid inputs should yield metadata JSON (happy case) + std::string get_account_info_response = R"({ + "jsonrpc": "2.0", + "result": { + "context": { + "apiVersion": "1.13.3", + "slot": 161038284 + }, + "value": { + "data": [ + "BGUN5hJf2zSue3S0I/fCq16UREt5NxP6mQdaq4cdGPs3Q8PG/R6KFUSgce78Nwk9Frvkd9bMbvTIKCRSDy88nZQgAAAAU1BFQ0lBTCBTQVVDRQAAAAAAAAAAAAAAAAAAAAAAAAAKAAAAAAAAAAAAAAAAAMgAAABodHRwczovL2JhZmtyZWlmNHd4NTR3anI3cGdmdWczd2xhdHIzbmZudHNmd25ndjZldXNlYmJxdWV6cnhlbmo2Y2s0LmlwZnMuZHdlYi5saW5rP2V4dD0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOgDAQIAAABlDeYSX9s0rnt0tCP3wqtelERLeTcT+pkHWquHHRj7NwFiDUmu+U8sXOOZQXL36xmknL+Zzd/z3uw2G0ERMo8Eth4BAgABAf8BAAEBoivvbAzLh2kD2cSu6IQIqGQDGeoh/UEDizyp6mLT1tUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "base64" + ], + "executable": false, + "lamports": 5616720, + "owner": "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s", + "rentEpoch": 361 + } + }, + "id": 1 + })"; + const std::string valid_metadata_response = + R"({"attributes":[{"trait_type":"hair","value":"green & blue"},{"trait_type":"pontus","value":"no"}],"description":"","external_url":"","image":"https://bafkreiagsgqhjudpta6trhjuv5y2n2exsrhbkkprl64tvg2mftjsdm3vgi.ipfs.dweb.link?ext=png","name":"SPECIAL SAUCE","properties":{"category":"image","creators":[{"address":"7oUUEdptZnZVhSet4qobU9PtpPfiNUEJ8ftPnrC6YEaa","share":98},{"address":"tsU33UT3K2JTfLgHUo7hdzRhRe4wth885cqVbM8WLiq","share":2}],"files":[{"type":"image/png","uri":"https://bafkreiagsgqhjudpta6trhjuv5y2n2exsrhbkkprl64tvg2mftjsdm3vgi.ipfs.dweb.link?ext=png"}],"maxSupply":0},"seller_fee_basis_points":1000,"symbol":""})"; + auto network_url = GetNetwork(mojom::kLocalhostChainId, mojom::CoinType::SOL); + SetMetaplexMetadataInterceptor( + network_url, get_account_info_response, + GURL("https://" + "bafkreif4wx54wjr7pgfug3wlatr3nfntsfwngv6eusebbquezrxenj6ck4.ipfs." + "dweb.link/?ext="), + valid_metadata_response); + TestGetMetaplexMetadata("5ZXToo7froykjvjnpHtTLYr9u2tW3USMwPg3sNkiaQVh", + valid_metadata_response, + mojom::SolanaProviderError::kSuccess, ""); + + // Invalid nft_account_address yields internal error. + SetMetaplexMetadataInterceptor( + network_url, get_account_info_response, + GURL("https://" + "bafkreif4wx54wjr7pgfug3wlatr3nfntsfwngv6eusebbquezrxenj6ck4.ipfs." + "dweb.link/?ext="), + valid_metadata_response); + TestGetMetaplexMetadata("Invalid", "", + mojom::SolanaProviderError::kInternalError, + l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); + + // Non 200 getAccountInfo response of yields internal server error. + SetHTTPRequestTimeoutInterceptor(); + TestGetMetaplexMetadata("5ZXToo7froykjvjnpHtTLYr9u2tW3USMwPg3sNkiaQVh", "", + mojom::SolanaProviderError::kInternalError, + l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); + + // Invalid getAccountInfo response JSON yields internal error + SetMetaplexMetadataInterceptor( + network_url, "Invalid json response", + GURL("https://" + "bafkreif4wx54wjr7pgfug3wlatr3nfntsfwngv6eusebbquezrxenj6ck4.ipfs." + "dweb.link/?ext="), + valid_metadata_response); + TestGetMetaplexMetadata("5ZXToo7froykjvjnpHtTLYr9u2tW3USMwPg3sNkiaQVh", "", + mojom::SolanaProviderError::kInternalError, + l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); + + // Valid response JSON, invalid account info (missing result.value.owner + // field) info yields parse error + get_account_info_response = R"({ + "jsonrpc": "2.0", + "result": { + "context": { + "apiVersion": "1.13.3", + "slot": 161038284 + }, + "value": { + "data": [ + "BGUN5hJf2zSue3S0I/fCq16UREt5NxP6mQdaq4cdGPs3Q8PG/R6KFUSgce78Nwk9Frvkd9bMbvTIKCRSDy88nZQgAAAAU1BFQ0lBTCBTQVVDRQAAAAAAAAAAAAAAAAAAAAAAAAAKAAAAAAAAAAAAAAAAAMgAAABodHRwczovL2JhZmtyZWlmNHd4NTR3anI3cGdmdWczd2xhdHIzbmZudHNmd25ndjZldXNlYmJxdWV6cnhlbmo2Y2s0LmlwZnMuZHdlYi5saW5rP2V4dD0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOgDAQIAAABlDeYSX9s0rnt0tCP3wqtelERLeTcT+pkHWquHHRj7NwFiDUmu+U8sXOOZQXL36xmknL+Zzd/z3uw2G0ERMo8Eth4BAgABAf8BAAEBoivvbAzLh2kD2cSu6IQIqGQDGeoh/UEDizyp6mLT1tUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "base64" + ], + "executable": false, + "lamports": 5616720, + "rentEpoch": 361 + } + }, + "id": 1 + })"; + SetMetaplexMetadataInterceptor( + network_url, get_account_info_response, + GURL("https://" + "bafkreif4wx54wjr7pgfug3wlatr3nfntsfwngv6eusebbquezrxenj6ck4.ipfs." + "dweb.link/?ext="), + valid_metadata_response); + TestGetMetaplexMetadata("5ZXToo7froykjvjnpHtTLYr9u2tW3USMwPg3sNkiaQVh", "", + mojom::SolanaProviderError::kParsingError, + l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); + + // Valid response JSON, parsable account info, but invalid account info data + // (invalid base64) yields parse error + get_account_info_response = R"({ + "jsonrpc": "2.0", + "result": { + "context": { + "apiVersion": "1.13.3", + "slot": 161038284 + }, + "value": { + "data": [ + "*Invalid Base64*", + "base64" + ], + "executable": false, + "lamports": 5616720, + "owner": "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s", + "rentEpoch": 361 + } + }, + "id": 1 + })"; + SetMetaplexMetadataInterceptor( + network_url, get_account_info_response, + GURL("https://" + "bafkreif4wx54wjr7pgfug3wlatr3nfntsfwngv6eusebbquezrxenj6ck4.ipfs." + "dweb.link/?ext="), + valid_metadata_response); + TestGetMetaplexMetadata("5ZXToo7froykjvjnpHtTLYr9u2tW3USMwPg3sNkiaQVh", "", + mojom::SolanaProviderError::kParsingError, + l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); + + // Valid response JSON, parsable account info, invalid account info data + // (valid base64, but invalid borsh encoded metadata) yields parse error + get_account_info_response = R"({ + "jsonrpc": "2.0", + "result": { + "context": { + "apiVersion": "1.13.3", + "slot": 161038284 + }, + "value": { + "data": [ + "d2hvb3BzIQ==", + "base64" + ], + "executable": false, + "lamports": 5616720, + "owner": "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s", + "rentEpoch": 361 + } + }, + "id": 1 + })"; + SetMetaplexMetadataInterceptor( + network_url, get_account_info_response, + GURL("https://" + "bafkreif4wx54wjr7pgfug3wlatr3nfntsfwngv6eusebbquezrxenj6ck4.ipfs." + "dweb.link/?ext="), + valid_metadata_response); + TestGetMetaplexMetadata("5ZXToo7froykjvjnpHtTLYr9u2tW3USMwPg3sNkiaQVh", "", + mojom::SolanaProviderError::kParsingError, + l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); + + // Valid response JSON, parsable account info, invalid account info data + // (valid base64, valid borsh encoding, but when decoded the URI is not a + // valid URI) + get_account_info_response = R"({ + "jsonrpc": "2.0", + "result": { + "context": { + "apiVersion": "1.13.3", + "slot": 161038284 + }, + "value": { + "data": [ + "BGUN5hJf2zSue3S0I/fCq16UREt5NxP6mQdaq4cdGPs3Q8PG/R6KFUSgce78Nwk9Frvkd9bMbvTIKCRSDy88nZQgAAAAU1BFQ0lBTCBTQVVDRQAAAAAAAAAAAAAAAAAAAAAAAAAKAAAAAAAAAAAAAAAAAAsAAABpbnZhbGlkIHVybOgDAQIAAABlDeYSX9s0rnt0tCP3wqtelERLeTcT+pkHWquHHRj7NwFiDUmu+U8sXOOZQXL36xmknL+Zzd/z3uw2G0ERMo8Eth4BAgABAf8BAAEBoivvbAzLh2kD2cSu6IQIqGQDGeoh/UEDizyp6mLT1tUA", + "base64" + ], + "executable": false, + "lamports": 5616720, + "owner": "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s", + "rentEpoch": 361 + } + }, + "id": 1 + })"; + SetMetaplexMetadataInterceptor( + network_url, get_account_info_response, + GURL("https://" + "bafkreif4wx54wjr7pgfug3wlatr3nfntsfwngv6eusebbquezrxenj6ck4.ipfs." + "dweb.link/?ext="), + valid_metadata_response); + TestGetMetaplexMetadata("5ZXToo7froykjvjnpHtTLYr9u2tW3USMwPg3sNkiaQVh", "", + mojom::SolanaProviderError::kParsingError, + l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); + + // TODO(nvonpentz) FetchTokenMetadata needs to be tested elsewhere +} + } // namespace brave_wallet diff --git a/components/brave_wallet/browser/solana_data_decoder_utils_unittest.cc b/components/brave_wallet/browser/solana_data_decoder_utils_unittest.cc index ffa18bd92da6..243ed6b204e4 100644 --- a/components/brave_wallet/browser/solana_data_decoder_utils_unittest.cc +++ b/components/brave_wallet/browser/solana_data_decoder_utils_unittest.cc @@ -17,6 +17,7 @@ class SolanaDataDecoderUtilsTest : public testing::Test { }; TEST_F(SolanaDataDecoderUtilsTest, DecodeMetadataUri) { + // Valid borsh encoding and URI yields expected URI auto uri_encoded = base::Base64Decode( "BGUN5hJf2zSue3S0I/fCq16UREt5NxP6mQdaq4cdGPs3Q8PG/" "R6KFUSgce78Nwk9Frvkd9bMbvTIKCRSDy88nZQgAAAAU1BFQ0lBTCBTQVVDRQAAAAAAAAAAA" @@ -35,11 +36,28 @@ TEST_F(SolanaDataDecoderUtilsTest, DecodeMetadataUri) { ASSERT_TRUE(uri_encoded); auto uri = DecodeMetadataUri(*uri_encoded); ASSERT_TRUE(uri); - EXPECT_EQ((*uri).spec(), + EXPECT_EQ(uri.value().spec(), "https://" "bafkreif4wx54wjr7pgfug3wlatr3nfntsfwngv6eusebbquezrxenj6ck4.ipfs." "dweb.link/?ext="); - ASSERT_FALSE(DecodeMetadataUri({})); + + // Valid borsh encoding, but invalid URI is parsed but yields empty URI + uri_encoded = base::Base64Decode( + "BGUN5hJf2zSue3S0I/fCq16UREt5NxP6mQdaq4cdGPs3Q8PG/" + "R6KFUSgce78Nwk9Frvkd9bMbvTIKCRSDy88nZQgAAAAU1BFQ0lBTCBTQVVDRQAAAAAAAAAAA" + "AAAAAAAAAAAAAAKAAAAAAAAAAAAAAAAAAsAAABpbnZhbGlkIHVybOgDAQIAAABlDeYSX9s0r" + "nt0tCP3wqtelERLeTcT+pkHWquHHRj7NwFiDUmu+U8sXOOZQXL36xmknL+Zzd/" + "z3uw2G0ERMo8Eth4BAgABAf8BAAEBoivvbAzLh2kD2cSu6IQIqGQDGeoh/" + "UEDizyp6mLT1tUA"); + ASSERT_TRUE(uri_encoded); + uri = DecodeMetadataUri(*uri_encoded); + ASSERT_TRUE(uri); + EXPECT_EQ(uri.value().spec(), ""); + + // Invalid borsh encoding is not parsed + uri_encoded = base::Base64Decode("d2hvb3BzIQ=="); + ASSERT_TRUE(uri_encoded); + ASSERT_FALSE(DecodeMetadataUri(*uri_encoded)); } } // namespace brave_wallet From ceea2098295e2d5964568c418fbf81c0ca11c308 Mon Sep 17 00:00:00 2001 From: nvonpentz <12549658+nvonpentz@users.noreply.github.com> Date: Tue, 29 Nov 2022 12:37:50 -0500 Subject: [PATCH 04/17] Fix header import for solana_data_decoder_utils.cc --- components/brave_wallet/browser/solana_data_decoder_utils.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/brave_wallet/browser/solana_data_decoder_utils.cc b/components/brave_wallet/browser/solana_data_decoder_utils.cc index 4c2bd131f1f8..aab7a1349b40 100644 --- a/components/brave_wallet/browser/solana_data_decoder_utils.cc +++ b/components/brave_wallet/browser/solana_data_decoder_utils.cc @@ -3,7 +3,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ -#include "brave/components/brave_wallet/browser/solana_instruction_data_decoder.h" +#include "brave/components/brave_wallet/browser/solana_data_decoder_utils.h" #include #include From 7ddd8d01587c157f67b5e1d5ce773a0fdd877a01 Mon Sep 17 00:00:00 2001 From: nvonpentz <12549658+nvonpentz@users.noreply.github.com> Date: Tue, 29 Nov 2022 15:28:59 -0500 Subject: [PATCH 05/17] Update JsonRpcService::FetchTokenMetadata Instead of constantly casing on an CoinType argument to choose the correct error (ProviderError or SolanaProviderError), use an integer value and cast at the end. --- .../brave_wallet/browser/json_rpc_service.cc | 113 ++++-------------- .../brave_wallet/browser/json_rpc_service.h | 17 +-- .../browser/json_rpc_service_unittest.cc | 9 +- 3 files changed, 34 insertions(+), 105 deletions(-) diff --git a/components/brave_wallet/browser/json_rpc_service.cc b/components/brave_wallet/browser/json_rpc_service.cc index 57952df5a3f7..271d4306fd62 100644 --- a/components/brave_wallet/browser/json_rpc_service.cc +++ b/components/brave_wallet/browser/json_rpc_service.cc @@ -2087,28 +2087,18 @@ void JsonRpcService::OnGetTokenUri(GetTokenMetadataCallback callback, } FetchTokenMetadata( - url, mojom::CoinType::ETH, - base::BindOnce(&JsonRpcService::CompleteGetTokenMetadataEth, - weak_ptr_factory_.GetWeakPtr(), std::move(callback))); + url, base::BindOnce(&JsonRpcService::CompleteGetTokenMetadataEth, + weak_ptr_factory_.GetWeakPtr(), std::move(callback))); } void JsonRpcService::OnSanitizeTokenMetadata( - mojom::CoinType coin, GetTokenMetadataIntermediateCallback callback, data_decoder::JsonSanitizer::Result result) { - mojom::ProviderErrorUnionPtr error; if (result.error) { - if (coin == mojom::CoinType::SOL) { - error = mojom::ProviderErrorUnion::NewSolanaProviderError( - mojom::SolanaProviderError::kParsingError); - } else { - error = mojom::ProviderErrorUnion::NewProviderError( - mojom::ProviderError::kParsingError); - } - VLOG(1) << "Data URI JSON validation error:" << *result.error; - std::move(callback).Run("", std::move(error), - l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); + std::move(callback).Run( + "", static_cast(mojom::JsonRpcError::kParsingError), + l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); return; } @@ -2117,76 +2107,45 @@ void JsonRpcService::OnSanitizeTokenMetadata( metadata_json = result.value.value(); } - if (coin == mojom::CoinType::SOL) { - error = mojom::ProviderErrorUnion::NewSolanaProviderError( - mojom::SolanaProviderError::kSuccess); - } else { - error = mojom::ProviderErrorUnion::NewProviderError( - mojom::ProviderError::kSuccess); - } - std::move(callback).Run(metadata_json, std::move(error), ""); + std::move(callback).Run(metadata_json, 0, ""); // 0 is kSuccess } void JsonRpcService::OnGetTokenMetadataPayload( - mojom::CoinType coin, GetTokenMetadataIntermediateCallback callback, APIRequestResult api_request_result) { mojom::ProviderErrorUnionPtr error; if (!api_request_result.Is2XXResponseCode()) { - if (coin == mojom::CoinType::SOL) { - error = mojom::ProviderErrorUnion::NewSolanaProviderError( - mojom::SolanaProviderError::kInternalError); - } else { - error = mojom::ProviderErrorUnion::NewProviderError( - mojom::ProviderError::kInternalError); - } std::move(callback).Run( - "", std::move(error), + "", static_cast(mojom::JsonRpcError::kInternalError), l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); return; } // Invalid JSON becomes an empty string after sanitization if (api_request_result.body().empty()) { - if (coin == mojom::CoinType::SOL) { - error = mojom::ProviderErrorUnion::NewSolanaProviderError( - mojom::SolanaProviderError::kParsingError); - } else { - error = mojom::ProviderErrorUnion::NewProviderError( - mojom::ProviderError::kParsingError); - } - std::move(callback).Run("", std::move(error), - l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); + std::move(callback).Run( + "", static_cast(mojom::JsonRpcError::kParsingError), + l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); return; } - if (coin == mojom::CoinType::SOL) { - error = mojom::ProviderErrorUnion::NewSolanaProviderError( - mojom::SolanaProviderError::kSuccess); - } else { - error = mojom::ProviderErrorUnion::NewProviderError( - mojom::ProviderError::kSuccess); - } - - std::move(callback).Run(api_request_result.body(), std::move(error), ""); + std::move(callback).Run(api_request_result.body(), 0, ""); // 0 is kSuccess } void JsonRpcService::CompleteGetTokenMetadataEth( GetTokenMetadataCallback callback, const std::string& response, - mojom::ProviderErrorUnionPtr error, + int error, const std::string& error_message) { - DCHECK(error && error->is_provider_error()); - std::move(callback).Run(response, error->get_provider_error(), error_message); + std::move(callback).Run(response, mojom::ProviderError(error), error_message); } void JsonRpcService::CompleteGetTokenMetadataSol( GetMetaplexMetadataCallback callback, const std::string& response, - mojom::ProviderErrorUnionPtr error, + int error, const std::string& error_message) { - DCHECK(error && error->is_solana_provider_error()); - std::move(callback).Run(response, error->get_solana_provider_error(), + std::move(callback).Run(response, mojom::SolanaProviderError(error), error_message); } @@ -2596,16 +2555,14 @@ void JsonRpcService::OnGetSolanaAccountInfoMetaplex( } FetchTokenMetadata( - *url, mojom::CoinType::SOL, + *url, base::BindOnce(&JsonRpcService::CompleteGetTokenMetadataSol, weak_ptr_factory_.GetWeakPtr(), std::move(callback))); } void JsonRpcService::FetchTokenMetadata( GURL url, - mojom::CoinType coin, GetTokenMetadataIntermediateCallback callback) { - mojom::ProviderErrorUnionPtr error; // Obtain JSON from the URL depending on the scheme. // IPFS, HTTPS, and data URIs are supported. // IPFS and HTTPS URIs require an additional request to fetch the metadata. @@ -2617,31 +2574,16 @@ void JsonRpcService::FetchTokenMetadata( #else if (scheme != url::kDataScheme && scheme != url::kHttpsScheme) { #endif - if (coin == mojom::CoinType::SOL) { - error = mojom::ProviderErrorUnion::NewSolanaProviderError( - mojom::SolanaProviderError::kInternalError); - } else { - error = mojom::ProviderErrorUnion::NewProviderError( - mojom::ProviderError::kMethodNotSupported); - } std::move(callback).Run( - "", std::move(error), - l10n_util::GetStringUTF8(IDS_WALLET_METHOD_NOT_SUPPORTED_ERROR)); + "", static_cast(mojom::JsonRpcError::kInternalError), + l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); return; } if (scheme == url::kDataScheme) { - if (coin == mojom::CoinType::SOL) { - error = mojom::ProviderErrorUnion::NewSolanaProviderError( - mojom::SolanaProviderError::kParsingError); - } else { - error = mojom::ProviderErrorUnion::NewProviderError( - mojom::ProviderError::kParsingError); - } - if (!eth::ParseDataURIAndExtractJSON(url, &metadata_json)) { std::move(callback).Run( - "", std::move(error), + "", static_cast(mojom::JsonRpcError::kParsingError), l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); return; } @@ -2650,8 +2592,7 @@ void JsonRpcService::FetchTokenMetadata( data_decoder::JsonSanitizer::Sanitize( std::move(metadata_json), base::BindOnce(&JsonRpcService::OnSanitizeTokenMetadata, - weak_ptr_factory_.GetWeakPtr(), coin, - std::move(callback))); + weak_ptr_factory_.GetWeakPtr(), std::move(callback))); return; } @@ -2659,22 +2600,16 @@ void JsonRpcService::FetchTokenMetadata( if (scheme == ipfs::kIPFSScheme && !ipfs::TranslateIPFSURI(url, &url, ipfs::GetDefaultNFTIPFSGateway(prefs_), false)) { - if (coin == mojom::CoinType::SOL) { - error = mojom::ProviderErrorUnion::NewSolanaProviderError( - mojom::SolanaProviderError::kParsingError); - } else { - error = mojom::ProviderErrorUnion::NewProviderError( - mojom::ProviderError::kParsingError); - } - std::move(callback).Run("", std::move(error), - l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); + std::move(callback).Run( + "", static_cast(mojom::JsonRpcError::kParsingError), + l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); return; } #endif auto internal_callback = base::BindOnce(&JsonRpcService::OnGetTokenMetadataPayload, - weak_ptr_factory_.GetWeakPtr(), coin, std::move(callback)); + weak_ptr_factory_.GetWeakPtr(), std::move(callback)); api_request_helper_->Request("GET", url, "", "", true, std::move(internal_callback)); } diff --git a/components/brave_wallet/browser/json_rpc_service.h b/components/brave_wallet/browser/json_rpc_service.h index a5f284209e03..b82768b34b10 100644 --- a/components/brave_wallet/browser/json_rpc_service.h +++ b/components/brave_wallet/browser/json_rpc_service.h @@ -90,10 +90,8 @@ class JsonRpcService : public KeyedService, public mojom::JsonRpcService { base::OnceCallback& logs, mojom::ProviderError error, const std::string& error_message)>; - using GetTokenMetadataIntermediateCallback = - base::OnceCallback; + using GetTokenMetadataIntermediateCallback = base::OnceCallback< + void(const std::string& response, int, const std::string& error_message)>; void GetBlockNumber(GetBlockNumberCallback callback); void GetFeeHistory(GetFeeHistoryCallback callback); @@ -372,7 +370,6 @@ class JsonRpcService : public KeyedService, public mojom::JsonRpcService { void GetMetaplexMetadata(const std::string& nft_account_address, GetMetaplexMetadataCallback callback) override; void FetchTokenMetadata(GURL url, - mojom::CoinType coin, GetTokenMetadataIntermediateCallback callback); void OnGetSolanaAccountInfoMetaplex( GetMetaplexMetadataCallback callback, @@ -549,22 +546,20 @@ class JsonRpcService : public KeyedService, public mojom::JsonRpcService { void OnGetTokenUri(GetTokenMetadataCallback callback, const APIRequestResult api_request_result); - void OnSanitizeTokenMetadata(mojom::CoinType coin, - GetTokenMetadataIntermediateCallback callback, + void OnSanitizeTokenMetadata(GetTokenMetadataIntermediateCallback callback, data_decoder::JsonSanitizer::Result result); - void OnGetTokenMetadataPayload(mojom::CoinType coin, - GetTokenMetadataIntermediateCallback callback, + void OnGetTokenMetadataPayload(GetTokenMetadataIntermediateCallback callback, APIRequestResult api_request_result); void CompleteGetTokenMetadataEth(GetTokenMetadataCallback callback, const std::string& response, - mojom::ProviderErrorUnionPtr error, + int error, const std::string& error_message); void CompleteGetTokenMetadataSol(GetMetaplexMetadataCallback callback, const std::string& response, - mojom::ProviderErrorUnionPtr error, + int error, const std::string& error_message); void OnGetSupportsInterface(GetSupportsInterfaceCallback callback, diff --git a/components/brave_wallet/browser/json_rpc_service_unittest.cc b/components/brave_wallet/browser/json_rpc_service_unittest.cc index 266035a1ebea..b3ade40ae838 100644 --- a/components/brave_wallet/browser/json_rpc_service_unittest.cc +++ b/components/brave_wallet/browser/json_rpc_service_unittest.cc @@ -3585,11 +3585,10 @@ TEST_F(JsonRpcServiceUnitTest, GetTokenMetadata) { SetTokenMetadataInterceptor( kERC721MetadataInterfaceId, mojom::kMainnetChainId, interface_supported_response, http_token_uri_response); - TestGetTokenMetadata( - "0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", - mojom::kMainnetChainId, kERC721MetadataInterfaceId, "", - mojom::ProviderError::kMethodNotSupported, - l10n_util::GetStringUTF8(IDS_WALLET_METHOD_NOT_SUPPORTED_ERROR)); + TestGetTokenMetadata("0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", + mojom::kMainnetChainId, kERC721MetadataInterfaceId, "", + mojom::ProviderError::kInternalError, + l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); // Invalid metadata response (2 total) // (1/2) Timeout From ee3343e89cbf3f30512474d268fc59810589c16e Mon Sep 17 00:00:00 2001 From: nvonpentz <12549658+nvonpentz@users.noreply.github.com> Date: Tue, 29 Nov 2022 16:41:05 -0500 Subject: [PATCH 06/17] Make public functions private that don't need to be public, some are old --- .../brave_wallet/browser/json_rpc_service.h | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/components/brave_wallet/browser/json_rpc_service.h b/components/brave_wallet/browser/json_rpc_service.h index b82768b34b10..fc719c908ce4 100644 --- a/components/brave_wallet/browser/json_rpc_service.h +++ b/components/brave_wallet/browser/json_rpc_service.h @@ -306,12 +306,6 @@ class JsonRpcService : public KeyedService, public mojom::JsonRpcService { mojom::ProviderError error, const std::string& error_message)>; - void GetTokenMetadata(const std::string& contract_address, - const std::string& token_id, - const std::string& chain_id, - const std::string& interface_id, - GetTokenMetadataCallback callback); - void GetERC721Metadata(const std::string& contract_address, const std::string& token_id, const std::string& chain_id, @@ -369,14 +363,6 @@ class JsonRpcService : public KeyedService, public mojom::JsonRpcService { GetSPLTokenAccountBalanceCallback callback) override; void GetMetaplexMetadata(const std::string& nft_account_address, GetMetaplexMetadataCallback callback) override; - void FetchTokenMetadata(GURL url, - GetTokenMetadataIntermediateCallback callback); - void OnGetSolanaAccountInfoMetaplex( - GetMetaplexMetadataCallback callback, - absl::optional account_info, - mojom::SolanaProviderError error, - const std::string& error_message); - using SendSolanaTransactionCallback = base::OnceCallback account_info, + mojom::SolanaProviderError error, + const std::string& error_message); void CompleteGetTokenMetadataEth(GetTokenMetadataCallback callback, const std::string& response, int error, From 1ef001cffe507f071c1eeae50f1eb63fd6bb5b1f Mon Sep 17 00:00:00 2001 From: nvonpentz <12549658+nvonpentz@users.noreply.github.com> Date: Tue, 29 Nov 2022 16:41:56 -0500 Subject: [PATCH 07/17] Remove redundant JsonRpcService:: prefix when calling GetTokenMetadata --- components/brave_wallet/browser/json_rpc_service.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/brave_wallet/browser/json_rpc_service.cc b/components/brave_wallet/browser/json_rpc_service.cc index 271d4306fd62..2316950b962b 100644 --- a/components/brave_wallet/browser/json_rpc_service.cc +++ b/components/brave_wallet/browser/json_rpc_service.cc @@ -1963,7 +1963,7 @@ void JsonRpcService::GetERC721Metadata(const std::string& contract_address, const std::string& token_id, const std::string& chain_id, GetTokenMetadataCallback callback) { - JsonRpcService::GetTokenMetadata(contract_address, token_id, chain_id, + GetTokenMetadata(contract_address, token_id, chain_id, kERC721MetadataInterfaceId, std::move(callback)); } @@ -1972,7 +1972,7 @@ void JsonRpcService::GetERC1155Metadata(const std::string& contract_address, const std::string& token_id, const std::string& chain_id, GetTokenMetadataCallback callback) { - JsonRpcService::GetTokenMetadata(contract_address, token_id, chain_id, + GetTokenMetadata(contract_address, token_id, chain_id, kERC1155MetadataInterfaceId, std::move(callback)); } From d81835b5739e37e4b100df8d7788dec93dd30817 Mon Sep 17 00:00:00 2001 From: nvonpentz <12549658+nvonpentz@users.noreply.github.com> Date: Tue, 29 Nov 2022 17:05:24 -0500 Subject: [PATCH 08/17] Rename *TokenMetadata functions * Make it clear which function is for SOL and ETH by reading the function name alone * Rename FetchMetadataToken to FetchMetadata which is slightly more intuitive --- .../brave_wallet/browser/json_rpc_service.cc | 59 +++-- .../brave_wallet/browser/json_rpc_service.h | 35 ++- .../browser/json_rpc_service_unittest.cc | 208 +++++++++--------- .../brave_wallet/common/brave_wallet.mojom | 2 +- 4 files changed, 150 insertions(+), 154 deletions(-) diff --git a/components/brave_wallet/browser/json_rpc_service.cc b/components/brave_wallet/browser/json_rpc_service.cc index 2316950b962b..019e8e264914 100644 --- a/components/brave_wallet/browser/json_rpc_service.cc +++ b/components/brave_wallet/browser/json_rpc_service.cc @@ -1962,26 +1962,24 @@ void JsonRpcService::ContinueGetERC721TokenBalance( void JsonRpcService::GetERC721Metadata(const std::string& contract_address, const std::string& token_id, const std::string& chain_id, - GetTokenMetadataCallback callback) { - GetTokenMetadata(contract_address, token_id, chain_id, - kERC721MetadataInterfaceId, - std::move(callback)); + GetEthTokenMetadataCallback callback) { + GetEthTokenMetadata(contract_address, token_id, chain_id, + kERC721MetadataInterfaceId, std::move(callback)); } void JsonRpcService::GetERC1155Metadata(const std::string& contract_address, const std::string& token_id, const std::string& chain_id, - GetTokenMetadataCallback callback) { - GetTokenMetadata(contract_address, token_id, chain_id, - kERC1155MetadataInterfaceId, - std::move(callback)); + GetEthTokenMetadataCallback callback) { + GetEthTokenMetadata(contract_address, token_id, chain_id, + kERC1155MetadataInterfaceId, std::move(callback)); } -void JsonRpcService::GetTokenMetadata(const std::string& contract_address, - const std::string& token_id, - const std::string& chain_id, - const std::string& interface_id, - GetTokenMetadataCallback callback) { +void JsonRpcService::GetEthTokenMetadata(const std::string& contract_address, + const std::string& token_id, + const std::string& chain_id, + const std::string& interface_id, + GetEthTokenMetadataCallback callback) { auto network_url = GetNetworkURL(prefs_, chain_id, mojom::CoinType::ETH); if (!network_url.is_valid()) { std::move(callback).Run( @@ -2041,7 +2039,7 @@ void JsonRpcService::OnGetSupportsInterfaceTokenMetadata( const std::string& contract_address, const std::string& function_signature, const GURL& network_url, - GetTokenMetadataCallback callback, + GetEthTokenMetadataCallback callback, bool is_supported, mojom::ProviderError error, const std::string& error_message) { @@ -2066,7 +2064,7 @@ void JsonRpcService::OnGetSupportsInterfaceTokenMetadata( true, network_url, std::move(internal_callback)); } -void JsonRpcService::OnGetTokenUri(GetTokenMetadataCallback callback, +void JsonRpcService::OnGetTokenUri(GetEthTokenMetadataCallback callback, APIRequestResult api_request_result) { if (!api_request_result.Is2XXResponseCode()) { std::move(callback).Run( @@ -2086,8 +2084,8 @@ void JsonRpcService::OnGetTokenUri(GetTokenMetadataCallback callback, return; } - FetchTokenMetadata( - url, base::BindOnce(&JsonRpcService::CompleteGetTokenMetadataEth, + FetchMetadata( + url, base::BindOnce(&JsonRpcService::CompleteGetEthTokenMetadata, weak_ptr_factory_.GetWeakPtr(), std::move(callback))); } @@ -2132,16 +2130,16 @@ void JsonRpcService::OnGetTokenMetadataPayload( std::move(callback).Run(api_request_result.body(), 0, ""); // 0 is kSuccess } -void JsonRpcService::CompleteGetTokenMetadataEth( - GetTokenMetadataCallback callback, +void JsonRpcService::CompleteGetEthTokenMetadata( + GetEthTokenMetadataCallback callback, const std::string& response, int error, const std::string& error_message) { std::move(callback).Run(response, mojom::ProviderError(error), error_message); } -void JsonRpcService::CompleteGetTokenMetadataSol( - GetMetaplexMetadataCallback callback, +void JsonRpcService::CompleteGetSolTokenMetadata( + GetSolTokenMetadataCallback callback, const std::string& response, int error, const std::string& error_message) { @@ -2503,8 +2501,8 @@ void JsonRpcService::OnGetSPLTokenAccountBalance( mojom::SolanaProviderError::kSuccess, ""); } -void JsonRpcService::GetMetaplexMetadata(const std::string& nft_account_address, - GetMetaplexMetadataCallback callback) { +void JsonRpcService::GetSolTokenMetadata(const std::string& nft_account_address, + GetSolTokenMetadataCallback callback) { // Derive metadata PDA for the NFT accounts absl::optional associated_metadata_account = SolanaKeyring::GetAssociatedMetadataAccount(nft_account_address); @@ -2516,7 +2514,7 @@ void JsonRpcService::GetMetaplexMetadata(const std::string& nft_account_address, } auto account_info_metadata_callback = - base::BindOnce(&JsonRpcService::OnGetSolanaAccountInfoMetaplex, + base::BindOnce(&JsonRpcService::OnGetSolanaAccountInfoTokenMetadata, weak_ptr_factory_.GetWeakPtr(), std::move(callback)); // Call getAccountInfo for the metadata PDA @@ -2529,8 +2527,8 @@ void JsonRpcService::GetMetaplexMetadata(const std::string& nft_account_address, solana::ConverterForGetAccountInfo()); } -void JsonRpcService::OnGetSolanaAccountInfoMetaplex( - GetMetaplexMetadataCallback callback, +void JsonRpcService::OnGetSolanaAccountInfoTokenMetadata( + GetSolTokenMetadataCallback callback, absl::optional account_info, mojom::SolanaProviderError error, const std::string& error_message) { @@ -2554,13 +2552,12 @@ void JsonRpcService::OnGetSolanaAccountInfoMetaplex( return; } - FetchTokenMetadata( - *url, - base::BindOnce(&JsonRpcService::CompleteGetTokenMetadataSol, - weak_ptr_factory_.GetWeakPtr(), std::move(callback))); + FetchMetadata(*url, base::BindOnce( + &JsonRpcService::CompleteGetSolTokenMetadata, + weak_ptr_factory_.GetWeakPtr(), std::move(callback))); } -void JsonRpcService::FetchTokenMetadata( +void JsonRpcService::FetchMetadata( GURL url, GetTokenMetadataIntermediateCallback callback) { // Obtain JSON from the URL depending on the scheme. diff --git a/components/brave_wallet/browser/json_rpc_service.h b/components/brave_wallet/browser/json_rpc_service.h index fc719c908ce4..44869f621934 100644 --- a/components/brave_wallet/browser/json_rpc_service.h +++ b/components/brave_wallet/browser/json_rpc_service.h @@ -301,7 +301,7 @@ class JsonRpcService : public KeyedService, public mojom::JsonRpcService { const std::string& chain_id, GetERC721TokenBalanceCallback callback) override; - using GetTokenMetadataCallback = + using GetEthTokenMetadataCallback = base::OnceCallback; @@ -309,12 +309,12 @@ class JsonRpcService : public KeyedService, public mojom::JsonRpcService { void GetERC721Metadata(const std::string& contract_address, const std::string& token_id, const std::string& chain_id, - GetTokenMetadataCallback callback) override; + GetEthTokenMetadataCallback callback) override; void GetERC1155Metadata(const std::string& contract_address, const std::string& token_id, const std::string& chain_id, - GetTokenMetadataCallback callback) override; + GetEthTokenMetadataCallback callback) override; void EthGetLogs(const std::string& chain_id, const std::string& from_block, @@ -361,8 +361,8 @@ class JsonRpcService : public KeyedService, public mojom::JsonRpcService { const std::string& token_mint_address, const std::string& chain_id, GetSPLTokenAccountBalanceCallback callback) override; - void GetMetaplexMetadata(const std::string& nft_account_address, - GetMetaplexMetadataCallback callback) override; + void GetSolTokenMetadata(const std::string& nft_account_address, + GetSolTokenMetadataCallback callback) override; using SendSolanaTransactionCallback = base::OnceCallback account_info, mojom::SolanaProviderError error, const std::string& error_message); - void CompleteGetTokenMetadataEth(GetTokenMetadataCallback callback, + void CompleteGetEthTokenMetadata(GetEthTokenMetadataCallback callback, const std::string& response, int error, const std::string& error_message); - void CompleteGetTokenMetadataSol(GetMetaplexMetadataCallback callback, + void CompleteGetSolTokenMetadata(GetSolTokenMetadataCallback callback, const std::string& response, int error, const std::string& error_message); diff --git a/components/brave_wallet/browser/json_rpc_service_unittest.cc b/components/brave_wallet/browser/json_rpc_service_unittest.cc index b3ade40ae838..379812a883d8 100644 --- a/components/brave_wallet/browser/json_rpc_service_unittest.cc +++ b/components/brave_wallet/browser/json_rpc_service_unittest.cc @@ -923,7 +923,7 @@ class JsonRpcServiceUnitTest : public testing::Test { })); } - void SetMetaplexMetadataInterceptor( + void SetSolTokenMetadataInterceptor( const GURL& expected_rpc_url, const std::string& get_account_info_response, const GURL& expected_metadata_url, @@ -1123,15 +1123,15 @@ class JsonRpcServiceUnitTest : public testing::Test { run_loop.Run(); } - void TestGetTokenMetadata(const std::string& contract, - const std::string& token_id, - const std::string& chain_id, - const std::string& interface_id, - const std::string& expected_response, - mojom::ProviderError expected_error, - const std::string& expected_error_message) { + void TestGetEthTokenMetadata(const std::string& contract, + const std::string& token_id, + const std::string& chain_id, + const std::string& interface_id, + const std::string& expected_response, + mojom::ProviderError expected_error, + const std::string& expected_error_message) { base::RunLoop run_loop; - json_rpc_service_->GetTokenMetadata( + json_rpc_service_->GetEthTokenMetadata( contract, token_id, chain_id, interface_id, base::BindLambdaForTesting([&](const std::string& response, mojom::ProviderError error, @@ -1417,12 +1417,12 @@ class JsonRpcServiceUnitTest : public testing::Test { loop.Run(); } - void TestGetMetaplexMetadata(const std::string& nft_account_address, + void TestGetSolTokenMetadata(const std::string& nft_account_address, const std::string& expected_response, mojom::SolanaProviderError expected_error, const std::string& expected_error_message) { base::RunLoop loop; - json_rpc_service_->GetMetaplexMetadata( + json_rpc_service_->GetSolTokenMetadata( nft_account_address, base::BindLambdaForTesting([&](const std::string& response, mojom::SolanaProviderError error, @@ -3386,7 +3386,7 @@ TEST_F(JsonRpcServiceUnitTest, GetERC721OwnerOf) { EXPECT_TRUE(callback_called); } -TEST_F(JsonRpcServiceUnitTest, GetTokenMetadata) { +TEST_F(JsonRpcServiceUnitTest, GetEthTokenMetadata) { const std::string https_token_uri_response = R"({ "jsonrpc":"2.0", "id":1, @@ -3442,29 +3442,29 @@ TEST_F(JsonRpcServiceUnitTest, GetTokenMetadata) { // Invalid inputs // (1/3) Invalid contract address - TestGetTokenMetadata("", "0x1", mojom::kMainnetChainId, - kERC721MetadataInterfaceId, "", - mojom::ProviderError::kInvalidParams, - l10n_util::GetStringUTF8(IDS_WALLET_INVALID_PARAMETERS)); + TestGetEthTokenMetadata( + "", "0x1", mojom::kMainnetChainId, kERC721MetadataInterfaceId, "", + mojom::ProviderError::kInvalidParams, + l10n_util::GetStringUTF8(IDS_WALLET_INVALID_PARAMETERS)); // (2/3) Invalid token ID - TestGetTokenMetadata("0x06012c8cf97BEaD5deAe237070F9587f8E7A266d", "", - mojom::kMainnetChainId, kERC721MetadataInterfaceId, "", - mojom::ProviderError::kInvalidParams, - l10n_util::GetStringUTF8(IDS_WALLET_INVALID_PARAMETERS)); + TestGetEthTokenMetadata( + "0x06012c8cf97BEaD5deAe237070F9587f8E7A266d", "", mojom::kMainnetChainId, + kERC721MetadataInterfaceId, "", mojom::ProviderError::kInvalidParams, + l10n_util::GetStringUTF8(IDS_WALLET_INVALID_PARAMETERS)); // (3/3) Invalid chain ID - TestGetTokenMetadata("0x06012c8cf97BEaD5deAe237070F9587f8E7A266d", "0x1", "", - kERC721MetadataInterfaceId, "", - mojom::ProviderError::kInvalidParams, - l10n_util::GetStringUTF8(IDS_WALLET_INVALID_PARAMETERS)); + TestGetEthTokenMetadata( + "0x06012c8cf97BEaD5deAe237070F9587f8E7A266d", "0x1", "", + kERC721MetadataInterfaceId, "", mojom::ProviderError::kInvalidParams, + l10n_util::GetStringUTF8(IDS_WALLET_INVALID_PARAMETERS)); // Mismatched // (4/4) Unknown interfaceID - TestGetTokenMetadata("0x06012c8cf97BEaD5deAe237070F9587f8E7A266d", "0x1", "", - kERC721InterfaceId, "", - mojom::ProviderError::kInvalidParams, - l10n_util::GetStringUTF8(IDS_WALLET_INVALID_PARAMETERS)); + TestGetEthTokenMetadata( + "0x06012c8cf97BEaD5deAe237070F9587f8E7A266d", "0x1", "", + kERC721InterfaceId, "", mojom::ProviderError::kInvalidParams, + l10n_util::GetStringUTF8(IDS_WALLET_INVALID_PARAMETERS)); // Valid inputs // (1/3) HTTP URI @@ -3472,26 +3472,26 @@ TEST_F(JsonRpcServiceUnitTest, GetTokenMetadata) { kERC721MetadataInterfaceId, mojom::kMainnetChainId, interface_supported_response, https_token_uri_response, https_metadata_response); - TestGetTokenMetadata("0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", - mojom::kMainnetChainId, kERC721MetadataInterfaceId, - https_metadata_response, mojom::ProviderError::kSuccess, - ""); + TestGetEthTokenMetadata("0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", + mojom::kMainnetChainId, kERC721MetadataInterfaceId, + https_metadata_response, + mojom::ProviderError::kSuccess, ""); // (2/3) IPFS URI SetTokenMetadataInterceptor(kERC721MetadataInterfaceId, mojom::kLocalhostChainId, interface_supported_response, ipfs_token_uri_response, ipfs_metadata_response); - TestGetTokenMetadata("0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d", "0x719", - mojom::kLocalhostChainId, kERC721MetadataInterfaceId, - ipfs_metadata_response, mojom::ProviderError::kSuccess, - ""); + TestGetEthTokenMetadata("0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d", "0x719", + mojom::kLocalhostChainId, kERC721MetadataInterfaceId, + ipfs_metadata_response, + mojom::ProviderError::kSuccess, ""); // (3/3) Data URI SetTokenMetadataInterceptor( kERC721MetadataInterfaceId, mojom::kMainnetChainId, interface_supported_response, data_token_uri_response); - TestGetTokenMetadata( + TestGetEthTokenMetadata( "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d", "0x719", mojom::kMainnetChainId, kERC721MetadataInterfaceId, R"({"attributes":"","description":"Non fungible lion","image":"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MDAgNTAwIj48cGF0aCBkPSIiLz48L3N2Zz4=","name":"NFL"})", @@ -3503,32 +3503,32 @@ TEST_F(JsonRpcServiceUnitTest, GetTokenMetadata) { kERC721MetadataInterfaceId, mojom::kMainnetChainId, interface_supported_response, https_token_uri_response, "", net::HTTP_REQUEST_TIMEOUT); - TestGetTokenMetadata("0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d", "0x719", - mojom::kMainnetChainId, kERC721MetadataInterfaceId, "", - mojom::ProviderError::kInternalError, - l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); + TestGetEthTokenMetadata("0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d", "0x719", + mojom::kMainnetChainId, kERC721MetadataInterfaceId, + "", mojom::ProviderError::kInternalError, + l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); // (2/4) Invalid JSON SetTokenMetadataInterceptor(kERC721MetadataInterfaceId, mojom::kMainnetChainId, invalid_json); - TestGetTokenMetadata("0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d", "0x719", - mojom::kMainnetChainId, kERC721MetadataInterfaceId, "", - mojom::ProviderError::kParsingError, - l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); + TestGetEthTokenMetadata("0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d", "0x719", + mojom::kMainnetChainId, kERC721MetadataInterfaceId, + "", mojom::ProviderError::kParsingError, + l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); // (3/4) Request exceeds provider limit SetTokenMetadataInterceptor(kERC721MetadataInterfaceId, mojom::kMainnetChainId, exceeds_limit_json); - TestGetTokenMetadata("0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d", "0x719", - mojom::kMainnetChainId, kERC721MetadataInterfaceId, "", - mojom::ProviderError::kLimitExceeded, - "Request exceeds defined limit"); + TestGetEthTokenMetadata("0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d", "0x719", + mojom::kMainnetChainId, kERC721MetadataInterfaceId, + "", mojom::ProviderError::kLimitExceeded, + "Request exceeds defined limit"); // (4/4) Interface not supported SetTokenMetadataInterceptor(kERC721MetadataInterfaceId, mojom::kMainnetChainId, interface_not_supported_response); - TestGetTokenMetadata( + TestGetEthTokenMetadata( "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d", "0x719", mojom::kMainnetChainId, kERC721MetadataInterfaceId, "", mojom::ProviderError::kMethodNotSupported, @@ -3540,55 +3540,55 @@ TEST_F(JsonRpcServiceUnitTest, GetTokenMetadata) { kERC721MetadataInterfaceId, mojom::kMainnetChainId, interface_supported_response, https_token_uri_response, "", net::HTTP_OK, net::HTTP_REQUEST_TIMEOUT); - TestGetTokenMetadata("0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", - mojom::kMainnetChainId, kERC721MetadataInterfaceId, "", - mojom::ProviderError::kInternalError, - l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); + TestGetEthTokenMetadata("0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", + mojom::kMainnetChainId, kERC721MetadataInterfaceId, + "", mojom::ProviderError::kInternalError, + l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); // (2/6) Invalid Provider JSON SetTokenMetadataInterceptor(kERC721MetadataInterfaceId, mojom::kMainnetChainId, interface_supported_response, invalid_json); - TestGetTokenMetadata("0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", - mojom::kMainnetChainId, kERC721MetadataInterfaceId, "", - mojom::ProviderError::kParsingError, - l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); + TestGetEthTokenMetadata("0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", + mojom::kMainnetChainId, kERC721MetadataInterfaceId, + "", mojom::ProviderError::kParsingError, + l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); // (3/6) Invalid JSON in data URI SetTokenMetadataInterceptor( kERC721MetadataInterfaceId, mojom::kMainnetChainId, interface_supported_response, data_token_uri_response_invalid_json); - TestGetTokenMetadata("0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", - mojom::kMainnetChainId, kERC721MetadataInterfaceId, "", - mojom::ProviderError::kParsingError, - l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); + TestGetEthTokenMetadata("0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", + mojom::kMainnetChainId, kERC721MetadataInterfaceId, + "", mojom::ProviderError::kParsingError, + l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); // (4/6) Empty string as JSON in data URI SetTokenMetadataInterceptor( kERC721MetadataInterfaceId, mojom::kMainnetChainId, interface_supported_response, data_token_uri_response_empty_string); - TestGetTokenMetadata("0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", - mojom::kMainnetChainId, kERC721MetadataInterfaceId, "", - mojom::ProviderError::kParsingError, - l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); + TestGetEthTokenMetadata("0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", + mojom::kMainnetChainId, kERC721MetadataInterfaceId, + "", mojom::ProviderError::kParsingError, + l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); // (5/6) Request exceeds limit SetTokenMetadataInterceptor(kERC721MetadataInterfaceId, mojom::kMainnetChainId, interface_supported_response, exceeds_limit_json); - TestGetTokenMetadata("0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", - mojom::kMainnetChainId, kERC721MetadataInterfaceId, "", - mojom::ProviderError::kLimitExceeded, - "Request exceeds defined limit"); + TestGetEthTokenMetadata("0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", + mojom::kMainnetChainId, kERC721MetadataInterfaceId, + "", mojom::ProviderError::kLimitExceeded, + "Request exceeds defined limit"); // (6/6) URI scheme is not suported (HTTP) SetTokenMetadataInterceptor( kERC721MetadataInterfaceId, mojom::kMainnetChainId, interface_supported_response, http_token_uri_response); - TestGetTokenMetadata("0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", - mojom::kMainnetChainId, kERC721MetadataInterfaceId, "", - mojom::ProviderError::kInternalError, - l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); + TestGetEthTokenMetadata("0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", + mojom::kMainnetChainId, kERC721MetadataInterfaceId, + "", mojom::ProviderError::kInternalError, + l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); // Invalid metadata response (2 total) // (1/2) Timeout @@ -3597,34 +3597,34 @@ TEST_F(JsonRpcServiceUnitTest, GetTokenMetadata) { interface_supported_response, https_token_uri_response, https_metadata_response, net::HTTP_OK, net::HTTP_OK, net::HTTP_REQUEST_TIMEOUT); - TestGetTokenMetadata("0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", - mojom::kMainnetChainId, kERC721MetadataInterfaceId, "", - mojom::ProviderError::kInternalError, - l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); + TestGetEthTokenMetadata("0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", + mojom::kMainnetChainId, kERC721MetadataInterfaceId, + "", mojom::ProviderError::kInternalError, + l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); // (2/2) Invalid JSON SetTokenMetadataInterceptor( kERC721MetadataInterfaceId, mojom::kMainnetChainId, interface_supported_response, ipfs_token_uri_response, invalid_json); - TestGetTokenMetadata("0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", - mojom::kMainnetChainId, kERC721MetadataInterfaceId, "", - mojom::ProviderError::kParsingError, - l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); + TestGetEthTokenMetadata("0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", + mojom::kMainnetChainId, kERC721MetadataInterfaceId, + "", mojom::ProviderError::kParsingError, + l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); // ERC1155 SetTokenMetadataInterceptor( kERC1155MetadataInterfaceId, mojom::kMainnetChainId, interface_supported_response, https_token_uri_response, https_metadata_response); - TestGetTokenMetadata("0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", - mojom::kMainnetChainId, kERC1155MetadataInterfaceId, - https_metadata_response, mojom::ProviderError::kSuccess, - ""); + TestGetEthTokenMetadata("0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", + mojom::kMainnetChainId, kERC1155MetadataInterfaceId, + https_metadata_response, + mojom::ProviderError::kSuccess, ""); } TEST_F(JsonRpcServiceUnitTest, GetERC721Metadata) { // Ensure GetERC721Metadata passes the correct interface ID to - // GetTokenMetadata + // GetEthTokenMetadata SetTokenMetadataInterceptor(kERC721MetadataInterfaceId, mojom::kMainnetChainId, R"({ @@ -3645,7 +3645,7 @@ TEST_F(JsonRpcServiceUnitTest, GetERC721Metadata) { TEST_F(JsonRpcServiceUnitTest, GetERC1155Metadata) { // Ensure GetERC1155Metadata passes the correct interface ID to - // GetTokenMetadata + // GetEthTokenMetadata SetTokenMetadataInterceptor(kERC1155MetadataInterfaceId, mojom::kMainnetChainId, R"({ @@ -5886,7 +5886,7 @@ TEST_F(JsonRpcServiceUnitTest, EthGetLogs) { std::move(expected_logs), mojom::ProviderError::kSuccess, ""); } -TEST_F(JsonRpcServiceUnitTest, GetMetaplexMetadata) { +TEST_F(JsonRpcServiceUnitTest, GetSolTokenMetadata) { // Valid inputs should yield metadata JSON (happy case) std::string get_account_info_response = R"({ "jsonrpc": "2.0", @@ -5911,41 +5911,41 @@ TEST_F(JsonRpcServiceUnitTest, GetMetaplexMetadata) { const std::string valid_metadata_response = R"({"attributes":[{"trait_type":"hair","value":"green & blue"},{"trait_type":"pontus","value":"no"}],"description":"","external_url":"","image":"https://bafkreiagsgqhjudpta6trhjuv5y2n2exsrhbkkprl64tvg2mftjsdm3vgi.ipfs.dweb.link?ext=png","name":"SPECIAL SAUCE","properties":{"category":"image","creators":[{"address":"7oUUEdptZnZVhSet4qobU9PtpPfiNUEJ8ftPnrC6YEaa","share":98},{"address":"tsU33UT3K2JTfLgHUo7hdzRhRe4wth885cqVbM8WLiq","share":2}],"files":[{"type":"image/png","uri":"https://bafkreiagsgqhjudpta6trhjuv5y2n2exsrhbkkprl64tvg2mftjsdm3vgi.ipfs.dweb.link?ext=png"}],"maxSupply":0},"seller_fee_basis_points":1000,"symbol":""})"; auto network_url = GetNetwork(mojom::kLocalhostChainId, mojom::CoinType::SOL); - SetMetaplexMetadataInterceptor( + SetSolTokenMetadataInterceptor( network_url, get_account_info_response, GURL("https://" "bafkreif4wx54wjr7pgfug3wlatr3nfntsfwngv6eusebbquezrxenj6ck4.ipfs." "dweb.link/?ext="), valid_metadata_response); - TestGetMetaplexMetadata("5ZXToo7froykjvjnpHtTLYr9u2tW3USMwPg3sNkiaQVh", + TestGetSolTokenMetadata("5ZXToo7froykjvjnpHtTLYr9u2tW3USMwPg3sNkiaQVh", valid_metadata_response, mojom::SolanaProviderError::kSuccess, ""); // Invalid nft_account_address yields internal error. - SetMetaplexMetadataInterceptor( + SetSolTokenMetadataInterceptor( network_url, get_account_info_response, GURL("https://" "bafkreif4wx54wjr7pgfug3wlatr3nfntsfwngv6eusebbquezrxenj6ck4.ipfs." "dweb.link/?ext="), valid_metadata_response); - TestGetMetaplexMetadata("Invalid", "", + TestGetSolTokenMetadata("Invalid", "", mojom::SolanaProviderError::kInternalError, l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); // Non 200 getAccountInfo response of yields internal server error. SetHTTPRequestTimeoutInterceptor(); - TestGetMetaplexMetadata("5ZXToo7froykjvjnpHtTLYr9u2tW3USMwPg3sNkiaQVh", "", + TestGetSolTokenMetadata("5ZXToo7froykjvjnpHtTLYr9u2tW3USMwPg3sNkiaQVh", "", mojom::SolanaProviderError::kInternalError, l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); // Invalid getAccountInfo response JSON yields internal error - SetMetaplexMetadataInterceptor( + SetSolTokenMetadataInterceptor( network_url, "Invalid json response", GURL("https://" "bafkreif4wx54wjr7pgfug3wlatr3nfntsfwngv6eusebbquezrxenj6ck4.ipfs." "dweb.link/?ext="), valid_metadata_response); - TestGetMetaplexMetadata("5ZXToo7froykjvjnpHtTLYr9u2tW3USMwPg3sNkiaQVh", "", + TestGetSolTokenMetadata("5ZXToo7froykjvjnpHtTLYr9u2tW3USMwPg3sNkiaQVh", "", mojom::SolanaProviderError::kInternalError, l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); @@ -5970,13 +5970,13 @@ TEST_F(JsonRpcServiceUnitTest, GetMetaplexMetadata) { }, "id": 1 })"; - SetMetaplexMetadataInterceptor( + SetSolTokenMetadataInterceptor( network_url, get_account_info_response, GURL("https://" "bafkreif4wx54wjr7pgfug3wlatr3nfntsfwngv6eusebbquezrxenj6ck4.ipfs." "dweb.link/?ext="), valid_metadata_response); - TestGetMetaplexMetadata("5ZXToo7froykjvjnpHtTLYr9u2tW3USMwPg3sNkiaQVh", "", + TestGetSolTokenMetadata("5ZXToo7froykjvjnpHtTLYr9u2tW3USMwPg3sNkiaQVh", "", mojom::SolanaProviderError::kParsingError, l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); @@ -6002,13 +6002,13 @@ TEST_F(JsonRpcServiceUnitTest, GetMetaplexMetadata) { }, "id": 1 })"; - SetMetaplexMetadataInterceptor( + SetSolTokenMetadataInterceptor( network_url, get_account_info_response, GURL("https://" "bafkreif4wx54wjr7pgfug3wlatr3nfntsfwngv6eusebbquezrxenj6ck4.ipfs." "dweb.link/?ext="), valid_metadata_response); - TestGetMetaplexMetadata("5ZXToo7froykjvjnpHtTLYr9u2tW3USMwPg3sNkiaQVh", "", + TestGetSolTokenMetadata("5ZXToo7froykjvjnpHtTLYr9u2tW3USMwPg3sNkiaQVh", "", mojom::SolanaProviderError::kParsingError, l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); @@ -6034,13 +6034,13 @@ TEST_F(JsonRpcServiceUnitTest, GetMetaplexMetadata) { }, "id": 1 })"; - SetMetaplexMetadataInterceptor( + SetSolTokenMetadataInterceptor( network_url, get_account_info_response, GURL("https://" "bafkreif4wx54wjr7pgfug3wlatr3nfntsfwngv6eusebbquezrxenj6ck4.ipfs." "dweb.link/?ext="), valid_metadata_response); - TestGetMetaplexMetadata("5ZXToo7froykjvjnpHtTLYr9u2tW3USMwPg3sNkiaQVh", "", + TestGetSolTokenMetadata("5ZXToo7froykjvjnpHtTLYr9u2tW3USMwPg3sNkiaQVh", "", mojom::SolanaProviderError::kParsingError, l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); @@ -6067,13 +6067,13 @@ TEST_F(JsonRpcServiceUnitTest, GetMetaplexMetadata) { }, "id": 1 })"; - SetMetaplexMetadataInterceptor( + SetSolTokenMetadataInterceptor( network_url, get_account_info_response, GURL("https://" "bafkreif4wx54wjr7pgfug3wlatr3nfntsfwngv6eusebbquezrxenj6ck4.ipfs." "dweb.link/?ext="), valid_metadata_response); - TestGetMetaplexMetadata("5ZXToo7froykjvjnpHtTLYr9u2tW3USMwPg3sNkiaQVh", "", + TestGetSolTokenMetadata("5ZXToo7froykjvjnpHtTLYr9u2tW3USMwPg3sNkiaQVh", "", mojom::SolanaProviderError::kParsingError, l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); diff --git a/components/brave_wallet/common/brave_wallet.mojom b/components/brave_wallet/common/brave_wallet.mojom index fdbfb4fd4acb..92958a5abcb4 100644 --- a/components/brave_wallet/common/brave_wallet.mojom +++ b/components/brave_wallet/common/brave_wallet.mojom @@ -975,7 +975,7 @@ interface JsonRpcService { (string amount, uint8 decimals, string uiAmountString, SolanaProviderError error, string error_message); // Returns the metadata json associated with the NFT account address - GetMetaplexMetadata(string nft_account_address) => (string response, SolanaProviderError error, string error_message); + GetSolTokenMetadata(string nft_account_address) => (string response, SolanaProviderError error, string error_message); }; enum TransactionStatus { From 0111f51c47d5c08e3bd3a03f7ffbce4e82d5c3cf Mon Sep 17 00:00:00 2001 From: nvonpentz <12549658+nvonpentz@users.noreply.github.com> Date: Tue, 29 Nov 2022 17:36:26 -0500 Subject: [PATCH 09/17] Small change: Add variable name in function signature in header file --- components/brave_wallet/browser/json_rpc_service.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/components/brave_wallet/browser/json_rpc_service.h b/components/brave_wallet/browser/json_rpc_service.h index 44869f621934..29128df2a660 100644 --- a/components/brave_wallet/browser/json_rpc_service.h +++ b/components/brave_wallet/browser/json_rpc_service.h @@ -90,8 +90,10 @@ class JsonRpcService : public KeyedService, public mojom::JsonRpcService { base::OnceCallback& logs, mojom::ProviderError error, const std::string& error_message)>; - using GetTokenMetadataIntermediateCallback = base::OnceCallback< - void(const std::string& response, int, const std::string& error_message)>; + using GetTokenMetadataIntermediateCallback = + base::OnceCallback; void GetBlockNumber(GetBlockNumberCallback callback); void GetFeeHistory(GetFeeHistoryCallback callback); From 1e7d8ff01254193358ffa04612567f416846fc61 Mon Sep 17 00:00:00 2001 From: nvonpentz <12549658+nvonpentz@users.noreply.github.com> Date: Tue, 29 Nov 2022 20:06:43 -0500 Subject: [PATCH 10/17] Add unit tests for JsonRpcService::FetchMetadata --- .../browser/json_rpc_service_unittest.cc | 65 +++++++++++++++++-- 1 file changed, 61 insertions(+), 4 deletions(-) diff --git a/components/brave_wallet/browser/json_rpc_service_unittest.cc b/components/brave_wallet/browser/json_rpc_service_unittest.cc index 379812a883d8..e84eb8ee8996 100644 --- a/components/brave_wallet/browser/json_rpc_service_unittest.cc +++ b/components/brave_wallet/browser/json_rpc_service_unittest.cc @@ -964,8 +964,11 @@ class JsonRpcServiceUnitTest : public testing::Test { !expected_cache_header.empty()); EXPECT_EQ(expected_cache_header, header_value); } - EXPECT_TRUE(request.headers.GetHeader("x-brave-key", &header_value)); - EXPECT_EQ(BUILDFLAG(BRAVE_SERVICES_KEY), header_value); + if (!expected_method.empty()) { + EXPECT_TRUE( + request.headers.GetHeader("x-brave-key", &header_value)); + EXPECT_EQ(BUILDFLAG(BRAVE_SERVICES_KEY), header_value); + } url_loader_factory_.ClearResponses(); url_loader_factory_.AddResponse(request.url.spec(), content); })); @@ -1144,6 +1147,23 @@ class JsonRpcServiceUnitTest : public testing::Test { run_loop.Run(); } + void TestFetchMetadata(const GURL& url, + const std::string& expected_response, + int expected_error, + const std::string& expected_error_message) { + base::RunLoop run_loop; + json_rpc_service_->FetchMetadata( + url, + base::BindLambdaForTesting([&](const std::string& response, int error, + const std::string& error_message) { + EXPECT_EQ(response, expected_response); + EXPECT_EQ(error, expected_error); + EXPECT_EQ(error_message, expected_error_message); + run_loop.Quit(); + })); + run_loop.Run(); + } + void TestEthGetLogs(const std::string& chain_id, const std::string& from_block, const std::string& to_block, @@ -5886,6 +5906,45 @@ TEST_F(JsonRpcServiceUnitTest, EthGetLogs) { std::move(expected_logs), mojom::ProviderError::kSuccess, ""); } +TEST_F(JsonRpcServiceUnitTest, FetchMetadata) { + // Invalid URL yields internal error + TestFetchMetadata(GURL("invalid url"), "", + static_cast(mojom::JsonRpcError::kInternalError), + l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); + + // Unsupported scheme yields internal error + TestFetchMetadata(GURL("file://host/path"), "", + static_cast(mojom::JsonRpcError::kInternalError), + l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); + + // Data URL with unsupported mime type yields parsing error + TestFetchMetadata( + GURL("data:text/" + "csv;base64,eyJpbWFnZV91cmwiOiAgImh0dHBzOi8vZXhhbXBsZS5jb20ifQ=="), + "", static_cast(mojom::JsonRpcError::kParsingError), + l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); + + // Valid URL but that results in HTTP timeout yields internal error + SetHTTPRequestTimeoutInterceptor(); + TestFetchMetadata(GURL("https://example.com"), "", + static_cast(mojom::JsonRpcError::kInternalError), + l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); + + // Valid URL but invalid json response yields parsing error + SetInvalidJsonInterceptor(); + TestFetchMetadata(GURL("https://example.com"), "", + static_cast(mojom::JsonRpcError::kParsingError), + l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); + + // All valid yields response json set via interceptor + GURL url = GURL("https://example.com"); + const std::string& metadata_json = + R"({"image_url":"https://example.com/image.jpg"})"; + SetInterceptor(url, "", "", metadata_json); + TestFetchMetadata(url, metadata_json, + static_cast(mojom::ProviderError::kSuccess), ""); +} + TEST_F(JsonRpcServiceUnitTest, GetSolTokenMetadata) { // Valid inputs should yield metadata JSON (happy case) std::string get_account_info_response = R"({ @@ -6076,8 +6135,6 @@ TEST_F(JsonRpcServiceUnitTest, GetSolTokenMetadata) { TestGetSolTokenMetadata("5ZXToo7froykjvjnpHtTLYr9u2tW3USMwPg3sNkiaQVh", "", mojom::SolanaProviderError::kParsingError, l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); - - // TODO(nvonpentz) FetchTokenMetadata needs to be tested elsewhere } } // namespace brave_wallet From ac75215accc3b3231292987df76412d0c1fc74d4 Mon Sep 17 00:00:00 2001 From: nvonpentz <12549658+nvonpentz@users.noreply.github.com> Date: Wed, 30 Nov 2022 11:38:43 -0500 Subject: [PATCH 11/17] Refactor: Separate NFT metadata fetching into new NftMetadataFetcher class --- components/brave_wallet/browser/BUILD.gn | 2 + .../brave_wallet/browser/json_rpc_service.cc | 257 +------ .../brave_wallet/browser/json_rpc_service.h | 64 +- .../browser/json_rpc_service_unittest.cc | 377 ++-------- .../browser/nft_metadata_fetcher.cc | 312 ++++++++ .../browser/nft_metadata_fetcher.h | 106 +++ .../browser/nft_metadata_fetcher_unittest.cc | 706 ++++++++++++++++++ components/brave_wallet/browser/test/BUILD.gn | 1 + 8 files changed, 1248 insertions(+), 577 deletions(-) create mode 100644 components/brave_wallet/browser/nft_metadata_fetcher.cc create mode 100644 components/brave_wallet/browser/nft_metadata_fetcher.h create mode 100644 components/brave_wallet/browser/nft_metadata_fetcher_unittest.cc diff --git a/components/brave_wallet/browser/BUILD.gn b/components/brave_wallet/browser/BUILD.gn index 849a53f7b5c7..cddbf5b3c837 100644 --- a/components/brave_wallet/browser/BUILD.gn +++ b/components/brave_wallet/browser/BUILD.gn @@ -91,6 +91,8 @@ static_library("browser") { "json_rpc_service.h", "keyring_service.cc", "keyring_service.h", + "nft_metadata_fetcher.cc", + "nft_metadata_fetcher.h", "nonce_tracker.cc", "nonce_tracker.h", "password_encryptor.cc", diff --git a/components/brave_wallet/browser/json_rpc_service.cc b/components/brave_wallet/browser/json_rpc_service.cc index 019e8e264914..9e2fa83cd6d7 100644 --- a/components/brave_wallet/browser/json_rpc_service.cc +++ b/components/brave_wallet/browser/json_rpc_service.cc @@ -31,7 +31,6 @@ #include "brave/components/brave_wallet/browser/json_rpc_requests_helper.h" #include "brave/components/brave_wallet/browser/json_rpc_response_parser.h" #include "brave/components/brave_wallet/browser/pref_names.h" -#include "brave/components/brave_wallet/browser/solana_data_decoder_utils.h" #include "brave/components/brave_wallet/browser/solana_keyring.h" #include "brave/components/brave_wallet/browser/solana_requests.h" #include "brave/components/brave_wallet/browser/solana_response_parser.h" @@ -49,7 +48,6 @@ #include "brave/components/brave_wallet/common/web3_provider_constants.h" #include "brave/components/decentralized_dns/core/constants.h" #include "brave/components/decentralized_dns/core/utils.h" -#include "brave/components/ipfs/buildflags/buildflags.h" #include "components/grit/brave_components_strings.h" #include "components/prefs/pref_service.h" #include "components/prefs/scoped_user_pref_update.h" @@ -58,11 +56,6 @@ #include "ui/base/l10n/l10n_util.h" #include "url/origin.h" -#if BUILDFLAG(ENABLE_IPFS) -#include "brave/components/ipfs/ipfs_constants.h" -#include "brave/components/ipfs/ipfs_utils.h" -#endif - using api_request_helper::APIRequestHelper; using decentralized_dns::EnsOffchainResolveMethod; @@ -183,6 +176,9 @@ JsonRpcService::JsonRpcService( api_request_helper_ens_offchain_ = std::make_unique( GetENSOffchainNetworkTrafficAnnotationTag(), url_loader_factory); } + + nft_metadata_fetcher_ = std::unique_ptr( + new NftMetadataFetcher(url_loader_factory, this, prefs_)); } JsonRpcService::JsonRpcService( @@ -1962,35 +1958,37 @@ void JsonRpcService::ContinueGetERC721TokenBalance( void JsonRpcService::GetERC721Metadata(const std::string& contract_address, const std::string& token_id, const std::string& chain_id, - GetEthTokenMetadataCallback callback) { - GetEthTokenMetadata(contract_address, token_id, chain_id, - kERC721MetadataInterfaceId, std::move(callback)); + GetERC721MetadataCallback callback) { + nft_metadata_fetcher_->GetEthTokenMetadata( + contract_address, token_id, chain_id, kERC721MetadataInterfaceId, + std::move(callback)); } void JsonRpcService::GetERC1155Metadata(const std::string& contract_address, const std::string& token_id, const std::string& chain_id, - GetEthTokenMetadataCallback callback) { - GetEthTokenMetadata(contract_address, token_id, chain_id, - kERC1155MetadataInterfaceId, std::move(callback)); + GetERC1155MetadataCallback callback) { + nft_metadata_fetcher_->GetEthTokenMetadata( + contract_address, token_id, chain_id, kERC1155MetadataInterfaceId, + std::move(callback)); } -void JsonRpcService::GetEthTokenMetadata(const std::string& contract_address, - const std::string& token_id, - const std::string& chain_id, - const std::string& interface_id, - GetEthTokenMetadataCallback callback) { +void JsonRpcService::GetEthTokenUri(const std::string& chain_id, + const std::string& contract_address, + const std::string& token_id, + const std::string& interface_id, + GetEthTokenUriCallback callback) { auto network_url = GetNetworkURL(prefs_, chain_id, mojom::CoinType::ETH); if (!network_url.is_valid()) { std::move(callback).Run( - "", mojom::ProviderError::kInvalidParams, - l10n_util::GetStringUTF8(IDS_WALLET_INVALID_PARAMETERS)); + GURL(), mojom::ProviderError::kInternalError, + l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); return; } if (!EthAddress::IsValidAddress(contract_address)) { std::move(callback).Run( - "", mojom::ProviderError::kInvalidParams, + GURL(), mojom::ProviderError::kInvalidParams, l10n_util::GetStringUTF8(IDS_WALLET_INVALID_PARAMETERS)); return; } @@ -1998,7 +1996,7 @@ void JsonRpcService::GetEthTokenMetadata(const std::string& contract_address, uint256_t token_id_uint = 0; if (!HexValueToUint256(token_id, &token_id_uint)) { std::move(callback).Run( - "", mojom::ProviderError::kInvalidParams, + GURL(), mojom::ProviderError::kInvalidParams, l10n_util::GetStringUTF8(IDS_WALLET_INVALID_PARAMETERS)); return; } @@ -2007,56 +2005,27 @@ void JsonRpcService::GetEthTokenMetadata(const std::string& contract_address, if (interface_id == kERC721MetadataInterfaceId) { if (!erc721::TokenUri(token_id_uint, &function_signature)) { std::move(callback).Run( - "", mojom::ProviderError::kInvalidParams, + GURL(), mojom::ProviderError::kInvalidParams, l10n_util::GetStringUTF8(IDS_WALLET_INVALID_PARAMETERS)); return; } } else if (interface_id == kERC1155MetadataInterfaceId) { if (!erc1155::Uri(token_id_uint, &function_signature)) { std::move(callback).Run( - "", mojom::ProviderError::kInvalidParams, + GURL(), mojom::ProviderError::kInvalidParams, l10n_util::GetStringUTF8(IDS_WALLET_INVALID_PARAMETERS)); return; } } else { // Unknown inteface ID std::move(callback).Run( - "", mojom::ProviderError::kInvalidParams, + GURL(), mojom::ProviderError::kInvalidParams, l10n_util::GetStringUTF8(IDS_WALLET_INVALID_PARAMETERS)); return; } auto internal_callback = - base::BindOnce(&JsonRpcService::OnGetSupportsInterfaceTokenMetadata, - weak_ptr_factory_.GetWeakPtr(), contract_address, - function_signature, network_url, std::move(callback)); - - GetSupportsInterface(contract_address, interface_id, chain_id, - std::move(internal_callback)); -} - -void JsonRpcService::OnGetSupportsInterfaceTokenMetadata( - const std::string& contract_address, - const std::string& function_signature, - const GURL& network_url, - GetEthTokenMetadataCallback callback, - bool is_supported, - mojom::ProviderError error, - const std::string& error_message) { - if (error != mojom::ProviderError::kSuccess) { - std::move(callback).Run("", error, error_message); - return; - } - - if (!is_supported) { - std::move(callback).Run( - "", mojom::ProviderError::kMethodNotSupported, - l10n_util::GetStringUTF8(IDS_WALLET_METHOD_NOT_SUPPORTED_ERROR)); - return; - } - - auto internal_callback = - base::BindOnce(&JsonRpcService::OnGetTokenUri, + base::BindOnce(&JsonRpcService::OnGetTokenUri2, weak_ptr_factory_.GetWeakPtr(), std::move(callback)); RequestInternal(eth::eth_call("", contract_address, "", "", "", @@ -2064,11 +2033,11 @@ void JsonRpcService::OnGetSupportsInterfaceTokenMetadata( true, network_url, std::move(internal_callback)); } -void JsonRpcService::OnGetTokenUri(GetEthTokenMetadataCallback callback, - APIRequestResult api_request_result) { +void JsonRpcService::OnGetTokenUri2(GetEthTokenUriCallback callback, + APIRequestResult api_request_result) { if (!api_request_result.Is2XXResponseCode()) { std::move(callback).Run( - "", mojom::ProviderError::kInternalError, + GURL(), mojom::ProviderError::kInternalError, l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); return; } @@ -2080,71 +2049,11 @@ void JsonRpcService::OnGetTokenUri(GetEthTokenMetadataCallback callback, std::string error_message; ParseErrorResult(api_request_result.body(), &error, &error_message); - std::move(callback).Run("", error, error_message); - return; - } - - FetchMetadata( - url, base::BindOnce(&JsonRpcService::CompleteGetEthTokenMetadata, - weak_ptr_factory_.GetWeakPtr(), std::move(callback))); -} - -void JsonRpcService::OnSanitizeTokenMetadata( - GetTokenMetadataIntermediateCallback callback, - data_decoder::JsonSanitizer::Result result) { - if (result.error) { - VLOG(1) << "Data URI JSON validation error:" << *result.error; - std::move(callback).Run( - "", static_cast(mojom::JsonRpcError::kParsingError), - l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); + std::move(callback).Run(url, error, error_message); return; } - std::string metadata_json; - if (result.value.has_value()) { - metadata_json = result.value.value(); - } - - std::move(callback).Run(metadata_json, 0, ""); // 0 is kSuccess -} - -void JsonRpcService::OnGetTokenMetadataPayload( - GetTokenMetadataIntermediateCallback callback, - APIRequestResult api_request_result) { - mojom::ProviderErrorUnionPtr error; - if (!api_request_result.Is2XXResponseCode()) { - std::move(callback).Run( - "", static_cast(mojom::JsonRpcError::kInternalError), - l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); - return; - } - - // Invalid JSON becomes an empty string after sanitization - if (api_request_result.body().empty()) { - std::move(callback).Run( - "", static_cast(mojom::JsonRpcError::kParsingError), - l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); - return; - } - - std::move(callback).Run(api_request_result.body(), 0, ""); // 0 is kSuccess -} - -void JsonRpcService::CompleteGetEthTokenMetadata( - GetEthTokenMetadataCallback callback, - const std::string& response, - int error, - const std::string& error_message) { - std::move(callback).Run(response, mojom::ProviderError(error), error_message); -} - -void JsonRpcService::CompleteGetSolTokenMetadata( - GetSolTokenMetadataCallback callback, - const std::string& response, - int error, - const std::string& error_message) { - std::move(callback).Run(response, mojom::SolanaProviderError(error), - error_message); + std::move(callback).Run(url, mojom::ProviderError::kSuccess, ""); } void JsonRpcService::GetERC1155TokenBalance( @@ -2503,112 +2412,8 @@ void JsonRpcService::OnGetSPLTokenAccountBalance( void JsonRpcService::GetSolTokenMetadata(const std::string& nft_account_address, GetSolTokenMetadataCallback callback) { - // Derive metadata PDA for the NFT accounts - absl::optional associated_metadata_account = - SolanaKeyring::GetAssociatedMetadataAccount(nft_account_address); - if (!associated_metadata_account) { - std::move(callback).Run( - "", mojom::SolanaProviderError::kInternalError, - l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); - return; - } - - auto account_info_metadata_callback = - base::BindOnce(&JsonRpcService::OnGetSolanaAccountInfoTokenMetadata, - weak_ptr_factory_.GetWeakPtr(), std::move(callback)); - - // Call getAccountInfo for the metadata PDA - auto account_info_response_callback = base::BindOnce( - &JsonRpcService::OnGetSolanaAccountInfo, weak_ptr_factory_.GetWeakPtr(), - std::move(account_info_metadata_callback)); - RequestInternal(solana::getAccountInfo(*associated_metadata_account), true, - network_urls_[mojom::CoinType::SOL], - std::move(account_info_response_callback), - solana::ConverterForGetAccountInfo()); -} - -void JsonRpcService::OnGetSolanaAccountInfoTokenMetadata( - GetSolTokenMetadataCallback callback, - absl::optional account_info, - mojom::SolanaProviderError error, - const std::string& error_message) { - if (error != mojom::SolanaProviderError::kSuccess || !account_info) { - std::move(callback).Run("", std::move(error), error_message); - return; - } - - absl::optional> metadata = - base::Base64Decode((*account_info).data); - if (!metadata) { - std::move(callback).Run("", mojom::SolanaProviderError::kParsingError, - l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); - return; - } - - absl::optional url = DecodeMetadataUri(*metadata); - if (!url || !url.value().is_valid()) { - std::move(callback).Run("", mojom::SolanaProviderError::kParsingError, - l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); - return; - } - - FetchMetadata(*url, base::BindOnce( - &JsonRpcService::CompleteGetSolTokenMetadata, - weak_ptr_factory_.GetWeakPtr(), std::move(callback))); -} - -void JsonRpcService::FetchMetadata( - GURL url, - GetTokenMetadataIntermediateCallback callback) { - // Obtain JSON from the URL depending on the scheme. - // IPFS, HTTPS, and data URIs are supported. - // IPFS and HTTPS URIs require an additional request to fetch the metadata. - std::string metadata_json; - std::string scheme = url.scheme(); -#if BUILDFLAG(ENABLE_IPFS) - if (scheme != url::kDataScheme && scheme != url::kHttpsScheme && - scheme != ipfs::kIPFSScheme) { -#else - if (scheme != url::kDataScheme && scheme != url::kHttpsScheme) { -#endif - std::move(callback).Run( - "", static_cast(mojom::JsonRpcError::kInternalError), - l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); - return; - } - - if (scheme == url::kDataScheme) { - if (!eth::ParseDataURIAndExtractJSON(url, &metadata_json)) { - std::move(callback).Run( - "", static_cast(mojom::JsonRpcError::kParsingError), - l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); - return; - } - - // Sanitize JSON - data_decoder::JsonSanitizer::Sanitize( - std::move(metadata_json), - base::BindOnce(&JsonRpcService::OnSanitizeTokenMetadata, - weak_ptr_factory_.GetWeakPtr(), std::move(callback))); - return; - } - -#if BUILDFLAG(ENABLE_IPFS) - if (scheme == ipfs::kIPFSScheme && - !ipfs::TranslateIPFSURI(url, &url, ipfs::GetDefaultNFTIPFSGateway(prefs_), - false)) { - std::move(callback).Run( - "", static_cast(mojom::JsonRpcError::kParsingError), - l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); - return; - } -#endif - - auto internal_callback = - base::BindOnce(&JsonRpcService::OnGetTokenMetadataPayload, - weak_ptr_factory_.GetWeakPtr(), std::move(callback)); - api_request_helper_->Request("GET", url, "", "", true, - std::move(internal_callback)); + nft_metadata_fetcher_->GetSolTokenMetadata(nft_account_address, + std::move(callback)); } void JsonRpcService::SendFilecoinTransaction( diff --git a/components/brave_wallet/browser/json_rpc_service.h b/components/brave_wallet/browser/json_rpc_service.h index 29128df2a660..cfa07175b218 100644 --- a/components/brave_wallet/browser/json_rpc_service.h +++ b/components/brave_wallet/browser/json_rpc_service.h @@ -19,6 +19,7 @@ #include "brave/components/api_request_helper/api_request_helper.h" #include "brave/components/brave_wallet/browser/brave_wallet_constants.h" #include "brave/components/brave_wallet/browser/ens_resolver_task.h" +#include "brave/components/brave_wallet/browser/nft_metadata_fetcher.h" #include "brave/components/brave_wallet/browser/sns_resolver_task.h" #include "brave/components/brave_wallet/browser/solana_transaction.h" #include "brave/components/brave_wallet/browser/unstoppable_domains_multichain_calls.h" @@ -29,7 +30,6 @@ #include "mojo/public/cpp/bindings/receiver_set.h" #include "mojo/public/cpp/bindings/remote.h" #include "mojo/public/cpp/bindings/remote_set.h" -#include "services/data_decoder/public/cpp/json_sanitizer.h" #include "url/gurl.h" #include "url/origin.h" @@ -43,6 +43,7 @@ class PrefService; namespace brave_wallet { class EnsResolverTask; +class NftMetadataFetcher; class JsonRpcService : public KeyedService, public mojom::JsonRpcService { public: @@ -90,11 +91,6 @@ class JsonRpcService : public KeyedService, public mojom::JsonRpcService { base::OnceCallback& logs, mojom::ProviderError error, const std::string& error_message)>; - using GetTokenMetadataIntermediateCallback = - base::OnceCallback; - void GetBlockNumber(GetBlockNumberCallback callback); void GetFeeHistory(GetFeeHistoryCallback callback); @@ -303,20 +299,27 @@ class JsonRpcService : public KeyedService, public mojom::JsonRpcService { const std::string& chain_id, GetERC721TokenBalanceCallback callback) override; - using GetEthTokenMetadataCallback = - base::OnceCallback; void GetERC721Metadata(const std::string& contract_address, const std::string& token_id, const std::string& chain_id, - GetEthTokenMetadataCallback callback) override; + GetERC721MetadataCallback callback) override; void GetERC1155Metadata(const std::string& contract_address, const std::string& token_id, const std::string& chain_id, - GetEthTokenMetadataCallback callback) override; + GetERC1155MetadataCallback callback) override; + // GetEthTokenUri should only be called after check whether the contract + // supports the ERC721 or ERC1155 interface + void GetEthTokenUri(const std::string& chain_id, + const std::string& contract_address, + const std::string& token_id, + const std::string& interface_id, + GetEthTokenUriCallback callback); void EthGetLogs(const std::string& chain_id, const std::string& from_block, @@ -519,49 +522,13 @@ class JsonRpcService : public KeyedService, public mojom::JsonRpcService { void OnGetERC721OwnerOf(GetERC721OwnerOfCallback callback, APIRequestResult api_request_result); - void GetEthTokenMetadata(const std::string& contract_address, - const std::string& token_id, - const std::string& chain_id, - const std::string& interface_id, - GetEthTokenMetadataCallback callback); - - void OnGetSupportsInterfaceTokenMetadata(const std::string& contract_address, - const std::string& signature, - const GURL& network_url, - GetEthTokenMetadataCallback callback, - bool is_supported, - mojom::ProviderError error, - const std::string& error_message); - void ContinueGetERC721TokenBalance(const std::string& account_address, GetERC721TokenBalanceCallback callback, const std::string& owner_address, mojom::ProviderError error, const std::string& error_message); - void OnGetTokenUri(GetEthTokenMetadataCallback callback, - const APIRequestResult api_request_result); - - void FetchMetadata(GURL url, GetTokenMetadataIntermediateCallback callback); - void OnSanitizeTokenMetadata(GetTokenMetadataIntermediateCallback callback, - data_decoder::JsonSanitizer::Result result); - - void OnGetTokenMetadataPayload(GetTokenMetadataIntermediateCallback callback, - APIRequestResult api_request_result); - - void OnGetSolanaAccountInfoTokenMetadata( - GetSolTokenMetadataCallback callback, - absl::optional account_info, - mojom::SolanaProviderError error, - const std::string& error_message); - void CompleteGetEthTokenMetadata(GetEthTokenMetadataCallback callback, - const std::string& response, - int error, - const std::string& error_message); - - void CompleteGetSolTokenMetadata(GetSolTokenMetadataCallback callback, - const std::string& response, - int error, - const std::string& error_message); + void OnGetTokenUri2(GetEthTokenUriCallback callback, + const APIRequestResult api_request_result); void OnGetSupportsInterface(GetSupportsInterfaceCallback callback, APIRequestResult api_request_result); @@ -616,6 +583,7 @@ class JsonRpcService : public KeyedService, public mojom::JsonRpcService { mojo::ReceiverSet receivers_; PrefService* prefs_ = nullptr; PrefService* local_state_prefs_ = nullptr; + std::unique_ptr nft_metadata_fetcher_; base::WeakPtrFactory weak_ptr_factory_; }; diff --git a/components/brave_wallet/browser/json_rpc_service_unittest.cc b/components/brave_wallet/browser/json_rpc_service_unittest.cc index e84eb8ee8996..a3d8a6ca1682 100644 --- a/components/brave_wallet/browser/json_rpc_service_unittest.cc +++ b/components/brave_wallet/browser/json_rpc_service_unittest.cc @@ -1126,37 +1126,20 @@ class JsonRpcServiceUnitTest : public testing::Test { run_loop.Run(); } - void TestGetEthTokenMetadata(const std::string& contract, - const std::string& token_id, - const std::string& chain_id, - const std::string& interface_id, - const std::string& expected_response, - mojom::ProviderError expected_error, - const std::string& expected_error_message) { + void TestGetEthTokenUri(const std::string& contract, + const std::string& token_id, + const std::string& chain_id, + const std::string& interface_id, + const GURL& expected_uri, + mojom::ProviderError expected_error, + const std::string& expected_error_message) { base::RunLoop run_loop; - json_rpc_service_->GetEthTokenMetadata( - contract, token_id, chain_id, interface_id, - base::BindLambdaForTesting([&](const std::string& response, + json_rpc_service_->GetEthTokenUri( + chain_id, contract, token_id, interface_id, + base::BindLambdaForTesting([&](const GURL& uri, mojom::ProviderError error, const std::string& error_message) { - EXPECT_EQ(response, expected_response); - EXPECT_EQ(error, expected_error); - EXPECT_EQ(error_message, expected_error_message); - run_loop.Quit(); - })); - run_loop.Run(); - } - - void TestFetchMetadata(const GURL& url, - const std::string& expected_response, - int expected_error, - const std::string& expected_error_message) { - base::RunLoop run_loop; - json_rpc_service_->FetchMetadata( - url, - base::BindLambdaForTesting([&](const std::string& response, int error, - const std::string& error_message) { - EXPECT_EQ(response, expected_response); + EXPECT_EQ(uri, expected_uri); EXPECT_EQ(error, expected_error); EXPECT_EQ(error_message, expected_error_message); run_loop.Quit(); @@ -1345,6 +1328,7 @@ class JsonRpcServiceUnitTest : public testing::Test { mojom::SolanaProviderError expected_error, const std::string& expected_error_message) { base::RunLoop run_loop; + VLOG(0) << "TestGetSolanaAccountInfo 0"; json_rpc_service_->GetSolanaAccountInfo( "vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg", base::BindLambdaForTesting( @@ -3406,242 +3390,6 @@ TEST_F(JsonRpcServiceUnitTest, GetERC721OwnerOf) { EXPECT_TRUE(callback_called); } -TEST_F(JsonRpcServiceUnitTest, GetEthTokenMetadata) { - const std::string https_token_uri_response = R"({ - "jsonrpc":"2.0", - "id":1, - "result":"0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002468747470733a2f2f696e76697369626c65667269656e64732e696f2f6170692f3138313700000000000000000000000000000000000000000000000000000000" - })"; - const std::string http_token_uri_response = R"({ - "jsonrpc":"2.0", - "id":1, - "result":"0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020687474703a2f2f696e76697369626c65667269656e64732e696f2f6170692f31" - })"; - const std::string data_token_uri_response = R"({ - "jsonrpc":"2.0", - "id":1, - "result": "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000135646174613a6170706c69636174696f6e2f6a736f6e3b6261736536342c65794a686448527961574a316447567a496a6f69496977695a47567a59334a7063485270623234694f694a4f623234675a6e56755a326c696247556762476c7662694973496d6c745957646c496a6f695a474630595470706257466e5a53397a646d6372654731734f324a68633255324e43785153453479576e6c434e474a586548566a656a4270595568534d474e4562335a4d4d32517a5a486b314d3031354e585a6a62574e3254577042643031444f58706b62574e7053556861634670595a454e694d326335535770425a3031445154464e5245466e546c524264306c714e44686a5230597759554e436131425453576c4d656a513454444e4f4d6c70364e4430694c434a755957316c496a6f69546b5a4d496e303d0000000000000000000000" - })"; - const std::string data_token_uri_response_invalid_json = R"({ - "jsonrpc":"2.0", - "id":1, - "result":"0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000085646174613a6170706c69636174696f6e2f6a736f6e3b6261736536342c65794a755957316c496a6f69546b5a4d49697767496d526c63324e796158423061573975496a6f69546d397549475a31626d6470596d786c49477870623234694c43416959585230636d6c696458526c637949364969497349434a706257466e5a5349364969493d000000000000000000000000000000000000000000000000000000" - })"; - const std::string data_token_uri_response_empty_string = R"({ - "jsonrpc":"2.0", - "id":1, - "result":"0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001d646174613a6170706c69636174696f6e2f6a736f6e3b6261736536342c000000" - })"; - const std::string interface_supported_response = R"({ - "jsonrpc":"2.0", - "id":1, - "result": "0x0000000000000000000000000000000000000000000000000000000000000001" - })"; - const std::string exceeds_limit_json = R"({ - "jsonrpc":"2.0", - "id":1, - "error": { - "code":-32005, - "message": "Request exceeds defined limit" - } - })"; - const std::string interface_not_supported_response = R"({ - "jsonrpc":"2.0", - "id":1, - "result":"0x0000000000000000000000000000000000000000000000000000000000000000" - })"; - const std::string invalid_json = - "It might make sense just to get some in case it catches on"; - const std::string ipfs_token_uri_response = R"({ - "jsonrpc":"2.0", - "id":1, - "result":"0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003a697066733a2f2f516d65536a53696e4870506e6d586d73704d6a776958794e367a533445397a63636172694752336a7863615774712f31383137000000000000" - })"; - const std::string ipfs_metadata_response = - R"({"attributes":[{"trait_type":"Mouth","value":"Bored Cigarette"},{"trait_type":"Fur","value":"Gray"},{"trait_type":"Background","value":"Aquamarine"},{"trait_type":"Clothes","value":"Tuxedo Tee"},{"trait_type":"Hat","value":"Bayc Hat Black"},{"trait_type":"Eyes","value":"Coins"}],"image":"ipfs://QmQ82uDT3JyUMsoZuaFBYuEucF654CYE5ktPUrnA5d4VDH"})"; - - // Invalid inputs - // (1/3) Invalid contract address - TestGetEthTokenMetadata( - "", "0x1", mojom::kMainnetChainId, kERC721MetadataInterfaceId, "", - mojom::ProviderError::kInvalidParams, - l10n_util::GetStringUTF8(IDS_WALLET_INVALID_PARAMETERS)); - - // (2/3) Invalid token ID - TestGetEthTokenMetadata( - "0x06012c8cf97BEaD5deAe237070F9587f8E7A266d", "", mojom::kMainnetChainId, - kERC721MetadataInterfaceId, "", mojom::ProviderError::kInvalidParams, - l10n_util::GetStringUTF8(IDS_WALLET_INVALID_PARAMETERS)); - - // (3/3) Invalid chain ID - TestGetEthTokenMetadata( - "0x06012c8cf97BEaD5deAe237070F9587f8E7A266d", "0x1", "", - kERC721MetadataInterfaceId, "", mojom::ProviderError::kInvalidParams, - l10n_util::GetStringUTF8(IDS_WALLET_INVALID_PARAMETERS)); - - // Mismatched - // (4/4) Unknown interfaceID - TestGetEthTokenMetadata( - "0x06012c8cf97BEaD5deAe237070F9587f8E7A266d", "0x1", "", - kERC721InterfaceId, "", mojom::ProviderError::kInvalidParams, - l10n_util::GetStringUTF8(IDS_WALLET_INVALID_PARAMETERS)); - - // Valid inputs - // (1/3) HTTP URI - SetTokenMetadataInterceptor( - kERC721MetadataInterfaceId, mojom::kMainnetChainId, - interface_supported_response, https_token_uri_response, - https_metadata_response); - TestGetEthTokenMetadata("0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", - mojom::kMainnetChainId, kERC721MetadataInterfaceId, - https_metadata_response, - mojom::ProviderError::kSuccess, ""); - - // (2/3) IPFS URI - SetTokenMetadataInterceptor(kERC721MetadataInterfaceId, - mojom::kLocalhostChainId, - interface_supported_response, - ipfs_token_uri_response, ipfs_metadata_response); - TestGetEthTokenMetadata("0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d", "0x719", - mojom::kLocalhostChainId, kERC721MetadataInterfaceId, - ipfs_metadata_response, - mojom::ProviderError::kSuccess, ""); - - // (3/3) Data URI - SetTokenMetadataInterceptor( - kERC721MetadataInterfaceId, mojom::kMainnetChainId, - interface_supported_response, data_token_uri_response); - TestGetEthTokenMetadata( - "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d", "0x719", - mojom::kMainnetChainId, kERC721MetadataInterfaceId, - R"({"attributes":"","description":"Non fungible lion","image":"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MDAgNTAwIj48cGF0aCBkPSIiLz48L3N2Zz4=","name":"NFL"})", - mojom::ProviderError::kSuccess, ""); - - // Invalid supportsInterface response - // (1/4) Timeout - SetTokenMetadataInterceptor( - kERC721MetadataInterfaceId, mojom::kMainnetChainId, - interface_supported_response, https_token_uri_response, "", - net::HTTP_REQUEST_TIMEOUT); - TestGetEthTokenMetadata("0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d", "0x719", - mojom::kMainnetChainId, kERC721MetadataInterfaceId, - "", mojom::ProviderError::kInternalError, - l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); - - // (2/4) Invalid JSON - SetTokenMetadataInterceptor(kERC721MetadataInterfaceId, - mojom::kMainnetChainId, invalid_json); - TestGetEthTokenMetadata("0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d", "0x719", - mojom::kMainnetChainId, kERC721MetadataInterfaceId, - "", mojom::ProviderError::kParsingError, - l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); - - // (3/4) Request exceeds provider limit - SetTokenMetadataInterceptor(kERC721MetadataInterfaceId, - mojom::kMainnetChainId, exceeds_limit_json); - TestGetEthTokenMetadata("0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d", "0x719", - mojom::kMainnetChainId, kERC721MetadataInterfaceId, - "", mojom::ProviderError::kLimitExceeded, - "Request exceeds defined limit"); - - // (4/4) Interface not supported - SetTokenMetadataInterceptor(kERC721MetadataInterfaceId, - mojom::kMainnetChainId, - interface_not_supported_response); - TestGetEthTokenMetadata( - "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d", "0x719", - mojom::kMainnetChainId, kERC721MetadataInterfaceId, "", - mojom::ProviderError::kMethodNotSupported, - l10n_util::GetStringUTF8(IDS_WALLET_METHOD_NOT_SUPPORTED_ERROR)); - - // Invalid tokenURI response (6 total) - // (1/6) Timeout - SetTokenMetadataInterceptor( - kERC721MetadataInterfaceId, mojom::kMainnetChainId, - interface_supported_response, https_token_uri_response, "", net::HTTP_OK, - net::HTTP_REQUEST_TIMEOUT); - TestGetEthTokenMetadata("0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", - mojom::kMainnetChainId, kERC721MetadataInterfaceId, - "", mojom::ProviderError::kInternalError, - l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); - - // (2/6) Invalid Provider JSON - SetTokenMetadataInterceptor(kERC721MetadataInterfaceId, - mojom::kMainnetChainId, - interface_supported_response, invalid_json); - TestGetEthTokenMetadata("0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", - mojom::kMainnetChainId, kERC721MetadataInterfaceId, - "", mojom::ProviderError::kParsingError, - l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); - - // (3/6) Invalid JSON in data URI - SetTokenMetadataInterceptor( - kERC721MetadataInterfaceId, mojom::kMainnetChainId, - interface_supported_response, data_token_uri_response_invalid_json); - TestGetEthTokenMetadata("0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", - mojom::kMainnetChainId, kERC721MetadataInterfaceId, - "", mojom::ProviderError::kParsingError, - l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); - - // (4/6) Empty string as JSON in data URI - SetTokenMetadataInterceptor( - kERC721MetadataInterfaceId, mojom::kMainnetChainId, - interface_supported_response, data_token_uri_response_empty_string); - TestGetEthTokenMetadata("0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", - mojom::kMainnetChainId, kERC721MetadataInterfaceId, - "", mojom::ProviderError::kParsingError, - l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); - - // (5/6) Request exceeds limit - SetTokenMetadataInterceptor(kERC721MetadataInterfaceId, - mojom::kMainnetChainId, - interface_supported_response, exceeds_limit_json); - TestGetEthTokenMetadata("0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", - mojom::kMainnetChainId, kERC721MetadataInterfaceId, - "", mojom::ProviderError::kLimitExceeded, - "Request exceeds defined limit"); - - // (6/6) URI scheme is not suported (HTTP) - SetTokenMetadataInterceptor( - kERC721MetadataInterfaceId, mojom::kMainnetChainId, - interface_supported_response, http_token_uri_response); - TestGetEthTokenMetadata("0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", - mojom::kMainnetChainId, kERC721MetadataInterfaceId, - "", mojom::ProviderError::kInternalError, - l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); - - // Invalid metadata response (2 total) - // (1/2) Timeout - SetTokenMetadataInterceptor( - kERC721MetadataInterfaceId, mojom::kMainnetChainId, - interface_supported_response, https_token_uri_response, - https_metadata_response, net::HTTP_OK, net::HTTP_OK, - net::HTTP_REQUEST_TIMEOUT); - TestGetEthTokenMetadata("0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", - mojom::kMainnetChainId, kERC721MetadataInterfaceId, - "", mojom::ProviderError::kInternalError, - l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); - - // (2/2) Invalid JSON - SetTokenMetadataInterceptor( - kERC721MetadataInterfaceId, mojom::kMainnetChainId, - interface_supported_response, ipfs_token_uri_response, invalid_json); - TestGetEthTokenMetadata("0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", - mojom::kMainnetChainId, kERC721MetadataInterfaceId, - "", mojom::ProviderError::kParsingError, - l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); - - // ERC1155 - SetTokenMetadataInterceptor( - kERC1155MetadataInterfaceId, mojom::kMainnetChainId, - interface_supported_response, https_token_uri_response, - https_metadata_response); - TestGetEthTokenMetadata("0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", - mojom::kMainnetChainId, kERC1155MetadataInterfaceId, - https_metadata_response, - mojom::ProviderError::kSuccess, ""); -} - TEST_F(JsonRpcServiceUnitTest, GetERC721Metadata) { // Ensure GetERC721Metadata passes the correct interface ID to // GetEthTokenMetadata @@ -5906,45 +5654,6 @@ TEST_F(JsonRpcServiceUnitTest, EthGetLogs) { std::move(expected_logs), mojom::ProviderError::kSuccess, ""); } -TEST_F(JsonRpcServiceUnitTest, FetchMetadata) { - // Invalid URL yields internal error - TestFetchMetadata(GURL("invalid url"), "", - static_cast(mojom::JsonRpcError::kInternalError), - l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); - - // Unsupported scheme yields internal error - TestFetchMetadata(GURL("file://host/path"), "", - static_cast(mojom::JsonRpcError::kInternalError), - l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); - - // Data URL with unsupported mime type yields parsing error - TestFetchMetadata( - GURL("data:text/" - "csv;base64,eyJpbWFnZV91cmwiOiAgImh0dHBzOi8vZXhhbXBsZS5jb20ifQ=="), - "", static_cast(mojom::JsonRpcError::kParsingError), - l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); - - // Valid URL but that results in HTTP timeout yields internal error - SetHTTPRequestTimeoutInterceptor(); - TestFetchMetadata(GURL("https://example.com"), "", - static_cast(mojom::JsonRpcError::kInternalError), - l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); - - // Valid URL but invalid json response yields parsing error - SetInvalidJsonInterceptor(); - TestFetchMetadata(GURL("https://example.com"), "", - static_cast(mojom::JsonRpcError::kParsingError), - l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); - - // All valid yields response json set via interceptor - GURL url = GURL("https://example.com"); - const std::string& metadata_json = - R"({"image_url":"https://example.com/image.jpg"})"; - SetInterceptor(url, "", "", metadata_json); - TestFetchMetadata(url, metadata_json, - static_cast(mojom::ProviderError::kSuccess), ""); -} - TEST_F(JsonRpcServiceUnitTest, GetSolTokenMetadata) { // Valid inputs should yield metadata JSON (happy case) std::string get_account_info_response = R"({ @@ -6137,4 +5846,66 @@ TEST_F(JsonRpcServiceUnitTest, GetSolTokenMetadata) { l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); } +TEST_F(JsonRpcServiceUnitTest, GetEthTokenUri) { + // Invalid contract address input + TestGetEthTokenUri("", "0x1", mojom::kMainnetChainId, + kERC721MetadataInterfaceId, GURL(), + mojom::ProviderError::kInvalidParams, + l10n_util::GetStringUTF8(IDS_WALLET_INVALID_PARAMETERS)); + + // Invalid token ID input + TestGetEthTokenUri("0x06012c8cf97BEaD5deAe237070F9587f8E7A266d", "", + mojom::kMainnetChainId, kERC721MetadataInterfaceId, GURL(), + mojom::ProviderError::kInvalidParams, + l10n_util::GetStringUTF8(IDS_WALLET_INVALID_PARAMETERS)); + + // Invalid chain ID input + TestGetEthTokenUri("0x06012c8cf97BEaD5deAe237070F9587f8E7A266d", "0x1", "", + kERC721MetadataInterfaceId, GURL(), + mojom::ProviderError::kInternalError, + l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); + + // Unknown interfaceID input + TestGetEthTokenUri("0x06012c8cf97BEaD5deAe237070F9587f8E7A266d", "0x1", + mojom::kMainnetChainId, "invalid interface", GURL(), + mojom::ProviderError::kInvalidParams, + l10n_util::GetStringUTF8(IDS_WALLET_INVALID_PARAMETERS)); + + // Valid inputs but HTTP Timeout + SetHTTPRequestTimeoutInterceptor(); + TestGetEthTokenUri("0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", + mojom::kMainnetChainId, kERC721MetadataInterfaceId, GURL(), + mojom::ProviderError::kInternalError, + l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); + + // Valid inputs, request exceeds limit response + SetLimitExceededJsonErrorResponse(); + TestGetEthTokenUri("0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", + mojom::kMainnetChainId, kERC721MetadataInterfaceId, GURL(), + mojom::ProviderError::kLimitExceeded, + "Request exceeds defined limit"); + + // Valid inputs, invalid provider JSON + SetInvalidJsonInterceptor(); + TestGetEthTokenUri("0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", + mojom::kMainnetChainId, kERC721MetadataInterfaceId, GURL(), + mojom::ProviderError::kParsingError, + l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); + + // Valid inputs, valid provider JSON, invalid URI + // TODO(nvonpentz) + + // All valid + SetInterceptor(GetNetwork(mojom::kMainnetChainId, mojom::CoinType::ETH), + "eth_call", "", R"({ + "jsonrpc":"2.0", + "id":1, + "result":"0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002468747470733a2f2f696e76697369626c65667269656e64732e696f2f6170692f3138313700000000000000000000000000000000000000000000000000000000" + })"); + TestGetEthTokenUri("0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", + mojom::kMainnetChainId, kERC721MetadataInterfaceId, + GURL("https://invisiblefriends.io/api/1817"), + mojom::ProviderError::kSuccess, ""); +} + } // namespace brave_wallet diff --git a/components/brave_wallet/browser/nft_metadata_fetcher.cc b/components/brave_wallet/browser/nft_metadata_fetcher.cc new file mode 100644 index 000000000000..14e321780b7c --- /dev/null +++ b/components/brave_wallet/browser/nft_metadata_fetcher.cc @@ -0,0 +1,312 @@ +/* Copyright 2022 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 http://mozilla.org/MPL/2.0/. */ + +#include "brave/components/brave_wallet/browser/nft_metadata_fetcher.h" + +#include +#include + +#include "base/base64.h" +#include "brave/components/brave_wallet/browser/brave_wallet_utils.h" +#include "brave/components/brave_wallet/browser/eth_data_builder.h" +#include "brave/components/brave_wallet/browser/eth_response_parser.h" +#include "brave/components/brave_wallet/browser/json_rpc_service.h" +#include "brave/components/brave_wallet/browser/solana_data_decoder_utils.h" +#include "brave/components/brave_wallet/common/hex_utils.h" +#include "brave/components/ipfs/buildflags/buildflags.h" +#include "services/network/public/cpp/shared_url_loader_factory.h" +#include "ui/base/l10n/l10n_util.h" + +#if BUILDFLAG(ENABLE_IPFS) +#include "brave/components/ipfs/ipfs_constants.h" +#include "brave/components/ipfs/ipfs_utils.h" +#endif + +namespace { + +net::NetworkTrafficAnnotationTag GetNetworkTrafficAnnotationTag() { + return net::DefineNetworkTrafficAnnotation("json_rpc_service", R"( + semantics { + sender: "NFT Metata Fetcher" + description: + "This service is used to fetch NFT metadata " + "on behalf of the user interacting with the native Brave wallet." + trigger: + "Triggered by uses of the native Brave wallet." + data: + "NFT Metadata JSON." + destination: WEBSITE + } + policy { + cookies_allowed: NO + setting: + "You can enable or disable this feature on chrome://flags." + policy_exception_justification: + "Not implemented." + } + )"); +} + +} // namespace + +namespace brave_wallet { + +NftMetadataFetcher::NftMetadataFetcher( + scoped_refptr url_loader_factory, + JsonRpcService* json_rpc_service, + PrefService* prefs) + : api_request_helper_(new APIRequestHelper(GetNetworkTrafficAnnotationTag(), + url_loader_factory)), + json_rpc_service_(json_rpc_service), + prefs_(prefs), + weak_ptr_factory_(this) {} + +NftMetadataFetcher::~NftMetadataFetcher() = default; + +void NftMetadataFetcher::GetEthTokenMetadata( + const std::string& contract_address, + const std::string& token_id, + const std::string& chain_id, + const std::string& interface_id, + GetEthTokenMetadataCallback callback) { + auto network_url = GetNetworkURL(prefs_, chain_id, mojom::CoinType::ETH); + if (!network_url.is_valid()) { + std::move(callback).Run( + "", mojom::ProviderError::kInvalidParams, + l10n_util::GetStringUTF8(IDS_WALLET_INVALID_PARAMETERS)); + return; + } + + if (!EthAddress::IsValidAddress(contract_address)) { + std::move(callback).Run( + "", mojom::ProviderError::kInvalidParams, + l10n_util::GetStringUTF8(IDS_WALLET_INVALID_PARAMETERS)); + return; + } + + auto internal_callback = + base::BindOnce(&NftMetadataFetcher::OnGetSupportsInterfaceTokenMetadata, + weak_ptr_factory_.GetWeakPtr(), contract_address, + interface_id, token_id, chain_id, std::move(callback)); + + json_rpc_service_->GetSupportsInterface( + contract_address, interface_id, chain_id, std::move(internal_callback)); +} + +void NftMetadataFetcher::OnGetSupportsInterfaceTokenMetadata( + const std::string& contract_address, + const std::string& interface_id, + const std::string& token_id, + const std::string& chain_id, + GetEthTokenMetadataCallback callback, + bool is_supported, + mojom::ProviderError error, + const std::string& error_message) { + if (error != mojom::ProviderError::kSuccess) { + std::move(callback).Run("", error, error_message); + return; + } + + if (!is_supported) { + std::move(callback).Run( + "", mojom::ProviderError::kMethodNotSupported, + l10n_util::GetStringUTF8(IDS_WALLET_METHOD_NOT_SUPPORTED_ERROR)); + return; + } + + auto internal_callback = + base::BindOnce(&NftMetadataFetcher::OnGetTokenUriTokenMetadata, + weak_ptr_factory_.GetWeakPtr(), std::move(callback)); + + json_rpc_service_->GetEthTokenUri(chain_id, contract_address, token_id, + interface_id, std::move(internal_callback)); +} + +void NftMetadataFetcher::OnGetTokenUriTokenMetadata( + GetEthTokenMetadataCallback callback, + const GURL& uri, + mojom::ProviderError error, + const std::string& error_message) { + if (error != mojom::ProviderError::kSuccess) { + std::move(callback).Run("", error, error_message); + return; + } + + if (!uri.is_valid()) { + std::move(callback).Run( + "", mojom::ProviderError::kInternalError, + l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); + return; + } + + auto internal_callback = + base::BindOnce(&NftMetadataFetcher::CompleteGetEthTokenMetadata, + weak_ptr_factory_.GetWeakPtr(), std::move(callback)); + FetchMetadata(uri, std::move(internal_callback)); +} + +void NftMetadataFetcher::FetchMetadata( + GURL url, + GetTokenMetadataIntermediateCallback callback) { + // Obtain JSON from the URL depending on the scheme. + // IPFS, HTTPS, and data URIs are supported. + // IPFS and HTTPS URIs require an additional request to fetch the metadata. + std::string metadata_json; + std::string scheme = url.scheme(); +#if BUILDFLAG(ENABLE_IPFS) + if (scheme != url::kDataScheme && scheme != url::kHttpsScheme && + scheme != ipfs::kIPFSScheme) { +#else + if (scheme != url::kDataScheme && scheme != url::kHttpsScheme) { +#endif + std::move(callback).Run( + "", static_cast(mojom::JsonRpcError::kInternalError), + l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); + return; + } + + if (scheme == url::kDataScheme) { + if (!eth::ParseDataURIAndExtractJSON(url, &metadata_json)) { + std::move(callback).Run( + "", static_cast(mojom::JsonRpcError::kParsingError), + l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); + return; + } + + // Sanitize JSON + data_decoder::JsonSanitizer::Sanitize( + std::move(metadata_json), + base::BindOnce(&NftMetadataFetcher::OnSanitizeTokenMetadata, + weak_ptr_factory_.GetWeakPtr(), std::move(callback))); + return; + } + +#if BUILDFLAG(ENABLE_IPFS) + if (scheme == ipfs::kIPFSScheme && + !ipfs::TranslateIPFSURI(url, &url, ipfs::GetDefaultNFTIPFSGateway(prefs_), + false)) { + std::move(callback).Run( + "", static_cast(mojom::JsonRpcError::kParsingError), + l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); + return; + } +#endif + + auto internal_callback = + base::BindOnce(&NftMetadataFetcher::OnGetTokenMetadataPayload, + weak_ptr_factory_.GetWeakPtr(), std::move(callback)); + api_request_helper_->Request("GET", url, "", "", true, + std::move(internal_callback)); +} + +void NftMetadataFetcher::OnSanitizeTokenMetadata( + GetTokenMetadataIntermediateCallback callback, + data_decoder::JsonSanitizer::Result result) { + if (result.error) { + VLOG(1) << "Data URI JSON validation error:" << *result.error; + std::move(callback).Run( + "", static_cast(mojom::JsonRpcError::kParsingError), + l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); + return; + } + + std::string metadata_json; + if (result.value.has_value()) { + metadata_json = result.value.value(); + } + + std::move(callback).Run(metadata_json, 0, ""); // 0 is kSuccess +} + +void NftMetadataFetcher::OnGetTokenMetadataPayload( + GetTokenMetadataIntermediateCallback callback, + APIRequestResult api_request_result) { + mojom::ProviderErrorUnionPtr error; + if (!api_request_result.Is2XXResponseCode()) { + std::move(callback).Run( + "", static_cast(mojom::JsonRpcError::kInternalError), + l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); + return; + } + + // Invalid JSON becomes an empty string after sanitization + if (api_request_result.body().empty()) { + std::move(callback).Run( + "", static_cast(mojom::JsonRpcError::kParsingError), + l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); + return; + } + + std::move(callback).Run(api_request_result.body(), 0, ""); // 0 is kSuccess +} + +void NftMetadataFetcher::CompleteGetEthTokenMetadata( + GetEthTokenMetadataCallback callback, + const std::string& response, + int error, + const std::string& error_message) { + std::move(callback).Run(response, mojom::ProviderError(error), error_message); +} + +void NftMetadataFetcher::GetSolTokenMetadata( + const std::string& nft_account_address, + GetSolTokenMetadataCallback callback) { + // Derive metadata PDA for the NFT accounts + absl::optional associated_metadata_account = + SolanaKeyring::GetAssociatedMetadataAccount(nft_account_address); + if (!associated_metadata_account) { + std::move(callback).Run( + "", mojom::SolanaProviderError::kInternalError, + l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); + return; + } + + auto internal_callback = + base::BindOnce(&NftMetadataFetcher::OnGetSolanaAccountInfoTokenMetadata, + weak_ptr_factory_.GetWeakPtr(), std::move(callback)); + json_rpc_service_->GetSolanaAccountInfo(*associated_metadata_account, + std::move(internal_callback)); +} + +void NftMetadataFetcher::OnGetSolanaAccountInfoTokenMetadata( + GetSolTokenMetadataCallback callback, + absl::optional account_info, + mojom::SolanaProviderError error, + const std::string& error_message) { + if (error != mojom::SolanaProviderError::kSuccess || !account_info) { + std::move(callback).Run("", std::move(error), error_message); + return; + } + + absl::optional> metadata = + base::Base64Decode((*account_info).data); + if (!metadata) { + std::move(callback).Run("", mojom::SolanaProviderError::kParsingError, + l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); + return; + } + + absl::optional url = DecodeMetadataUri(*metadata); + if (!url || !url.value().is_valid()) { + std::move(callback).Run("", mojom::SolanaProviderError::kParsingError, + l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); + return; + } + + FetchMetadata(*url, base::BindOnce( + &NftMetadataFetcher::CompleteGetSolTokenMetadata, + weak_ptr_factory_.GetWeakPtr(), std::move(callback))); +} + +void NftMetadataFetcher::CompleteGetSolTokenMetadata( + GetSolTokenMetadataCallback callback, + const std::string& response, + int error, + const std::string& error_message) { + std::move(callback).Run(response, mojom::SolanaProviderError(error), + error_message); +} + +} // namespace brave_wallet diff --git a/components/brave_wallet/browser/nft_metadata_fetcher.h b/components/brave_wallet/browser/nft_metadata_fetcher.h new file mode 100644 index 000000000000..233937453c8c --- /dev/null +++ b/components/brave_wallet/browser/nft_metadata_fetcher.h @@ -0,0 +1,106 @@ +/* Copyright 2022 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_NFT_METADATA_FETCHER_H_ +#define BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_NFT_METADATA_FETCHER_H_ + +#include +#include + +#include "base/memory/raw_ptr.h" +#include "base/memory/weak_ptr.h" +#include "brave/components/api_request_helper/api_request_helper.h" +#include "brave/components/brave_wallet/browser/json_rpc_service.h" +#include "services/data_decoder/public/cpp/json_sanitizer.h" + +class PrefService; + +namespace brave_wallet { + +class JsonRpcService; + +class NftMetadataFetcher { + public: + NftMetadataFetcher( + scoped_refptr url_loader_factory, + JsonRpcService* json_rpc_service, + PrefService* prefs); + + NftMetadataFetcher(const NftMetadataFetcher&) = delete; + NftMetadataFetcher& operator=(NftMetadataFetcher&) = delete; + ~NftMetadataFetcher(); + + using APIRequestHelper = api_request_helper::APIRequestHelper; + using APIRequestResult = api_request_helper::APIRequestResult; + using GetTokenMetadataIntermediateCallback = + base::OnceCallback; + using GetEthTokenMetadataCallback = + base::OnceCallback; + using GetSolTokenMetadataCallback = + base::OnceCallback; + void GetEthTokenMetadata(const std::string& contract_address, + const std::string& token_id, + const std::string& chain_id, + const std::string& interface_id, + GetEthTokenMetadataCallback callback); + void GetSolTokenMetadata(const std::string& nft_account_address, + GetSolTokenMetadataCallback callback); + + private: + void OnGetSupportsInterfaceTokenMetadata(const std::string& contract_address, + const std::string& interface_id, + const std::string& token_id, + const std::string& chain_id, + // const GURL& network_url, + GetEthTokenMetadataCallback callback, + bool is_supported, + mojom::ProviderError error, + const std::string& error_message); + + void OnGetTokenUriTokenMetadata(GetEthTokenMetadataCallback callback, + const GURL& uri, + mojom::ProviderError error, + const std::string& error_message); + + void FetchMetadata(GURL url, GetTokenMetadataIntermediateCallback callback); + void OnSanitizeTokenMetadata(GetTokenMetadataIntermediateCallback callback, + data_decoder::JsonSanitizer::Result result); + + void OnGetTokenMetadataPayload(GetTokenMetadataIntermediateCallback callback, + APIRequestResult api_request_result); + + void OnGetSolanaAccountInfoTokenMetadata( + GetSolTokenMetadataCallback callback, + absl::optional account_info, + mojom::SolanaProviderError error, + const std::string& error_message); + void CompleteGetEthTokenMetadata(GetEthTokenMetadataCallback callback, + const std::string& response, + int error, + const std::string& error_message); + + void CompleteGetSolTokenMetadata(GetSolTokenMetadataCallback callback, + const std::string& response, + int error, + const std::string& error_message); + + friend class NftMetadataFetcherUnitTest; + + scoped_refptr url_loader_factory_; + std::unique_ptr api_request_helper_; + raw_ptr json_rpc_service_ = nullptr; + raw_ptr prefs_ = nullptr; + base::WeakPtrFactory weak_ptr_factory_; +}; + +} // namespace brave_wallet + +#endif // BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_NFT_METADATA_FETCHER_H_ diff --git a/components/brave_wallet/browser/nft_metadata_fetcher_unittest.cc b/components/brave_wallet/browser/nft_metadata_fetcher_unittest.cc new file mode 100644 index 000000000000..d896f906013f --- /dev/null +++ b/components/brave_wallet/browser/nft_metadata_fetcher_unittest.cc @@ -0,0 +1,706 @@ +/* Copyright (c) 2022 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 http://mozilla.org/MPL/2.0/. */ + +#include "brave/components/brave_wallet/browser/nft_metadata_fetcher.h" + +#include + +#include "base/test/bind.h" +#include "base/test/task_environment.h" +#include "brave/components/brave_wallet/browser/brave_wallet_prefs.h" +#include "brave/components/brave_wallet/browser/brave_wallet_utils.h" +#include "brave/components/brave_wallet/browser/json_rpc_service.h" +#include "brave/components/brave_wallet/common/hash_utils.h" +// #include "brave/components/brave_wallet/common/hex_utils.h" +#include "components/sync_preferences/testing_pref_service_syncable.h" +#include "services/data_decoder/public/cpp/test_support/in_process_data_decoder.h" +#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h" +#include "services/network/test/test_url_loader_factory.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "ui/base/l10n/l10n_util.h" + +#include "brave/components/ipfs/ipfs_service.h" +// #include "brave/components/ipfs/ipfs_utils.h" +// #include "brave/components/ipfs/pref_names.h" +namespace brave_wallet { + +namespace { + +constexpr char https_metadata_response[] = + R"({"attributes":[{"trait_type":"Feet","value":"Green Shoes"},{"trait_type":"Legs","value":"Tan Pants"},{"trait_type":"Suspenders","value":"White Suspenders"},{"trait_type":"Upper Body","value":"Indigo Turtleneck"},{"trait_type":"Sleeves","value":"Long Sleeves"},{"trait_type":"Hat","value":"Yellow / Blue Pointy Beanie"},{"trait_type":"Eyes","value":"White Nerd Glasses"},{"trait_type":"Mouth","value":"Toothpick"},{"trait_type":"Ears","value":"Bing Bong Stick"},{"trait_type":"Right Arm","value":"Swinging"},{"trait_type":"Left Arm","value":"Diamond Hand"},{"trait_type":"Background","value":"Blue"}],"description":"5,000 animated Invisible Friends hiding in the metaverse. A collection by Markus Magnusson & Random Character Collective.","image":"https://rcc.mypinata.cloud/ipfs/QmXmuSenZRnofhGMz2NyT3Yc4Zrty1TypuiBKDcaBsNw9V/1817.gif","name":"Invisible Friends #1817"})"; + +} // namespace + +class NftMetadataFetcherUnitTest : public testing::Test { + public: + NftMetadataFetcherUnitTest() + : shared_url_loader_factory_( + base::MakeRefCounted( + &url_loader_factory_)) {} + void SetUp() override { + brave_wallet::RegisterProfilePrefs(prefs_.registry()); + ipfs::IpfsService::RegisterProfilePrefs(prefs_.registry()); + json_rpc_service_ = std::make_unique( + shared_url_loader_factory_, &prefs_); + // NftMetadataFetcher nft_metadata_fetcher_(shared_url_loader_factory_, + // json_rpc_service_.get(), GetPrefs()); + nft_metadata_fetcher_ = std::make_unique( + shared_url_loader_factory_, json_rpc_service_.get(), GetPrefs()); + + // nft_metadata_fetcher_ = + // std::make_unique(json_rpc_service_); + // nft_metadata_fetcher_ = NftMetadataFetcher(json_rpc_service_.get()); + } + + PrefService* GetPrefs() { return &prefs_; } + + GURL GetNetwork(const std::string& chain_id, mojom::CoinType coin) { + return brave_wallet::GetNetworkURL(GetPrefs(), chain_id, coin); + } + + void TestFetchMetadata(const GURL& url, + const std::string& expected_response, + int expected_error, + const std::string& expected_error_message) { + base::RunLoop run_loop; + nft_metadata_fetcher_->FetchMetadata( + url, + base::BindLambdaForTesting([&](const std::string& response, int error, + const std::string& error_message) { + EXPECT_EQ(response, expected_response); + EXPECT_EQ(error, expected_error); + EXPECT_EQ(error_message, expected_error_message); + run_loop.Quit(); + })); + run_loop.Run(); + } + + void TestGetEthTokenMetadata(const std::string& contract, + const std::string& token_id, + const std::string& chain_id, + const std::string& interface_id, + const std::string& expected_response, + mojom::ProviderError expected_error, + const std::string& expected_error_message) { + base::RunLoop run_loop; + nft_metadata_fetcher_->GetEthTokenMetadata( + contract, token_id, chain_id, interface_id, + base::BindLambdaForTesting([&](const std::string& response, + mojom::ProviderError error, + const std::string& error_message) { + EXPECT_EQ(response, expected_response); + EXPECT_EQ(error, expected_error); + EXPECT_EQ(error_message, expected_error_message); + run_loop.Quit(); + })); + run_loop.Run(); + } + + void TestGetSolTokenMetadata(const std::string& nft_account_address, + const std::string& expected_response, + mojom::SolanaProviderError expected_error, + const std::string& expected_error_message) { + base::RunLoop loop; + nft_metadata_fetcher_->GetSolTokenMetadata( + nft_account_address, + base::BindLambdaForTesting([&](const std::string& response, + mojom::SolanaProviderError error, + const std::string& error_message) { + EXPECT_EQ(response, expected_response); + EXPECT_EQ(error, expected_error); + EXPECT_EQ(error_message, expected_error_message); + loop.Quit(); + })); + loop.Run(); + } + + void SetInterceptor(const GURL& expected_url, const std::string& content) { + url_loader_factory_.SetInterceptor(base::BindLambdaForTesting( + [&, expected_url, content](const network::ResourceRequest& request) { + EXPECT_EQ(request.url, expected_url); + url_loader_factory_.ClearResponses(); + url_loader_factory_.AddResponse(request.url.spec(), content); + })); + } + + void SetInvalidJsonInterceptor() { + url_loader_factory_.SetInterceptor(base::BindLambdaForTesting( + [&](const network::ResourceRequest& request) { + url_loader_factory_.ClearResponses(); + url_loader_factory_.AddResponse(request.url.spec(), "Answer is 42"); + })); + } + + void SetHTTPRequestTimeoutInterceptor() { + url_loader_factory_.SetInterceptor(base::BindLambdaForTesting( + [&](const network::ResourceRequest& request) { + url_loader_factory_.ClearResponses(); + url_loader_factory_.AddResponse(request.url.spec(), "", + net::HTTP_REQUEST_TIMEOUT); + })); + } + + void SetTokenMetadataInterceptor( + const std::string& interface_id, + const std::string& chain_id, + const std::string& supports_interface_provider_response, + const std::string& token_uri_provider_response = "", + const std::string& metadata_response = "", + net::HttpStatusCode supports_interface_status = net::HTTP_OK, + net::HttpStatusCode token_uri_status = net::HTTP_OK, + net::HttpStatusCode metadata_status = net::HTTP_OK) { + GURL network_url = + GetNetworkURL(GetPrefs(), chain_id, mojom::CoinType::ETH); + ASSERT_TRUE(network_url.is_valid()); + url_loader_factory_.SetInterceptor(base::BindLambdaForTesting( + [&, interface_id, supports_interface_provider_response, + token_uri_provider_response, metadata_response, + supports_interface_status, token_uri_status, metadata_status, + network_url](const network::ResourceRequest& request) { + url_loader_factory_.ClearResponses(); + if (request.method == + "POST") { // An eth_call, either to supportsInterface or tokenURI + base::StringPiece request_string( + request.request_body->elements() + ->at(0) + .As() + .AsStringPiece()); + bool is_supports_interface_req = + request_string.find(GetFunctionHash( + "supportsInterface(bytes4)")) != std::string::npos; + if (is_supports_interface_req) { + ASSERT_NE(request_string.find(interface_id.substr(2)), + std::string::npos); + EXPECT_EQ(request.url.spec(), network_url); + url_loader_factory_.AddResponse( + network_url.spec(), supports_interface_provider_response, + supports_interface_status); + return; + } else { + std::string function_hash; + if (interface_id == kERC721MetadataInterfaceId) { + function_hash = GetFunctionHash("tokenURI(uint256)"); + } else { + function_hash = GetFunctionHash("uri(uint256)"); + } + ASSERT_NE(request_string.find(function_hash), std::string::npos); + url_loader_factory_.AddResponse(network_url.spec(), + token_uri_provider_response, + token_uri_status); + return; + } + } else { // A HTTP GET to fetch the metadata json from the web + url_loader_factory_.AddResponse(request.url.spec(), + metadata_response, metadata_status); + return; + } + })); + } + + void SetSolTokenMetadataInterceptor( + const GURL& expected_rpc_url, + const std::string& get_account_info_response, + const GURL& expected_metadata_url, + const std::string& metadata_response) { + ASSERT_TRUE(expected_rpc_url.is_valid()); + ASSERT_TRUE(expected_metadata_url.is_valid()); + url_loader_factory_.SetInterceptor(base::BindLambdaForTesting( + [&, expected_rpc_url, get_account_info_response, expected_metadata_url, + metadata_response](const network::ResourceRequest& request) { + url_loader_factory_.AddResponse(expected_rpc_url.spec(), + get_account_info_response); + url_loader_factory_.AddResponse(expected_metadata_url.spec(), + metadata_response); + })); + } + + protected: + base::test::TaskEnvironment task_environment_; + sync_preferences::TestingPrefServiceSyncable prefs_; + network::TestURLLoaderFactory url_loader_factory_; + data_decoder::test::InProcessDataDecoder in_process_data_decoder_; + scoped_refptr shared_url_loader_factory_; + std::unique_ptr json_rpc_service_; + // NftMetadataFetcher nft_metadata_fetcher_; + std::unique_ptr nft_metadata_fetcher_; +}; + +TEST_F(NftMetadataFetcherUnitTest, FetchMetadata) { + // Invalid URL yields internal error + TestFetchMetadata(GURL("invalid url"), "", + static_cast(mojom::JsonRpcError::kInternalError), + l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); + + // Unsupported scheme yields internal error + TestFetchMetadata(GURL("file://host/path"), "", + static_cast(mojom::JsonRpcError::kInternalError), + l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); + + // Data URL with unsupported mime type yields parsing error + TestFetchMetadata( + GURL("data:text/" + "csv;base64,eyJpbWFnZV91cmwiOiAgImh0dHBzOi8vZXhhbXBsZS5jb20ifQ=="), + "", static_cast(mojom::JsonRpcError::kParsingError), + l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); + + // Valid URL but that results in HTTP timeout yields internal error + SetHTTPRequestTimeoutInterceptor(); + TestFetchMetadata(GURL("https://example.com"), "", + static_cast(mojom::JsonRpcError::kInternalError), + l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); + + // Valid URL but invalid json response yields parsing error + SetInvalidJsonInterceptor(); + TestFetchMetadata(GURL("https://example.com"), "", + static_cast(mojom::JsonRpcError::kParsingError), + l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); + + // All valid yields response json set via interceptor + GURL url = GURL("https://example.com"); + const std::string& metadata_json = + R"({"image_url":"https://example.com/image.jpg"})"; + SetInterceptor(url, metadata_json); + TestFetchMetadata(url, metadata_json, + static_cast(mojom::ProviderError::kSuccess), ""); +} + +TEST_F(NftMetadataFetcherUnitTest, GetEthTokenMetadata) { + const std::string https_token_uri_response = R"({ + "jsonrpc":"2.0", + "id":1, + "result":"0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002468747470733a2f2f696e76697369626c65667269656e64732e696f2f6170692f3138313700000000000000000000000000000000000000000000000000000000" + })"; + const std::string http_token_uri_response = R"({ + "jsonrpc":"2.0", + "id":1, + "result":"0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020687474703a2f2f696e76697369626c65667269656e64732e696f2f6170692f31" + })"; + const std::string data_token_uri_response = R"({ + "jsonrpc":"2.0", + "id":1, + "result": "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000135646174613a6170706c69636174696f6e2f6a736f6e3b6261736536342c65794a686448527961574a316447567a496a6f69496977695a47567a59334a7063485270623234694f694a4f623234675a6e56755a326c696247556762476c7662694973496d6c745957646c496a6f695a474630595470706257466e5a53397a646d6372654731734f324a68633255324e43785153453479576e6c434e474a586548566a656a4270595568534d474e4562335a4d4d32517a5a486b314d3031354e585a6a62574e3254577042643031444f58706b62574e7053556861634670595a454e694d326335535770425a3031445154464e5245466e546c524264306c714e44686a5230597759554e436131425453576c4d656a513454444e4f4d6c70364e4430694c434a755957316c496a6f69546b5a4d496e303d0000000000000000000000" + })"; + const std::string data_token_uri_response_invalid_json = R"({ + "jsonrpc":"2.0", + "id":1, + "result":"0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000085646174613a6170706c69636174696f6e2f6a736f6e3b6261736536342c65794a755957316c496a6f69546b5a4d49697767496d526c63324e796158423061573975496a6f69546d397549475a31626d6470596d786c49477870623234694c43416959585230636d6c696458526c637949364969497349434a706257466e5a5349364969493d000000000000000000000000000000000000000000000000000000" + })"; + const std::string data_token_uri_response_empty_string = R"({ + "jsonrpc":"2.0", + "id":1, + "result":"0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001d646174613a6170706c69636174696f6e2f6a736f6e3b6261736536342c000000" + })"; + const std::string interface_supported_response = R"({ + "jsonrpc":"2.0", + "id":1, + "result": "0x0000000000000000000000000000000000000000000000000000000000000001" + })"; + const std::string exceeds_limit_json = R"({ + "jsonrpc":"2.0", + "id":1, + "error": { + "code":-32005, + "message": "Request exceeds defined limit" + } + })"; + const std::string interface_not_supported_response = R"({ + "jsonrpc":"2.0", + "id":1, + "result":"0x0000000000000000000000000000000000000000000000000000000000000000" + })"; + const std::string invalid_json = + "It might make sense just to get some in case it catches on"; + const std::string ipfs_token_uri_response = R"({ + "jsonrpc":"2.0", + "id":1, + "result":"0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003a697066733a2f2f516d65536a53696e4870506e6d586d73704d6a776958794e367a533445397a63636172694752336a7863615774712f31383137000000000000" + })"; + const std::string ipfs_metadata_response = + R"({"attributes":[{"trait_type":"Mouth","value":"Bored Cigarette"},{"trait_type":"Fur","value":"Gray"},{"trait_type":"Background","value":"Aquamarine"},{"trait_type":"Clothes","value":"Tuxedo Tee"},{"trait_type":"Hat","value":"Bayc Hat Black"},{"trait_type":"Eyes","value":"Coins"}],"image":"ipfs://QmQ82uDT3JyUMsoZuaFBYuEucF654CYE5ktPUrnA5d4VDH"})"; + + // Invalid inputs + // (1/3) Invalid contract address + TestGetEthTokenMetadata( + "", "0x1", mojom::kMainnetChainId, kERC721MetadataInterfaceId, "", + mojom::ProviderError::kInvalidParams, + l10n_util::GetStringUTF8(IDS_WALLET_INVALID_PARAMETERS)); + + // (2/3) Invalid token ID + SetTokenMetadataInterceptor( + kERC721MetadataInterfaceId, mojom::kMainnetChainId, + interface_supported_response, https_token_uri_response, + https_metadata_response); + TestGetEthTokenMetadata( + "0x06012c8cf97BEaD5deAe237070F9587f8E7A266d", "", mojom::kMainnetChainId, + kERC721MetadataInterfaceId, "", mojom::ProviderError::kInvalidParams, + l10n_util::GetStringUTF8(IDS_WALLET_INVALID_PARAMETERS)); + + // (3/3) Invalid chain ID + TestGetEthTokenMetadata( + "0x06012c8cf97BEaD5deAe237070F9587f8E7A266d", "0x1", "", + kERC721MetadataInterfaceId, "", mojom::ProviderError::kInvalidParams, + l10n_util::GetStringUTF8(IDS_WALLET_INVALID_PARAMETERS)); + + // Mismatched + // (4/4) Unknown interfaceID + SetTokenMetadataInterceptor( + kERC721MetadataInterfaceId, mojom::kMainnetChainId, + interface_supported_response, https_token_uri_response, + https_metadata_response); + TestGetEthTokenMetadata( + "0x06012c8cf97BEaD5deAe237070F9587f8E7A266d", "0x1", + mojom::kMainnetChainId, "invalid interface", "", + mojom::ProviderError::kInvalidParams, + l10n_util::GetStringUTF8(IDS_WALLET_INVALID_PARAMETERS)); + + // Valid inputs + // (1/3) HTTP URI + SetTokenMetadataInterceptor( + kERC721MetadataInterfaceId, mojom::kMainnetChainId, + interface_supported_response, https_token_uri_response, + https_metadata_response); + TestGetEthTokenMetadata("0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", + mojom::kMainnetChainId, kERC721MetadataInterfaceId, + https_metadata_response, + mojom::ProviderError::kSuccess, ""); + + // (2/3) IPFS URI + SetTokenMetadataInterceptor(kERC721MetadataInterfaceId, + mojom::kLocalhostChainId, + interface_supported_response, + ipfs_token_uri_response, ipfs_metadata_response); + TestGetEthTokenMetadata("0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d", "0x719", + mojom::kLocalhostChainId, kERC721MetadataInterfaceId, + ipfs_metadata_response, + mojom::ProviderError::kSuccess, ""); + + // (3/3) Data URI + SetTokenMetadataInterceptor( + kERC721MetadataInterfaceId, mojom::kMainnetChainId, + interface_supported_response, data_token_uri_response); + TestGetEthTokenMetadata( + "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d", "0x719", + mojom::kMainnetChainId, kERC721MetadataInterfaceId, + R"({"attributes":"","description":"Non fungible lion","image":"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MDAgNTAwIj48cGF0aCBkPSIiLz48L3N2Zz4=","name":"NFL"})", + mojom::ProviderError::kSuccess, ""); + + // Invalid supportsInterface response + // (1/4) Timeout + SetTokenMetadataInterceptor( + kERC721MetadataInterfaceId, mojom::kMainnetChainId, + interface_supported_response, https_token_uri_response, "", + net::HTTP_REQUEST_TIMEOUT); + TestGetEthTokenMetadata("0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d", "0x719", + mojom::kMainnetChainId, kERC721MetadataInterfaceId, + "", mojom::ProviderError::kInternalError, + l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); + + // (2/4) Invalid JSON + SetTokenMetadataInterceptor(kERC721MetadataInterfaceId, + mojom::kMainnetChainId, invalid_json); + TestGetEthTokenMetadata("0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d", "0x719", + mojom::kMainnetChainId, kERC721MetadataInterfaceId, + "", mojom::ProviderError::kParsingError, + l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); + + // (3/4) Request exceeds provider limit + SetTokenMetadataInterceptor(kERC721MetadataInterfaceId, + mojom::kMainnetChainId, exceeds_limit_json); + TestGetEthTokenMetadata("0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d", "0x719", + mojom::kMainnetChainId, kERC721MetadataInterfaceId, + "", mojom::ProviderError::kLimitExceeded, + "Request exceeds defined limit"); + + // (4/4) Interface not supported + SetTokenMetadataInterceptor(kERC721MetadataInterfaceId, + mojom::kMainnetChainId, + interface_not_supported_response); + TestGetEthTokenMetadata( + "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d", "0x719", + mojom::kMainnetChainId, kERC721MetadataInterfaceId, "", + mojom::ProviderError::kMethodNotSupported, + l10n_util::GetStringUTF8(IDS_WALLET_METHOD_NOT_SUPPORTED_ERROR)); + + // Invalid tokenURI response (6 total) + // (1/6) Timeout + SetTokenMetadataInterceptor( + kERC721MetadataInterfaceId, mojom::kMainnetChainId, + interface_supported_response, https_token_uri_response, "", net::HTTP_OK, + net::HTTP_REQUEST_TIMEOUT); + TestGetEthTokenMetadata("0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", + mojom::kMainnetChainId, kERC721MetadataInterfaceId, + "", mojom::ProviderError::kInternalError, + l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); + + // (2/6) Invalid Provider JSON + SetTokenMetadataInterceptor(kERC721MetadataInterfaceId, + mojom::kMainnetChainId, + interface_supported_response, invalid_json); + TestGetEthTokenMetadata("0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", + mojom::kMainnetChainId, kERC721MetadataInterfaceId, + "", mojom::ProviderError::kParsingError, + l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); + + // (3/6) Invalid JSON in data URI + SetTokenMetadataInterceptor( + kERC721MetadataInterfaceId, mojom::kMainnetChainId, + interface_supported_response, data_token_uri_response_invalid_json); + TestGetEthTokenMetadata("0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", + mojom::kMainnetChainId, kERC721MetadataInterfaceId, + "", mojom::ProviderError::kParsingError, + l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); + + // (4/6) Empty string as JSON in data URI + SetTokenMetadataInterceptor( + kERC721MetadataInterfaceId, mojom::kMainnetChainId, + interface_supported_response, data_token_uri_response_empty_string); + TestGetEthTokenMetadata("0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", + mojom::kMainnetChainId, kERC721MetadataInterfaceId, + "", mojom::ProviderError::kParsingError, + l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); + + // (5/6) Request exceeds limit + SetTokenMetadataInterceptor(kERC721MetadataInterfaceId, + mojom::kMainnetChainId, + interface_supported_response, exceeds_limit_json); + TestGetEthTokenMetadata("0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", + mojom::kMainnetChainId, kERC721MetadataInterfaceId, + "", mojom::ProviderError::kLimitExceeded, + "Request exceeds defined limit"); + + // (6/6) URI scheme is not suported (HTTP) + SetTokenMetadataInterceptor( + kERC721MetadataInterfaceId, mojom::kMainnetChainId, + interface_supported_response, http_token_uri_response); + TestGetEthTokenMetadata("0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", + mojom::kMainnetChainId, kERC721MetadataInterfaceId, + "", mojom::ProviderError::kInternalError, + l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); + + // Invalid metadata response (2 total) + // (1/2) Timeout + SetTokenMetadataInterceptor( + kERC721MetadataInterfaceId, mojom::kMainnetChainId, + interface_supported_response, https_token_uri_response, + https_metadata_response, net::HTTP_OK, net::HTTP_OK, + net::HTTP_REQUEST_TIMEOUT); + TestGetEthTokenMetadata("0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", + mojom::kMainnetChainId, kERC721MetadataInterfaceId, + "", mojom::ProviderError::kInternalError, + l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); + + // (2/2) Invalid JSON + SetTokenMetadataInterceptor( + kERC721MetadataInterfaceId, mojom::kMainnetChainId, + interface_supported_response, ipfs_token_uri_response, invalid_json); + TestGetEthTokenMetadata("0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", + mojom::kMainnetChainId, kERC721MetadataInterfaceId, + "", mojom::ProviderError::kParsingError, + l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); + + // ERC1155 + SetTokenMetadataInterceptor( + kERC1155MetadataInterfaceId, mojom::kMainnetChainId, + interface_supported_response, https_token_uri_response, + https_metadata_response); + TestGetEthTokenMetadata("0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", + mojom::kMainnetChainId, kERC1155MetadataInterfaceId, + https_metadata_response, + mojom::ProviderError::kSuccess, ""); +} + +TEST_F(NftMetadataFetcherUnitTest, GetSolTokenMetadata) { + // Valid inputs should yield metadata JSON (happy case) + std::string get_account_info_response = R"({ + "jsonrpc": "2.0", + "result": { + "context": { + "apiVersion": "1.13.3", + "slot": 161038284 + }, + "value": { + "data": [ + "BGUN5hJf2zSue3S0I/fCq16UREt5NxP6mQdaq4cdGPs3Q8PG/R6KFUSgce78Nwk9Frvkd9bMbvTIKCRSDy88nZQgAAAAU1BFQ0lBTCBTQVVDRQAAAAAAAAAAAAAAAAAAAAAAAAAKAAAAAAAAAAAAAAAAAMgAAABodHRwczovL2JhZmtyZWlmNHd4NTR3anI3cGdmdWczd2xhdHIzbmZudHNmd25ndjZldXNlYmJxdWV6cnhlbmo2Y2s0LmlwZnMuZHdlYi5saW5rP2V4dD0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOgDAQIAAABlDeYSX9s0rnt0tCP3wqtelERLeTcT+pkHWquHHRj7NwFiDUmu+U8sXOOZQXL36xmknL+Zzd/z3uw2G0ERMo8Eth4BAgABAf8BAAEBoivvbAzLh2kD2cSu6IQIqGQDGeoh/UEDizyp6mLT1tUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "base64" + ], + "executable": false, + "lamports": 5616720, + "owner": "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s", + "rentEpoch": 361 + } + }, + "id": 1 + })"; + const std::string valid_metadata_response = + R"({"attributes":[{"trait_type":"hair","value":"green & blue"},{"trait_type":"pontus","value":"no"}],"description":"","external_url":"","image":"https://bafkreiagsgqhjudpta6trhjuv5y2n2exsrhbkkprl64tvg2mftjsdm3vgi.ipfs.dweb.link?ext=png","name":"SPECIAL SAUCE","properties":{"category":"image","creators":[{"address":"7oUUEdptZnZVhSet4qobU9PtpPfiNUEJ8ftPnrC6YEaa","share":98},{"address":"tsU33UT3K2JTfLgHUo7hdzRhRe4wth885cqVbM8WLiq","share":2}],"files":[{"type":"image/png","uri":"https://bafkreiagsgqhjudpta6trhjuv5y2n2exsrhbkkprl64tvg2mftjsdm3vgi.ipfs.dweb.link?ext=png"}],"maxSupply":0},"seller_fee_basis_points":1000,"symbol":""})"; + auto network_url = GetNetwork(mojom::kSolanaMainnet, mojom::CoinType::SOL); + SetSolTokenMetadataInterceptor( + network_url, get_account_info_response, + GURL("https://" + "bafkreif4wx54wjr7pgfug3wlatr3nfntsfwngv6eusebbquezrxenj6ck4.ipfs." + "dweb.link/?ext="), + valid_metadata_response); + TestGetSolTokenMetadata("5ZXToo7froykjvjnpHtTLYr9u2tW3USMwPg3sNkiaQVh", + valid_metadata_response, + mojom::SolanaProviderError::kSuccess, ""); + + // Invalid nft_account_address yields internal error. + SetSolTokenMetadataInterceptor( + network_url, get_account_info_response, + GURL("https://" + "bafkreif4wx54wjr7pgfug3wlatr3nfntsfwngv6eusebbquezrxenj6ck4.ipfs." + "dweb.link/?ext="), + valid_metadata_response); + TestGetSolTokenMetadata("Invalid", "", + mojom::SolanaProviderError::kInternalError, + l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); + + // Non 200 getAccountInfo response of yields internal server error. + SetHTTPRequestTimeoutInterceptor(); + TestGetSolTokenMetadata("5ZXToo7froykjvjnpHtTLYr9u2tW3USMwPg3sNkiaQVh", "", + mojom::SolanaProviderError::kInternalError, + l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); + + // Invalid getAccountInfo response JSON yields internal error + SetSolTokenMetadataInterceptor( + network_url, "Invalid json response", + GURL("https://" + "bafkreif4wx54wjr7pgfug3wlatr3nfntsfwngv6eusebbquezrxenj6ck4.ipfs." + "dweb.link/?ext="), + valid_metadata_response); + TestGetSolTokenMetadata("5ZXToo7froykjvjnpHtTLYr9u2tW3USMwPg3sNkiaQVh", "", + mojom::SolanaProviderError::kInternalError, + l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); + + // Valid response JSON, invalid account info (missing result.value.owner + // field) info yields parse error + get_account_info_response = R"({ + "jsonrpc": "2.0", + "result": { + "context": { + "apiVersion": "1.13.3", + "slot": 161038284 + }, + "value": { + "data": [ + "BGUN5hJf2zSue3S0I/fCq16UREt5NxP6mQdaq4cdGPs3Q8PG/R6KFUSgce78Nwk9Frvkd9bMbvTIKCRSDy88nZQgAAAAU1BFQ0lBTCBTQVVDRQAAAAAAAAAAAAAAAAAAAAAAAAAKAAAAAAAAAAAAAAAAAMgAAABodHRwczovL2JhZmtyZWlmNHd4NTR3anI3cGdmdWczd2xhdHIzbmZudHNmd25ndjZldXNlYmJxdWV6cnhlbmo2Y2s0LmlwZnMuZHdlYi5saW5rP2V4dD0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOgDAQIAAABlDeYSX9s0rnt0tCP3wqtelERLeTcT+pkHWquHHRj7NwFiDUmu+U8sXOOZQXL36xmknL+Zzd/z3uw2G0ERMo8Eth4BAgABAf8BAAEBoivvbAzLh2kD2cSu6IQIqGQDGeoh/UEDizyp6mLT1tUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "base64" + ], + "executable": false, + "lamports": 5616720, + "rentEpoch": 361 + } + }, + "id": 1 + })"; + SetSolTokenMetadataInterceptor( + network_url, get_account_info_response, + GURL("https://" + "bafkreif4wx54wjr7pgfug3wlatr3nfntsfwngv6eusebbquezrxenj6ck4.ipfs." + "dweb.link/?ext="), + valid_metadata_response); + TestGetSolTokenMetadata("5ZXToo7froykjvjnpHtTLYr9u2tW3USMwPg3sNkiaQVh", "", + mojom::SolanaProviderError::kParsingError, + l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); + + // Valid response JSON, parsable account info, but invalid account info data + // (invalid base64) yields parse error + get_account_info_response = R"({ + "jsonrpc": "2.0", + "result": { + "context": { + "apiVersion": "1.13.3", + "slot": 161038284 + }, + "value": { + "data": [ + "*Invalid Base64*", + "base64" + ], + "executable": false, + "lamports": 5616720, + "owner": "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s", + "rentEpoch": 361 + } + }, + "id": 1 + })"; + SetSolTokenMetadataInterceptor( + network_url, get_account_info_response, + GURL("https://" + "bafkreif4wx54wjr7pgfug3wlatr3nfntsfwngv6eusebbquezrxenj6ck4.ipfs." + "dweb.link/?ext="), + valid_metadata_response); + TestGetSolTokenMetadata("5ZXToo7froykjvjnpHtTLYr9u2tW3USMwPg3sNkiaQVh", "", + mojom::SolanaProviderError::kParsingError, + l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); + + // Valid response JSON, parsable account info, invalid account info data + // (valid base64, but invalid borsh encoded metadata) yields parse error + get_account_info_response = R"({ + "jsonrpc": "2.0", + "result": { + "context": { + "apiVersion": "1.13.3", + "slot": 161038284 + }, + "value": { + "data": [ + "d2hvb3BzIQ==", + "base64" + ], + "executable": false, + "lamports": 5616720, + "owner": "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s", + "rentEpoch": 361 + } + }, + "id": 1 + })"; + SetSolTokenMetadataInterceptor( + network_url, get_account_info_response, + GURL("https://" + "bafkreif4wx54wjr7pgfug3wlatr3nfntsfwngv6eusebbquezrxenj6ck4.ipfs." + "dweb.link/?ext="), + valid_metadata_response); + TestGetSolTokenMetadata("5ZXToo7froykjvjnpHtTLYr9u2tW3USMwPg3sNkiaQVh", "", + mojom::SolanaProviderError::kParsingError, + l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); + + // Valid response JSON, parsable account info, invalid account info data + // (valid base64, valid borsh encoding, but when decoded the URI is not a + // valid URI) + get_account_info_response = R"({ + "jsonrpc": "2.0", + "result": { + "context": { + "apiVersion": "1.13.3", + "slot": 161038284 + }, + "value": { + "data": [ + "BGUN5hJf2zSue3S0I/fCq16UREt5NxP6mQdaq4cdGPs3Q8PG/R6KFUSgce78Nwk9Frvkd9bMbvTIKCRSDy88nZQgAAAAU1BFQ0lBTCBTQVVDRQAAAAAAAAAAAAAAAAAAAAAAAAAKAAAAAAAAAAAAAAAAAAsAAABpbnZhbGlkIHVybOgDAQIAAABlDeYSX9s0rnt0tCP3wqtelERLeTcT+pkHWquHHRj7NwFiDUmu+U8sXOOZQXL36xmknL+Zzd/z3uw2G0ERMo8Eth4BAgABAf8BAAEBoivvbAzLh2kD2cSu6IQIqGQDGeoh/UEDizyp6mLT1tUA", + "base64" + ], + "executable": false, + "lamports": 5616720, + "owner": "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s", + "rentEpoch": 361 + } + }, + "id": 1 + })"; + SetSolTokenMetadataInterceptor( + network_url, get_account_info_response, + GURL("https://" + "bafkreif4wx54wjr7pgfug3wlatr3nfntsfwngv6eusebbquezrxenj6ck4.ipfs." + "dweb.link/?ext="), + valid_metadata_response); + TestGetSolTokenMetadata("5ZXToo7froykjvjnpHtTLYr9u2tW3USMwPg3sNkiaQVh", "", + mojom::SolanaProviderError::kParsingError, + l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); +} + +} // namespace brave_wallet diff --git a/components/brave_wallet/browser/test/BUILD.gn b/components/brave_wallet/browser/test/BUILD.gn index 460ee1d2830e..b8e04f2ae035 100644 --- a/components/brave_wallet/browser/test/BUILD.gn +++ b/components/brave_wallet/browser/test/BUILD.gn @@ -45,6 +45,7 @@ source_set("brave_wallet_unit_tests") { "//brave/components/brave_wallet/browser/json_rpc_response_parser_unittest.cc", "//brave/components/brave_wallet/browser/json_rpc_service_test_utils_unittest.cc", "//brave/components/brave_wallet/browser/json_rpc_service_unittest.cc", + "//brave/components/brave_wallet/browser/nft_metadata_fetcher_unittest.cc", "//brave/components/brave_wallet/browser/password_encryptor_unittest.cc", "//brave/components/brave_wallet/browser/permission_utils_unittest.cc", "//brave/components/brave_wallet/browser/rlp_decode_unittest.cc", From 6306ecc0e4d4873a4204e084567bd1bf78c90f7b Mon Sep 17 00:00:00 2001 From: nvonpentz <12549658+nvonpentz@users.noreply.github.com> Date: Fri, 2 Dec 2022 11:29:08 -0500 Subject: [PATCH 12/17] Address feedback for DecodeMetadataUri (borsh) * Revert "Address feedback: Separate some decode functions from solana_instruction_decoder to new solana_data_decoder_utils" (af24b3082ccb45feb34f78070290d438b1374af8). * Guards against out of bounds access in DecodeMetadataUri and add a test for that case * Do not use reinterpret_cast --- components/brave_wallet/browser/BUILD.gn | 2 - .../brave_wallet/browser/json_rpc_service.cc | 1 + .../browser/nft_metadata_fetcher.cc | 67 +++++- .../browser/nft_metadata_fetcher.h | 8 + .../browser/nft_metadata_fetcher_unittest.cc | 79 +++++++ .../browser/solana_data_decoder_utils.cc | 204 ----------------- .../browser/solana_data_decoder_utils.h | 56 ----- .../solana_data_decoder_utils_unittest.cc | 63 ------ .../solana_instruction_data_decoder.cc | 214 +++++++++++++++--- ...olana_instruction_data_decoder_unittest.cc | 1 + components/brave_wallet/browser/test/BUILD.gn | 1 - 11 files changed, 333 insertions(+), 363 deletions(-) delete mode 100644 components/brave_wallet/browser/solana_data_decoder_utils.cc delete mode 100644 components/brave_wallet/browser/solana_data_decoder_utils.h delete mode 100644 components/brave_wallet/browser/solana_data_decoder_utils_unittest.cc diff --git a/components/brave_wallet/browser/BUILD.gn b/components/brave_wallet/browser/BUILD.gn index cddbf5b3c837..0a7ef84b0e28 100644 --- a/components/brave_wallet/browser/BUILD.gn +++ b/components/brave_wallet/browser/BUILD.gn @@ -103,8 +103,6 @@ static_library("browser") { "solana_account_meta.h", "solana_block_tracker.cc", "solana_block_tracker.h", - "solana_data_decoder_utils.cc", - "solana_data_decoder_utils.h", "solana_instruction.cc", "solana_instruction.h", "solana_instruction_builder.cc", diff --git a/components/brave_wallet/browser/json_rpc_service.cc b/components/brave_wallet/browser/json_rpc_service.cc index 9e2fa83cd6d7..37e67381216c 100644 --- a/components/brave_wallet/browser/json_rpc_service.cc +++ b/components/brave_wallet/browser/json_rpc_service.cc @@ -31,6 +31,7 @@ #include "brave/components/brave_wallet/browser/json_rpc_requests_helper.h" #include "brave/components/brave_wallet/browser/json_rpc_response_parser.h" #include "brave/components/brave_wallet/browser/pref_names.h" +#include "brave/components/brave_wallet/browser/solana_instruction_data_decoder.h" #include "brave/components/brave_wallet/browser/solana_keyring.h" #include "brave/components/brave_wallet/browser/solana_requests.h" #include "brave/components/brave_wallet/browser/solana_response_parser.h" diff --git a/components/brave_wallet/browser/nft_metadata_fetcher.cc b/components/brave_wallet/browser/nft_metadata_fetcher.cc index 14e321780b7c..6c7740869107 100644 --- a/components/brave_wallet/browser/nft_metadata_fetcher.cc +++ b/components/brave_wallet/browser/nft_metadata_fetcher.cc @@ -13,7 +13,6 @@ #include "brave/components/brave_wallet/browser/eth_data_builder.h" #include "brave/components/brave_wallet/browser/eth_response_parser.h" #include "brave/components/brave_wallet/browser/json_rpc_service.h" -#include "brave/components/brave_wallet/browser/solana_data_decoder_utils.h" #include "brave/components/brave_wallet/common/hex_utils.h" #include "brave/components/ipfs/buildflags/buildflags.h" #include "services/network/public/cpp/shared_url_loader_factory.h" @@ -309,4 +308,70 @@ void NftMetadataFetcher::CompleteGetSolTokenMetadata( error_message); } +// static +absl::optional NftMetadataFetcher::DecodeUint32( + const std::vector& input, + size_t& offset) { + if (offset >= input.size() || input.size() - offset < sizeof(uint32_t)) { + return absl::nullopt; + } + + // Read bytes in little endian order. + base::span s = + base::make_span(input.begin() + offset, sizeof(uint32_t)); + uint32_t uint32_le = *reinterpret_cast(s.data()); + + offset += sizeof(uint32_t); + +#if defined(ARCH_CPU_LITTLE_ENDIAN) + return uint32_le; +#else + return base::ByteSwap(uint32_le); +#endif +} + +// static +// Expects a the bytes of a Borsh encoded Metadata struct (see +// https://docs.rs/spl-token-metadata/latest/spl_token_metadata/state/struct.Metadata.html) +// and returns the URI string in of the nested Data struct (see +// https://docs.rs/spl-token-metadata/latest/spl_token_metadata/state/struct.Data.html) +// as a GURL. +absl::optional NftMetadataFetcher::DecodeMetadataUri( + const std::vector data) { + size_t offset = 0; + offset = offset + /* Skip first byte for metadata.key */ 1 + + /* Skip next 32 bytes for `metadata.update_authority` */ 32 + + /* Skip next 32 bytes for `metadata.mint` */ 32; + + // Skip next field, metdata.data.name, a string + // whose length is represented by a leading 32 bit integer + auto length = DecodeUint32(data, offset); + if (!length) { + return absl::nullopt; + } + offset += static_cast(*length); + + // Skip next field, `metdata.data.symbol`, a string + // whose length is represented by a leading 32 bit integer + length = DecodeUint32(data, offset); + if (!length) { + return absl::nullopt; + } + offset += static_cast(*length); + + // Parse next field, metadata.data.uri, a string + length = DecodeUint32(data, offset); + if (!length) { + return absl::nullopt; + } + + // Prevent out of bounds access in case length value incorrent + if (data.size() <= offset + *length) { + return absl::nullopt; + } + std::string uri = + std::string(data.begin() + offset, data.begin() + offset + *length); + return GURL(uri); +} + } // namespace brave_wallet diff --git a/components/brave_wallet/browser/nft_metadata_fetcher.h b/components/brave_wallet/browser/nft_metadata_fetcher.h index 233937453c8c..6cc8b410f731 100644 --- a/components/brave_wallet/browser/nft_metadata_fetcher.h +++ b/components/brave_wallet/browser/nft_metadata_fetcher.h @@ -8,6 +8,7 @@ #include #include +#include #include "base/memory/raw_ptr.h" #include "base/memory/weak_ptr.h" @@ -93,6 +94,13 @@ class NftMetadataFetcher { const std::string& error_message); friend class NftMetadataFetcherUnitTest; + FRIEND_TEST_ALL_PREFIXES(NftMetadataFetcherUnitTest, DecodeMetadataUri); + + static absl::optional DecodeUint32( + const std::vector& input, + size_t& offset); + static absl::optional DecodeMetadataUri( + const std::vector data); scoped_refptr url_loader_factory_; std::unique_ptr api_request_helper_; diff --git a/components/brave_wallet/browser/nft_metadata_fetcher_unittest.cc b/components/brave_wallet/browser/nft_metadata_fetcher_unittest.cc index d896f906013f..c7fdb3778512 100644 --- a/components/brave_wallet/browser/nft_metadata_fetcher_unittest.cc +++ b/components/brave_wallet/browser/nft_metadata_fetcher_unittest.cc @@ -7,6 +7,7 @@ #include +#include "base/base64.h" #include "base/test/bind.h" #include "base/test/task_environment.h" #include "brave/components/brave_wallet/browser/brave_wallet_prefs.h" @@ -703,4 +704,82 @@ TEST_F(NftMetadataFetcherUnitTest, GetSolTokenMetadata) { l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); } +TEST_F(NftMetadataFetcherUnitTest, DecodeMetadataUri) { + // Valid borsh encoding and URI yields expected URI + std::vector uri_borsh_encoded = { + 4, 101, 13, 230, 18, 95, 219, 52, 174, 123, 116, 180, 35, 247, 194, 171, + 94, 148, 68, 75, 121, 55, 19, 250, 153, 7, 90, 171, 135, 29, 24, 251, 55, + 67, 195, 198, 253, 30, 138, 21, 68, 160, 113, 238, 252, 55, 9, 61, 22, + 187, 228, 119, 214, 204, 110, 244, 200, 40, 36, 82, 15, 47, 60, 157, 148, + 32, 0, 0, 0, 83, 80, 69, 67, 73, 65, 76, 32, 83, 65, 85, 67, 69, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, // the next four bytes encode length of the URI string + // (200) + 200, 0, 0, 0, 104, 116, 116, 112, 115, 58, 47, 47, 98, 97, 102, 107, 114, + 101, 105, 102, 52, 119, 120, 53, 52, 119, 106, 114, 55, 112, 103, 102, + 117, 103, 51, 119, 108, 97, 116, 114, 51, 110, 102, 110, 116, 115, 102, + 119, 110, 103, 118, 54, 101, 117, 115, 101, 98, 98, 113, 117, 101, 122, + 114, 120, 101, 110, 106, 54, 99, 107, 52, 46, 105, 112, 102, 115, 46, 100, + 119, 101, 98, 46, 108, 105, 110, 107, 63, 101, 120, 116, 61, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 232, 3, 0, 0, 1, 1, 255, 1, 0, 1, 1, 162, 43, + 239, 108, 12, 203, 135, 105, 3, 217, 196, 174, 232, 132, 8, 168, 100, 3, + 25, 234, 33, 253, 65, 3, 139, 60, 169, 234, 98, 211, 214, 213, 0}; + auto uri = nft_metadata_fetcher_->DecodeMetadataUri(uri_borsh_encoded); + ASSERT_TRUE(uri); + EXPECT_EQ(uri.value().spec(), + "https://" + "bafkreif4wx54wjr7pgfug3wlatr3nfntsfwngv6eusebbquezrxenj6ck4.ipfs." + "dweb.link/?ext="); + + // Invalid borsh encoding due to incorrect claimed length of metadata URI + // string (too large) fails to decode (out of bounds check) + uri_borsh_encoded = { + 4, 101, 13, 230, 18, 95, 219, 52, 174, 123, 116, 180, 35, 247, 194, 171, + 94, 148, 68, 75, 121, 55, 19, 250, 153, 7, 90, 171, 135, 29, 24, 251, 55, + 67, 195, 198, 253, 30, 138, 21, 68, 160, 113, 238, 252, 55, 9, 61, 22, + 187, 228, 119, 214, 204, 110, 244, 200, 40, 36, 82, 15, 47, 60, 157, 148, + 32, 0, 0, 0, 83, 80, 69, 67, 73, 65, 76, 32, 83, 65, 85, 67, 69, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + // next four bytes encode the URI of the string, which have been overrided + // to be incorrect (too large) + 255, 255, 255, 0, 104, 116, 116, 112, 115, 58, 47, 47, 98, 97, 102, 107, + 114, 101, 105, 102, 52, 119, 120, 53, 52, 119, 106, 114, 55, 112, 103, + 102, 117, 103, 51, 119, 108, 97, 116, 114, 51, 110, 102, 110, 116, 115, + 102, 119, 110, 103, 118, 54, 101, 117, 115, 101, 98, 98, 113, 117, 101, + 122, 114, 120, 101, 110, 106, 54, 99, 107, 52, 46, 105, 112, 102, 115, 46, + 100, 119, 101, 98, 46, 108, 105, 110, 107, 63, 101, 120, 116, 61, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 232, 3, 0, 0, 1, 1, 255, 1, 0, 1, 1, 162, + 43, 239, 108, 12, 203, 135, 105, 3, 217, 196, 174, 232, 132, 8, 168, 100, + 3, 25, 234, 33, 253, 65, 3, 139, 60, 169, 234, 98, 211, 214, 213, 0}; + uri = nft_metadata_fetcher_->DecodeMetadataUri(uri_borsh_encoded); + ASSERT_FALSE(uri); + + // Valid borsh encoding, but invalid URI is parsed but yields empty URI + auto uri_borsh_encoded2 = base::Base64Decode( + "BGUN5hJf2zSue3S0I/fCq16UREt5NxP6mQdaq4cdGPs3Q8PG/" + "R6KFUSgce78Nwk9Frvkd9bMbvTIKCRSDy88nZQgAAAAU1BFQ0lBTCBTQVVDRQAAAAAAAAAAA" + "AAAAAAAAAAAAAAKAAAAAAAAAAAAAAAAAAsAAABpbnZhbGlkIHVybOgDAQIAAABlDeYSX9s0r" + "nt0tCP3wqtelERLeTcT+pkHWquHHRj7NwFiDUmu+U8sXOOZQXL36xmknL+Zzd/" + "z3uw2G0ERMo8Eth4BAgABAf8BAAEBoivvbAzLh2kD2cSu6IQIqGQDGeoh/" + "UEDizyp6mLT1tUA"); + ASSERT_TRUE(uri_borsh_encoded2); + uri = nft_metadata_fetcher_->DecodeMetadataUri(*uri_borsh_encoded2); + ASSERT_TRUE(uri); + EXPECT_EQ(uri.value().spec(), ""); + + // Invalid borsh encoding is not parsed + uri_borsh_encoded2 = base::Base64Decode("d2hvb3BzIQ=="); + ASSERT_TRUE(uri_borsh_encoded2); + ASSERT_FALSE(nft_metadata_fetcher_->DecodeMetadataUri(*uri_borsh_encoded2)); +} + } // namespace brave_wallet diff --git a/components/brave_wallet/browser/solana_data_decoder_utils.cc b/components/brave_wallet/browser/solana_data_decoder_utils.cc deleted file mode 100644 index aab7a1349b40..000000000000 --- a/components/brave_wallet/browser/solana_data_decoder_utils.cc +++ /dev/null @@ -1,204 +0,0 @@ -/* Copyright (c) 2022 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 http://mozilla.org/MPL/2.0/. */ - -#include "brave/components/brave_wallet/browser/solana_data_decoder_utils.h" - -#include -#include - -#include "base/containers/flat_map.h" -#include "base/containers/span.h" -#include "base/no_destructor.h" -#include "base/notreached.h" -#include "base/sys_byteorder.h" -#include "brave/components/brave_wallet/common/brave_wallet_constants.h" -#include "brave/components/brave_wallet/common/solana_utils.h" -#include "build/build_config.h" -#include "components/grit/brave_components_strings.h" -#include "ui/base/l10n/l10n_util.h" - -namespace brave_wallet { - -constexpr uint8_t kAuthorityTypeMax = 3; -constexpr size_t kMaxStringSize32Bit = 4294967291u; - -absl::optional DecodeUint8(const std::vector& input, - size_t& offset) { - if (offset >= input.size() || input.size() - offset < sizeof(uint8_t)) { - return absl::nullopt; - } - - offset += sizeof(uint8_t); - return input[offset - sizeof(uint8_t)]; -} - -absl::optional DecodeUint8String(const std::vector& input, - size_t& offset) { - auto ret = DecodeUint8(input, offset); - if (!ret) - return absl::nullopt; - return base::NumberToString(*ret); -} - -absl::optional DecodeAuthorityTypeString( - const std::vector& input, - size_t& offset) { - auto ret = DecodeUint8(input, offset); - if (ret && *ret <= kAuthorityTypeMax) - return base::NumberToString(*ret); - return absl::nullopt; -} - -absl::optional DecodeUint32(const std::vector& input, - size_t& offset) { - if (offset >= input.size() || input.size() - offset < sizeof(uint32_t)) { - return absl::nullopt; - } - - // Read bytes in little endian order. - base::span s = - base::make_span(input.begin() + offset, sizeof(uint32_t)); - uint32_t uint32_le = *reinterpret_cast(s.data()); - - offset += sizeof(uint32_t); - -#if defined(ARCH_CPU_LITTLE_ENDIAN) - return uint32_le; -#else - return base::ByteSwap(uint32_le); -#endif -} - -absl::optional DecodeUint32String( - const std::vector& input, - size_t& offset) { - auto ret = DecodeUint32(input, offset); - if (!ret) - return absl::nullopt; - return base::NumberToString(*ret); -} - -absl::optional DecodeUint64(const std::vector& input, - size_t& offset) { - if (offset >= input.size() || input.size() - offset < sizeof(uint64_t)) { - return absl::nullopt; - } - - // Read bytes in little endian order. - base::span s = - base::make_span(input.begin() + offset, sizeof(uint64_t)); - uint64_t uint64_le = *reinterpret_cast(s.data()); - - offset += sizeof(uint64_t); - -#if defined(ARCH_CPU_LITTLE_ENDIAN) - return uint64_le; -#else - return base::ByteSwap(uint64_le); -#endif -} - -absl::optional DecodeUint64String( - const std::vector& input, - size_t& offset) { - auto ret = DecodeUint64(input, offset); - if (!ret) - return absl::nullopt; - return base::NumberToString(*ret); -} - -absl::optional DecodePublicKey(const std::vector& input, - size_t& offset) { - if (offset >= input.size() || input.size() - offset < kSolanaPubkeySize) - return absl::nullopt; - - offset += kSolanaPubkeySize; - return Base58Encode(std::vector( - input.begin() + offset - kSolanaPubkeySize, input.begin() + offset)); -} - -absl::optional DecodeOptionalPublicKey( - const std::vector& input, - size_t& offset) { - if (offset == input.size()) { - return absl::nullopt; - } - - // First byte is 0 or 1 to indicate if public key is passed. - // And the rest bytes are the actual public key. - if (input[offset] == 0) { - offset++; - return ""; // No public key is passed. - } else if (input[offset] == 1) { - offset++; - return DecodePublicKey(input, offset); - } else { - return absl::nullopt; - } -} - -// bincode::serialize uses two u32 together for the string length and a byte -// array for the actual strings. The first u32 represents the lower bytes of -// the length, the second represents the upper bytes. The upper bytes will have -// non-zero value only when the length exceeds the maximum of u32. -// We currently cap the length here to be the max size of std::string -// on 32 bit systems, it's safe to do so because currently we don't expect any -// valid cases would have strings larger than it. -absl::optional DecodeString(const std::vector& input, - size_t& offset) { - auto len_lower = DecodeUint32(input, offset); - if (!len_lower || *len_lower > kMaxStringSize32Bit) - return absl::nullopt; - auto len_upper = DecodeUint32(input, offset); - if (!len_upper || *len_upper != 0) { // Non-zero means len exceeds u32 max. - return absl::nullopt; - } - - if (offset + *len_lower > input.size()) - return absl::nullopt; - - offset += *len_lower; - return std::string(reinterpret_cast(&input[offset - *len_lower]), - *len_lower); -} - -// Expects a the bytes of a Borsh encoded Metadata struct (see -// https://docs.rs/spl-token-metadata/latest/spl_token_metadata/state/struct.Metadata.html) -// and returns the URI string in of the nested Data struct (see -// https://docs.rs/spl-token-metadata/latest/spl_token_metadata/state/struct.Data.html) -// as a GURL. -absl::optional DecodeMetadataUri(const std::vector data) { - size_t offset = 0; - offset = offset + /* Skip first byte for metadata.key */ 1 + - /* Skip next 32 bytes for `metadata.update_authority` */ 32 + - /* Skip next 32 bytes for `metadata.mint` */ 32; - - // Skip next field, metdata.data.name, a string - // whose length is represented by a leading 32 bit integer - auto length = DecodeUint32(data, offset); - if (!length) { - return absl::nullopt; - } - offset += static_cast(*length); - - // Skip next field, `metdata.data.symbol`, a string - // whose length is represented by a leading 32 bit integer - length = DecodeUint32(data, offset); - if (!length) { - return absl::nullopt; - } - offset += static_cast(*length); - - // Parse next field, metadata.data.uri, a string - length = DecodeUint32(data, offset); - if (!length) { - return absl::nullopt; - } - std::string uri = - std::string(reinterpret_cast(&data[offset]), *length); - return GURL(uri); -} - -} // namespace brave_wallet diff --git a/components/brave_wallet/browser/solana_data_decoder_utils.h b/components/brave_wallet/browser/solana_data_decoder_utils.h deleted file mode 100644 index 017d216dc195..000000000000 --- a/components/brave_wallet/browser/solana_data_decoder_utils.h +++ /dev/null @@ -1,56 +0,0 @@ -/* Copyright (c) 2022 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 http://mozilla.org/MPL/2.0/. */ - -#ifndef BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_SOLANA_DATA_DECODER_UTILS_H_ -#define BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_SOLANA_DATA_DECODER_UTILS_H_ - -#include -#include - -#include "brave/components/brave_wallet/browser/solana_instruction_decoded_data.h" -#include "brave/components/brave_wallet/common/brave_wallet.mojom.h" -#include "third_party/abseil-cpp/absl/types/optional.h" - -namespace brave_wallet { - -absl::optional DecodeUint8(const std::vector& input, - size_t& offset); -absl::optional DecodeUint8String(const std::vector& input, - size_t& offset); -absl::optional DecodeAuthorityTypeString( - const std::vector& input, - size_t& offset); - -absl::optional DecodeUint32(const std::vector& input, - size_t& offset); - -absl::optional DecodeUint32String( - const std::vector& input, - size_t& offset); - -absl::optional DecodeMetadataUri(const std::vector data); - -absl::optional DecodeUint64(const std::vector& input, - size_t& offset); - -absl::optional DecodeUint64String( - const std::vector& input, - size_t& offset); - -absl::optional DecodePublicKey(const std::vector& input, - size_t& offset); - -absl::optional DecodeOptionalPublicKey( - const std::vector& input, - size_t& offset); - -absl::optional DecodeString(const std::vector& input, - size_t& offset); - -absl::optional DecodeMetadataUri(const std::vector data); - -} // namespace brave_wallet - -#endif // BRAVE_COMPONENTS_BRAVE_WALLET_BROWSER_SOLANA_DATA_DECODER_UTILS_H_ diff --git a/components/brave_wallet/browser/solana_data_decoder_utils_unittest.cc b/components/brave_wallet/browser/solana_data_decoder_utils_unittest.cc deleted file mode 100644 index 243ed6b204e4..000000000000 --- a/components/brave_wallet/browser/solana_data_decoder_utils_unittest.cc +++ /dev/null @@ -1,63 +0,0 @@ -/* Copyright (c) 2022 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 http://mozilla.org/MPL/2.0/. */ - -#include "brave/components/brave_wallet/browser/solana_data_decoder_utils.h" - -#include "base/base64.h" -#include "testing/gtest/include/gtest/gtest.h" - -namespace brave_wallet { - -class SolanaDataDecoderUtilsTest : public testing::Test { - public: - SolanaDataDecoderUtilsTest() = default; - ~SolanaDataDecoderUtilsTest() override = default; -}; - -TEST_F(SolanaDataDecoderUtilsTest, DecodeMetadataUri) { - // Valid borsh encoding and URI yields expected URI - auto uri_encoded = base::Base64Decode( - "BGUN5hJf2zSue3S0I/fCq16UREt5NxP6mQdaq4cdGPs3Q8PG/" - "R6KFUSgce78Nwk9Frvkd9bMbvTIKCRSDy88nZQgAAAAU1BFQ0lBTCBTQVVDRQAAAAAAAAAAA" - "AAAAAAAAAAAAAAKAAAAAAAAAAAAAAAAAMgAAABodHRwczovL2JhZmtyZWlmNHd4NTR3anI3c" - "GdmdWczd2xhdHIzbmZudHNmd25ndjZldXNlYmJxdWV6cnhlbmo2Y2s0LmlwZnMuZHdlYi5sa" - "W5rP2V4dD0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - "AAAAAAAAAAAAAAAAOgDAQIAAABlDeYSX9s0rnt0tCP3wqtelERLeTcT+" - "pkHWquHHRj7NwFiDUmu+U8sXOOZQXL36xmknL+Zzd/" - "z3uw2G0ERMo8Eth4BAgABAf8BAAEBoivvbAzLh2kD2cSu6IQIqGQDGeoh/" - "UEDizyp6mLT1tUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="); - ASSERT_TRUE(uri_encoded); - auto uri = DecodeMetadataUri(*uri_encoded); - ASSERT_TRUE(uri); - EXPECT_EQ(uri.value().spec(), - "https://" - "bafkreif4wx54wjr7pgfug3wlatr3nfntsfwngv6eusebbquezrxenj6ck4.ipfs." - "dweb.link/?ext="); - - // Valid borsh encoding, but invalid URI is parsed but yields empty URI - uri_encoded = base::Base64Decode( - "BGUN5hJf2zSue3S0I/fCq16UREt5NxP6mQdaq4cdGPs3Q8PG/" - "R6KFUSgce78Nwk9Frvkd9bMbvTIKCRSDy88nZQgAAAAU1BFQ0lBTCBTQVVDRQAAAAAAAAAAA" - "AAAAAAAAAAAAAAKAAAAAAAAAAAAAAAAAAsAAABpbnZhbGlkIHVybOgDAQIAAABlDeYSX9s0r" - "nt0tCP3wqtelERLeTcT+pkHWquHHRj7NwFiDUmu+U8sXOOZQXL36xmknL+Zzd/" - "z3uw2G0ERMo8Eth4BAgABAf8BAAEBoivvbAzLh2kD2cSu6IQIqGQDGeoh/" - "UEDizyp6mLT1tUA"); - ASSERT_TRUE(uri_encoded); - uri = DecodeMetadataUri(*uri_encoded); - ASSERT_TRUE(uri); - EXPECT_EQ(uri.value().spec(), ""); - - // Invalid borsh encoding is not parsed - uri_encoded = base::Base64Decode("d2hvb3BzIQ=="); - ASSERT_TRUE(uri_encoded); - ASSERT_FALSE(DecodeMetadataUri(*uri_encoded)); -} - -} // namespace brave_wallet diff --git a/components/brave_wallet/browser/solana_instruction_data_decoder.cc b/components/brave_wallet/browser/solana_instruction_data_decoder.cc index 0d0ee424564f..3e67e6c7d4f1 100644 --- a/components/brave_wallet/browser/solana_instruction_data_decoder.cc +++ b/components/brave_wallet/browser/solana_instruction_data_decoder.cc @@ -13,7 +13,6 @@ #include "base/no_destructor.h" #include "base/notreached.h" #include "base/sys_byteorder.h" -#include "brave/components/brave_wallet/browser/solana_data_decoder_utils.h" #include "brave/components/brave_wallet/common/brave_wallet_constants.h" #include "brave/components/brave_wallet/common/solana_utils.h" #include "build/build_config.h" @@ -34,6 +33,9 @@ enum class ParamTypes { kAuthorityType, }; +constexpr uint8_t kAuthorityTypeMax = 3; +constexpr size_t kMaxStringSize32Bit = 4294967291u; + // Tuple of param name, localized name, and type. using ParamNameTypeTuple = std::tuple; @@ -597,53 +599,145 @@ GetTokenInstructionAccountParams() { return *params; } -absl::optional DecodeSystemInstructionType( - const std::vector& data, +absl::optional DecodeUint8(const std::vector& input, + size_t& offset) { + if (offset >= input.size() || input.size() - offset < sizeof(uint8_t)) { + return absl::nullopt; + } + + offset += sizeof(uint8_t); + return input[offset - sizeof(uint8_t)]; +} + +absl::optional DecodeUint8String(const std::vector& input, + size_t& offset) { + auto ret = DecodeUint8(input, offset); + if (!ret) + return absl::nullopt; + return base::NumberToString(*ret); +} + +absl::optional DecodeAuthorityTypeString( + const std::vector& input, size_t& offset) { - auto ins_type = DecodeUint32(data, offset); - if (!ins_type || *ins_type > static_cast( - mojom::SolanaSystemInstruction::kMaxValue)) + auto ret = DecodeUint8(input, offset); + if (ret && *ret <= kAuthorityTypeMax) + return base::NumberToString(*ret); + return absl::nullopt; +} + +absl::optional DecodeUint32(const std::vector& input, + size_t& offset) { + if (offset >= input.size() || input.size() - offset < sizeof(uint32_t)) { return absl::nullopt; - return static_cast(*ins_type); + } + + // Read bytes in little endian order. + base::span s = + base::make_span(input.begin() + offset, sizeof(uint32_t)); + uint32_t uint32_le = *reinterpret_cast(s.data()); + + offset += sizeof(uint32_t); + +#if defined(ARCH_CPU_LITTLE_ENDIAN) + return uint32_le; +#else + return base::ByteSwap(uint32_le); +#endif } -absl::optional DecodeTokenInstructionType( - const std::vector& data, +absl::optional DecodeUint32String( + const std::vector& input, size_t& offset) { - auto ins_type = DecodeUint8(data, offset); - if (!ins_type || *ins_type > static_cast( - mojom::SolanaTokenInstruction::kMaxValue)) + auto ret = DecodeUint32(input, offset); + if (!ret) return absl::nullopt; - return static_cast(*ins_type); + return base::NumberToString(*ret); } -const std::vector* DecodeInstructionType( - const std::string& program_id, - const std::vector& data, - size_t& offset, - SolanaInstructionDecodedData& decoded_data) { - if (program_id == mojom::kSolanaSystemProgramId) { - if (auto ins_type = DecodeSystemInstructionType(data, offset)) { - auto* ret = &GetSystemInstructionParams().at(*ins_type); - decoded_data.sys_ins_type = std::move(ins_type); - decoded_data.account_params = - GetSystemInstructionAccountParams().at(*ins_type); - return ret; - } - } else if (program_id == mojom::kSolanaTokenProgramId) { - if (auto ins_type = DecodeTokenInstructionType(data, offset)) { - auto* ret = &GetTokenInstructionParams().at(*ins_type); - decoded_data.token_ins_type = std::move(ins_type); - decoded_data.account_params = - GetTokenInstructionAccountParams().at(*ins_type); - return ret; - } +absl::optional DecodeUint64(const std::vector& input, + size_t& offset) { + if (offset >= input.size() || input.size() - offset < sizeof(uint64_t)) { + return absl::nullopt; } - return nullptr; + // Read bytes in little endian order. + base::span s = + base::make_span(input.begin() + offset, sizeof(uint64_t)); + uint64_t uint64_le = *reinterpret_cast(s.data()); + + offset += sizeof(uint64_t); + +#if defined(ARCH_CPU_LITTLE_ENDIAN) + return uint64_le; +#else + return base::ByteSwap(uint64_le); +#endif } -} // namespace +absl::optional DecodeUint64String( + const std::vector& input, + size_t& offset) { + auto ret = DecodeUint64(input, offset); + if (!ret) + return absl::nullopt; + return base::NumberToString(*ret); +} + +absl::optional DecodePublicKey(const std::vector& input, + size_t& offset) { + if (offset >= input.size() || input.size() - offset < kSolanaPubkeySize) + return absl::nullopt; + + offset += kSolanaPubkeySize; + return Base58Encode(std::vector( + input.begin() + offset - kSolanaPubkeySize, input.begin() + offset)); +} + +absl::optional DecodeOptionalPublicKey( + const std::vector& input, + size_t& offset) { + if (offset == input.size()) { + return absl::nullopt; + } + + // First byte is 0 or 1 to indicate if public key is passed. + // And the rest bytes are the actual public key. + if (input[offset] == 0) { + offset++; + return ""; // No public key is passed. + } else if (input[offset] == 1) { + offset++; + return DecodePublicKey(input, offset); + } else { + return absl::nullopt; + } +} + +// bincode::serialize uses two u32 together for the string length and a byte +// array for the actual strings. The first u32 represents the lower bytes of +// the length, the second represents the upper bytes. The upper bytes will have +// non-zero value only when the length exceeds the maximum of u32. +// We currently cap the length here to be the max size of std::string +// on 32 bit systems, it's safe to do so because currently we don't expect any +// valid cases would have strings larger than it. +absl::optional DecodeString(const std::vector& input, + size_t& offset) { + auto len_lower = DecodeUint32(input, offset); + if (!len_lower || *len_lower > kMaxStringSize32Bit) + return absl::nullopt; + auto len_upper = DecodeUint32(input, offset); + if (!len_upper || *len_upper != 0) { // Non-zero means len exceeds u32 max. + return absl::nullopt; + } + + if (offset + *len_lower > input.size()) + return absl::nullopt; + + offset += *len_lower; + return std::string(reinterpret_cast(&input[offset - *len_lower]), + *len_lower); +} bool DecodeParamType(const ParamNameTypeTuple& name_type_tuple, const std::vector data, @@ -689,6 +783,54 @@ bool DecodeParamType(const ParamNameTypeTuple& name_type_tuple, return true; } +absl::optional DecodeSystemInstructionType( + const std::vector& data, + size_t& offset) { + auto ins_type = DecodeUint32(data, offset); + if (!ins_type || *ins_type > static_cast( + mojom::SolanaSystemInstruction::kMaxValue)) + return absl::nullopt; + return static_cast(*ins_type); +} + +absl::optional DecodeTokenInstructionType( + const std::vector& data, + size_t& offset) { + auto ins_type = DecodeUint8(data, offset); + if (!ins_type || *ins_type > static_cast( + mojom::SolanaTokenInstruction::kMaxValue)) + return absl::nullopt; + return static_cast(*ins_type); +} + +const std::vector* DecodeInstructionType( + const std::string& program_id, + const std::vector& data, + size_t& offset, + SolanaInstructionDecodedData& decoded_data) { + if (program_id == mojom::kSolanaSystemProgramId) { + if (auto ins_type = DecodeSystemInstructionType(data, offset)) { + auto* ret = &GetSystemInstructionParams().at(*ins_type); + decoded_data.sys_ins_type = std::move(ins_type); + decoded_data.account_params = + GetSystemInstructionAccountParams().at(*ins_type); + return ret; + } + } else if (program_id == mojom::kSolanaTokenProgramId) { + if (auto ins_type = DecodeTokenInstructionType(data, offset)) { + auto* ret = &GetTokenInstructionParams().at(*ins_type); + decoded_data.token_ins_type = std::move(ins_type); + decoded_data.account_params = + GetTokenInstructionAccountParams().at(*ins_type); + return ret; + } + } + + return nullptr; +} + +} // namespace + absl::optional Decode( const std::vector& data, const std::string& program_id) { diff --git a/components/brave_wallet/browser/solana_instruction_data_decoder_unittest.cc b/components/brave_wallet/browser/solana_instruction_data_decoder_unittest.cc index c243578cde36..aeb6eb18f421 100644 --- a/components/brave_wallet/browser/solana_instruction_data_decoder_unittest.cc +++ b/components/brave_wallet/browser/solana_instruction_data_decoder_unittest.cc @@ -9,6 +9,7 @@ #include #include +#include "base/base64.h" #include "brave/components/brave_wallet/common/brave_wallet.mojom.h" #include "brave/components/brave_wallet/common/brave_wallet_constants.h" #include "testing/gtest/include/gtest/gtest.h" diff --git a/components/brave_wallet/browser/test/BUILD.gn b/components/brave_wallet/browser/test/BUILD.gn index b8e04f2ae035..28aa45b91de4 100644 --- a/components/brave_wallet/browser/test/BUILD.gn +++ b/components/brave_wallet/browser/test/BUILD.gn @@ -53,7 +53,6 @@ source_set("brave_wallet_unit_tests") { "//brave/components/brave_wallet/browser/sns_resolver_task_unittest.cc", "//brave/components/brave_wallet/browser/solana_account_meta_unittest.cc", "//brave/components/brave_wallet/browser/solana_block_tracker_unittest.cc", - "//brave/components/brave_wallet/browser/solana_data_decoder_utils_unittest.cc", "//brave/components/brave_wallet/browser/solana_instruction_builder_unittest.cc", "//brave/components/brave_wallet/browser/solana_instruction_data_decoder_unittest.cc", "//brave/components/brave_wallet/browser/solana_instruction_decoded_data_unittest.cc", From b8b318a91da3ffa867f9cf3378e6dfb57cce53f7 Mon Sep 17 00:00:00 2001 From: nvonpentz <12549658+nvonpentz@users.noreply.github.com> Date: Fri, 2 Dec 2022 14:01:49 -0500 Subject: [PATCH 13/17] Misc. fixes from final pass before review * Remove unused solana_instruction_data_decoder.h heade in json_rpc_service.cc * Rename functions - JsonRpcService::OnGetTokenUri2 -> JsonRpcService::OnGetEthTokenUri - NftMetadataFetcher::OnGetTokenUriTokenMetadata -> NftMetadataFetcher::JsonRpcService::OnGetEthTokenUri - NftMetadataFetcher::OnGetSupportsInterfaceTokenMetadata -> NftMetadataFetcher::OnGetSupportsInterface * Reorganize functions in nft_metadata_fetcher.h * Remove VLOG(0) debug line in json_rpc_service_unittest.cc * Revert change to SetInterceptor() in json_rpc_service_unittest.cc that is no longer relevant * Add comment explaining GetTokenMetadataIntermediateCallback purpose * Remove commented code in nft_metadata_fetcher_unittest.cc * Remove unused import added to solana_instruction_data_decoder_unittest.cc * Resolve TODO comment by adding missing GetEthTokenUri test case --- .../brave_wallet/browser/json_rpc_service.cc | 7 ++- .../brave_wallet/browser/json_rpc_service.h | 4 +- .../browser/json_rpc_service_unittest.cc | 21 +++++--- .../browser/nft_metadata_fetcher.cc | 17 +++---- .../browser/nft_metadata_fetcher.h | 50 +++++++++---------- .../browser/nft_metadata_fetcher_unittest.cc | 11 +--- ...olana_instruction_data_decoder_unittest.cc | 1 - 7 files changed, 51 insertions(+), 60 deletions(-) diff --git a/components/brave_wallet/browser/json_rpc_service.cc b/components/brave_wallet/browser/json_rpc_service.cc index 37e67381216c..bb846cc866d6 100644 --- a/components/brave_wallet/browser/json_rpc_service.cc +++ b/components/brave_wallet/browser/json_rpc_service.cc @@ -31,7 +31,6 @@ #include "brave/components/brave_wallet/browser/json_rpc_requests_helper.h" #include "brave/components/brave_wallet/browser/json_rpc_response_parser.h" #include "brave/components/brave_wallet/browser/pref_names.h" -#include "brave/components/brave_wallet/browser/solana_instruction_data_decoder.h" #include "brave/components/brave_wallet/browser/solana_keyring.h" #include "brave/components/brave_wallet/browser/solana_requests.h" #include "brave/components/brave_wallet/browser/solana_response_parser.h" @@ -2026,7 +2025,7 @@ void JsonRpcService::GetEthTokenUri(const std::string& chain_id, } auto internal_callback = - base::BindOnce(&JsonRpcService::OnGetTokenUri2, + base::BindOnce(&JsonRpcService::OnGetEthTokenUri, weak_ptr_factory_.GetWeakPtr(), std::move(callback)); RequestInternal(eth::eth_call("", contract_address, "", "", "", @@ -2034,8 +2033,8 @@ void JsonRpcService::GetEthTokenUri(const std::string& chain_id, true, network_url, std::move(internal_callback)); } -void JsonRpcService::OnGetTokenUri2(GetEthTokenUriCallback callback, - APIRequestResult api_request_result) { +void JsonRpcService::OnGetEthTokenUri(GetEthTokenUriCallback callback, + APIRequestResult api_request_result) { if (!api_request_result.Is2XXResponseCode()) { std::move(callback).Run( GURL(), mojom::ProviderError::kInternalError, diff --git a/components/brave_wallet/browser/json_rpc_service.h b/components/brave_wallet/browser/json_rpc_service.h index cfa07175b218..185d1e572c62 100644 --- a/components/brave_wallet/browser/json_rpc_service.h +++ b/components/brave_wallet/browser/json_rpc_service.h @@ -527,8 +527,8 @@ class JsonRpcService : public KeyedService, public mojom::JsonRpcService { const std::string& owner_address, mojom::ProviderError error, const std::string& error_message); - void OnGetTokenUri2(GetEthTokenUriCallback callback, - const APIRequestResult api_request_result); + void OnGetEthTokenUri(GetEthTokenUriCallback callback, + const APIRequestResult api_request_result); void OnGetSupportsInterface(GetSupportsInterfaceCallback callback, APIRequestResult api_request_result); diff --git a/components/brave_wallet/browser/json_rpc_service_unittest.cc b/components/brave_wallet/browser/json_rpc_service_unittest.cc index a3d8a6ca1682..7dd5603d5c21 100644 --- a/components/brave_wallet/browser/json_rpc_service_unittest.cc +++ b/components/brave_wallet/browser/json_rpc_service_unittest.cc @@ -964,11 +964,8 @@ class JsonRpcServiceUnitTest : public testing::Test { !expected_cache_header.empty()); EXPECT_EQ(expected_cache_header, header_value); } - if (!expected_method.empty()) { - EXPECT_TRUE( - request.headers.GetHeader("x-brave-key", &header_value)); - EXPECT_EQ(BUILDFLAG(BRAVE_SERVICES_KEY), header_value); - } + EXPECT_TRUE(request.headers.GetHeader("x-brave-key", &header_value)); + EXPECT_EQ(BUILDFLAG(BRAVE_SERVICES_KEY), header_value); url_loader_factory_.ClearResponses(); url_loader_factory_.AddResponse(request.url.spec(), content); })); @@ -1328,7 +1325,6 @@ class JsonRpcServiceUnitTest : public testing::Test { mojom::SolanaProviderError expected_error, const std::string& expected_error_message) { base::RunLoop run_loop; - VLOG(0) << "TestGetSolanaAccountInfo 0"; json_rpc_service_->GetSolanaAccountInfo( "vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg", base::BindLambdaForTesting( @@ -5892,8 +5888,17 @@ TEST_F(JsonRpcServiceUnitTest, GetEthTokenUri) { mojom::ProviderError::kParsingError, l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); - // Valid inputs, valid provider JSON, invalid URI - // TODO(nvonpentz) + // Valid inputs, valid RPC response JSON, valid RLP encoding, invalid URI + SetInterceptor(GetNetwork(mojom::kMainnetChainId, mojom::CoinType::ETH), + "eth_call", "", R"({ + "jsonrpc":"2.0", + "id":1, + "result":"0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000b696e76616c69642075726c000000000000000000000000000000000000000000" + })"); + TestGetEthTokenUri("0x59468516a8259058bad1ca5f8f4bff190d30e066", "0x719", + mojom::kMainnetChainId, kERC721MetadataInterfaceId, GURL(), + mojom::ProviderError::kParsingError, + l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); // All valid SetInterceptor(GetNetwork(mojom::kMainnetChainId, mojom::CoinType::ETH), diff --git a/components/brave_wallet/browser/nft_metadata_fetcher.cc b/components/brave_wallet/browser/nft_metadata_fetcher.cc index 6c7740869107..1c757106dad0 100644 --- a/components/brave_wallet/browser/nft_metadata_fetcher.cc +++ b/components/brave_wallet/browser/nft_metadata_fetcher.cc @@ -86,7 +86,7 @@ void NftMetadataFetcher::GetEthTokenMetadata( } auto internal_callback = - base::BindOnce(&NftMetadataFetcher::OnGetSupportsInterfaceTokenMetadata, + base::BindOnce(&NftMetadataFetcher::OnGetSupportsInterface, weak_ptr_factory_.GetWeakPtr(), contract_address, interface_id, token_id, chain_id, std::move(callback)); @@ -94,7 +94,7 @@ void NftMetadataFetcher::GetEthTokenMetadata( contract_address, interface_id, chain_id, std::move(internal_callback)); } -void NftMetadataFetcher::OnGetSupportsInterfaceTokenMetadata( +void NftMetadataFetcher::OnGetSupportsInterface( const std::string& contract_address, const std::string& interface_id, const std::string& token_id, @@ -116,18 +116,17 @@ void NftMetadataFetcher::OnGetSupportsInterfaceTokenMetadata( } auto internal_callback = - base::BindOnce(&NftMetadataFetcher::OnGetTokenUriTokenMetadata, + base::BindOnce(&NftMetadataFetcher::OnGetEthTokenUri, weak_ptr_factory_.GetWeakPtr(), std::move(callback)); json_rpc_service_->GetEthTokenUri(chain_id, contract_address, token_id, interface_id, std::move(internal_callback)); } -void NftMetadataFetcher::OnGetTokenUriTokenMetadata( - GetEthTokenMetadataCallback callback, - const GURL& uri, - mojom::ProviderError error, - const std::string& error_message) { +void NftMetadataFetcher::OnGetEthTokenUri(GetEthTokenMetadataCallback callback, + const GURL& uri, + mojom::ProviderError error, + const std::string& error_message) { if (error != mojom::ProviderError::kSuccess) { std::move(callback).Run("", error, error_message); return; @@ -365,7 +364,7 @@ absl::optional NftMetadataFetcher::DecodeMetadataUri( return absl::nullopt; } - // Prevent out of bounds access in case length value incorrent + // Prevent out of bounds access in case length value incorrent if (data.size() <= offset + *length) { return absl::nullopt; } diff --git a/components/brave_wallet/browser/nft_metadata_fetcher.h b/components/brave_wallet/browser/nft_metadata_fetcher.h index 6cc8b410f731..136d20fcf451 100644 --- a/components/brave_wallet/browser/nft_metadata_fetcher.h +++ b/components/brave_wallet/browser/nft_metadata_fetcher.h @@ -35,49 +35,48 @@ class NftMetadataFetcher { using APIRequestHelper = api_request_helper::APIRequestHelper; using APIRequestResult = api_request_helper::APIRequestResult; - using GetTokenMetadataIntermediateCallback = - base::OnceCallback; using GetEthTokenMetadataCallback = base::OnceCallback; - using GetSolTokenMetadataCallback = - base::OnceCallback; void GetEthTokenMetadata(const std::string& contract_address, const std::string& token_id, const std::string& chain_id, const std::string& interface_id, GetEthTokenMetadataCallback callback); + using GetSolTokenMetadataCallback = + base::OnceCallback; void GetSolTokenMetadata(const std::string& nft_account_address, GetSolTokenMetadataCallback callback); private: - void OnGetSupportsInterfaceTokenMetadata(const std::string& contract_address, - const std::string& interface_id, - const std::string& token_id, - const std::string& chain_id, - // const GURL& network_url, - GetEthTokenMetadataCallback callback, - bool is_supported, - mojom::ProviderError error, - const std::string& error_message); - - void OnGetTokenUriTokenMetadata(GetEthTokenMetadataCallback callback, - const GURL& uri, - mojom::ProviderError error, - const std::string& error_message); - + void OnGetSupportsInterface(const std::string& contract_address, + const std::string& interface_id, + const std::string& token_id, + const std::string& chain_id, + // const GURL& network_url, + GetEthTokenMetadataCallback callback, + bool is_supported, + mojom::ProviderError error, + const std::string& error_message); + + void OnGetEthTokenUri(GetEthTokenMetadataCallback callback, + const GURL& uri, + mojom::ProviderError error, + const std::string& error_message); + // GetTokenMetadataIntermediateCallbacks convert the int error to a + // mojom::ProviderError or mojom::SolanaProviderError + using GetTokenMetadataIntermediateCallback = + base::OnceCallback; void FetchMetadata(GURL url, GetTokenMetadataIntermediateCallback callback); void OnSanitizeTokenMetadata(GetTokenMetadataIntermediateCallback callback, data_decoder::JsonSanitizer::Result result); - void OnGetTokenMetadataPayload(GetTokenMetadataIntermediateCallback callback, APIRequestResult api_request_result); - void OnGetSolanaAccountInfoTokenMetadata( GetSolTokenMetadataCallback callback, absl::optional account_info, @@ -87,7 +86,6 @@ class NftMetadataFetcher { const std::string& response, int error, const std::string& error_message); - void CompleteGetSolTokenMetadata(GetSolTokenMetadataCallback callback, const std::string& response, int error, diff --git a/components/brave_wallet/browser/nft_metadata_fetcher_unittest.cc b/components/brave_wallet/browser/nft_metadata_fetcher_unittest.cc index c7fdb3778512..6140f68a0b7f 100644 --- a/components/brave_wallet/browser/nft_metadata_fetcher_unittest.cc +++ b/components/brave_wallet/browser/nft_metadata_fetcher_unittest.cc @@ -14,7 +14,7 @@ #include "brave/components/brave_wallet/browser/brave_wallet_utils.h" #include "brave/components/brave_wallet/browser/json_rpc_service.h" #include "brave/components/brave_wallet/common/hash_utils.h" -// #include "brave/components/brave_wallet/common/hex_utils.h" +#include "brave/components/ipfs/ipfs_service.h" #include "components/sync_preferences/testing_pref_service_syncable.h" #include "services/data_decoder/public/cpp/test_support/in_process_data_decoder.h" #include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h" @@ -22,9 +22,6 @@ #include "testing/gtest/include/gtest/gtest.h" #include "ui/base/l10n/l10n_util.h" -#include "brave/components/ipfs/ipfs_service.h" -// #include "brave/components/ipfs/ipfs_utils.h" -// #include "brave/components/ipfs/pref_names.h" namespace brave_wallet { namespace { @@ -45,14 +42,8 @@ class NftMetadataFetcherUnitTest : public testing::Test { ipfs::IpfsService::RegisterProfilePrefs(prefs_.registry()); json_rpc_service_ = std::make_unique( shared_url_loader_factory_, &prefs_); - // NftMetadataFetcher nft_metadata_fetcher_(shared_url_loader_factory_, - // json_rpc_service_.get(), GetPrefs()); nft_metadata_fetcher_ = std::make_unique( shared_url_loader_factory_, json_rpc_service_.get(), GetPrefs()); - - // nft_metadata_fetcher_ = - // std::make_unique(json_rpc_service_); - // nft_metadata_fetcher_ = NftMetadataFetcher(json_rpc_service_.get()); } PrefService* GetPrefs() { return &prefs_; } diff --git a/components/brave_wallet/browser/solana_instruction_data_decoder_unittest.cc b/components/brave_wallet/browser/solana_instruction_data_decoder_unittest.cc index aeb6eb18f421..c243578cde36 100644 --- a/components/brave_wallet/browser/solana_instruction_data_decoder_unittest.cc +++ b/components/brave_wallet/browser/solana_instruction_data_decoder_unittest.cc @@ -9,7 +9,6 @@ #include #include -#include "base/base64.h" #include "brave/components/brave_wallet/common/brave_wallet.mojom.h" #include "brave/components/brave_wallet/common/brave_wallet_constants.h" #include "testing/gtest/include/gtest/gtest.h" From e893788d2361cf7c9bf4b0362fef5a6f8c39196d Mon Sep 17 00:00:00 2001 From: nvonpentz <12549658+nvonpentz@users.noreply.github.com> Date: Tue, 6 Dec 2022 11:59:12 -0500 Subject: [PATCH 14/17] Address feedback: * Rename nft_account_address -> token_mint_address * Add comment with link to documentation for deriving nft metadata accounts * Revert change: continue using kInvalidParams instead of kInternalError in the case that the network URL is invalid for JsonRpcService::GetEthTokenUri * Do not call std::move() on when passing error type * Do not use a const reference for metadata seed constant string * Make sure the int error value is valid before sending via mojo * Use "base::Base64Decode(account_info->data)" syntax since it's easier to read * Use const reference for vector of bytes input to NftMetadataFetcher::DecodeMetadataUri function * Move DecodeUint32 in anonymous namespace in c++ instead of a member function --- .../brave_wallet/browser/json_rpc_service.cc | 4 +- .../brave_wallet/browser/json_rpc_service.h | 2 +- .../browser/json_rpc_service_unittest.cc | 4 +- .../browser/nft_metadata_fetcher.cc | 65 ++++++++++--------- .../browser/nft_metadata_fetcher.h | 7 +- .../browser/nft_metadata_fetcher_unittest.cc | 6 +- .../brave_wallet/browser/solana_keyring.cc | 9 ++- .../brave_wallet/common/brave_wallet.mojom | 2 +- 8 files changed, 52 insertions(+), 47 deletions(-) diff --git a/components/brave_wallet/browser/json_rpc_service.cc b/components/brave_wallet/browser/json_rpc_service.cc index bb846cc866d6..fb4f31958001 100644 --- a/components/brave_wallet/browser/json_rpc_service.cc +++ b/components/brave_wallet/browser/json_rpc_service.cc @@ -1981,8 +1981,8 @@ void JsonRpcService::GetEthTokenUri(const std::string& chain_id, auto network_url = GetNetworkURL(prefs_, chain_id, mojom::CoinType::ETH); if (!network_url.is_valid()) { std::move(callback).Run( - GURL(), mojom::ProviderError::kInternalError, - l10n_util::GetStringUTF8(IDS_WALLET_INTERNAL_ERROR)); + GURL(), mojom::ProviderError::kInvalidParams, + l10n_util::GetStringUTF8(IDS_WALLET_INVALID_PARAMETERS)); return; } diff --git a/components/brave_wallet/browser/json_rpc_service.h b/components/brave_wallet/browser/json_rpc_service.h index 185d1e572c62..1d615575cad2 100644 --- a/components/brave_wallet/browser/json_rpc_service.h +++ b/components/brave_wallet/browser/json_rpc_service.h @@ -366,7 +366,7 @@ class JsonRpcService : public KeyedService, public mojom::JsonRpcService { const std::string& token_mint_address, const std::string& chain_id, GetSPLTokenAccountBalanceCallback callback) override; - void GetSolTokenMetadata(const std::string& nft_account_address, + void GetSolTokenMetadata(const std::string& token_mint_address, GetSolTokenMetadataCallback callback) override; using SendSolanaTransactionCallback = base::OnceCallback DecodeUint32(const std::vector& input, + size_t& offset) { + if (offset >= input.size() || input.size() - offset < sizeof(uint32_t)) { + return absl::nullopt; + } + + // Read bytes in little endian order. + base::span s = + base::make_span(input.begin() + offset, sizeof(uint32_t)); + uint32_t uint32_le = *reinterpret_cast(s.data()); + + offset += sizeof(uint32_t); + +#if defined(ARCH_CPU_LITTLE_ENDIAN) + return uint32_le; +#else + return base::ByteSwap(uint32_le); +#endif +} + net::NetworkTrafficAnnotationTag GetNetworkTrafficAnnotationTag() { return net::DefineNetworkTrafficAnnotation("json_rpc_service", R"( semantics { @@ -245,15 +265,18 @@ void NftMetadataFetcher::CompleteGetEthTokenMetadata( const std::string& response, int error, const std::string& error_message) { - std::move(callback).Run(response, mojom::ProviderError(error), error_message); + mojom::ProviderError mojo_err = static_cast(error); + if (!mojom::IsKnownEnumValue(mojo_err)) + mojo_err = mojom::ProviderError::kUnknown; + std::move(callback).Run(response, mojo_err, error_message); } void NftMetadataFetcher::GetSolTokenMetadata( - const std::string& nft_account_address, + const std::string& token_mint_address, GetSolTokenMetadataCallback callback) { // Derive metadata PDA for the NFT accounts absl::optional associated_metadata_account = - SolanaKeyring::GetAssociatedMetadataAccount(nft_account_address); + SolanaKeyring::GetAssociatedMetadataAccount(token_mint_address); if (!associated_metadata_account) { std::move(callback).Run( "", mojom::SolanaProviderError::kInternalError, @@ -274,12 +297,13 @@ void NftMetadataFetcher::OnGetSolanaAccountInfoTokenMetadata( mojom::SolanaProviderError error, const std::string& error_message) { if (error != mojom::SolanaProviderError::kSuccess || !account_info) { - std::move(callback).Run("", std::move(error), error_message); + std::move(callback).Run("", error, error_message); return; } absl::optional> metadata = - base::Base64Decode((*account_info).data); + base::Base64Decode(account_info->data); + if (!metadata) { std::move(callback).Run("", mojom::SolanaProviderError::kParsingError, l10n_util::GetStringUTF8(IDS_WALLET_PARSING_ERROR)); @@ -303,30 +327,11 @@ void NftMetadataFetcher::CompleteGetSolTokenMetadata( const std::string& response, int error, const std::string& error_message) { - std::move(callback).Run(response, mojom::SolanaProviderError(error), - error_message); -} - -// static -absl::optional NftMetadataFetcher::DecodeUint32( - const std::vector& input, - size_t& offset) { - if (offset >= input.size() || input.size() - offset < sizeof(uint32_t)) { - return absl::nullopt; - } - - // Read bytes in little endian order. - base::span s = - base::make_span(input.begin() + offset, sizeof(uint32_t)); - uint32_t uint32_le = *reinterpret_cast(s.data()); - - offset += sizeof(uint32_t); - -#if defined(ARCH_CPU_LITTLE_ENDIAN) - return uint32_le; -#else - return base::ByteSwap(uint32_le); -#endif + mojom::SolanaProviderError mojo_err = + static_cast(error); + if (!mojom::IsKnownEnumValue(mojo_err)) + mojo_err = mojom::SolanaProviderError::kUnknown; + std::move(callback).Run(response, mojo_err, error_message); } // static @@ -336,7 +341,7 @@ absl::optional NftMetadataFetcher::DecodeUint32( // https://docs.rs/spl-token-metadata/latest/spl_token_metadata/state/struct.Data.html) // as a GURL. absl::optional NftMetadataFetcher::DecodeMetadataUri( - const std::vector data) { + const std::vector& data) { size_t offset = 0; offset = offset + /* Skip first byte for metadata.key */ 1 + /* Skip next 32 bytes for `metadata.update_authority` */ 32 + diff --git a/components/brave_wallet/browser/nft_metadata_fetcher.h b/components/brave_wallet/browser/nft_metadata_fetcher.h index 136d20fcf451..a0aa669469e1 100644 --- a/components/brave_wallet/browser/nft_metadata_fetcher.h +++ b/components/brave_wallet/browser/nft_metadata_fetcher.h @@ -48,7 +48,7 @@ class NftMetadataFetcher { base::OnceCallback; - void GetSolTokenMetadata(const std::string& nft_account_address, + void GetSolTokenMetadata(const std::string& token_mint_address, GetSolTokenMetadataCallback callback); private: @@ -94,11 +94,8 @@ class NftMetadataFetcher { friend class NftMetadataFetcherUnitTest; FRIEND_TEST_ALL_PREFIXES(NftMetadataFetcherUnitTest, DecodeMetadataUri); - static absl::optional DecodeUint32( - const std::vector& input, - size_t& offset); static absl::optional DecodeMetadataUri( - const std::vector data); + const std::vector& data); scoped_refptr url_loader_factory_; std::unique_ptr api_request_helper_; diff --git a/components/brave_wallet/browser/nft_metadata_fetcher_unittest.cc b/components/brave_wallet/browser/nft_metadata_fetcher_unittest.cc index 6140f68a0b7f..1ff8e8db1b7f 100644 --- a/components/brave_wallet/browser/nft_metadata_fetcher_unittest.cc +++ b/components/brave_wallet/browser/nft_metadata_fetcher_unittest.cc @@ -90,13 +90,13 @@ class NftMetadataFetcherUnitTest : public testing::Test { run_loop.Run(); } - void TestGetSolTokenMetadata(const std::string& nft_account_address, + void TestGetSolTokenMetadata(const std::string& token_mint_address, const std::string& expected_response, mojom::SolanaProviderError expected_error, const std::string& expected_error_message) { base::RunLoop loop; nft_metadata_fetcher_->GetSolTokenMetadata( - nft_account_address, + token_mint_address, base::BindLambdaForTesting([&](const std::string& response, mojom::SolanaProviderError error, const std::string& error_message) { @@ -538,7 +538,7 @@ TEST_F(NftMetadataFetcherUnitTest, GetSolTokenMetadata) { valid_metadata_response, mojom::SolanaProviderError::kSuccess, ""); - // Invalid nft_account_address yields internal error. + // Invalid token_mint_address yields internal error. SetSolTokenMetadataInterceptor( network_url, get_account_info_response, GURL("https://" diff --git a/components/brave_wallet/browser/solana_keyring.cc b/components/brave_wallet/browser/solana_keyring.cc index dedbcd117fca..278bda7f1fa9 100644 --- a/components/brave_wallet/browser/solana_keyring.cc +++ b/components/brave_wallet/browser/solana_keyring.cc @@ -166,10 +166,13 @@ absl::optional SolanaKeyring::GetAssociatedTokenAccount( } // static +// Derive metadata account using metadata seed constant, token metadata program +// id, and the mint address as the seeds +// https://docs.metaplex.com/programs/token-metadata/accounts#metadata absl::optional SolanaKeyring::GetAssociatedMetadataAccount( - const std::string& nft_account_address) { + const std::string& token_mint_address) { std::vector> seeds; - const std::string& metadata_seed_constant = "metadata"; + const std::string metadata_seed_constant = "metadata"; std::vector metaplex_seed_constant_bytes( metadata_seed_constant.begin(), metadata_seed_constant.end()); std::vector metadata_program_id_bytes; @@ -177,7 +180,7 @@ absl::optional SolanaKeyring::GetAssociatedMetadataAccount( if (!Base58Decode(mojom::kSolanaMetadataProgramId, &metadata_program_id_bytes, kSolanaPubkeySize) || - !Base58Decode(nft_account_address, &nft_account_address_bytes, + !Base58Decode(token_mint_address, &nft_account_address_bytes, kSolanaPubkeySize)) { return absl::nullopt; } diff --git a/components/brave_wallet/common/brave_wallet.mojom b/components/brave_wallet/common/brave_wallet.mojom index 92958a5abcb4..ca242982bff2 100644 --- a/components/brave_wallet/common/brave_wallet.mojom +++ b/components/brave_wallet/common/brave_wallet.mojom @@ -975,7 +975,7 @@ interface JsonRpcService { (string amount, uint8 decimals, string uiAmountString, SolanaProviderError error, string error_message); // Returns the metadata json associated with the NFT account address - GetSolTokenMetadata(string nft_account_address) => (string response, SolanaProviderError error, string error_message); + GetSolTokenMetadata(string token_mint_address) => (string response, SolanaProviderError error, string error_message); }; enum TransactionStatus { From 0cb61a390185f851d2639f85ee987c4a0aae42d4 Mon Sep 17 00:00:00 2001 From: nvonpentz <12549658+nvonpentz@users.noreply.github.com> Date: Tue, 6 Dec 2022 13:11:25 -0500 Subject: [PATCH 15/17] Complete renaming of nft_account_address -> token_mint_address --- components/brave_wallet/browser/json_rpc_service.cc | 4 ++-- .../brave_wallet/browser/json_rpc_service_unittest.cc | 6 +++--- components/brave_wallet/browser/solana_keyring.cc | 6 +++--- components/brave_wallet/browser/solana_keyring.h | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/components/brave_wallet/browser/json_rpc_service.cc b/components/brave_wallet/browser/json_rpc_service.cc index fb4f31958001..951ab98392bc 100644 --- a/components/brave_wallet/browser/json_rpc_service.cc +++ b/components/brave_wallet/browser/json_rpc_service.cc @@ -2410,9 +2410,9 @@ void JsonRpcService::OnGetSPLTokenAccountBalance( mojom::SolanaProviderError::kSuccess, ""); } -void JsonRpcService::GetSolTokenMetadata(const std::string& nft_account_address, +void JsonRpcService::GetSolTokenMetadata(const std::string& token_mint_address, GetSolTokenMetadataCallback callback) { - nft_metadata_fetcher_->GetSolTokenMetadata(nft_account_address, + nft_metadata_fetcher_->GetSolTokenMetadata(token_mint_address, std::move(callback)); } diff --git a/components/brave_wallet/browser/json_rpc_service_unittest.cc b/components/brave_wallet/browser/json_rpc_service_unittest.cc index fad91200527c..219d3bfccfd5 100644 --- a/components/brave_wallet/browser/json_rpc_service_unittest.cc +++ b/components/brave_wallet/browser/json_rpc_service_unittest.cc @@ -1417,13 +1417,13 @@ class JsonRpcServiceUnitTest : public testing::Test { loop.Run(); } - void TestGetSolTokenMetadata(const std::string& nft_account_address, + void TestGetSolTokenMetadata(const std::string& token_mint_address, const std::string& expected_response, mojom::SolanaProviderError expected_error, const std::string& expected_error_message) { base::RunLoop loop; json_rpc_service_->GetSolTokenMetadata( - nft_account_address, + token_mint_address, base::BindLambdaForTesting([&](const std::string& response, mojom::SolanaProviderError error, const std::string& error_message) { @@ -5685,7 +5685,7 @@ TEST_F(JsonRpcServiceUnitTest, GetSolTokenMetadata) { valid_metadata_response, mojom::SolanaProviderError::kSuccess, ""); - // Invalid nft_account_address yields internal error. + // Invalid token_mint_address yields internal error. SetSolTokenMetadataInterceptor( network_url, get_account_info_response, GURL("https://" diff --git a/components/brave_wallet/browser/solana_keyring.cc b/components/brave_wallet/browser/solana_keyring.cc index 278bda7f1fa9..2a670b9e3973 100644 --- a/components/brave_wallet/browser/solana_keyring.cc +++ b/components/brave_wallet/browser/solana_keyring.cc @@ -176,18 +176,18 @@ absl::optional SolanaKeyring::GetAssociatedMetadataAccount( std::vector metaplex_seed_constant_bytes( metadata_seed_constant.begin(), metadata_seed_constant.end()); std::vector metadata_program_id_bytes; - std::vector nft_account_address_bytes; + std::vector token_mint_address_bytes; if (!Base58Decode(mojom::kSolanaMetadataProgramId, &metadata_program_id_bytes, kSolanaPubkeySize) || - !Base58Decode(token_mint_address, &nft_account_address_bytes, + !Base58Decode(token_mint_address, &token_mint_address_bytes, kSolanaPubkeySize)) { return absl::nullopt; } seeds.push_back(std::move(metaplex_seed_constant_bytes)); seeds.push_back(std::move(metadata_program_id_bytes)); - seeds.push_back(std::move(nft_account_address_bytes)); + seeds.push_back(std::move(token_mint_address_bytes)); return FindProgramDerivedAddress(seeds, mojom::kSolanaMetadataProgramId); } diff --git a/components/brave_wallet/browser/solana_keyring.h b/components/brave_wallet/browser/solana_keyring.h index 9e1a698370dc..49e80bbbf9a2 100644 --- a/components/brave_wallet/browser/solana_keyring.h +++ b/components/brave_wallet/browser/solana_keyring.h @@ -40,7 +40,7 @@ class SolanaKeyring : public HDKeyring { const std::string& wallet_address); static absl::optional GetAssociatedMetadataAccount( - const std::string& nft_account_address); + const std::string& token_mint_address); private: std::string GetAddressInternal(HDKeyBase* hd_key) const override; From 34006ea5d37fd19cb5fcc30c47effc5394039245 Mon Sep 17 00:00:00 2001 From: nvonpentz <12549658+nvonpentz@users.noreply.github.com> Date: Tue, 6 Dec 2022 14:07:24 -0500 Subject: [PATCH 16/17] Address nit: add period at the end of comment --- components/brave_wallet/browser/solana_keyring.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/brave_wallet/browser/solana_keyring.cc b/components/brave_wallet/browser/solana_keyring.cc index 2a670b9e3973..63fc14076401 100644 --- a/components/brave_wallet/browser/solana_keyring.cc +++ b/components/brave_wallet/browser/solana_keyring.cc @@ -167,7 +167,7 @@ absl::optional SolanaKeyring::GetAssociatedTokenAccount( // static // Derive metadata account using metadata seed constant, token metadata program -// id, and the mint address as the seeds +// id, and the mint address as the seeds. // https://docs.metaplex.com/programs/token-metadata/accounts#metadata absl::optional SolanaKeyring::GetAssociatedMetadataAccount( const std::string& token_mint_address) { From e43f7c4d752ce1c7b06aa2a104bfef19681e96e4 Mon Sep 17 00:00:00 2001 From: William Muli Date: Wed, 7 Dec 2022 18:21:39 +0300 Subject: [PATCH 17/17] Add support for solana NFt metadata --- .../brave_wallet_ui/common/async/handlers.ts | 17 ++-- .../brave_wallet_ui/common/async/lib.ts | 11 +++ .../common/hooks/assets-management.ts | 2 +- .../desktop/portfolio-asset-item/index.tsx | 14 +-- .../views/portfolio/portfolio-asset.tsx | 4 +- .../views/portfolio/portfolio-overview.tsx | 6 +- .../shared/create-placeholder-icon/index.tsx | 4 +- .../components/nft-details/nft-details.tsx | 89 ++++++++++++------- .../page/async/wallet_page_async_handler.ts | 19 ++-- 9 files changed, 102 insertions(+), 64 deletions(-) diff --git a/components/brave_wallet_ui/common/async/handlers.ts b/components/brave_wallet_ui/common/async/handlers.ts index e45dec506774..c73e5f9ea8d9 100644 --- a/components/brave_wallet_ui/common/async/handlers.ts +++ b/components/brave_wallet_ui/common/async/handlers.ts @@ -54,7 +54,8 @@ import { sendEthTransaction, sendFilTransaction, sendSolTransaction, - sendSPLTransaction + sendSPLTransaction, + getNFTMetadata } from './lib' import { Store } from './types' import InteractionNotifier from './interactionNotifier' @@ -298,12 +299,12 @@ handler.on(WalletActions.getAllTokensList.type, async (store) => { }) handler.on(WalletActions.addUserAsset.type, async (store: Store, payload: BraveWallet.BlockchainToken) => { - const { braveWalletService, jsonRpcService } = getAPIProxy() - if (payload.isErc721) { - // Get NFTMetadata - const result = await jsonRpcService.getERC721Metadata(payload.contractAddress, payload.tokenId, payload.chainId) - if (!result.error) { - const response = JSON.parse(result.response) + const { braveWalletService } = getAPIProxy() + + if (payload.isErc721 || payload.isNft) { + const result = await getNFTMetadata(payload) + if (!result?.error) { + const response = result?.response && JSON.parse(result.response) payload.logo = response.image || payload.logo } } @@ -311,7 +312,7 @@ handler.on(WalletActions.addUserAsset.type, async (store: Store, payload: BraveW const result = await braveWalletService.addUserAsset(payload) // Refresh balances here for adding ERC721 tokens if result is successful - if (payload.isErc721 && result.success) { + if ((payload.isErc721 || payload.isNft) && result.success) { refreshBalancesPricesAndHistory(store) } store.dispatch(WalletActions.addUserAssetError(!result.success)) diff --git a/components/brave_wallet_ui/common/async/lib.ts b/components/brave_wallet_ui/common/async/lib.ts index c65553b9cad5..8feee3db4dcc 100644 --- a/components/brave_wallet_ui/common/async/lib.ts +++ b/components/brave_wallet_ui/common/async/lib.ts @@ -1032,3 +1032,14 @@ export function getEthTxManagerProxy () { const { ethTxManagerProxy } = getAPIProxy() return ethTxManagerProxy } + +export async function getNFTMetadata (token: BraveWallet.BlockchainToken) { + const { jsonRpcService } = getAPIProxy() + if (token.coin === BraveWallet.CoinType.ETH) { + return await jsonRpcService.getERC721Metadata(token.contractAddress, token.tokenId, token.chainId) + } else if (token.coin === BraveWallet.CoinType.SOL) { + return await jsonRpcService.getSolTokenMetadata(token.contractAddress) + } + + return undefined +} diff --git a/components/brave_wallet_ui/common/hooks/assets-management.ts b/components/brave_wallet_ui/common/hooks/assets-management.ts index 9b3701a2f993..9a97a4735cee 100644 --- a/components/brave_wallet_ui/common/hooks/assets-management.ts +++ b/components/brave_wallet_ui/common/hooks/assets-management.ts @@ -38,7 +38,7 @@ export default function useAssetManagement () { onAddUserAsset(token) // We handle refreshing balances for ERC721 tokens in the addUserAsset handler. - if (!token.isErc721) { + if (!(token.isErc721 || token.isNft)) { dispatch(WalletActions.refreshBalancesAndPriceHistory()) } } diff --git a/components/brave_wallet_ui/components/desktop/portfolio-asset-item/index.tsx b/components/brave_wallet_ui/components/desktop/portfolio-asset-item/index.tsx index 48518e62a4a4..1d26f3122fe5 100644 --- a/components/brave_wallet_ui/components/desktop/portfolio-asset-item/index.tsx +++ b/components/brave_wallet_ui/components/desktop/portfolio-asset-item/index.tsx @@ -62,11 +62,13 @@ export const PortfolioAssetItem = ({ const [assetNetworkSkeletonWidth, setAssetNetworkSkeletonWidth] = React.useState(0) // memos & computed + const isNonFungibleToken = React.useMemo(() => token.isNft || token.isErc721, [token.isNft, token.isErc721]) + const AssetIconWithPlaceholder = React.useMemo(() => { - return withPlaceholderIcon(token.isErc721 && !isDataURL(token.logo) ? NftIcon : AssetIcon, { size: 'big', marginLeft: 0, marginRight: 8 }) - }, [token.isErc721]) + return withPlaceholderIcon(isNonFungibleToken && !isDataURL(token.logo) ? NftIcon : AssetIcon, { size: 'big', marginLeft: 0, marginRight: 8 }) + }, [isNonFungibleToken, token.logo]) - const formattedAssetBalance = token.isErc721 + const formattedAssetBalance = isNonFungibleToken ? new Amount(assetBalance) .divideByDecimals(token.decimals) .format() @@ -83,7 +85,7 @@ export const PortfolioAssetItem = ({ }, [fiatBalance, defaultCurrencies.fiat]) const isLoading = React.useMemo(() => { - return formattedAssetBalance === '' && !token.isErc721 + return formattedAssetBalance === '' && !isNonFungibleToken }, [formattedAssetBalance, token]) const tokensNetwork = React.useMemo(() => { @@ -116,8 +118,6 @@ export const PortfolioAssetItem = ({ return ( <> {token.visible && - // Selecting an erc721 token is temp disabled until UI is ready for viewing NFTs - // or when showing loading skeleton @@ -163,7 +163,7 @@ export const PortfolioAssetItem = ({ hideBalances={hideBalances ?? false} > - {!(token.isErc721 || token.isNft) && + {!isNonFungibleToken && <> {formattedFiatBalance ? ( {formattedFiatBalance} diff --git a/components/brave_wallet_ui/components/desktop/views/portfolio/portfolio-asset.tsx b/components/brave_wallet_ui/components/desktop/views/portfolio/portfolio-asset.tsx index 7605a36ad7b3..93acb826df54 100644 --- a/components/brave_wallet_ui/components/desktop/views/portfolio/portfolio-asset.tsx +++ b/components/brave_wallet_ui/components/desktop/views/portfolio/portfolio-asset.tsx @@ -258,7 +258,7 @@ export const PortfolioAsset = (props: Props) => { const visibleAssetOptions = userAssetList .filter((token) => token.asset.visible && - !token.asset.isErc721 + !(token.asset.isErc721 || token.asset.isNft) ) if (visibleAssetOptions.length === 0) { @@ -706,7 +706,7 @@ export const PortfolioAsset = (props: Props) => { { }, [visibleTokensForFilteredAccount, selectedAccountFilter, networkList, fullAssetBalance]) const visibleAssetOptions = React.useMemo((): UserAssetInfoType[] => { - return userAssetList.filter(({ asset }) => asset.visible && !asset.isErc721) + return userAssetList.filter(({ asset }) => asset.visible && !(asset.isErc721 || asset.isNft)) }, [userAssetList]) // This will scrape all of the user's accounts and combine the fiat value for every asset @@ -194,13 +194,13 @@ export const PortfolioOverview = () => { history.push(`${WalletRoutes.Portfolio}/${asset.symbol}`) return } else if (asset.isErc721) { - history.push(`${WalletRoutes.Portfolio}/${asset.contractAddress}/${asset.tokenId}`) + history.push(`${WalletRoutes.Portfolio}/${asset.contractAddress}/${asset.tokenId}`) } else { history.push(`${WalletRoutes.Portfolio}/${asset.contractAddress}`) } dispatch(WalletPageActions.selectAsset({ asset, timeFrame: selectedTimeline })) - if (asset.isErc721 && nftMetadata) { + if ((asset.isErc721 || asset.isNft) && nftMetadata) { // reset nft metadata dispatch(WalletPageActions.updateNFTMetadata(undefined)) } diff --git a/components/brave_wallet_ui/components/shared/create-placeholder-icon/index.tsx b/components/brave_wallet_ui/components/shared/create-placeholder-icon/index.tsx index 6f312bc9df3d..1440bb117253 100644 --- a/components/brave_wallet_ui/components/shared/create-placeholder-icon/index.tsx +++ b/components/brave_wallet_ui/components/shared/create-placeholder-icon/index.tsx @@ -56,13 +56,15 @@ function withPlaceholderIcon (WrappedComponent: React.ComponentType, config [network.symbol, asset.symbol] ) + const isNonFungibleToken = React.useMemo(() => asset.isNft || asset.isErc721, [asset.isNft, asset.isErc721]) + const tokenImageURL = stripERC20TokenImageURL(asset.logo) const isRemoteURL = isRemoteImageURL(tokenImageURL) const isStorybook = asset.logo.startsWith('static/media/components/brave_wallet_ui/') const isValidIcon = React.useMemo(() => { if (isRemoteURL || isDataURL(asset.logo)) { - return tokenImageURL?.includes('data:image/') || isIpfs(tokenImageURL) ? true : isValidIconExtension(new URL(asset.logo).pathname) + return tokenImageURL?.includes('data:image/') || isIpfs(tokenImageURL) || isNonFungibleToken ? true : isValidIconExtension(new URL(asset.logo).pathname) } if (isStorybook) { return true diff --git a/components/brave_wallet_ui/nft/components/nft-details/nft-details.tsx b/components/brave_wallet_ui/nft/components/nft-details/nft-details.tsx index 6b7f33862243..704ab33fab02 100644 --- a/components/brave_wallet_ui/nft/components/nft-details/nft-details.tsx +++ b/components/brave_wallet_ui/nft/components/nft-details/nft-details.tsx @@ -88,40 +88,55 @@ export const NftDetails = ({ selectedAsset, nftMetadata, nftMetadataError, token {nftMetadata && <> - + - {nftMetadata.contractInformation.name} { - selectedAsset.tokenId + {nftMetadata.contractInformation.name}{' '} + {selectedAsset.tokenId ? '#' + new Amount(selectedAsset.tokenId).toNumber() - : '' - } + : ''} {/* TODO: Add floorFiatPrice & floorCryptoPrice when data is available from backend: https://github.com/brave/brave-browser/issues/22627 */} {/* {CurrencySymbols[defaultCurrencies.fiat]}{nftMetadata.floorFiatPrice} */} {/* {nftMetadata.floorCryptoPrice} {selectedNetwork.symbol} */} - {getLocale('braveWalletNFTDetailBlockchain')} - {nftMetadata.chainName} + + {getLocale('braveWalletNFTDetailBlockchain')} + + + {nftMetadata.chainName} + - {getLocale('braveWalletNFTDetailTokenStandard')} - {nftMetadata.tokenType} + + {getLocale('braveWalletNFTDetailTokenStandard')} + + + {nftMetadata.tokenType} + - {getLocale('braveWalletNFTDetailTokenID')} + + {getLocale('braveWalletNFTDetailTokenID')} + - { - selectedAsset.tokenId - ? '#' + new Amount(selectedAsset.tokenId).toNumber() - : '' - } + {selectedAsset.tokenId + ? '#' + new Amount(selectedAsset.tokenId).toNumber() + : ''} - - + + @@ -129,24 +144,30 @@ export const NftDetails = ({ selectedAsset, nftMetadata, nftMetadataError, token {/* TODO: Add nft logo when data is available from backend: https://github.com/brave/brave-browser/issues/22627 */} {/* */} - {nftMetadata.contractInformation.name} - {nftMetadata.contractInformation.website && nftMetadata.contractInformation.twitter && nftMetadata.contractInformation.facebook && - - - - - - - - - - - - - - } + + {nftMetadata.contractInformation.name} + + {nftMetadata.contractInformation.website && + nftMetadata.contractInformation.twitter && + nftMetadata.contractInformation.facebook && ( + + + + + + + + + + + + + + )} - {nftMetadata.contractInformation.description} + + {nftMetadata.contractInformation.description} + } diff --git a/components/brave_wallet_ui/page/async/wallet_page_async_handler.ts b/components/brave_wallet_ui/page/async/wallet_page_async_handler.ts index f8a8facf6ab8..b23ff2d5760f 100644 --- a/components/brave_wallet_ui/page/async/wallet_page_async_handler.ts +++ b/components/brave_wallet_ui/page/async/wallet_page_async_handler.ts @@ -29,7 +29,8 @@ import { } from '../constants/action_types' import { findHardwareAccountInfo, - getKeyringIdFromAddress + getKeyringIdFromAddress, + getNFTMetadata } from '../../common/async/lib' import { NewUnapprovedTxAdded } from '../../common/constants/action_types' import { Store } from '../../common/async/types' @@ -141,7 +142,7 @@ handler.on(WalletPageActions.selectAsset.type, async (store: Store, payload: Upd const priceHistory = await assetRatioService.getPriceHistory(getTokenParam(selectedAsset), defaultFiat, payload.timeFrame) store.dispatch(WalletPageActions.updatePriceInfo({ priceHistory: priceHistory, defaultFiatPrice: defaultPrices.values[0], defaultCryptoPrice: defaultPrices.values[1], timeFrame: payload.timeFrame })) - if (payload.asset.isErc721) { + if (payload.asset.isErc721 || payload.asset.isNft) { store.dispatch(WalletPageActions.getNFTMetadata(payload.asset)) } } else { @@ -273,15 +274,17 @@ handler.on(WalletPageActions.openWalletSettings.type, async (store) => { handler.on(WalletPageActions.getNFTMetadata.type, async (store, payload: BraveWallet.BlockchainToken) => { store.dispatch(WalletPageActions.setIsFetchingNFTMetadata(true)) - const jsonRpcService = getWalletPageApiProxy().jsonRpcService - const result = await jsonRpcService.getERC721Metadata(payload.contractAddress, payload.tokenId, payload.chainId) - - if (!result.error) { - const response = JSON.parse(result.response) + const result = await getNFTMetadata(payload) + if (!result?.error) { + const response = result?.response && JSON.parse(result.response) const tokenNetwork = getTokensNetwork(getWalletState(store).networkList, payload) const nftMetadata: NFTMetadataReturnType = { chainName: tokenNetwork.chainName, - tokenType: 'ERC721', // getNFTMetadata currently supports only ERC721 standard. When other standards are supported, this value should be dynamic + tokenType: payload.coin === BraveWallet.CoinType.ETH + ? 'ERC721' + : payload.coin === BraveWallet.CoinType.SOL + ? 'SPL' + : '', tokenID: payload.tokenId, imageURL: response.image.startsWith('data:image/') ? response.image : httpifyIpfsUrl(response.image), imageMimeType: 'image/*',