-
Notifications
You must be signed in to change notification settings - Fork 30
Add token transactions endpoint for the API Server #1991
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
base: master
Are you sure you want to change the base?
Conversation
c55d771 to
d804d77
Compare
| self.token_transactions_table.entry(token_id).or_default().insert( | ||
| block_height, | ||
| transaction_ids | ||
| .into_iter() | ||
| .enumerate() | ||
| .map(|(idx, tx_id)| TokenTransaction { | ||
| global_tx_index: next_tx_idx + idx as i64, | ||
| tx_id, | ||
| }) | ||
| .collect(), | ||
| ); |
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.
Since this whole thing will break if the passed block_height is not the previous block height plus 1, plz assert on this fact.
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.
Not sure what you mean here, it should be okay to have gaps in the block_height
| .last() | ||
| .expect("not empty") | ||
| .last() | ||
| .expect("not empty") |
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 guess this will break if the previous call to set_token_transactions_at_height had empty transaction_ids.
If so, plz assert that transaction_ids is not empty.
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.
Added a check if the ids are empty just return Ok
| for transaction_id in transaction_ids { | ||
| self.tx | ||
| .execute( | ||
| r#" | ||
| INSERT INTO ml.token_transactions (token_id, block_height, transaction_id) | ||
| VALUES ($1, $2, $3) | ||
| ON CONFLICT (token_id, block_height, transaction_id) | ||
| DO NOTHING; | ||
| "#, | ||
| &[&token_id.encode(), &height, &transaction_id.encode()], | ||
| ) | ||
| .await | ||
| .map_err(|e| ApiServerStorageError::LowLevelStorageError(e.to_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.
So, as I can see, in the in-memory implementation, this "global_tx_index" is defined per token.
Also, calling set_token_transactions_at_height twice for the same height will overwrite the previous data.
And here:
- global_tx_index is indeed global, but it's unique for each "(token_id, block_height, transaction_id)" combination. I.e. for each particular token there will be gaps in the indices.
On the other hand, it's definitely not the same as the "normal" global tx index (e.g. because here transactions in a block are sorted by their ids, instead of their actual order inside the block; also, the "normal" global index starts from 0 as I can see, and this one starts from 1). - calling
set_token_transactions_at_heighttwice for the same height will keep the previous data.
So,
- plz make both implementations consistent; but first plz write some tests that check the produces indices.
- regarding what that "global_tx_index" should mean - IMO it should be the same as the "normal" global index, because otherwise the API will be confusing.
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 think it should be conistant now between the implementations both being unique globaly not per token_id. And fixed the name to tx_global_index everywhere.
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.
And what about the part:
regarding what that "global_tx_index" should mean - IMO it should be the same as the "normal" global index, because otherwise the API will be confusing.
?
d804d77 to
3eccbe6
Compare
0f040e3 to
a856b93
Compare
a856b93 to
1d47121
Compare
1d47121 to
0f1d451
Compare
api-server/api-server-common/src/storage/impls/in_memory/mod.rs
Outdated
Show resolved
Hide resolved
|
|
||
| /// Return a page of TX IDs that reference this token_id, with a limit of len and older | ||
| /// tx_global_index than the specified. | ||
| /// The tx_global_index is only ordered by block height and are not continuous for a specific | ||
| /// token_id. | ||
| async fn get_token_transactions( | ||
| &self, | ||
| token_id: TokenId, | ||
| len: u32, | ||
| tx_global_index: u64, | ||
| ) -> Result<Vec<TokenTransaction>, ApiServerStorageError>; |
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.
-
"is only ordered by block height and are not continuous" - inconsistent "is" and "are".
-
In general, can you plz put more effort into writing documentation comments? "than the specified" - it's not even correct English.
|
|
||
| #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
| pub struct TokenTransaction { | ||
| pub tx_global_index: u64, |
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.
You've made names consistent, but you haven't made the actual indices consistent. This hasn't made things less confusing.
| for transaction_id in transaction_ids { | ||
| self.tx | ||
| .execute( | ||
| r#" | ||
| INSERT INTO ml.token_transactions (token_id, block_height, transaction_id) | ||
| VALUES ($1, $2, $3) | ||
| ON CONFLICT (token_id, block_height, transaction_id) | ||
| DO NOTHING; | ||
| "#, | ||
| &[&token_id.encode(), &height, &transaction_id.encode()], | ||
| ) | ||
| .await | ||
| .map_err(|e| ApiServerStorageError::LowLevelStorageError(e.to_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.
And what about the part:
regarding what that "global_tx_index" should mean - IMO it should be the same as the "normal" global index, because otherwise the API will be confusing.
?
| SELECT tx_global_index, transaction_id | ||
| FROM ml.token_transactions |
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.
So, this tx_global_index is different from tx_global_index inside ml.transactions. Such inconsistency, if unavoidable, should be justified - comments must be written about why it has to be so. Also, probably we should invent different names for different indices. Also, we should probably introduce some newtypes to use instead of u64, so that one index can't be accidentally confused with another.
But I don't understand why they should be different:
- Can we remove
tx_global_indexfromml.token_transactionsaltogether and fetch it fromml.transactionsinstead? - If the above adds too much overhead, then perhaps
set_token_transactions_at_heightshould accepttx_global_indexvalues together with tx ids, instead of relying on them being automatically generated by the storage. As I can see, in places whereset_token_transactions_at_heightis called, thetx_global_indexs are already known.
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.
Went with option number 2.
| block_height: BlockHeight, | ||
| ) -> Result<(), ApiServerStorageError>; | ||
|
|
||
| /// Append new token transactions with increasing tx_global_index at this block height |
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.
Sorry, but by reading this I still don't understand what this function will do if called 2 times in a row for the same height. "Append" sounds too vague. I'd say that the explanation deserves a separate sentence.
Also, as I can see, this "append" behavior is still inconsistent between the two implementations. E.g. if an existing tx id is passed, the in-memory implementation will ignore it and postgres one will add it with new index.
I'm not actually suggesting making the "append" behavior consistent, because this may be tricky and in reality it's unnecessary. But the interface IMO should define how a function should be used and how it shouldn't. So e.g. if some combination of parameters produces inconsistent results, then it can be said that the result of the function is undefined in such a case, and it's better to have a doc comment that mentions it. Additionally, IMO implementations should better check for undefined cases and panic, at least in debug.
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.
Now both implementations will replace the entry if a duplicate (token_id, tx_id, block_height) is inserted.
| let random_token_id = TokenId::new(H256::random_using(&mut rng)); | ||
| let token_transactions: Vec<_> = (0..10) | ||
| .map(|idx| { | ||
| let random_tx_id = Id::<Transaction>::new(H256::random_using(&mut rng)); |
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.
FYI: Id also has the random_using method, i.e. this line could be a bit shorter.
| // Unmint tokens | ||
| let coins_after_unmint = (coins_after_mint - token_supply_change_fee).unwrap(); | ||
| let tokens_to_unmint = Amount::from_atoms(1); | ||
| let tokens_leff_after_unmint = (amount_to_mint - tokens_to_unmint).unwrap(); |
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.
typo - leff
| let token_issuance_fee = | ||
| tf.chainstate.get_chain_config().fungible_token_issuance_fee(); | ||
|
|
||
| let issuance = test_utils::token_utils::random_token_issuance_v1( |
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.
Any particular reason not to use issue_and_mint_tokens_from_genesis from helpers?
(if it's not generic enough, then perhaps it can be generalized more)
| // 2. Token Authority Management Commands | ||
| // ------------------------------------------------------------------------ | ||
|
|
||
| // Helper to create simple command txs |
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.
Outdated comment?
(I don't see any helper here)
| assert_eq!(txs.len(), 6); | ||
| let ids: Vec<_> = txs.iter().map(|t| t.tx_id).collect(); | ||
| assert!(ids.contains(&tx_freeze_id)); | ||
| assert!(ids.contains(&tx_unfreeze_id)); | ||
| assert!(ids.contains(&tx_metadata_id)); | ||
| assert!(ids.contains(&tx_authority_id)); |
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.
IMO it'd be a bit more robust if you collected ids to a set and then compared it as a whole to the expected set of ids (thus you'd check that the previously checked ids are still there). Same in other places.
(If you don't want to repeat expected tx ids every time, you could have the expected set as a mutable var and extend it with new ids every time.)
| AddressableError, | ||
| #[error("Block timestamp too high: {0}")] | ||
| TimestampTooHigh(BlockTimestamp), | ||
| #[error("Tx global index too hight: {0}")] |
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.
hight -> high
|
|
||
| /// Returns a page of transaction IDs that reference this `token_id`, limited to `len` entries | ||
| /// and with a `tx_global_index` older than the specified value. | ||
| /// The `tx_global_index` and is not continuous for a specific `token_id`. |
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.
The
tx_global_indexand is not continuous
It's ironic that this change was made as a response to the review comment "can you plz put more effort into writing documentation comments".
| TokenTransaction, TransactionInfo, TransactionWithBlockInfo, Utxo, UtxoLock, UtxoWithExtraInfo, | ||
| }; | ||
|
|
||
| use itertools::Itertools as _; |
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.
Put it after the std imports?
| ON CONFLICT (token_id, transaction_id, block_height) | ||
| DO NOTHING; |
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 contradicts the documentation - "If the pair already exists at that block_height, the tx_global_index is updated."
P.S. It'd be nice to have a test for this in the storage test suite.
| Ok(self | ||
| .token_transactions_table | ||
| .get(&token_id) | ||
| .map_or_else(Vec::new, |transactions| { | ||
| transactions | ||
| .iter() | ||
| .rev() | ||
| .flat_map(|(_, txs)| txs.iter().map(|tx| &tx.0)) | ||
| .flat_map(|tx| (tx.tx_global_index < tx_global_index).then_some(tx)) | ||
| .cloned() | ||
| .take(len as usize) | ||
| .collect() | ||
| })) |
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 doesn't look correct:
- The transactions in the inner set are not ordered by the global index. So, if there are more than
lentransactions at a particular height, the function will return arbitrarylentransactions, not the latestlenones. - Even if the inner set was ordered by the global index first, the initial
.iter().rev().flat_map(...)would still mess it up, because it produces an iterator that goes backwards over the heights, but forward over transactions. So, the transactions wouldn't be ordered by the global index anyway and taking the firstlenof them is wrong.
P.S. It'd be nice to have a test for this in the storage test suite.
Add a new endpoint that will return a paginated list of transaction IDs that involve a certain token_id