Skip to content
25 changes: 16 additions & 9 deletions api-server/api-server-common/src/storage/impls/in_memory/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ impl Ord for TokenTransactionOrderedByTxId {
}
}

/// An offset that doesn't fit a usize is past the end of any in-memory result set.
/// Skipping by usize::MAX yields an empty page instead of wrapping to a small number
/// (which would return the first page) on targets with a narrower pointer width.
fn offset_to_usize(offset: u64) -> usize {
usize::try_from(offset).unwrap_or(usize::MAX)
}

#[derive(Debug, Clone)]
/// An in-memory api-server storage, meant for tests.
///
Expand Down Expand Up @@ -266,7 +273,7 @@ impl ApiServerInMemoryStorage {
&self,
token_id: TokenId,
len: u32,
tx_global_index: u64,
offset: u64,
) -> Result<Vec<TokenTransaction>, ApiServerStorageError> {
Ok(self
.token_transactions_table
Expand All @@ -280,7 +287,7 @@ impl ApiServerInMemoryStorage {
txs.sort_by_key(|tx| std::cmp::Reverse(tx.tx_global_index));
txs
})
.flat_map(|tx| (tx.tx_global_index < tx_global_index).then_some(tx))
.skip(offset_to_usize(offset))
.cloned()
.take(len as usize)
.collect()
Expand Down Expand Up @@ -333,7 +340,7 @@ impl ApiServerInMemoryStorage {
},
)
})
.skip(offset as usize)
.skip(offset_to_usize(offset))
.take(len as usize)
.collect())
}
Expand Down Expand Up @@ -525,7 +532,7 @@ impl ApiServerInMemoryStorage {
offset: u64,
) -> Result<Vec<(OrderId, Order)>, ApiServerStorageError> {
let len = len as usize;
let offset = offset as usize;
let offset = offset_to_usize(offset);

if offset >= self.orders_table.len() {
return Ok(vec![]);
Expand Down Expand Up @@ -558,7 +565,7 @@ impl ApiServerInMemoryStorage {
offset: u64,
) -> Result<Vec<(OrderId, Order)>, ApiServerStorageError> {
let len = len as usize;
let offset = offset as usize;
let offset = offset_to_usize(offset);

let mut order_data: Vec<_> = self
.orders_table
Expand Down Expand Up @@ -687,7 +694,7 @@ impl ApiServerInMemoryStorage {
offset: u64,
) -> Result<Vec<(PoolId, PoolDataWithExtraInfo)>, ApiServerStorageError> {
let len = len as usize;
let offset = offset as usize;
let offset = offset_to_usize(offset);
let decorated = self.sorted_live_pools()?;
if offset >= decorated.len() {
return Ok(vec![]);
Expand Down Expand Up @@ -738,7 +745,7 @@ impl ApiServerInMemoryStorage {
offset: u64,
) -> Result<Vec<(PoolId, PoolDataWithExtraInfo)>, ApiServerStorageError> {
let len = len as usize;
let offset = offset as usize;
let offset = offset_to_usize(offset);
// deepest pledge first, ties broken by the encoded pool address (byte order),
// exactly like the postgres listing; the balance and the sort key are decoded
// once per entry, and a pool whose stored balance fails to decode is surfaced
Expand Down Expand Up @@ -952,7 +959,7 @@ impl ApiServerInMemoryStorage {
.fungible_token_data
.keys()
.chain(self.nft_token_issuances.keys())
.skip(offset as usize)
.skip(offset_to_usize(offset))
.take(len as usize)
.copied()
.collect())
Expand Down Expand Up @@ -983,7 +990,7 @@ impl ApiServerInMemoryStorage {
String::from_utf8_lossy(value_ticker).to_ascii_lowercase();
(lowercased_value_ticker.contains(&lowercased_ticker)).then_some(key)
}))
.skip(offset as usize)
.skip(offset_to_usize(offset))
.take(len as usize)
.copied()
.collect())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,9 @@ impl ApiServerStorageRead for ApiServerInMemoryStorageTransactionalRo<'_> {
&self,
token_id: TokenId,
len: u32,
tx_global_index: u64,
offset: u64,
) -> Result<Vec<TokenTransaction>, ApiServerStorageError> {
self.transaction.get_token_transactions(token_id, len, tx_global_index)
self.transaction.get_token_transactions(token_id, len, offset)
}

async fn get_block(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -366,9 +366,9 @@ impl ApiServerStorageRead for ApiServerInMemoryStorageTransactionalRw<'_> {
&self,
token_id: TokenId,
len: u32,
tx_global_index: u64,
offset: u64,
) -> Result<Vec<TokenTransaction>, ApiServerStorageError> {
self.transaction.get_token_transactions(token_id, len, tx_global_index)
self.transaction.get_token_transactions(token_id, len, offset)
}

async fn get_latest_blocktimestamps(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,13 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> {
.map_err(|_| ApiServerStorageError::TxGlobalIndexTooHigh(tx_global_index))
}

/// An offset that doesn't fit a bigint is past the end of any result set.
/// Postgres rejects a negative OFFSET, so callers map overflow to an empty
/// page instead of wrapping.
fn offset_to_bigint(offset: u64) -> Option<i64> {
i64::try_from(offset).ok()
}

/// Whether the schema exists and carries a storage version. Note that a `true`
/// result does NOT mean the stored version is current: the caller must compare
/// [`Self::get_storage_version`] with the current version and reinitialize when
Expand Down Expand Up @@ -783,21 +790,24 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> {
&self,
token_id: TokenId,
len: u32,
tx_global_index: u64,
offset: u64,
) -> Result<Vec<TokenTransaction>, ApiServerStorageError> {
let tx_global_index = Self::tx_global_index_to_postgres_friendly(tx_global_index)?;
let Some(offset) = Self::offset_to_bigint(offset) else {
return Ok(Vec::new());
};
let len = len as i64;
let rows = self
.tx
.query(
r#"
SELECT tx_global_index, transaction_id
FROM ml.token_transactions
WHERE token_id = $1 AND tx_global_index < $2
WHERE token_id = $1
ORDER BY tx_global_index DESC
OFFSET $2
LIMIT $3;
"#,
&[&token_id.encode(), &tx_global_index, &len],
&[&token_id.encode(), &offset, &len],
)
.await
.map_err(|e| ApiServerStorageError::LowLevelStorageError(e.to_string()))?;
Expand Down Expand Up @@ -2193,7 +2203,9 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> {
chain_config: &ChainConfig,
) -> Result<Vec<(PoolId, PoolDataWithExtraInfo)>, ApiServerStorageError> {
let len = len as i64;
let offset = offset as i64;
let Some(offset) = Self::offset_to_bigint(offset) else {
return Ok(Vec::new());
};
let rows = self
.tx
.query(
Expand Down Expand Up @@ -2297,7 +2309,9 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> {
chain_config: &ChainConfig,
) -> Result<Vec<(PoolId, PoolDataWithExtraInfo)>, ApiServerStorageError> {
let len = len as i64;
let offset = offset as i64;
let Some(offset) = Self::offset_to_bigint(offset) else {
return Ok(Vec::new());
};
let rows = self
.tx
.query(
Expand Down Expand Up @@ -2481,7 +2495,9 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> {
offset: u64,
) -> Result<Vec<TransactionWithBlockInfo>, ApiServerStorageError> {
let len = len as i64;
let offset = offset as i64;
let Some(offset) = Self::offset_to_bigint(offset) else {
return Ok(Vec::new());
};
let rows = self
.tx
.query(
Expand Down Expand Up @@ -3088,9 +3104,7 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> {
offset: u64,
) -> Result<Vec<TokenId>, ApiServerStorageError> {
let len = len as i64;
// An offset too big for a bigint is past the end of any result set. Postgres rejects
// a negative OFFSET, so return an empty page rather than let the cast wrap.
let Ok(offset) = i64::try_from(offset) else {
let Some(offset) = Self::offset_to_bigint(offset) else {
return Ok(Vec::new());
};
self.tx
Expand Down Expand Up @@ -3126,9 +3140,7 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> {
ticker: &str,
) -> Result<Vec<TokenId>, ApiServerStorageError> {
let len = len as i64;
// An offset too big for a bigint is past the end of any result set. Postgres rejects
// a negative OFFSET, so return an empty page rather than let the cast wrap.
let Ok(offset) = i64::try_from(offset) else {
let Some(offset) = Self::offset_to_bigint(offset) else {
return Ok(Vec::new());
};
let escaped_ticker = escape_for_like(ticker);
Expand Down Expand Up @@ -3709,7 +3721,9 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> {
chain_config: &ChainConfig,
) -> Result<Vec<(OrderId, Order)>, ApiServerStorageError> {
let len = len as i64;
let offset = offset as i64;
let Some(offset) = Self::offset_to_bigint(offset) else {
return Ok(Vec::new());
};
self.tx
.query(
r#"
Expand Down Expand Up @@ -3742,7 +3756,9 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> {
chain_config: &ChainConfig,
) -> Result<Vec<(OrderId, Order)>, ApiServerStorageError> {
let len = len as i64;
let offset = offset as i64;
let Some(offset) = Self::offset_to_bigint(offset) else {
return Ok(Vec::new());
};
self.tx
.query(
r#"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,10 @@ impl ApiServerStorageRead for ApiServerPostgresTransactionalRo<'_> {
&self,
token_id: TokenId,
len: u32,
tx_global_index: u64,
offset: u64,
) -> Result<Vec<TokenTransaction>, ApiServerStorageError> {
let conn = QueryFromConnection::new(self.connection.as_ref().expect(CONN_ERR));
let res = conn.get_token_transactions(token_id, len, tx_global_index).await?;
let res = conn.get_token_transactions(token_id, len, offset).await?;

Ok(res)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -510,10 +510,10 @@ impl ApiServerStorageRead for ApiServerPostgresTransactionalRw<'_> {
&self,
token_id: TokenId,
len: u32,
tx_global_index: u64,
offset: u64,
) -> Result<Vec<TokenTransaction>, ApiServerStorageError> {
let conn = QueryFromConnection::new(self.connection.as_ref().expect(CONN_ERR));
let res = conn.get_token_transactions(token_id, len, tx_global_index).await?;
let res = conn.get_token_transactions(token_id, len, offset).await?;

Ok(res)
}
Expand Down
8 changes: 5 additions & 3 deletions api-server/api-server-common/src/storage/storage_api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -652,13 +652,15 @@ pub trait ApiServerStorageRead: Sync {
) -> Result<Vec<Id<Transaction>>, ApiServerStorageError>;

/// 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` is not continuous for a specific `token_id`.
/// and starting `offset` entries into the full, newest-first list for the token. An offset
/// greater than or equal to the number of transactions for the token yields an empty page.
/// As with the other offset-paginated queries, the cost of a request grows linearly with the
/// offset.
async fn get_token_transactions(
&self,
token_id: TokenId,
len: u32,
tx_global_index: u64,
offset: u64,
) -> Result<Vec<TokenTransaction>, ApiServerStorageError>;

async fn get_best_block(&self) -> Result<BlockAuxData, ApiServerStorageError>;
Expand Down
14 changes: 7 additions & 7 deletions api-server/scanner-lib/src/sync/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1505,7 +1505,7 @@ async fn token_transactions_storage_check(#[case] seed: Seed) {
// Check count: Issue(1) + Mint(1) = 2
let db_tx = local_state.storage().transaction_ro().await.unwrap();
let txs = db_tx
.get_token_transactions(token_id, 100, u64::MAX)
.get_token_transactions(token_id, 100, 0)
.await
.unwrap()
.into_iter()
Expand Down Expand Up @@ -1597,7 +1597,7 @@ async fn token_transactions_storage_check(#[case] seed: Seed) {
// Verify Storage: 2 previous + 4 new = 6 transactions
let db_tx = local_state.storage().transaction_ro().await.unwrap();
let txs = db_tx
.get_token_transactions(token_id, 100, u64::MAX)
.get_token_transactions(token_id, 100, 0)
.await
.unwrap()
.into_iter()
Expand Down Expand Up @@ -1638,7 +1638,7 @@ async fn token_transactions_storage_check(#[case] seed: Seed) {
token_txs.insert(tx_spend_id);
let db_tx = local_state.storage().transaction_ro().await.unwrap();
let txs = db_tx
.get_token_transactions(token_id, 100, u64::MAX)
.get_token_transactions(token_id, 100, 0)
.await
.unwrap()
.into_iter()
Expand Down Expand Up @@ -1693,7 +1693,7 @@ async fn token_transactions_storage_check(#[case] seed: Seed) {
// Verify Storage: Order creation involves the token (in 'Give'), so it should be indexed.
let db_tx = local_state.storage().transaction_ro().await.unwrap();
let txs = db_tx
.get_token_transactions(token_id, 100, u64::MAX)
.get_token_transactions(token_id, 100, 0)
.await
.unwrap()
.into_iter()
Expand Down Expand Up @@ -1743,7 +1743,7 @@ async fn token_transactions_storage_check(#[case] seed: Seed) {
// Verify Storage: Fill Order should be indexed for the token
let db_tx = local_state.storage().transaction_ro().await.unwrap();
let txs = db_tx
.get_token_transactions(token_id, 100, u64::MAX)
.get_token_transactions(token_id, 100, 0)
.await
.unwrap()
.into_iter()
Expand Down Expand Up @@ -1785,7 +1785,7 @@ async fn token_transactions_storage_check(#[case] seed: Seed) {
// Verify Storage: Conclude Order should be indexed for the token
let db_tx = local_state.storage().transaction_ro().await.unwrap();
let txs = db_tx
.get_token_transactions(token_id, 100, u64::MAX)
.get_token_transactions(token_id, 100, 0)
.await
.unwrap()
.into_iter()
Expand Down Expand Up @@ -1827,7 +1827,7 @@ async fn token_transactions_storage_check(#[case] seed: Seed) {
// Verify Storage: Conclude Order should be indexed for the token
let db_tx = local_state.storage().transaction_ro().await.unwrap();
let txs = db_tx
.get_token_transactions(token_id, 100, u64::MAX)
.get_token_transactions(token_id, 100, 0)
.await
.unwrap()
.into_iter()
Expand Down
7 changes: 2 additions & 5 deletions api-server/stack-test-suite/tests/v2/token_transactions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ async fn ok(#[case] seed: Seed) {
let (token_id, expected_transactions) = rx.await.unwrap();
let num_tx = expected_transactions.len();

let url = format!("/api/v2/token/{token_id}/transactions?offset=999&items={num_tx}");
let url = format!("/api/v2/token/{token_id}/transactions?offset=0&items={num_tx}");

let response = reqwest::get(format!("http://{}:{}{url}", addr.ip(), addr.port()))
.await
Expand Down Expand Up @@ -296,10 +296,7 @@ async fn ok(#[case] seed: Seed) {
let offset = rng.random_range(1..num_tx);
let items = num_tx - offset;

let tx_global_index = &arr_body[offset - 1].get("tx_global_index").unwrap();
eprintln!("tx_global_index: '{tx_global_index}'");
let url =
format!("/api/v2/token/{token_id}/transactions?offset={tx_global_index}&items={items}");
let url = format!("/api/v2/token/{token_id}/transactions?offset={offset}&items={items}");

let response = reqwest::get(format!("http://{}:{}{url}", addr.ip(), addr.port()))
.await
Expand Down
Loading
Loading