From 9e3c45bad666692c3fa1c413412153bc3b782165 Mon Sep 17 00:00:00 2001 From: Oliver Anyanwu Date: Tue, 28 Jul 2026 09:27:02 +0200 Subject: [PATCH] Do not return duplicate token ids from the api server The fungible_token table is keyed on (token_id, block_height), so a token that has been updated has one row per height it changed at. The query behind the /token endpoint selected token_id without deduplicating, so such tokens were listed once per stored version. The count used for the nft offset had the same problem, since it counted rows rather than tokens. Select distinct ids and count distinct token ids instead. Also extend the storage test suite to store a token at a second height and assert that the returned ids contain no duplicates. --- .../src/storage/impls/postgres/queries.rs | 12 +++---- api-server/storage-test-suite/src/basic.rs | 32 ++++++++++++++++++- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/api-server/api-server-common/src/storage/impls/postgres/queries.rs b/api-server/api-server-common/src/storage/impls/postgres/queries.rs index 1247279429..e4bea055a7 100644 --- a/api-server/api-server-common/src/storage/impls/postgres/queries.rs +++ b/api-server/api-server-common/src/storage/impls/postgres/queries.rs @@ -2527,15 +2527,15 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> { .query( r#" WITH count_tokens AS ( - SELECT count(token_id) FROM ml.fungible_token + SELECT count(DISTINCT token_id) FROM ml.fungible_token ) - (SELECT token_id + (SELECT DISTINCT token_id FROM ml.fungible_token ORDER BY token_id OFFSET $1 LIMIT $2) UNION ALL - (SELECT nft_id + (SELECT DISTINCT nft_id FROM ml.nft_issuance ORDER BY nft_id OFFSET GREATEST($1 - (SELECT * FROM count_tokens), 0) @@ -2572,16 +2572,16 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> { .query( r#" WITH count_tokens AS ( - SELECT count(token_id) FROM ml.fungible_token WHERE ticker ILIKE $3 + SELECT count(DISTINCT token_id) FROM ml.fungible_token WHERE ticker ILIKE $3 ) - (SELECT token_id + (SELECT DISTINCT token_id FROM ml.fungible_token WHERE ticker ILIKE $3 ORDER BY token_id OFFSET $1 LIMIT $2) UNION ALL - (SELECT nft_id + (SELECT DISTINCT nft_id FROM ml.nft_issuance WHERE ticker ILIKE $3 ORDER BY nft_id diff --git a/api-server/storage-test-suite/src/basic.rs b/api-server/storage-test-suite/src/basic.rs index 664b813a8a..b31c1badf9 100644 --- a/api-server/storage-test-suite/src/basic.rs +++ b/api-server/storage-test-suite/src/basic.rs @@ -14,7 +14,10 @@ // limitations under the License. use serialization::extras::non_empty_vec::DataOrNoVec; -use std::{collections::BTreeMap, sync::Arc}; +use std::{ + collections::{BTreeMap, BTreeSet}, + sync::Arc, +}; use crate::helpers::make_trial; use crate::make_test; @@ -1858,6 +1861,33 @@ where let ids = db_tx.get_token_ids_by_ticker(6, 0, &format!("A{c}")).await.unwrap(); assert!(ids.is_empty()); } + + // Fungible token data is stored per block height, so a token that gets updated has + // a row for every height it changed at. It must still be listed only once. + db_tx + .set_fungible_token_issuance( + random_token_id1, + block_height.next_height(), + token_data.clone(), + ) + .await + .unwrap(); + + let ids = db_tx.get_token_ids(6, 0).await.unwrap(); + let unique_ids: BTreeSet<_> = ids.iter().collect(); + assert_eq!( + ids.len(), + unique_ids.len(), + "get_token_ids returned duplicates" + ); + + let ids = db_tx.get_token_ids_by_ticker(6, 0, &token_ticker).await.unwrap(); + let unique_ids: BTreeSet<_> = ids.iter().collect(); + assert_eq!( + ids.len(), + unique_ids.len(), + "get_token_ids_by_ticker returned duplicates" + ); } // test coin and token statistics