From dd008f15fe1aff7a285875044abd5d08dde506e9 Mon Sep 17 00:00:00 2001 From: Simone Arpe Date: Tue, 24 Jan 2023 15:40:05 +0100 Subject: [PATCH] Add support for Solana NFT in NFT detail screen (#16747) Adds support for Solana NFT in NFT detail screen. Compared to the ETH NFT the following changes have been introduced: * Shows SPL as new token standard * Contract address label has become Mint address * Mint address link points to NFT contract address using a different logic (consistent with iOS version) * Mint address shown is truncated in the middle (consistent with iOS version) --- .../browser/app/domain/PortfolioModel.java | 61 +++++---- .../activities/NftDetailActivity.java | 118 +++++++++++++----- .../adapters/WalletCoinAdapter.java | 6 +- .../model/WalletListItemModel.java | 4 +- .../crypto_wallet/util/AddressUtils.java | 12 +- .../crypto_wallet/util/AsyncUtils.java | 31 ++++- .../browser/crypto_wallet/util/Utils.java | 2 +- .../java/res/layout/activity_nft_detail.xml | 23 ++-- .../android/strings/android_brave_strings.grd | 8 +- 9 files changed, 196 insertions(+), 69 deletions(-) diff --git a/android/java/org/chromium/chrome/browser/app/domain/PortfolioModel.java b/android/java/org/chromium/chrome/browser/app/domain/PortfolioModel.java index a5910df4447..c4dcdee657c 100644 --- a/android/java/org/chromium/chrome/browser/app/domain/PortfolioModel.java +++ b/android/java/org/chromium/chrome/browser/app/domain/PortfolioModel.java @@ -18,6 +18,7 @@ import org.chromium.brave_wallet.mojom.AssetRatioService; import org.chromium.brave_wallet.mojom.BlockchainRegistry; import org.chromium.brave_wallet.mojom.BlockchainToken; import org.chromium.brave_wallet.mojom.BraveWalletService; +import org.chromium.brave_wallet.mojom.CoinType; import org.chromium.brave_wallet.mojom.EthTxManagerProxy; import org.chromium.brave_wallet.mojom.JsonRpcService; import org.chromium.brave_wallet.mojom.KeyringService; @@ -80,29 +81,46 @@ public class PortfolioModel implements BraveWalletServiceObserverImplDelegate { public void prepareNftListMetaData(List nftList, NetworkInfo networkInfo, PortfolioHelper portfolioHelper) { mPortfolioHelper = portfolioHelper; - List ercNfts = JavaUtils.filter(nftList, nft -> nft.isErc721); + // Filter out and calculate the size of supported NFTs. + // The total sum will be used by `MultiResponseHandler` to detect + // when `setWhenAllCompletedAction()` can be processed. + int erc721NftsSize = JavaUtils.filter(nftList, (nft -> nft.isErc721)).size(); + int solanaNftsSize = JavaUtils.filter(nftList, (nft -> nft.coin == CoinType.SOL)).size(); List nftDataModels = new ArrayList<>(); AsyncUtils.MultiResponseHandler nftMetaDataHandler = - new AsyncUtils.MultiResponseHandler(ercNfts.size()); + new AsyncUtils.MultiResponseHandler(erc721NftsSize + solanaNftsSize); - ArrayList nftMetaDatas = new ArrayList<>(); + ArrayList nftMetadataList = new ArrayList<>(); for (BlockchainToken userAsset : nftList) { if (userAsset.isErc721) { - AsyncUtils.GetNftMetaDataContext nftMetaData = new AsyncUtils.GetNftMetaDataContext( - nftMetaDataHandler.singleResponseComplete); - nftMetaData.asset = userAsset; + AsyncUtils.GetNftErc721MetadataContext nftMetadata = + new AsyncUtils.GetNftErc721MetadataContext( + nftMetaDataHandler.singleResponseComplete); + nftMetadata.asset = userAsset; mJsonRpcService.getErc721Metadata(userAsset.contractAddress, userAsset.tokenId, - userAsset.chainId, nftMetaData); - nftMetaDatas.add(nftMetaData); - } else if (userAsset.isNft) { // other nfts e.g. solana - nftDataModels.add(new NftDataModel(userAsset, networkInfo, null)); + userAsset.chainId, nftMetadata); + nftMetadataList.add(nftMetadata); + } else if (userAsset.isNft) { + if (userAsset.coin == CoinType.SOL) { + // Solana NFTs. + AsyncUtils.GetNftSolanaMetadataContext nftMetadata = + new AsyncUtils.GetNftSolanaMetadataContext( + nftMetaDataHandler.singleResponseComplete); + nftMetadata.asset = userAsset; + mJsonRpcService.getSolTokenMetadata(userAsset.contractAddress, nftMetadata); + nftMetadataList.add(nftMetadata); + + } else { + // Other NFTs. + nftDataModels.add(new NftDataModel(userAsset, networkInfo, null)); + } } } nftMetaDataHandler.setWhenAllCompletedAction(() -> { - for (AsyncUtils.GetNftMetaDataContext metaData : nftMetaDatas) { - nftDataModels.add(new NftDataModel(metaData.asset, networkInfo, - new Erc721MetaData(metaData.erc721Metadata, metaData.errorCode, - metaData.errorMessage))); + for (AsyncUtils.BaseGetNftMetadataContext metadata : nftMetadataList) { + nftDataModels.add(new NftDataModel(metadata.asset, networkInfo, + new NftMetadata(metadata.tokenMetadata, metadata.errorCode, + metadata.errorMessage))); } _mNftModels.postValue(nftDataModels); }); @@ -143,24 +161,25 @@ public class PortfolioModel implements BraveWalletServiceObserverImplDelegate { public static class NftDataModel { public BlockchainToken token; public NetworkInfo networkInfo; - public Erc721MetaData erc721MetaData; + public NftMetadata nftMetadata; public NftDataModel( - BlockchainToken token, NetworkInfo networkInfo, Erc721MetaData erc721MetaData) { + BlockchainToken token, NetworkInfo networkInfo, NftMetadata nftMetadata) { this.token = token; this.networkInfo = networkInfo; - this.erc721MetaData = erc721MetaData; + this.nftMetadata = nftMetadata; } } - public static class Erc721MetaData implements Serializable { - public String mDescription; + public static class NftMetadata implements Serializable { public String mImageUrl; public String mName; public int mErrCode; public String mErrMsg; + // ERC721 only, but it may be present anyway in other standards. + public String mDescription; - public Erc721MetaData(String jsonString, int mErrCode, String mErrMsg) { + public NftMetadata(String jsonString, int mErrCode, String mErrMsg) { this.mErrCode = mErrCode; this.mErrMsg = mErrMsg; try { @@ -172,7 +191,7 @@ public class PortfolioModel implements BraveWalletServiceObserverImplDelegate { } } - public Erc721MetaData(String mDescription, String mImageUrl, String mName) { + public NftMetadata(String mDescription, String mImageUrl, String mName) { this.mDescription = mDescription; this.mImageUrl = mImageUrl; this.mName = mName; diff --git a/android/java/org/chromium/chrome/browser/crypto_wallet/activities/NftDetailActivity.java b/android/java/org/chromium/chrome/browser/crypto_wallet/activities/NftDetailActivity.java index 4cbf8e569ad..ac3780f29f3 100644 --- a/android/java/org/chromium/chrome/browser/crypto_wallet/activities/NftDetailActivity.java +++ b/android/java/org/chromium/chrome/browser/crypto_wallet/activities/NftDetailActivity.java @@ -28,10 +28,12 @@ import com.bumptech.glide.request.target.Target; import com.bumptech.glide.request.transition.DrawableCrossFadeTransition; import org.chromium.brave_wallet.mojom.BlockchainToken; +import org.chromium.brave_wallet.mojom.CoinType; import org.chromium.brave_wallet.mojom.NetworkInfo; import org.chromium.chrome.R; import org.chromium.chrome.browser.app.BraveActivity; import org.chromium.chrome.browser.app.domain.PortfolioModel; +import org.chromium.chrome.browser.crypto_wallet.util.AddressUtils; import org.chromium.chrome.browser.crypto_wallet.util.AndroidUtils; import org.chromium.chrome.browser.crypto_wallet.util.ImageLoader; import org.chromium.chrome.browser.crypto_wallet.util.Utils; @@ -39,15 +41,22 @@ import org.chromium.chrome.browser.util.LiveDataUtil; import org.chromium.chrome.browser.util.TabUtils; import org.chromium.ui.text.NoUnderlineClickableSpan; +import java.util.Locale; + public class NftDetailActivity extends BraveWalletBaseActivity { private static final String TOKEN_ID_FORMAT = "#%s"; - private static final String NFT_URL_FORMAT = "%s/token/%s?a=%s"; + private static final String NFT_ERC721_URL_FORMAT = "%s/token/%s?a=%s"; + private static final String NFT_SPL_URL_FORMAT = "%s/address/%s"; + private static final String NFT_SPL_URL_FORMAT_WITH_CLUSTER = + NFT_SPL_URL_FORMAT + "/?cluster=%s"; private static final String CHAIN_ID = "chainId"; private static final String ASSET_NAME = "assetName"; private static final String ASSET_CONTRACT_ADDRESS = "assetContractAddress"; private static final String NFT_TOKEN_ID_HEX = "nftTokenIdHex"; - private static final String NFT_META_DATA = "nftMetaData"; + private static final String NFT_META_DATA = "nftMetadata"; + private static final String NFT_IS_ERC_721 = "nftIsErc721"; + private static final String COIN_TYPE = "coinType"; private String mNftName; private String mChainId; @@ -55,19 +64,26 @@ public class NftDetailActivity extends BraveWalletBaseActivity { private String mNftTokenId; private String mNftTokenHex; + private boolean mIsErc721; + + private int mCoinType; + private ImageView mNftImageView; private TextView mImageNotAvailableText; private TextView mNetworkNameView; private Button mBtnSend; private TextView mTokenStandardView; - private TextView mTokenIdView; + private TextView mTokenAddressLabelView; + private TextView mTokenAddressView; private TextView mDescriptionContentView; private TextView mNftDetailTitleView; private TextView mNftNameView; private ViewGroup mNftDescriptionLayout; + private ViewGroup mNftTokenStandardLayout; + private ViewGroup mNftTokenAddressLayout; private Toolbar mToolbar; - private PortfolioModel.Erc721MetaData mErc721MetaData; + private PortfolioModel.NftMetadata mNftMetadata; @Override protected void triggerLayoutInflation() { @@ -80,8 +96,9 @@ public class NftDetailActivity extends BraveWalletBaseActivity { mContractAddress = intent.getStringExtra(ASSET_CONTRACT_ADDRESS); mNftTokenHex = intent.getStringExtra(NFT_TOKEN_ID_HEX); mNftTokenId = Utils.hexToIntString(mNftTokenHex); - mErc721MetaData = - (PortfolioModel.Erc721MetaData) intent.getSerializableExtra(NFT_META_DATA); + mNftMetadata = (PortfolioModel.NftMetadata) intent.getSerializableExtra(NFT_META_DATA); + mIsErc721 = intent.getBooleanExtra(NFT_IS_ERC_721, false); + mCoinType = intent.getIntExtra(COIN_TYPE, -1); } // Calculate half screen height and assign it to NFT image view, @@ -102,6 +119,7 @@ public class NftDetailActivity extends BraveWalletBaseActivity { mBtnSend = findViewById(R.id.btn_send); // TODO(simone): Enable if it's the NFT owner. + // GitHub issue: https://github.com/brave/brave-browser/issues/27802. mBtnSend.setVisibility(View.GONE); mNftDetailTitleView = findViewById(R.id.nft_detail_title); @@ -111,15 +129,28 @@ public class NftDetailActivity extends BraveWalletBaseActivity { mNftNameView.setText(mNftName); mNetworkNameView = findViewById(R.id.blockchain_content); - + mNftTokenStandardLayout = findViewById(R.id.nft_token_standard); + mNftTokenAddressLayout = findViewById(R.id.nft_token_address); mTokenStandardView = findViewById(R.id.token_standard_content); - mTokenStandardView.setText(R.string.brave_wallet_nft_erc_721); + mTokenAddressLabelView = findViewById(R.id.token_address_label); - mTokenIdView = findViewById(R.id.token_id_content); + if (mIsErc721) { + mTokenStandardView.setText(R.string.brave_wallet_nft_erc_721); + mTokenAddressLabelView.setText(R.string.brave_wallet_nft_token_id); + } else if (mCoinType == CoinType.SOL) { + mTokenStandardView.setText(R.string.brave_wallet_nft_sol_spl); + mTokenAddressLabelView.setText(R.string.brave_wallet_nft_mint_address); + } else { + // Not ERC 721, nor Solana NFT. + // Hiding incompatible lables. + AndroidUtils.gone(mNftTokenStandardLayout, mNftTokenAddressLayout); + } + + mTokenAddressView = findViewById(R.id.token_address_content); mNftDescriptionLayout = findViewById(R.id.nft_description); - setMetadata(mErc721MetaData); + setMetadata(mNftMetadata); BraveActivity braveActivity = BraveActivity.getBraveActivity(); assert braveActivity != null; @@ -133,31 +164,60 @@ public class NftDetailActivity extends BraveWalletBaseActivity { ? defaultNetwork.blockExplorerUrls[0] : ""; - String tokenStr = String.format(TOKEN_ID_FORMAT, mNftTokenId); - SpannableString spannable = new SpannableString(tokenStr); + SpannableString spannable; + if (mIsErc721) { + String tokenStr = + String.format(Locale.ENGLISH, TOKEN_ID_FORMAT, mNftTokenId); + spannable = new SpannableString(tokenStr); - if (!TextUtils.isEmpty(blockExplorerUrl)) { - String url = String.format( - NFT_URL_FORMAT, blockExplorerUrl, mContractAddress, mNftTokenId); - NoUnderlineClickableSpan linkSpan = - new NoUnderlineClickableSpan(this, R.color.brave_link, - (textView) -> { TabUtils.openLinkWithFocus(this, url); }); - spannable.setSpan( - linkSpan, 0, spannable.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + if (!TextUtils.isEmpty(blockExplorerUrl)) { + String url = String.format(Locale.ENGLISH, NFT_ERC721_URL_FORMAT, + blockExplorerUrl, mContractAddress, mNftTokenId); + createClickableLink(blockExplorerUrl, url, spannable); + } + + } else { + String mintAddress = AddressUtils.getTruncatedAddress(mContractAddress); + spannable = new SpannableString(mintAddress); + + if (!TextUtils.isEmpty(blockExplorerUrl)) { + // Blockchain explorer URLs may contain a cluster endpoint. + // When present it must be appended at the end of the formatted URL. + String[] splitBlockExplorerUrl = blockExplorerUrl.split("/\\?cluster="); + String baseUrl = splitBlockExplorerUrl[0]; + String url; + if (splitBlockExplorerUrl.length > 1) { + String cluster = splitBlockExplorerUrl[1]; + url = String.format(Locale.ENGLISH, NFT_SPL_URL_FORMAT_WITH_CLUSTER, + baseUrl, mContractAddress, cluster); + } else { + url = String.format(Locale.ENGLISH, NFT_SPL_URL_FORMAT, baseUrl, + mContractAddress); + } + createClickableLink(blockExplorerUrl, url, spannable); + } } - mTokenIdView.setText(spannable); - mTokenIdView.setMovementMethod(LinkMovementMethod.getInstance()); + + mTokenAddressView.setText(spannable); + mTokenAddressView.setMovementMethod(LinkMovementMethod.getInstance()); onInitialLayoutInflationComplete(); }); } - private void setMetadata(PortfolioModel.Erc721MetaData erc721MetaData) { - if (erc721MetaData == null) return; + private void createClickableLink( + String blockExplorerUrl, String url, SpannableString spannable) { + NoUnderlineClickableSpan linkSpan = new NoUnderlineClickableSpan( + this, R.color.brave_link, (textView) -> { TabUtils.openLinkWithFocus(this, url); }); + spannable.setSpan(linkSpan, 0, spannable.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + private void setMetadata(PortfolioModel.NftMetadata nftMetadata) { + if (nftMetadata == null) return; // In case of no errors proceed to assign description and fetch NFT image. - if (erc721MetaData.mErrCode == 0) { - String description = erc721MetaData.mDescription; - String imageUrl = erc721MetaData.mImageUrl; + if (nftMetadata.mErrCode == 0) { + String description = nftMetadata.mDescription; + String imageUrl = nftMetadata.mImageUrl; if (!TextUtils.isEmpty(description)) { mDescriptionContentView.setText(description); } else { @@ -206,7 +266,9 @@ public class NftDetailActivity extends BraveWalletBaseActivity { intent.putExtra(ASSET_NAME, asset.name); intent.putExtra(ASSET_CONTRACT_ADDRESS, asset.contractAddress); intent.putExtra(NFT_TOKEN_ID_HEX, asset.tokenId); - intent.putExtra(NFT_META_DATA, nftDataModel.erc721MetaData); + intent.putExtra(NFT_META_DATA, nftDataModel.nftMetadata); + intent.putExtra(NFT_IS_ERC_721, nftDataModel.token.isErc721); + intent.putExtra(COIN_TYPE, asset.coin); return intent; } } diff --git a/android/java/org/chromium/chrome/browser/crypto_wallet/adapters/WalletCoinAdapter.java b/android/java/org/chromium/chrome/browser/crypto_wallet/adapters/WalletCoinAdapter.java index 2d90099b118..87438a7bd53 100644 --- a/android/java/org/chromium/chrome/browser/crypto_wallet/adapters/WalletCoinAdapter.java +++ b/android/java/org/chromium/chrome/browser/crypto_wallet/adapters/WalletCoinAdapter.java @@ -1,7 +1,7 @@ /* Copyright (c) 2021 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/. */ + * You can obtain one at https://mozilla.org/MPL/2.0/. */ package org.chromium.chrome.browser.crypto_wallet.adapters; @@ -196,8 +196,8 @@ public class WalletCoinAdapter extends RecyclerView.Adapter @@ -144,6 +148,7 @@ style="@style/BraveWalletTextView" android:text="@string/brave_wallet_nft_blockchain" android:textColor="@color/wallet_secondary_text_color" + android:textSize="16sp" android:textStyle="bold" /> @@ -167,6 +173,7 @@ style="@style/BraveWalletTextView" android:text="@string/brave_wallet_nft_token_standard" android:textColor="@color/wallet_secondary_text_color" + android:textSize="16sp" android:textStyle="bold" /> diff --git a/browser/ui/android/strings/android_brave_strings.grd b/browser/ui/android/strings/android_brave_strings.grd index af07b9704d0..d382050209a 100644 --- a/browser/ui/android/strings/android_brave_strings.grd +++ b/browser/ui/android/strings/android_brave_strings.grd @@ -2055,12 +2055,18 @@ Are you sure you want to do this? Token Standard - + ERC 721 + + SPL + Token ID + + Mint Address + Image not available