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

Core part for local nft pinning #16073

Merged
merged 15 commits into from
Jan 25, 2023
Merged

Core part for local nft pinning #16073

merged 15 commits into from
Jan 25, 2023

Conversation

cypt4
Copy link
Collaborator

@cypt4 cypt4 commented Nov 23, 2022

Resolves brave/brave-browser#19283
image

Submitter Checklist:

  • I confirm that no security/privacy review is needed, or that I have requested one
  • There is a ticket for my issue
  • Used Github auto-closing keywords in the PR description above
  • Wrote a good PR/commit description
  • Squashed any review feedback or "fixup" commits before merge, so that history is a record of what happened in the repo, not your PR
  • Added appropriate labels (QA/Yes or QA/No; release-notes/include or release-notes/exclude; OS/...) to the associated issue
  • Checked the PR locally: npm run test -- brave_browser_tests, npm run test -- brave_unit_tests, npm run lint, npm run gn_check, npm run tslint
  • Ran git rebase master (if needed)

Reviewer Checklist:

  • A security review is not needed, or a link to one is included in the PR description
  • New files have MPL-2.0 license header
  • Adequate test coverage exists to prevent regressions
  • Major classes, functions and non-trivial code blocks are well-commented
  • Changes in component dependencies are properly reflected in gn
  • Code follows the style guide
  • Test plan is specified in PR before merging

After-merge Checklist:

Test Plan:

This is core part, so there is actually nothing to be tested

@cypt4 cypt4 force-pushed the brave_19283_2 branch 3 times, most recently from af506e2 to fa5f781 Compare November 27, 2022 12:34
@cypt4 cypt4 marked this pull request as ready for review November 29, 2022 09:58
@cypt4 cypt4 requested review from a team as code owners November 29, 2022 09:58
@github-actions
Copy link
Contributor

⚠️ PR head is an unsigned commit
commit: fa5f781
reason: unsigned
Please follow the handbook to configure commit signing
cc: @cypt4

@yrliou yrliou self-requested a review November 30, 2022 19:22
@cypt4 cypt4 force-pushed the brave_19283_2 branch 3 times, most recently from ba27385 to 3886be5 Compare December 2, 2022 22:20
@supermassive supermassive self-requested a review December 8, 2022 03:59
Copy link
Member

@yrliou yrliou left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some early feedback on the first commit, haven't checked the change in IpfsService.

Comment on lines 47 to 55
static bool GetAddPinsResultFromJSON(const std::string& json,
ipfs::AddPinResult* add_pin_result);
static bool GetGetPinsResultFromJSON(const std::string& json,
ipfs::GetPinsResult* result);
static bool GetRemovePinsResultFromJSON(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think returning absl::optionalipfs::*PinResult is preferred, let's start with these new functions.

return false;
}

const base::Value* pins_arr = records_v->FindKey("Pins");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const base::Dict* dict = records_v->GetIfDict();
Then we can use dict->FindList and dict->FindInt below.
By using dict->FindList, we don't need to check type explicitly as FindList will return nullptr if it's not list type.

return false;
}

auto progress = records_v->FindIntKey("Progress");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this one is deprecated and should use Value::Dict::FindList() instead.

return false;
}

const base::Value* pins_arr = records_v->FindKey("Pins");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DITTO

return false;
}

const base::Value* keys = records_v->FindKey("Keys");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const base::Dict* dict = records_v->GetIfDict(); then use dict->FindDict directly.

VLOG(1) << "Missing Type for " << it.first;
return false;
}
const std::string* type = it.second.FindStringKey("Type");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FindStringKey is deprecated.

components/ipfs/pin/ipfs_pin_rpc_types.h Show resolved Hide resolved
Comment on lines 28 to 25
IpfsBasePinService::IpfsBasePinService() {}

IpfsBasePinService::~IpfsBasePinService() {}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

= default for both

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cypt4 Just a friendly reminder, ^ is not resolved yet.

private:
void OnAddPinResult(bool status, absl::optional<AddPinResult> result);

PrefService* prefs_service_;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

raw_ptr for pointer fields in this file

~AddPinResult();
AddPinResult(const AddPinResult&);
std::vector<std::string> pins;
int progress;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

int progress = 0;

base::Value::Dict& update_dict = update->GetDict();

for (const auto& cid : cids_) {
base::Value::List* list = update_dict.FindList(cid);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EnsureList

absl::optional<AddPinResult> result) {
if (status && result) {
for (const auto& cid : cids_) {
if (std::find(result->pins.begin(), result->pins.end(), cid) ==
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

base::Contains


bool success = response.Is2XXResponseCode();

if (!success) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just if (!response.Is2XXResponseCode())
same for methods below

for (const auto& cid : cids) {
gurl = net::AppendQueryParameter(gurl, kArgQueryParam, cid);
}
LOG(ERROR) << "XXZZZ " << gurl.spec();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Y is missing

components/ipfs/ipfs_service.cc Show resolved Hide resolved
"POST", gurl, std::string(), std::string(), false,
base::BindOnce(&IpfsService::OnPinRemoveResult, base::Unretained(this),
iter, std::move(callback)),
{{net::HttpRequestHeaders::kOrigin,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{{net::HttpRequestHeaders::kOrigin,
        url::Origin::Create(gurl).Serialize()}}

worth an anonymous function to stop copypasting.


ipfs::GetPinsResult result;
bool parse_result =
IPFSJSONParser::GetGetPinsResultFromJSON(response.body(), &result);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use response.value_body() (returns base::Value) after rebasing. #15933 just merged.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same for methods below

Comment on lines 86 to 91
using AddPinCallback =
base::OnceCallback<void(bool, absl::optional<AddPinResult>)>;
using RemovePinCallback =
base::OnceCallback<void(bool, absl::optional<RemovePinResult>)>;
using GetPinsCallback =
base::OnceCallback<void(bool, absl::optional<GetPinsResult>)>;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For these three callbacks let's remove bool argument and consider empty optional as failure

Comment on lines 203 to 196
base::SequencedTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&IpfsLocalPinService::AddGcTask, base::Unretained(this)),
base::Minutes(1));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there is no guarantee that after a minute delay from start IpfsLocalPinService is always alive. We might get browser shutdown crash here.

Comment on lines 221 to 215
ipfs_base_pin_service_->AddJob(std::make_unique<AddLocalPinJob>(
prefs_service_, ipfs_service_, prefix, cids,
base::BindOnce(&IpfsLocalPinService::OnAddJobFinished,
base::Unretained(this), std::move(callback))));
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is quite hard to track how lifetimes of callback and this correlate. Better pass it by weak ptr and be safe.

components/ipfs/ipfs_service.h Show resolved Hide resolved
}

void IpfsBasePinService::OnDaemonStarted() {
ipfs_service_->GetConnectedPeers(base::NullCallback(), 2);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why NullCallback? What is the purpose of GetConnectedPeers call?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To ensure that daemon is fully loaded

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a comment then.

components/ipfs/pin/ipfs_base_pin_service.cc Show resolved Hide resolved
Comment on lines 127 to 131
PrefService* prefs_;

// JsonRpcService is used to fetch token metadata
JsonRpcService* json_rpc_service_;
ipfs::IpfsLocalPinService* local_pin_service_;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

raw_ptr


void BraveWalletAutoPinService::OnTokenRemoved(BlockchainTokenPtr token) {
const auto iter =
std::remove_if(queue_.begin(), queue_.end(),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

base::EraseIf from cxx20_erase_deque.h

IntentData::IntentData(const BlockchainTokenPtr& token,
Operation operation,
absl::optional<std::string> service)
: token(token.Clone()), operation(operation), service(service) {}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

service(std::move(service))


void BraveWalletAutoPinService::Restore() {
brave_wallet_service_->GetUserAssets(
mojom::kMainnetChainId, mojom::CoinType::ETH,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it intentional to get assets for mainnet always?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added new GetAllUserAssets


namespace brave_wallet {

enum Operation { ADD = 0, DELETE = 1, VALIDATE = 2 };
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per style guide please use kAdd, kDelete and kValidate

Comment on lines 221 to 242
if (!result) {
AddOrExecute(std::make_unique<IntentData>(current_->token, Operation::ADD,
current_->service));
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should that be if (result) ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We re-add token if it is corrupted here

Comment on lines 72 to 94
virtual mojom::TokenPinStatusPtr GetTokenStatus(
absl::optional<std::string> service,
const mojom::BlockchainTokenPtr& token);
virtual absl::optional<base::Time> GetLastValidateTime(
absl::optional<std::string> service,
const mojom::BlockchainTokenPtr& token);
virtual std::set<std::string> GetTokens(
const absl::optional<std::string>& service);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const absl::optionalstd::string& service

Comment on lines 249 to 340
if (!service) {
local_pin_service_->ValidatePins(
GetPath(absl::nullopt, token), cids.value(),
base::BindOnce(&BraveWalletPinService::OnTokenValidated,
base::Unretained(this), service, std::move(callback),
std::move(token)));
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this ok we don't do anything if there as service string? callback would not be resolved then.

Comment on lines 404 to 538
if (!service) {
local_pin_service_->AddPins(
GetPath(service, token), cids,
base::BindOnce(&BraveWalletPinService::OnTokenPinned,
base::Unretained(this), absl::nullopt,
std::move(callback), std::move(token)));
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also losing callback if there is a service.

Comment on lines 393 to 512
cids.push_back(ExtractCID(token_url).value());
auto* image = parsed_result->FindStringKey("image");
if (image) {
cids.push_back(ExtractCID(*image).value());
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it safe to call .value() here? It is unclear where validation happens.

Comment on lines +182 to +258
return base::StrCat({kNftPart, ".", service.value_or(kLocalService), ".",
base::NumberToString(static_cast<int>(token->coin)), ".",
token->chain_id, ".", token->contract_address, ".",
token->token_id});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(same for BraveWalletPinService::GetTokens)
Is it safe to make pref path this way? What if any part of it has a dot?
Do we really need keep such hierarchy in prefs? Maybe just an array of {service, coin, chain_id, etc...} ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is needed some times to resolve tokens by group like service where they are pinned.
I've added protection in GetPath method

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cypt4 Note that token_id might be an empty string for other NFTs that's not ERC721, do we need to have this detailed hierarchy? Are all service, coin, chain_id, contract_address, token_id needs to be in the key path or we only need a few of them like service for grouping?

return mojom::TokenPinStatusCode::STATUS_PINNED;
} else if (status == "pinning_in_progress") {
return mojom::TokenPinStatusCode::STATUS_PINNING_IN_PROGRESS;
} else if (status == "unpining_in_progress") {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Everywhere in the file pining->pinning

DictionaryPrefUpdate update(prefs_service_, kIPFSPinnedCids);
base::Value::Dict& update_dict = update->GetDict();

bool verification_pased = true;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

passed

@cypt4 cypt4 force-pushed the brave_19283_2 branch 2 times, most recently from 8fdd79a to 077c9eb Compare December 22, 2022 12:17
<message name="IDS_SETTINGS_IPFS_NFT_AUTO_PINNING_ENABLED_LABEL" desc="The text label for NFT auto-pinning to IPFS feature.">
Automatically pin NFTs to the local IPFS node
</message>
<message name="IDS_SETTINGS_IPFS_NFT_AUTO_PINNING_ENABLED_DESC" desc="The description for IPFS companion switch in settings">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Content of desc seems wrong here?

Comment on lines +182 to +258
return base::StrCat({kNftPart, ".", service.value_or(kLocalService), ".",
base::NumberToString(static_cast<int>(token->coin)), ".",
token->chain_id, ".", token->contract_address, ".",
token->token_id});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cypt4 Note that token_id might be an empty string for other NFTs that's not ERC721, do we need to have this detailed hierarchy? Are all service, coin, chain_id, contract_address, token_id needs to be in the key path or we only need a few of them like service for grouping?

components/brave_wallet/common/brave_wallet.mojom Outdated Show resolved Hide resolved
Copy link
Contributor

@stoletheminerals stoletheminerals left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to clarify some IPFS basics. When pinning files that are already present on IPFS and has a CID we don't actually fetch them in the browser process, but it happens in the Kubo daemon? Should we check that the files we pin are images and not something else? If we do so can you point me to where in the code it happens?

const std::string& path) {
std::vector<std::string> parts =
base::SplitString(path, ".", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
if (parts.size() != 6) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we store data like this instead of using something more common (e.g. JSON)? Is it specific to chromium?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes we store a lot of things in the PrefsService, like saved accounts, assets, NFTs. Using raw JSON means we should have some sort of transactions for write operations. PrefsService already has such mechanism.

weak_ptr_factory_.GetWeakPtr(), service,
std::move(callback), std::move(token)));
} else {
// Remote pinning not implemented yet
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we locally pin files that are already remotely pinned (by Pinata for example)?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, NFTs from OpenSea or other NFT platforms are most likely are pinned in some way. But they could be unpinned in any moment.

if (image) {
auto image_cid = ExtractCID(*image);
if (image_cid) {
cids.push_back(image_cid.value());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we check if the file we pin is an image?

#if BUILDFLAG(ENABLE_IPFS)
brave_wallet::BraveWalletAutoPinServiceFactory::GetServiceForContext(profile);
Copy link
Collaborator

@bridiver bridiver Jul 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these being called here in the first place? You should be using BrowserContextKeyedServiceFactory:: ServiceIsCreatedWithContext

@@ -44,6 +44,7 @@
#endif

#if BUILDFLAG(ENABLE_IPFS)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is a brave wallet service guarded by enabled_ipfs, they are not guarded by enable_ipfs in the BUILD.gn

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

header buildflags and gn buildflags should always match regardless of whether the code is only use when some other buildflag is enabled

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Allow users to pin NFTs to user's local IPFS node
6 participants