-
Notifications
You must be signed in to change notification settings - Fork 965
Solana auto discovery #16530
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
Solana auto discovery #16530
Conversation
53e81a8
to
d22fdc1
Compare
eda0ab3
to
aba79e0
Compare
base::BindOnce(&AssetDiscoveryManager::MergeDiscoveredSolanaAssets, | ||
weak_ptr_factory_.GetWeakPtr(), | ||
triggered_by_accounts_added)); | ||
for (const auto& account_address : account_addresses) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we limit to a fixed number of addresses? Thinking of edge cases where the wallet might have thousands Solana accounts.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll need to go back and find the conversation, but I remember having a conversation with @darkdh about how there was an optimized way to limit the number of accounts we checked. It may have just been a naive solution like only check upto a list of 100, but I can't remember. Let me dig a bit more and get back to you on this for a recommendation because I agree that we don't want to be checking 100s or 1000s of accounts.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From the looks of it this is only addresses in our wallet and most usage encourages people to keep assets in the same account rather than split to many different ones, so I don't think this edge case is a concern.
e5d7570
to
8f439f3
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No blockers, but left a few comments as nits
// static | ||
absl::optional<std::string> AssetDiscoveryManager::DecodeMintAddress( | ||
const std::vector<uint8_t>& data) { | ||
if (data.size() < 32) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
think this should be != unless theres a case where we expect it's a bit longer that I'm not thinking of
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is a bit longer :)
https://github.com/solana-labs/solana-program-library/blob/f97a3dc7cf0e6b8e346d473a8c9d02de7b213cfd/token/program/src/state.rs#L86-L105
@nvonpentz please add a comment that we are parsing this.
I actually already implemented some code to parse that struct but needed different field
brave-core/components/brave_wallet/browser/sns_resolver_task.cc
Lines 150 to 167 in ca4b76e
static absl::optional<SplAccountData> FromBytes( | |
base::span<const uint8_t> data_span) { | |
// https://github.com/solana-labs/solana-program-library/blob/f97a3dc7cf0e6b8e346d473a8c9d02de7b213cfd/token/program/src/state.rs#L129 | |
const size_t kSplAccountDataSize = 165; | |
absl::optional<SplAccountData> result; | |
if (data_span.size() != kSplAccountDataSize) | |
return result; | |
result.emplace(); | |
// https://github.com/solana-labs/solana-program-library/blob/f97a3dc7cf0e6b8e346d473a8c9d02de7b213cfd/token/program/src/state.rs#L133 | |
const size_t owner_offset = 32; | |
auto address = SolanaAddress::FromBytes( | |
data_span.subspan(owner_offset, kSolanaPubkeySize)); | |
if (!address) | |
return absl::nullopt; | |
result->owner = *address; | |
return result; |
We should extract all these parsing to some utility place one day.
base::BindOnce(&AssetDiscoveryManager::MergeDiscoveredSolanaAssets, | ||
weak_ptr_factory_.GetWeakPtr(), | ||
triggered_by_accounts_added)); | ||
for (const auto& account_address : account_addresses) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From the looks of it this is only addresses in our wallet and most usage encourages people to keep assets in the same account rather than split to many different ones, so I don't think this edge case is a concern.
bool ParseGetTokenAccountsByOwner( | ||
const base::Value& json_value, | ||
std::vector<absl::optional<SolanaAccountInfo>>* accounts) { | ||
DCHECK(accounts); | ||
|
||
auto result = ParseResultDict(json_value); | ||
if (!result) | ||
return false; | ||
|
||
const auto* account_dicts_list = result->FindList("value"); | ||
if (!account_dicts_list) | ||
return false; | ||
|
||
// For each account dict, get the "account" key and parse the value as an | ||
// SolanaAccountInfo | ||
for (const auto& account_dict : *account_dicts_list) { | ||
const auto* account_dict_value = account_dict.GetIfDict(); | ||
if (!account_dict_value) | ||
return false; | ||
|
||
const auto* account_value = account_dict_value->Find("account"); | ||
if (!account_value) | ||
return false; | ||
|
||
absl::optional<SolanaAccountInfo> account_info; | ||
if (!ParseGetAccountInfoPayload(account_value->GetDict(), &account_info)) | ||
return false; | ||
|
||
accounts->push_back(std::move(account_info)); | ||
} | ||
|
||
return true; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems to me we should return std::vector<SolanaAccountInfo>*
here(no optional)
ParseGetAccountInfoPayload
also called to parse getAccountInfo
response which may have no account found for pubkey https://docs.solana.com/developing/clients/jsonrpc-api#results
For ParseGetTokenAccountsByOwner
null accounts are not expected and we should just skip them when populating found accounts list.
|
||
void AssetDiscoveryManager::MergeDiscoveredSolanaAssets( | ||
bool triggered_by_accounts_added, | ||
const std::vector<std::vector<std::string>>& |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd suggest using SolanaAddress
instead of string
so code around becomes a bit more typesafe and clear
std::vector<uint8_t> pub_key_bytes(data.begin(), data.begin() + 32); | ||
std::string pub_key = Base58Encode(pub_key_bytes); | ||
return pub_key; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please return as optional<SolanaAddress>
.
Nit: avoid temporary vector here and use base::span
8f439f3
to
03265e3
Compare
base::flat_set<std::string> discovered_mint_addresses; | ||
for (const auto& discovered_contract_address_list : | ||
all_discovered_contract_addresses) { | ||
for (const auto& discovered_contract_address : | ||
discovered_contract_address_list) { | ||
discovered_mint_addresses.insert(discovered_contract_address.ToBase58()); | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Complexity of this is O(n^2).
Makes sense to collect everything in std::vector and convert inplace to flat_set when binding to OnGetSolanaTokenRegistry callback
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM with one comment
03265e3
to
50fb38d
Compare
* Add JsonRpcService::GetSolanaTokenAccountsByOwner * Implement AssetDiscoveryManager::DiscoverSolAssets * Modify KeyringService such NotifyAccountsAdded is called for SOL accounts * Update BraveWalletService::DiscoverAssetsOnAllSupportedChains such to pass a list of SOL addresses to use in asset discovery * Modify AssetDiscoveryManager::DiscoverAssetsOnAllSupportedChainsAccounts added, and AssetDiscoveryManager::DiscoverAssetsOnAllSupportedChainsRefresh to initiate asset discovery for SOL and ETH chains
50fb38d
to
a5cb516
Compare
Resolves brave/brave-browser#27246
Security review - https://github.com/brave/security/issues/1161
Summary of changes
Adds support for automatic 'discovery' of Solana assets, meaning when new Solana accounts are added or when the frontend calls DiscoverAssetsOnAllSupportedChains(), we issue getTokenAccountsByOwner RPC calls to fetch the associated SPL token mint addresses owned by each address, checks if those tokens are in our registry, and adds them as visible asset if so.
This PR integrates this logic in the AssetDiscoveryManager which already has similar logic for Ethereum asset discovery.
I squashed to one commit, so I created this list to guide review:
JsonRpcService::GetSolanaTokenAccountsByOwner
methods that implement the getTokenAccountsByOwner method. 1 RPC request will return all the tokens owned by a single address.components/brave_wallet/browser/solana_requests.cc
to create the request bodies for the RPC callconvert_uint64_in_object_array_to_string
to support conversions of fields that are not at the top level of the JSON object in the array.AssetDiscoveryManager::DiscoverSolAssets
which takes a list of Solana account addresses, and callsJsonRpcService::GetSolanaTokenAccountsByOwner
for each concurrently using a barrier callbackAssetDiscoveryManager::OnGetSolanaTokenAccountsByOwner
to handle the result of an individual RPC calls by decoding the data (base64, then borsh) to get the mint addresses returned in the RPC call, calls the barrier callback with the decoded list of mint addressesAssetDiscoveryManager::DecodeMintAddress
does the borsh decoding to get the addressAssetDiscoveryManager::MergeDiscoveredSolanaAssets
is called with a list of lists of discovered mint addresses (for for each address). It dedupes the addresses, then fetches the Sol token RegistryAssetDiscoveryManager::OnGetSolanaTokenRegistry
cross references the returned mint addresses with the list of token registry, and adds the token if it's in the registryAssetDiscoveryManager::CompleteDiscoverAssets
to trigger notifications and/or callbacks for testsNotifyAccountsAdded()
is also called when SOL accounts are addedDiscoverAssetsOnAllSupportedChainsAccountsAdded
, and if it's SOL, callDiscoverSolAssets
BraveWalletService::DiscoverAssetsOnAllSupportedChains
(this is what's called by frontends)BraveWalletService::DiscoverAssetsOnAllSupportedChains
such that it also fetches the users' Solana addresses to perform asset discovery with (callsAssetDiscoveryManager::DiscoverAssetsOnAllSupportedChainsRefresh
)AssetDiscoveryManager::DiscoverAssetsOnAllSupportedChainsRefresh
such that it takes a map of mojom::CoinType -> vector of addresses, representing the addresses for each CoinType the user has, and now callDiscoverSolAssets
as well asDiscoverEthAssets
(eth)Submitter Checklist:
QA/Yes
orQA/No
;release-notes/include
orrelease-notes/exclude
;OS/...
) to the associated issuenpm run test -- brave_browser_tests
,npm run test -- brave_unit_tests
wikinpm run lint
,npm run presubmit
wiki,npm run gn_check
,npm run tslint
git rebase master
(if needed)Reviewer Checklist:
gn
After-merge Checklist:
changes has landed on
Test Plan:
Set up an account on Solana mainnet that has tokens listed our token registry, but are not added as visible assets. Add this account using each of these methods, and verify the asset is now a visible asset:
When frontends have implemented calling the
DiscoverAssetsOnAllSupportedChains()
upon page refresh, then Solana tokens should automatically be added because of the work in this PR, but I don't believe there's a way to test that now, so that should be tested when those changes land.