-
Notifications
You must be signed in to change notification settings - Fork 2.8k
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
Pagination queries for balances
endpoint
#2490
base: master
Are you sure you want to change the base?
Changes from all commits
d4a6596
4e0450b
aa7ee75
449775d
b2fe429
4131855
2dd9aef
05f6851
5b010e4
17a7ef4
73f50c0
c172d1b
fdd2c11
6dbc7a6
32d680f
088c50b
695f99a
f109d1b
27970eb
dbc32f3
f866c54
b1bfb20
99e7b65
d7c3b02
037351d
3ae5222
af64206
fd2b019
9c8ce1e
0c92b6a
a943e2b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -79,9 +79,11 @@ impl BalanceQuery { | |
Ok(balance) | ||
} | ||
|
||
// TODO: This API should be migrated to the indexer for better support and | ||
// discontinued within fuel-core. | ||
#[graphql(complexity = "query_costs().balance_query")] | ||
#[graphql( | ||
complexity = "query_costs().balance_query + query_costs().storage_iterator \ | ||
+ (query_costs().storage_read + first.unwrap_or_default() as usize) * child_complexity \ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The cost computation is hard to understand for me, maybe it's normal but it might requires a comment. What I don't understand :
What is logic to me is two solutions :
(query_costs().storage_read + last.unwrap_or_default() as usize) * child_complexity (because last is higher and include all the precents numbers)
(query_costs().storage_read + (last.unwrap_or_default() as usize - first.unwrap_or_default() as usize)) * child_complexity |
||
+ (query_costs().storage_read + last.unwrap_or_default() as usize) * child_complexity" | ||
)] | ||
async fn balances( | ||
&self, | ||
ctx: &Context<'_>, | ||
|
@@ -92,18 +94,21 @@ impl BalanceQuery { | |
before: Option<String>, | ||
) -> async_graphql::Result<Connection<AssetId, Balance, EmptyFields, EmptyFields>> | ||
{ | ||
if before.is_some() || after.is_some() { | ||
return Err(anyhow!("pagination is not yet supported").into()) | ||
} | ||
let query = ctx.read_view()?; | ||
if !query.balances_enabled && (before.is_some() || after.is_some()) { | ||
return Err(anyhow!( | ||
"Can not use pagination when balances indexation is not available" | ||
) | ||
.into()) | ||
} | ||
let base_asset_id = *ctx | ||
.data_unchecked::<ConsensusProvider>() | ||
.latest_consensus_params() | ||
.base_asset_id(); | ||
let owner = filter.owner.into(); | ||
crate::schema::query_pagination(after, before, first, last, |_, direction| { | ||
crate::schema::query_pagination(after, before, first, last, |start, direction| { | ||
Ok(query | ||
.balances(&owner, direction, &base_asset_id) | ||
.balances(&owner, (*start).map(Into::into), direction, &base_asset_id) | ||
.map(|result| { | ||
result.map(|balance| (balance.asset_id.into(), balance.into())) | ||
})) | ||
|
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
|
@@ -233,15 +233,49 @@ impl OffChainDatabase for OffChainIterableKeyValueView { | |||||||
} | ||||||||
} | ||||||||
|
||||||||
fn balances( | ||||||||
&self, | ||||||||
fn balances<'a>( | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, it is a tricky function because of how we handle balances.
I remember: if you don't have coins but have messages, they will not appear during the iteration. The problem with the current solution is when Either we need to handle it differently, or come up with another solution. It would be nice if we could insert base asset balance into the mid of the iterator. Maybe we could change the indexation and make it always add empy indexation for the base asset(like entry in the balances table with 0 amount just for balances)? Then during iteration we call always fetch its value by using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||||
&'a self, | ||||||||
owner: &Address, | ||||||||
base_asset_id: &AssetId, | ||||||||
start: Option<AssetId>, | ||||||||
base_asset_id: &'a AssetId, | ||||||||
direction: IterDirection, | ||||||||
) -> BoxedIter<'_, StorageResult<(AssetId, TotalBalanceAmount)>> { | ||||||||
) -> BoxedIter<'a, StorageResult<(AssetId, TotalBalanceAmount)>> { | ||||||||
match (direction, start) { | ||||||||
(IterDirection::Forward, None) => { | ||||||||
self.base_asset_first(owner, base_asset_id, direction) | ||||||||
} | ||||||||
(IterDirection::Forward, Some(asset_id)) => { | ||||||||
if asset_id == *base_asset_id { | ||||||||
self.base_asset_first(owner, base_asset_id, direction) | ||||||||
} else { | ||||||||
let start = (asset_id != *base_asset_id) | ||||||||
.then_some(CoinBalancesKey::new(owner, &asset_id)); | ||||||||
Comment on lines
+251
to
+252
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why are we redoing the comparaison here as we are already in the
Suggested change
|
||||||||
self.non_base_asset_balances(owner, start, direction, base_asset_id) | ||||||||
} | ||||||||
} | ||||||||
(IterDirection::Reverse, None) => { | ||||||||
self.non_base_assets_first(start, owner, base_asset_id, direction) | ||||||||
} | ||||||||
(IterDirection::Reverse, Some(asset_id)) => { | ||||||||
if asset_id == *base_asset_id { | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can leverage pattern matching as you did in |
||||||||
self.base_asset_balance(base_asset_id, owner) | ||||||||
} else { | ||||||||
self.non_base_assets_first(start, owner, base_asset_id, direction) | ||||||||
} | ||||||||
} | ||||||||
} | ||||||||
} | ||||||||
} | ||||||||
|
||||||||
impl OffChainIterableKeyValueView { | ||||||||
fn base_asset_balance( | ||||||||
&self, | ||||||||
base_asset_id: &AssetId, | ||||||||
owner: &Address, | ||||||||
) -> BoxedIter<'_, Result<(AssetId, u128), StorageError>> { | ||||||||
let base_asset_id = *base_asset_id; | ||||||||
let base_balance = self.balance(owner, &base_asset_id, &base_asset_id); | ||||||||
let base_asset_balance = match base_balance { | ||||||||
match base_balance { | ||||||||
Ok(base_asset_balance) => { | ||||||||
if base_asset_balance != 0 { | ||||||||
iter::once(Ok((base_asset_id, base_asset_balance))).into_boxed() | ||||||||
|
@@ -250,37 +284,68 @@ impl OffChainDatabase for OffChainIterableKeyValueView { | |||||||
} | ||||||||
} | ||||||||
Err(err) => iter::once(Err(err)).into_boxed(), | ||||||||
}; | ||||||||
} | ||||||||
} | ||||||||
|
||||||||
let non_base_asset_balance = self | ||||||||
.iter_all_filtered_keys::<CoinBalances, _>(Some(owner), None, Some(direction)) | ||||||||
.filter_map(move |result| match result { | ||||||||
Ok(key) if *key.asset_id() != base_asset_id => Some(Ok(key)), | ||||||||
Ok(_) => None, | ||||||||
Err(err) => Some(Err(err)), | ||||||||
}) | ||||||||
.map(move |result| { | ||||||||
result.and_then(|key| { | ||||||||
let asset_id = key.asset_id(); | ||||||||
let coin_balance = | ||||||||
self.storage_as_ref::<CoinBalances>() | ||||||||
.get(&key)? | ||||||||
.unwrap_or_default() | ||||||||
.into_owned() as TotalBalanceAmount; | ||||||||
Ok((*asset_id, coin_balance)) | ||||||||
}) | ||||||||
fn non_base_asset_balances<'a>( | ||||||||
&'a self, | ||||||||
owner: &Address, | ||||||||
start: Option<CoinBalancesKey>, | ||||||||
direction: IterDirection, | ||||||||
base_asset_id: &'a AssetId, | ||||||||
) -> BoxedIter<'_, Result<(AssetId, u128), StorageError>> { | ||||||||
self.iter_all_filtered_keys::<CoinBalances, _>( | ||||||||
Some(owner), | ||||||||
start.as_ref(), | ||||||||
Some(direction), | ||||||||
) | ||||||||
.filter_map(move |result| match result { | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be valuable in the future to have a comment about this behavior because reading the comments on the review helped me but it's not really future proof and without it I wouldn't understand why |
||||||||
Ok(key) if *key.asset_id() != *base_asset_id => Some(Ok(key)), | ||||||||
Ok(_) => None, | ||||||||
Err(err) => Some(Err(err)), | ||||||||
}) | ||||||||
.map(move |result| { | ||||||||
result.and_then(|key| { | ||||||||
let asset_id = key.asset_id(); | ||||||||
let coin_balance = | ||||||||
self.storage_as_ref::<CoinBalances>() | ||||||||
.get(&key)? | ||||||||
.unwrap_or_default() | ||||||||
.into_owned() as TotalBalanceAmount; | ||||||||
Ok((*asset_id, coin_balance)) | ||||||||
}) | ||||||||
.into_boxed(); | ||||||||
}) | ||||||||
.into_boxed() | ||||||||
} | ||||||||
|
||||||||
if direction == IterDirection::Forward { | ||||||||
base_asset_balance | ||||||||
.chain(non_base_asset_balance) | ||||||||
.into_boxed() | ||||||||
} else { | ||||||||
non_base_asset_balance | ||||||||
.chain(base_asset_balance) | ||||||||
.into_boxed() | ||||||||
} | ||||||||
fn non_base_assets_first<'a>( | ||||||||
&'a self, | ||||||||
start: Option<AssetId>, | ||||||||
owner: &Address, | ||||||||
base_asset_id: &'a AssetId, | ||||||||
direction: IterDirection, | ||||||||
) -> BoxedIter<'_, Result<(AssetId, u128), StorageError>> { | ||||||||
let start = start.map(|asset_id| CoinBalancesKey::new(owner, &asset_id)); | ||||||||
let base_asset_balance = self.base_asset_balance(base_asset_id, owner); | ||||||||
let non_base_asset_balance = | ||||||||
self.non_base_asset_balances(owner, start, direction, base_asset_id); | ||||||||
non_base_asset_balance | ||||||||
.chain(base_asset_balance) | ||||||||
.into_boxed() | ||||||||
} | ||||||||
|
||||||||
fn base_asset_first<'a>( | ||||||||
&'a self, | ||||||||
owner: &Address, | ||||||||
base_asset_id: &'a AssetId, | ||||||||
direction: IterDirection, | ||||||||
) -> BoxedIter<'_, Result<(AssetId, u128), StorageError>> { | ||||||||
let base_asset_balance = self.base_asset_balance(base_asset_id, owner); | ||||||||
let non_base_asset_balances = | ||||||||
self.non_base_asset_balances(owner, None, direction, base_asset_id); | ||||||||
base_asset_balance | ||||||||
.chain(non_base_asset_balances) | ||||||||
.into_boxed() | ||||||||
} | ||||||||
} | ||||||||
|
||||||||
|
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.
This is not something we want to do in the future anymore ? In my mind it's still something we want to do in the future to decouple both use cases of the node.