Skip to content

Conversation

@OBorce
Copy link
Contributor

@OBorce OBorce commented Nov 16, 2025

Add a new endpoint that will return a paginated list of transaction IDs that involve a certain token_id

@OBorce OBorce force-pushed the feature/api-server-token-transactions branch 2 times, most recently from c55d771 to d804d77 Compare November 18, 2025 02:21
Comment on lines 997 to 1007
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(),
);
Copy link
Contributor

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.

Copy link
Contributor Author

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")
Copy link
Contributor

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.

Copy link
Contributor Author

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

Comment on lines 609 to 625
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()))?;
}
Copy link
Contributor

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:

  1. 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).
  2. calling set_token_transactions_at_height twice for the same height will keep the previous data.

So,

  1. plz make both implementations consistent; but first plz write some tests that check the produces indices.
  2. 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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

?

@OBorce OBorce force-pushed the feature/api-server-token-transactions branch from d804d77 to 3eccbe6 Compare November 24, 2025 05:53
@OBorce OBorce changed the base branch from master to add_changelogs November 24, 2025 05:53
@OBorce OBorce force-pushed the feature/api-server-token-transactions branch 3 times, most recently from 0f040e3 to a856b93 Compare November 24, 2025 09:04
Base automatically changed from add_changelogs to master November 24, 2025 10:31
@OBorce OBorce force-pushed the feature/api-server-token-transactions branch from a856b93 to 1d47121 Compare November 24, 2025 15:12
@OBorce OBorce force-pushed the feature/api-server-token-transactions branch from 1d47121 to 0f1d451 Compare November 24, 2025 15:41
Comment on lines 611 to 642

/// 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>;
Copy link
Contributor

Choose a reason for hiding this comment

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

  1. "is only ordered by block height and are not continuous" - inconsistent "is" and "are".

  2. 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,
Copy link
Contributor

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.

Comment on lines 609 to 625
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()))?;
}
Copy link
Contributor

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.

?

Comment on lines +512 to +513
SELECT tx_global_index, transaction_id
FROM ml.token_transactions
Copy link
Contributor

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:

  1. Can we remove tx_global_index from ml.token_transactions altogether and fetch it from ml.transactions instead?
  2. If the above adds too much overhead, then perhaps set_token_transactions_at_height should accept tx_global_index values together with tx ids, instead of relying on them being automatically generated by the storage. As I can see, in places where set_token_transactions_at_height is called, the tx_global_indexs are already known.

Copy link
Contributor Author

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
Copy link
Contributor

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.

Copy link
Contributor Author

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));
Copy link
Contributor

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();
Copy link
Contributor

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(
Copy link
Contributor

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
Copy link
Contributor

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)

Comment on lines +1448 to +1453
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));
Copy link
Contributor

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}")]
Copy link
Contributor

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`.
Copy link
Contributor

Choose a reason for hiding this comment

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

The tx_global_index and 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 _;
Copy link
Contributor

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?

Comment on lines +619 to +620
ON CONFLICT (token_id, transaction_id, block_height)
DO NOTHING;
Copy link
Contributor

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.

Comment on lines +203 to +215
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()
}))
Copy link
Contributor

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:

  1. The transactions in the inner set are not ordered by the global index. So, if there are more than len transactions at a particular height, the function will return arbitrary len transactions, not the latest len ones.
  2. 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 first len of them is wrong.

P.S. It'd be nice to have a test for this in the storage test suite.

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.

3 participants