From 354f905b662488f382bd710fd15a924d4b96e2cd Mon Sep 17 00:00:00 2001 From: Boris Oncev Date: Wed, 7 Aug 2024 16:46:17 +0200 Subject: [PATCH] add route to API Server for searching token ticker --- .../src/storage/impls/in_memory/mod.rs | 24 ++++++ .../impls/in_memory/transactional/read.rs | 9 +++ .../impls/in_memory/transactional/write.rs | 9 +++ .../src/storage/impls/postgres/queries.rs | 73 +++++++++++++++++-- .../impls/postgres/transactional/read.rs | 12 +++ .../impls/postgres/transactional/write.rs | 12 +++ .../src/storage/storage_api/mod.rs | 7 ++ api-server/stack-test-suite/tests/v2/mod.rs | 1 + api-server/storage-test-suite/src/basic.rs | 30 +++++++- api-server/web-server/src/api/v2.rs | 57 +++++++++++++++ 10 files changed, 227 insertions(+), 7 deletions(-) diff --git a/api-server/api-server-common/src/storage/impls/in_memory/mod.rs b/api-server/api-server-common/src/storage/impls/in_memory/mod.rs index 0d77b83f07..75c3e905f0 100644 --- a/api-server/api-server-common/src/storage/impls/in_memory/mod.rs +++ b/api-server/api-server-common/src/storage/impls/in_memory/mod.rs @@ -552,6 +552,30 @@ impl ApiServerInMemoryStorage { .collect()) } + fn find_token_ticker( + &self, + len: u32, + offset: u32, + ticker: Vec, + ) -> Result, ApiServerStorageError> { + Ok(self + .fungible_token_issuances + .iter() + .filter_map(|(key, value)| { + (value.values().last().expect("not empty").token_ticker == ticker).then_some(key) + }) + .chain(self.nft_token_issuances.iter().filter_map(|(key, value)| { + let value_ticker = match &value.values().last().expect("not empty") { + NftIssuance::V0(data) => data.metadata.ticker(), + }; + (*value_ticker == ticker).then_some(key) + })) + .skip(offset as usize) + .take(len as usize) + .copied() + .collect()) + } + fn get_statistic( &self, statistic: CoinOrTokenStatistic, diff --git a/api-server/api-server-common/src/storage/impls/in_memory/transactional/read.rs b/api-server/api-server-common/src/storage/impls/in_memory/transactional/read.rs index 911da386ef..7b769fc6c3 100644 --- a/api-server/api-server-common/src/storage/impls/in_memory/transactional/read.rs +++ b/api-server/api-server-common/src/storage/impls/in_memory/transactional/read.rs @@ -236,6 +236,15 @@ impl<'t> ApiServerStorageRead for ApiServerInMemoryStorageTransactionalRo<'t> { self.transaction.get_token_ids(len, offset) } + async fn find_token_ticker( + &self, + len: u32, + offset: u32, + ticker: Vec, + ) -> Result, ApiServerStorageError> { + self.transaction.find_token_ticker(len, offset, ticker) + } + async fn get_statistic( &self, statistic: CoinOrTokenStatistic, diff --git a/api-server/api-server-common/src/storage/impls/in_memory/transactional/write.rs b/api-server/api-server-common/src/storage/impls/in_memory/transactional/write.rs index 5169638254..87489ed11b 100644 --- a/api-server/api-server-common/src/storage/impls/in_memory/transactional/write.rs +++ b/api-server/api-server-common/src/storage/impls/in_memory/transactional/write.rs @@ -459,6 +459,15 @@ impl<'t> ApiServerStorageRead for ApiServerInMemoryStorageTransactionalRw<'t> { self.transaction.get_token_ids(len, offset) } + async fn find_token_ticker( + &self, + len: u32, + offset: u32, + ticker: Vec, + ) -> Result, ApiServerStorageError> { + self.transaction.find_token_ticker(len, offset, ticker) + } + async fn get_statistic( &self, statistic: CoinOrTokenStatistic, 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 e80cf037c8..59b4fce470 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 @@ -612,22 +612,36 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> { "CREATE TABLE ml.fungible_token ( token_id bytea NOT NULL, block_height bigint NOT NULL, + ticker bytea NOT NULL, issuance bytea NOT NULL, PRIMARY KEY (token_id, block_height) );", ) .await?; + // index when searching for token tickers + self.just_execute( + "CREATE INDEX fungible_token_ticker_index ON ml.fungible_token USING HASH (ticker);", + ) + .await?; + self.just_execute( "CREATE TABLE ml.nft_issuance ( nft_id bytea NOT NULL, block_height bigint NOT NULL, + ticker bytea NOT NULL, issuance bytea NOT NULL, PRIMARY KEY (nft_id) );", ) .await?; + // index when searching for token tickers + self.just_execute( + "CREATE INDEX nft_token_ticker_index ON ml.nft_issuance USING HASH (ticker);", + ) + .await?; + self.just_execute( "CREATE TABLE ml.statistics ( statistic TEXT NOT NULL, @@ -1736,10 +1750,10 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> { self.tx .execute( - "INSERT INTO ml.fungible_token (token_id, block_height, issuance) VALUES ($1, $2, $3) + "INSERT INTO ml.fungible_token (token_id, block_height, issuance, ticker) VALUES ($1, $2, $3, $4) ON CONFLICT (token_id, block_height) DO UPDATE - SET issuance = $3;", - &[&token_id.encode(), &height, &issuance.encode()], + SET issuance = $3, ticker = $4;", + &[&token_id.encode(), &height, &issuance.encode(), &issuance.token_ticker], ) .await .map_err(|e| ApiServerStorageError::LowLevelStorageError(e.to_string()))?; @@ -1833,6 +1847,51 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> { .collect() } + pub async fn find_token_ticker( + &self, + len: u32, + offset: u32, + ticker: Vec, + ) -> Result, ApiServerStorageError> { + let len = len as i64; + let offset = offset as i64; + self.tx + .query( + r#" + WITH count_tokens AS ( + SELECT count(token_id) FROM ml.fungible_token WHERE ticker = $3 + ) + (SELECT token_id + FROM ml.fungible_token + WHERE ticker = $3 + ORDER BY token_id + OFFSET $1 + LIMIT $2) + UNION ALL + (SELECT nft_id + FROM ml.nft_issuance + WHERE ticker = $3 + ORDER BY nft_id + OFFSET GREATEST($1 - (SELECT * FROM count_tokens), 0) + LIMIT CASE + WHEN ($1 - (SELECT * FROM count_tokens) >= -$2) + THEN ($2 + $1 - (SELECT * FROM count_tokens)) + ELSE 0 END); + "#, + &[&offset, &len, &ticker], + ) + .await + .map_err(|e| ApiServerStorageError::LowLevelStorageError(e.to_string()))? + .into_iter() + .map(|row| -> Result { + let token_id: Vec = row.get(0); + let token_id = TokenId::decode_all(&mut token_id.as_slice()) + .map_err(|_| ApiServerStorageError::AddressableError)?; + Ok(token_id) + }) + .collect() + } + pub async fn get_statistic( &self, statistic: CoinOrTokenStatistic, @@ -1985,10 +2044,14 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> { ) -> Result<(), ApiServerStorageError> { let height = Self::block_height_to_postgres_friendly(block_height); + let ticker = match &issuance { + NftIssuance::V0(data) => data.metadata.ticker(), + }; + self.tx .execute( - "INSERT INTO ml.nft_issuance (nft_id, block_height, issuance) VALUES ($1, $2, $3);", - &[&token_id.encode(), &height, &issuance.encode()], + "INSERT INTO ml.nft_issuance (nft_id, block_height, issuance, ticker) VALUES ($1, $2, $3, $4);", + &[&token_id.encode(), &height, &issuance.encode(), ticker], ) .await .map_err(|e| ApiServerStorageError::LowLevelStorageError(e.to_string()))?; diff --git a/api-server/api-server-common/src/storage/impls/postgres/transactional/read.rs b/api-server/api-server-common/src/storage/impls/postgres/transactional/read.rs index c1fd69df0d..b8a73c2517 100644 --- a/api-server/api-server-common/src/storage/impls/postgres/transactional/read.rs +++ b/api-server/api-server-common/src/storage/impls/postgres/transactional/read.rs @@ -333,6 +333,18 @@ impl<'a> ApiServerStorageRead for ApiServerPostgresTransactionalRo<'a> { Ok(res) } + async fn find_token_ticker( + &self, + len: u32, + offset: u32, + ticker: Vec, + ) -> Result, ApiServerStorageError> { + let conn = QueryFromConnection::new(self.connection.as_ref().expect(CONN_ERR)); + let res = conn.find_token_ticker(len, offset, ticker).await?; + + Ok(res) + } + async fn get_statistic( &self, statistic: CoinOrTokenStatistic, diff --git a/api-server/api-server-common/src/storage/impls/postgres/transactional/write.rs b/api-server/api-server-common/src/storage/impls/postgres/transactional/write.rs index 62ca7f3c06..2875028564 100644 --- a/api-server/api-server-common/src/storage/impls/postgres/transactional/write.rs +++ b/api-server/api-server-common/src/storage/impls/postgres/transactional/write.rs @@ -617,6 +617,18 @@ impl<'a> ApiServerStorageRead for ApiServerPostgresTransactionalRw<'a> { Ok(res) } + async fn find_token_ticker( + &self, + len: u32, + offset: u32, + ticker: Vec, + ) -> Result, ApiServerStorageError> { + let conn = QueryFromConnection::new(self.connection.as_ref().expect(CONN_ERR)); + let res = conn.find_token_ticker(len, offset, ticker).await?; + + Ok(res) + } + async fn get_statistic( &self, statistic: CoinOrTokenStatistic, diff --git a/api-server/api-server-common/src/storage/storage_api/mod.rs b/api-server/api-server-common/src/storage/storage_api/mod.rs index 7782f5e095..a6a75ebf7d 100644 --- a/api-server/api-server-common/src/storage/storage_api/mod.rs +++ b/api-server/api-server-common/src/storage/storage_api/mod.rs @@ -525,6 +525,13 @@ pub trait ApiServerStorageRead: Sync { offset: u32, ) -> Result, ApiServerStorageError>; + async fn find_token_ticker( + &self, + len: u32, + offset: u32, + ticker: Vec, + ) -> Result, ApiServerStorageError>; + async fn get_statistic( &self, statistic: CoinOrTokenStatistic, diff --git a/api-server/stack-test-suite/tests/v2/mod.rs b/api-server/stack-test-suite/tests/v2/mod.rs index c5fd70bcd2..293f82eb0d 100644 --- a/api-server/stack-test-suite/tests/v2/mod.rs +++ b/api-server/stack-test-suite/tests/v2/mod.rs @@ -32,6 +32,7 @@ mod pools; mod statistics; mod token; mod token_ids; +mod token_ticker; mod transaction; mod transaction_merkle_path; mod transaction_submit; diff --git a/api-server/storage-test-suite/src/basic.rs b/api-server/storage-test-suite/src/basic.rs index f3e1622dfe..2b38cf781e 100644 --- a/api-server/storage-test-suite/src/basic.rs +++ b/api-server/storage-test-suite/src/basic.rs @@ -1182,8 +1182,9 @@ where let (_, pk) = PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); let random_destination = Destination::PublicKeyHash(PublicKeyHash::from(&pk)); + let token_ticker = "XXXX".as_bytes().to_vec(); let token_data = FungibleTokenData { - token_ticker: "XXXX".as_bytes().to_vec(), + token_ticker: token_ticker.clone(), number_of_decimals: rng.gen_range(1..18), metadata_uri: "http://uri".as_bytes().to_vec(), circulating_supply: Amount::ZERO, @@ -1217,7 +1218,7 @@ where creator: None, name: "Name".as_bytes().to_vec(), description: "SomeNFT".as_bytes().to_vec(), - ticker: "XXXX".as_bytes().to_vec(), + ticker: token_ticker.clone(), icon_uri: DataOrNoVec::from(None), additional_metadata_uri: DataOrNoVec::from(None), media_uri: DataOrNoVec::from(None), @@ -1251,17 +1252,42 @@ where assert!(ids.contains(&random_token_id5)); assert!(ids.contains(&random_token_id6)); + // will return all token and nft ids + let ids = db_tx.find_token_ticker(6, 0, token_ticker.clone()).await.unwrap(); + assert!(ids.contains(&random_token_id1)); + assert!(ids.contains(&random_token_id2)); + assert!(ids.contains(&random_token_id3)); + + assert!(ids.contains(&random_token_id4)); + assert!(ids.contains(&random_token_id5)); + assert!(ids.contains(&random_token_id6)); + // will return the tokens first let ids = db_tx.get_token_ids(3, 0).await.unwrap(); assert!(ids.contains(&random_token_id1)); assert!(ids.contains(&random_token_id2)); assert!(ids.contains(&random_token_id3)); + // will return the tokens first + let ids = db_tx.find_token_ticker(3, 0, token_ticker.clone()).await.unwrap(); + assert!(ids.contains(&random_token_id1)); + assert!(ids.contains(&random_token_id2)); + assert!(ids.contains(&random_token_id3)); + // will return the nft second let ids = db_tx.get_token_ids(3, 3).await.unwrap(); assert!(ids.contains(&random_token_id4)); assert!(ids.contains(&random_token_id5)); assert!(ids.contains(&random_token_id6)); + + // will return the nft second + let ids = db_tx.find_token_ticker(3, 3, token_ticker).await.unwrap(); + assert!(ids.contains(&random_token_id4)); + assert!(ids.contains(&random_token_id5)); + assert!(ids.contains(&random_token_id6)); + + let ids = db_tx.find_token_ticker(0, 6, "NOT_FOUND".as_bytes().to_vec()).await.unwrap(); + assert!(ids.is_empty()); } // test coin and token statistics diff --git a/api-server/web-server/src/api/v2.rs b/api-server/web-server/src/api/v2.rs index 36dbdea532..64b1ef931b 100644 --- a/api-server/web-server/src/api/v2.rs +++ b/api-server/web-server/src/api/v2.rs @@ -114,6 +114,7 @@ pub fn routes< router .route("/token", get(token_ids)) .route("/token/:id", get(token)) + .route("/token/ticker/:ticker", get(token_tickers)) .route("/nft/:id", get(nft)) } @@ -1238,3 +1239,59 @@ pub async fn token_ids( Ok(Json(serde_json::Value::Array(token_ids))) } + +pub async fn token_tickers( + Path(ticker): Path, + Query(params): Query>, + State(state): State, Arc>>, +) -> Result { + const OFFSET: &str = "offset"; + const ITEMS: &str = "items"; + const DEFAULT_NUM_ITEMS: u32 = 10; + const MAX_NUM_ITEMS: u32 = 100; + + let offset = params + .get(OFFSET) + .map(|offset| u32::from_str(offset)) + .transpose() + .map_err(|_| { + ApiServerWebServerError::ClientError(ApiServerWebServerClientError::InvalidOffset) + })? + .unwrap_or_default(); + + let items = params + .get(ITEMS) + .map(|items| u32::from_str(items)) + .transpose() + .map_err(|_| { + ApiServerWebServerError::ClientError(ApiServerWebServerClientError::InvalidNumItems) + })? + .unwrap_or(DEFAULT_NUM_ITEMS); + ensure!( + items <= MAX_NUM_ITEMS, + ApiServerWebServerError::ClientError(ApiServerWebServerClientError::InvalidNumItems) + ); + let token_ids: Vec<_> = state + .db + .transaction_ro() + .await + .map_err(|e| { + logging::log::error!("internal error: {e}"); + ApiServerWebServerError::ServerError(ApiServerWebServerServerError::InternalServerError) + })? + .find_token_ticker(items, offset, ticker.into_bytes()) + .await + .map_err(|e| { + logging::log::error!("internal error: {e}"); + ApiServerWebServerError::ServerError(ApiServerWebServerServerError::InternalServerError) + })? + .into_iter() + .map(|token_id| { + serde_json::Value::String( + Address::new(&state.chain_config, token_id).expect("addressable").into_string(), + ) + }) + .collect(); + + Ok(Json(serde_json::Value::Array(token_ids))) +}