Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gateway-like urls pinning support #17505

Merged
merged 4 commits into from
Mar 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -857,15 +857,15 @@ 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',
braveWalletImportNftModalTitle: 'Import NFT',
braveWalletEditNftModalTitle: 'Edit NFT'
})
2 changes: 1 addition & 1 deletion components/resources/wallet_strings.grdp
Original file line number Diff line number Diff line change
Expand Up @@ -855,7 +855,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