Skip to content

Commit

Permalink
add route to API Server for searching token ticker
Browse files Browse the repository at this point in the history
  • Loading branch information
OBorce committed Aug 7, 2024
1 parent 7b6bc9b commit 354f905
Show file tree
Hide file tree
Showing 10 changed files with 227 additions and 7 deletions.
24 changes: 24 additions & 0 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 @@ -552,6 +552,30 @@ impl ApiServerInMemoryStorage {
.collect())
}

fn find_token_ticker(
&self,
len: u32,
offset: u32,
ticker: Vec<u8>,
) -> Result<Vec<TokenId>, 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<u8>,
) -> Result<Vec<TokenId>, ApiServerStorageError> {
self.transaction.find_token_ticker(len, offset, ticker)
}

async fn get_statistic(
&self,
statistic: CoinOrTokenStatistic,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<u8>,
) -> Result<Vec<TokenId>, ApiServerStorageError> {
self.transaction.find_token_ticker(len, offset, ticker)
}

async fn get_statistic(
&self,
statistic: CoinOrTokenStatistic,
Expand Down
73 changes: 68 additions & 5 deletions api-server/api-server-common/src/storage/impls/postgres/queries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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()))?;
Expand Down Expand Up @@ -1833,6 +1847,51 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> {
.collect()
}

pub async fn find_token_ticker(
&self,
len: u32,
offset: u32,
ticker: Vec<u8>,
) -> Result<Vec<TokenId>, 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<TokenId, ApiServerStorageError> {
let token_id: Vec<u8> = 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,
Expand Down Expand Up @@ -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()))?;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,18 @@ impl<'a> ApiServerStorageRead for ApiServerPostgresTransactionalRo<'a> {
Ok(res)
}

async fn find_token_ticker(
&self,
len: u32,
offset: u32,
ticker: Vec<u8>,
) -> Result<Vec<TokenId>, 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,18 @@ impl<'a> ApiServerStorageRead for ApiServerPostgresTransactionalRw<'a> {
Ok(res)
}

async fn find_token_ticker(
&self,
len: u32,
offset: u32,
ticker: Vec<u8>,
) -> Result<Vec<TokenId>, 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,
Expand Down
7 changes: 7 additions & 0 deletions api-server/api-server-common/src/storage/storage_api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,13 @@ pub trait ApiServerStorageRead: Sync {
offset: u32,
) -> Result<Vec<TokenId>, ApiServerStorageError>;

async fn find_token_ticker(
&self,
len: u32,
offset: u32,
ticker: Vec<u8>,
) -> Result<Vec<TokenId>, ApiServerStorageError>;

async fn get_statistic(
&self,
statistic: CoinOrTokenStatistic,
Expand Down
1 change: 1 addition & 0 deletions api-server/stack-test-suite/tests/v2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
30 changes: 28 additions & 2 deletions api-server/storage-test-suite/src/basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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
Expand Down
57 changes: 57 additions & 0 deletions api-server/web-server/src/api/v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}

Expand Down Expand Up @@ -1238,3 +1239,59 @@ pub async fn token_ids<T: ApiServerStorage>(

Ok(Json(serde_json::Value::Array(token_ids)))
}

pub async fn token_tickers<T: ApiServerStorage>(
Path(ticker): Path<String>,
Query(params): Query<BTreeMap<String, String>>,
State(state): State<ApiServerWebServerState<Arc<T>, Arc<impl TxSubmitClient>>>,
) -> Result<impl IntoResponse, ApiServerWebServerError> {
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)))
}

0 comments on commit 354f905

Please sign in to comment.