Skip to content

Commit 682e03f

Browse files
authored
Merge pull request #1990 from mintlayer/feature/api-server-ticker-partial-matches
API Server, add partial matching for token ticker
2 parents e9009c2 + c4de595 commit 682e03f

File tree

10 files changed

+104
-44
lines changed

10 files changed

+104
-44
lines changed

api-server/api-server-common/src/storage/impls/in_memory/mod.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -714,19 +714,26 @@ impl ApiServerInMemoryStorage {
714714
&self,
715715
len: u32,
716716
offset: u64,
717-
ticker: &[u8],
717+
ticker: &str,
718718
) -> Result<Vec<TokenId>, ApiServerStorageError> {
719+
let lowercased_ticker = ticker.to_ascii_lowercase();
720+
719721
Ok(self
720722
.fungible_token_data
721723
.iter()
722724
.filter_map(|(key, value)| {
723-
(value.values().last().expect("not empty").token_ticker == ticker).then_some(key)
725+
let token_ticker = &value.values().last().expect("not empty").token_ticker;
726+
let lowercased_token_ticker =
727+
String::from_utf8_lossy(token_ticker).to_ascii_lowercase();
728+
(lowercased_token_ticker.contains(&lowercased_ticker)).then_some(key)
724729
})
725730
.chain(self.nft_token_issuances.iter().filter_map(|(key, value)| {
726731
let value_ticker = match &value.values().last().expect("not empty").nft {
727732
NftIssuance::V0(data) => data.metadata.ticker(),
728733
};
729-
(value_ticker == ticker).then_some(key)
734+
let lowercased_value_ticker =
735+
String::from_utf8_lossy(value_ticker).to_ascii_lowercase();
736+
(lowercased_value_ticker.contains(&lowercased_ticker)).then_some(key)
730737
}))
731738
.skip(offset as usize)
732739
.take(len as usize)

api-server/api-server-common/src/storage/impls/in_memory/transactional/read.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ impl ApiServerStorageRead for ApiServerInMemoryStorageTransactionalRo<'_> {
268268
&self,
269269
len: u32,
270270
offset: u64,
271-
ticker: &[u8],
271+
ticker: &str,
272272
) -> Result<Vec<TokenId>, ApiServerStorageError> {
273273
self.transaction.get_token_ids_by_ticker(len, offset, ticker)
274274
}

api-server/api-server-common/src/storage/impls/in_memory/transactional/write.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -527,7 +527,7 @@ impl ApiServerStorageRead for ApiServerInMemoryStorageTransactionalRw<'_> {
527527
&self,
528528
len: u32,
529529
offset: u64,
530-
ticker: &[u8],
530+
ticker: &str,
531531
) -> Result<Vec<TokenId>, ApiServerStorageError> {
532532
self.transaction.get_token_ids_by_ticker(len, offset, ticker)
533533
}

api-server/api-server-common/src/storage/impls/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
// See the License for the specific language governing permissions and
1414
// limitations under the License.
1515

16-
pub const CURRENT_STORAGE_VERSION: u32 = 22;
16+
pub const CURRENT_STORAGE_VERSION: u32 = 23;
1717

1818
pub mod in_memory;
1919
pub mod postgres;

api-server/api-server-common/src/storage/impls/postgres/queries.rs

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -830,7 +830,7 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> {
830830
"CREATE TABLE ml.fungible_token (
831831
token_id bytea NOT NULL,
832832
block_height bigint NOT NULL,
833-
ticker bytea NOT NULL,
833+
ticker TEXT NOT NULL,
834834
authority TEXT NOT NULL,
835835
issuance bytea NOT NULL,
836836
PRIMARY KEY (token_id, block_height)
@@ -840,7 +840,7 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> {
840840

841841
// index when searching for token tickers
842842
self.just_execute(
843-
"CREATE INDEX fungible_token_ticker_index ON ml.fungible_token USING HASH (ticker);",
843+
"CREATE INDEX fungible_token_ticker_case_insensitive_idx ON ml.fungible_token (ticker text_pattern_ops);"
844844
)
845845
.await?;
846846

@@ -854,7 +854,7 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> {
854854
"CREATE TABLE ml.nft_issuance (
855855
nft_id bytea NOT NULL,
856856
block_height bigint NOT NULL,
857-
ticker bytea NOT NULL,
857+
ticker TEXT NOT NULL,
858858
issuance bytea NOT NULL,
859859
owner bytea,
860860
PRIMARY KEY (nft_id, block_height)
@@ -864,7 +864,7 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> {
864864

865865
// index when searching for token tickers
866866
self.just_execute(
867-
"CREATE INDEX nft_token_ticker_index ON ml.nft_issuance USING HASH (ticker);",
867+
"CREATE INDEX nft_token_ticker_case_insensitive_idx ON ml.nft_issuance (ticker text_pattern_ops);"
868868
)
869869
.await?;
870870

@@ -2215,7 +2215,7 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> {
22152215
&token_id.encode(),
22162216
&height,
22172217
&issuance.encode(),
2218-
&issuance.token_ticker,
2218+
&String::from_utf8(issuance.token_ticker).expect("Ticker is valid UTF-8 string"),
22192219
&authority.into_string(),
22202220
],
22212221
)
@@ -2254,7 +2254,7 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> {
22542254
&token_id.encode(),
22552255
&height,
22562256
&issuance.encode(),
2257-
&issuance.token_ticker,
2257+
&String::from_utf8(issuance.token_ticker).expect("Ticker is valid UTF-8 string"),
22582258
&authority.into_string(),
22592259
],
22602260
)
@@ -2397,34 +2397,36 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> {
23972397
&self,
23982398
len: u32,
23992399
offset: u64,
2400-
ticker: &[u8],
2400+
ticker: &str,
24012401
) -> Result<Vec<TokenId>, ApiServerStorageError> {
24022402
let len = len as i64;
24032403
let offset = offset as i64;
2404+
let escaped_ticker = escape_for_like(ticker);
2405+
let ticker_patern = format!("%{escaped_ticker}%");
24042406
self.tx
24052407
.query(
24062408
r#"
24072409
WITH count_tokens AS (
2408-
SELECT count(token_id) FROM ml.fungible_token WHERE ticker = $3
2410+
SELECT count(token_id) FROM ml.fungible_token WHERE ticker ILIKE $3
24092411
)
24102412
(SELECT token_id
24112413
FROM ml.fungible_token
2412-
WHERE ticker = $3
2414+
WHERE ticker ILIKE $3
24132415
ORDER BY token_id
24142416
OFFSET $1
24152417
LIMIT $2)
24162418
UNION ALL
24172419
(SELECT nft_id
24182420
FROM ml.nft_issuance
2419-
WHERE ticker = $3
2421+
WHERE ticker ILIKE $3
24202422
ORDER BY nft_id
24212423
OFFSET GREATEST($1 - (SELECT * FROM count_tokens), 0)
24222424
LIMIT CASE
24232425
WHEN ($1 - (SELECT * FROM count_tokens) >= -$2)
24242426
THEN ($2 + $1 - (SELECT * FROM count_tokens))
24252427
ELSE 0 END);
24262428
"#,
2427-
&[&offset, &len, &ticker],
2429+
&[&offset, &len, &ticker_patern],
24282430
)
24292431
.await
24302432
.map_err(|e| ApiServerStorageError::LowLevelStorageError(e.to_string()))?
@@ -2626,7 +2628,13 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> {
26262628
self.tx
26272629
.execute(
26282630
"INSERT INTO ml.nft_issuance (nft_id, block_height, issuance, ticker, owner) VALUES ($1, $2, $3, $4, $5);",
2629-
&[&token_id.encode(), &height, &issuance.encode(), ticker, &owner.encode()],
2631+
&[
2632+
&token_id.encode(),
2633+
&height,
2634+
&issuance.encode(),
2635+
&String::from_utf8(ticker.clone()).expect("Ticker is valid UTF-8 string"),
2636+
&owner.encode()
2637+
],
26302638
)
26312639
.await
26322640
.map_err(|e| ApiServerStorageError::LowLevelStorageError(e.to_string()))?;
@@ -2977,3 +2985,20 @@ fn amount_to_str(amount: Amount) -> String {
29772985
}
29782986
amount_str
29792987
}
2988+
2989+
// Escapes a string for use in a SQL LIKE clause
2990+
fn escape_for_like(input: &str) -> String {
2991+
let mut escaped = String::with_capacity(input.len());
2992+
for c in input.chars() {
2993+
match c {
2994+
'%' | '_' | '\\' => {
2995+
escaped.push('\\');
2996+
escaped.push(c);
2997+
}
2998+
_ => {
2999+
escaped.push(c);
3000+
}
3001+
}
3002+
}
3003+
escaped
3004+
}

api-server/api-server-common/src/storage/impls/postgres/transactional/read.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@ impl ApiServerStorageRead for ApiServerPostgresTransactionalRo<'_> {
378378
&self,
379379
len: u32,
380380
offset: u64,
381-
ticker: &[u8],
381+
ticker: &str,
382382
) -> Result<Vec<TokenId>, ApiServerStorageError> {
383383
let conn = QueryFromConnection::new(self.connection.as_ref().expect(CONN_ERR));
384384
let res = conn.get_token_ids_by_ticker(len, offset, ticker).await?;

api-server/api-server-common/src/storage/impls/postgres/transactional/write.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -714,7 +714,7 @@ impl ApiServerStorageRead for ApiServerPostgresTransactionalRw<'_> {
714714
&self,
715715
len: u32,
716716
offset: u64,
717-
ticker: &[u8],
717+
ticker: &str,
718718
) -> Result<Vec<TokenId>, ApiServerStorageError> {
719719
let conn = QueryFromConnection::new(self.connection.as_ref().expect(CONN_ERR));
720720
let res = conn.get_token_ids_by_ticker(len, offset, ticker).await?;

api-server/api-server-common/src/storage/storage_api/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -754,7 +754,7 @@ pub trait ApiServerStorageRead: Sync {
754754
&self,
755755
len: u32,
756756
offset: u64,
757-
ticker: &[u8],
757+
ticker: &str,
758758
) -> Result<Vec<TokenId>, ApiServerStorageError>;
759759

760760
async fn get_statistic(

api-server/storage-test-suite/src/basic.rs

Lines changed: 50 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1565,9 +1565,9 @@ where
15651565
let (_, pk) = PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr);
15661566
let random_destination = Destination::PublicKeyHash(PublicKeyHash::from(&pk));
15671567

1568-
let token_ticker = "XXXX".as_bytes().to_vec();
1568+
let token_ticker = String::from("AB\\CD");
15691569
let token_data = FungibleTokenData {
1570-
token_ticker: token_ticker.clone(),
1570+
token_ticker: token_ticker.as_bytes().to_vec().clone(),
15711571
number_of_decimals: rng.gen_range(1..18),
15721572
metadata_uri: "http://uri".as_bytes().to_vec(),
15731573
circulating_supply: Amount::ZERO,
@@ -1602,7 +1602,7 @@ where
16021602
creator: None,
16031603
name: "Name".as_bytes().to_vec(),
16041604
description: "SomeNFT".as_bytes().to_vec(),
1605-
ticker: token_ticker.clone(),
1605+
ticker: token_ticker.as_bytes().to_vec().clone(),
16061606
icon_uri: DataOrNoVec::from(None),
16071607
additional_metadata_uri: DataOrNoVec::from(None),
16081608
media_uri: DataOrNoVec::from(None),
@@ -1629,24 +1629,20 @@ where
16291629
.unwrap();
16301630

16311631
// will return all token and nft ids
1632-
let ids = db_tx.get_token_ids(6, 0).await.unwrap();
1633-
assert!(ids.contains(&random_token_id1));
1634-
assert!(ids.contains(&random_token_id2));
1635-
assert!(ids.contains(&random_token_id3));
1632+
let all_ids = db_tx.get_token_ids(6, 0).await.unwrap();
1633+
assert!(all_ids.contains(&random_token_id1));
1634+
assert!(all_ids.contains(&random_token_id2));
1635+
assert!(all_ids.contains(&random_token_id3));
16361636

1637-
assert!(ids.contains(&random_token_id4));
1638-
assert!(ids.contains(&random_token_id5));
1639-
assert!(ids.contains(&random_token_id6));
1637+
assert!(all_ids.contains(&random_token_id4));
1638+
assert!(all_ids.contains(&random_token_id5));
1639+
assert!(all_ids.contains(&random_token_id6));
16401640

16411641
// will return all token and nft ids
16421642
let ids = db_tx.get_token_ids_by_ticker(6, 0, &token_ticker).await.unwrap();
1643-
assert!(ids.contains(&random_token_id1));
1644-
assert!(ids.contains(&random_token_id2));
1645-
assert!(ids.contains(&random_token_id3));
1646-
1647-
assert!(ids.contains(&random_token_id4));
1648-
assert!(ids.contains(&random_token_id5));
1649-
assert!(ids.contains(&random_token_id6));
1643+
for id in &all_ids {
1644+
assert!(ids.contains(id));
1645+
}
16501646

16511647
// will return the tokens first
16521648
let ids = db_tx.get_token_ids(3, 0).await.unwrap();
@@ -1672,8 +1668,34 @@ where
16721668
assert!(ids.contains(&random_token_id5));
16731669
assert!(ids.contains(&random_token_id6));
16741670

1675-
let ids = db_tx.get_token_ids_by_ticker(0, 6, "NOT_FOUND".as_bytes()).await.unwrap();
1671+
let ids = db_tx.get_token_ids_by_ticker(0, 6, "NOT_FOUND").await.unwrap();
16761672
assert!(ids.is_empty());
1673+
1674+
// will return all token and nft ids for partial match
1675+
for partial_ticker in get_all_substrings(&token_ticker) {
1676+
let ids = db_tx.get_token_ids_by_ticker(6, 0, partial_ticker).await.unwrap();
1677+
for id in &all_ids {
1678+
assert!(ids.contains(id));
1679+
}
1680+
1681+
// check lowercase as well
1682+
let lowercase_partial_ticker = partial_ticker.to_ascii_lowercase();
1683+
let ids2 =
1684+
db_tx.get_token_ids_by_ticker(6, 0, &lowercase_partial_ticker).await.unwrap();
1685+
assert_eq!(ids, ids2);
1686+
}
1687+
1688+
// Try out patterns inside the ticker string, they should be escaped and not work
1689+
let ids = db_tx.get_token_ids_by_ticker(6, 0, "A%D").await.unwrap();
1690+
assert!(ids.is_empty());
1691+
1692+
let ids = db_tx.get_token_ids_by_ticker(6, 0, "A_").await.unwrap();
1693+
assert!(ids.is_empty());
1694+
1695+
for c in ['*', '+', '?'] {
1696+
let ids = db_tx.get_token_ids_by_ticker(6, 0, &format!("A{c}")).await.unwrap();
1697+
assert!(ids.is_empty());
1698+
}
16771699
}
16781700

16791701
// test coin and token statistics
@@ -2006,3 +2028,13 @@ where
20062028
]
20072029
.into_iter()
20082030
}
2031+
2032+
fn get_all_substrings(s: &str) -> Vec<&str> {
2033+
let mut substrings = Vec::new();
2034+
for i in 0..s.len() {
2035+
for j in i..s.len() {
2036+
substrings.push(s.get(i..=j).unwrap());
2037+
}
2038+
}
2039+
substrings
2040+
}

api-server/web-server/src/api/v2.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1383,11 +1383,7 @@ pub async fn token_ids_by_ticker<T: ApiServerStorage>(
13831383
logging::log::error!("internal error: {e}");
13841384
ApiServerWebServerError::ServerError(ApiServerWebServerServerError::InternalServerError)
13851385
})?
1386-
.get_token_ids_by_ticker(
1387-
offset_and_items.items,
1388-
offset_and_items.offset,
1389-
ticker.as_bytes(),
1390-
)
1386+
.get_token_ids_by_ticker(offset_and_items.items, offset_and_items.offset, &ticker)
13911387
.await
13921388
.map_err(|e| {
13931389
logging::log::error!("internal error: {e}");

0 commit comments

Comments
 (0)