Skip to content

Commit

Permalink
Gateway-like urls pinning support (#17505)
Browse files Browse the repository at this point in the history
* Gateway-like urls pinning support
Allow pinning of gateway-like urls
Resolves brave/brave-browser#28169
Show only pinned NFTs count in the IPFS banner
Resolves brave/brave-browser#28928
  • Loading branch information
cypt4 committed Mar 16, 2023
1 parent 96f91bc commit 5baa09a
Show file tree
Hide file tree
Showing 7 changed files with 189 additions and 15 deletions.
34 changes: 30 additions & 4 deletions components/brave_wallet/browser/brave_wallet_pin_service.cc
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,29 @@ absl::optional<mojom::WalletPinServiceErrorCode> StringToErrorCode(
return absl::nullopt;
}

absl::optional<std::string> ExtractCID(const std::string& ipfs_url) {
GURL gurl = GURL(ipfs_url);
absl::optional<std::string> ExtractIpfsUrl(const std::string& url) {
GURL gurl = GURL(url);

if (!gurl.is_valid()) {
return absl::nullopt;
}

if (!gurl.SchemeIs(ipfs::kIPFSScheme)) {
if (gurl.SchemeIs(ipfs::kIPFSScheme)) {
return gurl.spec();
}

auto source = ipfs::ExtractSourceFromGateway(gurl);
if (!source || !source->SchemeIs(ipfs::kIPFSScheme)) {
return absl::nullopt;
}

return source.value().spec();
}

absl::optional<std::string> ExtractCID(const std::string& url) {
GURL gurl = GURL(ExtractIpfsUrl(url).value_or(""));

if (!gurl.is_valid()) {
return absl::nullopt;
}

Expand Down Expand Up @@ -617,8 +632,19 @@ void BraveWalletPinService::OnTokenMetaDataReceived(
cids.push_back(metadata_cid.value());
cids.push_back(image_cid.value());

auto ipfs_image_url = ExtractIpfsUrl(*image_url);
if (!ipfs_image_url) {
auto pin_error = mojom::PinError::New(
mojom::WalletPinServiceErrorCode::ERR_NON_IPFS_TOKEN_URL,
"Can't find proper image field");
SetTokenStatus(service, token,
mojom::TokenPinStatusCode::STATUS_PINNING_FAILED, pin_error);
std::move(callback).Run(false, std::move(pin_error));
return;
}

content_type_checker_->CheckContentTypeSupported(
*image_url,
ipfs_image_url.value(),
base::BindOnce(&BraveWalletPinService::OnContentTypeChecked,
weak_ptr_factory_.GetWeakPtr(), std::move(service),
std::move(token), std::move(cids), std::move(callback)));
Expand Down
136 changes: 136 additions & 0 deletions components/brave_wallet/browser/brave_wallet_pin_service_unittest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ const char kMonkey3Path[] =
"nft.nftstorage.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x2";
const char kMonkey4Path[] =
"nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x3";
const char kMonkey5Path[] =
"nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x4";
const char kMonkey6Path[] =
"nft.local.60.0x1.0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d.0x5";

const char kMonkey1Url[] =
"ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/2413";
Expand All @@ -49,6 +53,10 @@ const char kMonkey3Url[] =
"ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/2777";
const char kMonkey4Url[] =
"ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/2888";
const char kMonkey5Url[] =
"https://ipfs.io/ipfs/QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/2888";
const char kMonkey6Url[] =
"https://google.com/QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/2888";

const char kMonkey1[] =
R"({"image":"ipfs://bafy1",
Expand Down Expand Up @@ -86,6 +94,24 @@ const char kMonkey4[] =
{"trait_type":"Eyes","value":"Closed"},
{"trait_type":"Clothes","value":"Toga"},
{"trait_type":"Hat","value":"Cowboy Hat"}]})";
const char kMonkey5[] =
R"({"image":"https://ipfs.io/ipfs/bafy3",
"attributes":[
{"trait_type":"Mouth","value":"Bored Cigarette"},
{"trait_type":"Fur","value":"Zombie"},
{"trait_type":"Background","value":"Purple"},
{"trait_type":"Eyes","value":"Closed"},
{"trait_type":"Clothes","value":"Toga"},
{"trait_type":"Hat","value":"Cowboy Hat"}]})";
const char kMonkey6[] =
R"({"image":"https://google.com/bafy3",
"attributes":[
{"trait_type":"Mouth","value":"Bored Cigarette"},
{"trait_type":"Fur","value":"Zombie"},
{"trait_type":"Background","value":"Purple"},
{"trait_type":"Eyes","value":"Closed"},
{"trait_type":"Clothes","value":"Toga"},
{"trait_type":"Hat","value":"Cowboy Hat"}]})";

base::Time g_overridden_now;
std::unique_ptr<ScopedTimeClockOverrides> OverrideWithTimeNow(
Expand Down Expand Up @@ -299,6 +325,116 @@ TEST_F(BraveWalletPinServiceTest, AddPin) {
}
}

TEST_F(BraveWalletPinServiceTest, AddPin_GatewayUrl) {
{
ON_CALL(*GetContentTypeChecker(), CheckContentTypeSupported(_, _))
.WillByDefault(::testing::Invoke(
[](const std::string& cid,
base::OnceCallback<void(absl::optional<bool>)> callback) {
std::move(callback).Run(true);
}));
}
// Right gateway
{
ON_CALL(*GetJsonRpcService(), GetERC721Metadata(_, _, _, _))
.WillByDefault(::testing::Invoke(
[](const std::string& contract_address, const std::string& token_id,
const std::string& chain_id,
MockJsonRpcService::GetERC721MetadataCallback callback) {
EXPECT_EQ("0x1", chain_id);
EXPECT_EQ("0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d",
contract_address);
EXPECT_EQ("0x4", token_id);
std::move(callback).Run(kMonkey5Url, kMonkey5,
mojom::ProviderError::kSuccess, "");
}));
ON_CALL(*GetIpfsLocalPinService(), AddPins(_, _, _))
.WillByDefault(::testing::Invoke(
[](const std::string& prefix, const std::vector<std::string>& cids,
ipfs::AddPinCallback callback) {
EXPECT_EQ(kMonkey5Path, prefix);
EXPECT_EQ("QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq",
cids.at(0));
EXPECT_EQ("bafy3", cids.at(1));
std::move(callback).Run(true);
}));

auto scoped_override = OverrideWithTimeNow(base::Time::FromTimeT(123u));

mojom::BlockchainTokenPtr token =
BraveWalletPinService::TokenFromPrefPath(kMonkey5Path);
token->is_erc721 = true;
absl::optional<bool> call_status;
service()->AddPin(
std::move(token), absl::nullopt,
base::BindLambdaForTesting(
[&call_status](bool result, mojom::PinErrorPtr error) {
call_status = result;
EXPECT_FALSE(error);
}));
EXPECT_TRUE(call_status.value());

const base::Value::Dict* token_record =
GetPrefs()
->GetDict(kPinnedNFTAssets)
.FindDictByDottedPath(kMonkey5Path);

base::Value::List expected_cids;
expected_cids.Append("QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq");
expected_cids.Append("bafy3");

EXPECT_EQ(BraveWalletPinService::StatusToString(
mojom::TokenPinStatusCode::STATUS_PINNED),
*(token_record->FindString("status")));
EXPECT_EQ(nullptr, token_record->FindDict("error"));
EXPECT_EQ(expected_cids, *(token_record->FindList("cids")));
EXPECT_EQ(base::Time::FromTimeT(123u),
base::ValueToTime(token_record->Find("validate_timestamp")));
}

// Wrong gateway
{
ON_CALL(*GetJsonRpcService(), GetERC721Metadata(_, _, _, _))
.WillByDefault(::testing::Invoke(
[](const std::string& contract_address, const std::string& token_id,
const std::string& chain_id,
MockJsonRpcService::GetERC721MetadataCallback callback) {
EXPECT_EQ("0x1", chain_id);
EXPECT_EQ("0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d",
contract_address);
EXPECT_EQ("0x5", token_id);
std::move(callback).Run(kMonkey6Url, kMonkey6,
mojom::ProviderError::kSuccess, "");
}));

mojom::BlockchainTokenPtr token =
BraveWalletPinService::TokenFromPrefPath(kMonkey6Path);
token->is_erc721 = true;
absl::optional<bool> call_status;
service()->AddPin(
std::move(token), absl::nullopt,
base::BindLambdaForTesting(
[&call_status](bool result, mojom::PinErrorPtr error) {
call_status = result;
EXPECT_TRUE(error);
}));

EXPECT_FALSE(call_status.value());

const base::Value::Dict* token_record =
GetPrefs()
->GetDict(kPinnedNFTAssets)
.FindDictByDottedPath(kMonkey6Path);

EXPECT_EQ(BraveWalletPinService::StatusToString(
mojom::TokenPinStatusCode::STATUS_PINNING_FAILED),
*(token_record->FindString("status")));
EXPECT_EQ(BraveWalletPinService::ErrorCodeToString(
mojom::WalletPinServiceErrorCode::ERR_NON_IPFS_TOKEN_URL),
token_record->FindByDottedPath("error.error_code")->GetString());
}
}

TEST_F(BraveWalletPinServiceTest, AddPin_ContentVerification) {
{
ON_CALL(*GetContentTypeChecker(), CheckContentTypeSupported(_, _))
Expand Down
16 changes: 14 additions & 2 deletions components/brave_wallet_ui/common/hooks/nft-pin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,22 @@ export function useNftPin () {
}, 0)
}, [nftsPinningStatus])

const inProgressNftCount = React.useMemo(() => {
return Object.keys(nftsPinningStatus).reduce((accumulator, currentValue) => {
const status = nftsPinningStatus[currentValue]
if (status?.code === BraveWallet.TokenPinStatusCode.STATUS_PINNING_IN_PROGRESS ||
status?.code === BraveWallet.TokenPinStatusCode.STATUS_PINNING_PENDING) {
return accumulator += 1
}

return accumulator
}, 0)
}, [nftsPinningStatus])

const pinningStatusSummary: BraveWallet.TokenPinStatusCode = React.useMemo(() => {
if (pinnableNfts.length === pinnedNftsCount) {
if (pinnedNftsCount > 0 && inProgressNftCount === 0) {
return BraveWallet.TokenPinStatusCode.STATUS_PINNED
} else if (pinnableNfts.length > pinnedNftsCount) {
} else if (inProgressNftCount > 0) {
return BraveWallet.TokenPinStatusCode.STATUS_PINNING_IN_PROGRESS
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,15 @@ export const NftIpfsBanner = ({ onDismiss }: Props) => {

// memos
const bannerStatus: BannerStatus = React.useMemo(() => {
if (!isAutoPinEnabled || pinnableNftsCount === 0) return 'start'
if (!isAutoPinEnabled) return 'start'

switch (status) {
case BraveWallet.TokenPinStatusCode.STATUS_PINNED:
return 'success'
case BraveWallet.TokenPinStatusCode.STATUS_PINNING_IN_PROGRESS:
return 'uploading'
default:
return 'success'
return 'start'
}
}, [status, pinnableNftsCount, isAutoPinEnabled])

Expand All @@ -79,8 +79,7 @@ export const NftIpfsBanner = ({ onDismiss }: Props) => {
</>
) : bannerStatus === 'success' ? (
`${getLocale('braveWalletNftPinningBannerSuccess')
.replace('$1', `${pinnedNftsCount}`)
.replace('$2', `${pinnableNftsCount}`)}`
.replace('$1', `${pinnedNftsCount}`)}`
) : (
`${getLocale('braveWalletNftPinningBannerUploading')}`
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const NftPinningStatus = (props: Props) => {
React.useEffect(() => {
switch (pinningStatusCode) {
case BraveWallet.TokenPinStatusCode.STATUS_PINNING_IN_PROGRESS:
case BraveWallet.TokenPinStatusCode.STATUS_PINNING_PENDING:
setmessage(getLocale('braveWalletNftPinningStatusPinning'))
setIcon(<UploadIcon />)
break
Expand Down
8 changes: 4 additions & 4 deletions components/brave_wallet_ui/stories/locale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -861,13 +861,13 @@ provideStrings({
braveWalletNftPinningCheckNftsButton: 'Check which NFTs of mine can be pinned',
braveWalletNftPinningBannerStart: 'Now you can run your IPFS and be part of web 3. Your NFT data will stay online forever and cannot be tampered with.',
braveWalletNftPinningBannerUploading: 'You\’re running IPFS node. File is being uploaded to IPFS.',
braveWalletNftPinningBannerSuccess: '$1 out of $2 NFTs have been successfully pinned to IPFS.',
braveWalletNftPinningBannerSuccess: '$1 NFTs have been successfully pinned to IPFS.',
braveWalletNftPinningBannerLearnMore: 'Learn more',
braveWalletNftPinningInspectHeading: '$1 out of $2 are available!',
braveWalletNftPinningUnableToPin: 'Unable to pin',
braveWalletNftPinningNodeRunningStatus: 'You\’re running IPFS node',
braveWalletNftPinningNodeNotRunningStatus: 'Local IPFS node is not running',
braveWalletNftPinningStatusPinned: 'NFT data is being pinned to your local IPFS node',
braveWalletNftPinningStatusPinning: 'Cannot be pinned to your local IPFS node',
braveWalletNftPinningStatusPinningFailed: 'Pinned to your local IPFS node'
braveWalletNftPinningStatusPinned: 'Pinned to your local IPFS node',
braveWalletNftPinningStatusPinning: 'NFT data is being pinned to your local IPFS node',
braveWalletNftPinningStatusPinningFailed: 'Cannot be pinned to your local IPFS node'
})
2 changes: 1 addition & 1 deletion components/resources/wallet_strings.grdp
Original file line number Diff line number Diff line change
Expand Up @@ -858,7 +858,7 @@
<message name="IDS_BRAVE_WALLET_NFT_PINNING_CHECK_NFTS_BUTTON" desc="Check pinnable NFTs button text">Check which NFTs of mine can be pinned</message>
<message name="IDS_BRAVE_WALLET_NFT_PINNING_BANNER_START" desc="NFT IPFS banner initial text">Now you can run your IPFS and be part of web 3. Your NFT data will stay online forever and cannot be tampered with.</message>
<message name="IDS_BRAVE_WALLET_NFT_PINNING_BANNER_UPLOADING" desc="NFT IPFS banner uploading text">You’re running IPFS node. File is being fetched to local IPFS node.</message>
<message name="IDS_BRAVE_WALLET_NFT_PINNING_BANNER_SUCCESS" desc="NFT IPFS banner success text"><ph name="PINNED">$1<ex>1</ex></ph> out of <ph name="PINNABLE">$2<ex>3</ex></ph> NFTs have been successfully pinned to IPFS.</message>
<message name="IDS_BRAVE_WALLET_NFT_PINNING_BANNER_SUCCESS" desc="NFT IPFS banner success text"><ph name="PINNED">$1<ex>1</ex></ph> NFTs have been successfully pinned to IPFS.</message>
<message name="IDS_BRAVE_WALLET_NFT_PINNING_BANNER_LEARN_MORE" desc="NFT IPFS banner learn more text">Learn more</message>
<message name="IDS_BRAVE_WALLET_NFT_PINNING_INSPECT_HEADING" desc="Inspect page heading"><ph name="PINNABLE">$1<ex>1</ex></ph> out of <ph name="TOTAL">$2<ex>3</ex></ph> are available!</message>
<message name="IDS_BRAVE_WALLET_NFT_PINNING_UNABLE_TO_PIN" desc="Inspect page unable to pin text">Unable to pin</message>
Expand Down

0 comments on commit 5baa09a

Please sign in to comment.