diff --git a/doc/API.md b/doc/API.md index e199701..9939680 100644 --- a/doc/API.md +++ b/doc/API.md @@ -229,9 +229,9 @@ For example: `{ "1": 87.882, "2": 87.882, "3": 87.882, "4": 87.882, "5": 81.129, ### `GET /colors[/:last_seen_color_id]` -Returns a list of the 25 `color_id`s +Returns a list of the 25 (`color_id`, `block_height`) pairs -if `last_seen_color_id` specified, returns a list of the next 25 `color_id`s after specified `last_seen_color_id` +if `last_seen_color_id` specified, returns a list of the next 25 pairs after specified `last_seen_color_id` ### `GET /color/:color_id` diff --git a/doc/schema.md b/doc/schema.md index 458c09f..e38718c 100644 --- a/doc/schema.md +++ b/doc/schema.md @@ -37,9 +37,6 @@ Each output results in the following new row: * `"O{txid}{vout}" → "{scriptpubkey}{value}"` -If transaction output include colored coins, it results in the following new row: - * `c{color_id}` -> ""` - When the indexer is synced up to the tip of the chain, the hash of the tip is saved as following: * `"t" → "{blockhash}"` @@ -57,6 +54,9 @@ Each spending input (except the coinbase) results in the following new rows (`S` * `"S{funding-txid:vout}{spending-txid:vin}" → ""` +If transaction output include colored coins, it results in the following new row: + * `"B{block-height}c{color-id}" → ""` (block_height is latest block height which colored coin used) + colored coin's issuances results in the following new rows (`I` is for issuing): * `"C{color-id}{issuance-height}I{issuing-txid}{value}" → ""` diff --git a/src/new_index/color.rs b/src/new_index/color.rs index b8d45a2..ee9102d 100644 --- a/src/new_index/color.rs +++ b/src/new_index/color.rs @@ -7,6 +7,8 @@ use crate::new_index::db::DBRow; use crate::new_index::schema::FullHash; use crate::util::{full_hash, Bytes}; +use super::schema::ColorIdRow; + #[derive(Serialize, Deserialize, Debug, PartialEq)] pub struct ColoredTxHistoryKey { pub color_id: ColorIdentifier, @@ -208,11 +210,10 @@ pub fn index_confirmed_colored_tx( ) { let history = colored_tx_history(tx, previous_txos_map); - rows.extend( - history.into_iter().map(|(color_id, info)| { - colored_history_row(&color_id, confirmed_height, info).into_row() - }), - ); + history.into_iter().for_each(|(color_id, info)| { + rows.push(colored_history_row(&color_id, confirmed_height, info).into_row()); + rows.push(ColorIdRow::new(confirmed_height, &color_id).into_row()); + }); } fn colored_history_row( @@ -526,13 +527,53 @@ mod tests { let mut rows = vec![]; index_confirmed_colored_tx(&tx, 10, &previous_txos_map, &mut rows); - assert_eq!(rows.len(), 4); + assert_eq!(rows.len(), 8); rows.sort_by(|a, b| a.key.cmp(&b.key)); let row0 = rows.get(0).unwrap(); let hex = hex::encode::>(row0.key.iter().cloned().collect()); // field | size | value | //---------------------|------|-----------------------------------------------------------------------| + // prefix | 1 | 'B'(0x42) | + // height | 4 | 10(0x0a000000) | + // code | 1 | 'c'(0x63) | + // color_id | 33 | c12dceb0cedd7c372c838fea8d46ae863a3c47b2ad0fb950e90ac9d531583ad35e | + assert_eq!(hex, "420a00000063c12dceb0cedd7c372c838fea8d46ae863a3c47b2ad0fb950e90ac9d531583ad35e"); + + let row1 = rows.get(1).unwrap(); + let hex = hex::encode::>(row1.key.iter().cloned().collect()); + // field | size | value | + //---------------------|------|-----------------------------------------------------------------------| + // prefix | 1 | 'B'(0x42) | + // height | 4 | 10(0x0a000000) | + // code | 1 | 'c'(0x63) | + // color_id | 33 | c12dceb0cedd7c372c838fea8d46ae863a3c47b2ad0fb950e90ac9d531583ad35e | + assert_eq!(hex, "420a00000063c12dceb0cedd7c372c838fea8d46ae863a3c47b2ad0fb950e90ac9d531583ad35e"); + + let row2 = rows.get(2).unwrap(); + let hex = hex::encode::>(row2.key.iter().cloned().collect()); + // field | size | value | + //---------------------|------|-----------------------------------------------------------------------| + // prefix | 1 | 'B'(0x42) | + // height | 4 | 10(0x0a000000) | + // code | 1 | 'c'(0x63) | + // color_id | 33 | c271c99cc3bc21757feed5b712744ebb0f770d5c41d99189f9457495747bf11050 | + assert_eq!(hex, "420a00000063c271c99cc3bc21757feed5b712744ebb0f770d5c41d99189f9457495747bf11050"); + + let row3 = rows.get(3).unwrap(); + let hex = hex::encode::>(row3.key.iter().cloned().collect()); + // field | size | value | + //---------------------|------|-----------------------------------------------------------------------| + // prefix | 1 | 'B'(0x42) | + // height | 4 | 10(0x0a000000) | + // code | 1 | 'c'(0x63) | + // color_id | 33 | c271c99cc3bc21757feed5b712744ebb0f770d5c41d99189f9457495747bf11050 | + assert_eq!(hex, "420a00000063c271c99cc3bc21757feed5b712744ebb0f770d5c41d99189f9457495747bf11050"); + + let row4 = rows.get(4).unwrap(); + let hex = hex::encode::>(row4.key.iter().cloned().collect()); + // field | size | value | + //---------------------|------|-----------------------------------------------------------------------| // prefix | 1 | 'C'(0x43) | // color_id | 33 | c12dceb0cedd7c372c838fea8d46ae863a3c47b2ad0fb950e90ac9d531583ad35e | // height | 4 | 10(0x0a000000) | @@ -541,8 +582,8 @@ mod tests { // value | 8 | 100(0x64) | assert_eq!(hex, "43c12dceb0cedd7c372c838fea8d46ae863a3c47b2ad0fb950e90ac9d531583ad35e0a0000000100000059abe954f5636c86484e5e2817d29b915e7f9a9f0294e87c438fd060694a8b1c6400000000000000"); - let row1 = rows.get(1).unwrap(); - let hex = hex::encode::>(row1.key.iter().cloned().collect()); + let row5 = rows.get(5).unwrap(); + let hex = hex::encode::>(row5.key.iter().cloned().collect()); // field | size | value | //---------------------|------|-----------------------------------------------------------------------| // prefix | 1 | 'C'(0x43) | @@ -553,8 +594,8 @@ mod tests { // value | 8 | 100(0x64) | assert_eq!(hex, "43c12dceb0cedd7c372c838fea8d46ae863a3c47b2ad0fb950e90ac9d531583ad35e0a0000000200000059abe954f5636c86484e5e2817d29b915e7f9a9f0294e87c438fd060694a8b1c6400000000000000"); - let row2 = rows.get(2).unwrap(); - let hex = hex::encode::>(row2.key.iter().cloned().collect()); + let row6 = rows.get(6).unwrap(); + let hex = hex::encode::>(row6.key.iter().cloned().collect()); // field | size | value | //---------------------|------|-----------------------------------------------------------------------| // prefix | 1 | 'C'(0x43) | @@ -565,8 +606,8 @@ mod tests { // value | 8 | 100(0x64) | assert_eq!(hex, "43c271c99cc3bc21757feed5b712744ebb0f770d5c41d99189f9457495747bf110500a0000000000000059abe954f5636c86484e5e2817d29b915e7f9a9f0294e87c438fd060694a8b1c6400000000000000"); - let row3 = rows.get(3).unwrap(); - let hex = hex::encode::>(row3.key.iter().cloned().collect()); + let row7 = rows.get(7).unwrap(); + let hex = hex::encode::>(row7.key.iter().cloned().collect()); // field | size | value | //---------------------|------|-----------------------------------------------------------------------| // prefix | 1 | 'C'(0x43) | diff --git a/src/new_index/query.rs b/src/new_index/query.rs index 4fdba1e..4c71a93 100644 --- a/src/new_index/query.rs +++ b/src/new_index/query.rs @@ -275,10 +275,19 @@ impl Query { Ok(map) } - pub fn get_colors(&self, last_seen_color_id: Option, limit: usize) -> Vec { + pub fn get_colors(&self, last_seen_color_id: Option, limit: usize) -> Vec<(ColorIdentifier, u32)> { + let (block_height, color_id) = if let Some(color_id) = last_seen_color_id { + match self.chain.get_height_by_color_id(&color_id) { + Some(height) => (height, Some(color_id)), + _ => (self.daemon.getblockchaininfo().ok().unwrap().blocks, None), + } + } else { + (self.daemon.getblockchaininfo().ok().unwrap().blocks, None) + }; + let colors = self .chain - .get_colors(last_seen_color_id, limit) + .get_colors(block_height, &color_id, limit) .expect("failed to get colors"); colors } diff --git a/src/new_index/schema.rs b/src/new_index/schema.rs index e4ca665..0927d1e 100644 --- a/src/new_index/schema.rs +++ b/src/new_index/schema.rs @@ -676,30 +676,29 @@ impl ChainQuery { update_stats(init_stats, &histories) } - pub fn get_colors(&self, last_seen_color_id: Option, limit: usize) -> Result> { - let colors: Vec = self - .store - .txstore_db() - .iter_scan(&ColorIdRow::prefix()) - .map(|row|{ - ColorIdRow::from_row(row) - }) - .map(|row| { - row.key.color_id - }) - .skip_while(|color_id| { - // skip until we reach the last_seen_color_id - last_seen_color_id.clone().map_or(false, |last_seen_color_id| last_seen_color_id != color_id.clone()) - }) - .skip(match last_seen_color_id { - Some(_) => 1, // skip the last_seen_color_id itself - None => 0, + pub fn get_colors(&self, block_height: u32, last_seen_color_id: &Option, limit: usize) -> Result> { + let colors = self.store.history_db().iter_scan_reverse( + &ColorIdRow::prefix(), + &ColorIdRow::filter_end(block_height, last_seen_color_id) + ).filter_map(|row|{ + let row = ColorIdRow::from_row(row); + let result = self.get_height_by_color_id(&row.key.color_id); + // Ignore color_id, block_height pair if the block_height is not the latest + match result { + Some(block_height) if (block_height <= row.key.block_height) => Some((row.key.color_id, row.key.block_height)), + _ => None, + } }) + .skip(1) // Ignore the first element, which is the last_seen_color_id itself .take(limit) - .collect(); + .collect::>(); Ok(colors) } + pub fn get_height_by_color_id(&self, color_id: &ColorIdentifier) -> Option { + self.colored_history_iter_scan_reverse(color_id).map(|c| ColoredTxHistoryRow::from_row(c).key.confirmed_height).next() + } + pub fn get_colored_stats(&self, color_id: &ColorIdentifier) -> Result { let cache: Option<(ColoredStats, usize)> = self .store @@ -1044,10 +1043,6 @@ fn add_transaction( if is_spendable(txo) { rows.push(TxOutRow::new(&txid, txo_index, txo).into_row()); } - - if let Some((color_id, _)) = txo.script_pubkey.split_color() { - rows.push(ColorIdRow::new(&color_id).into_row()) - } } } @@ -1361,40 +1356,55 @@ impl TxOutRow { } #[derive(Serialize, Deserialize)] -struct ColorIdKey { - code: u8, +pub struct ColorIdKey { + block_height: u32,// MUST be serialized as big-endian. color_id: ColorIdentifier, } -struct ColorIdRow { +// keys is "B{block-height}c{color-id}" (block_height is latest block height which colored coin used) +// value is "" +pub struct ColorIdRow { key: ColorIdKey, } impl ColorIdRow { - fn new(color_id: &ColorIdentifier) -> ColorIdRow { + pub fn new(block_height: u32, color_id: &ColorIdentifier) -> ColorIdRow { ColorIdRow { key: ColorIdKey { - code: b'c', + block_height: block_height, color_id: color_id.clone() } } } fn prefix() -> Bytes { - b"c".to_vec() + bincode::serialize(&(b'B')) + .unwrap() } - fn into_row(self) -> DBRow { + fn filter_end(block_height: u32, color_id: &Option) -> Bytes { + let filter = if let Some(color_id) = color_id { + bincode::serialize(&(b'B', block_height, b'c', &serialize_color_id(&color_id))) + .unwrap() + } else { + bincode::serialize(&(b'B', block_height + 1, b'c')) + .unwrap() + }; + filter + } + + pub fn into_row(self) -> DBRow { DBRow { - key: bincode::serialize(&(b'c', &serialize_color_id(&self.key.color_id))).unwrap(), + key: bincode::serialize(&(b'B', self.key.block_height, b'c', &serialize_color_id(&self.key.color_id))).unwrap(), value: vec![], } } - fn from_row(row: DBRow) -> Self { - let (_prefix, token_type, payload): (u8, u8, [u8; 32]) = + + pub fn from_row(row: DBRow) -> Self { + let (_prefix, block_height, _prefix2, token_type, payload): (u8, u32, u8, u8, [u8; 32]) = bincode::deserialize(&row.key).expect("failed to deserialize ColorIdRow"); ColorIdRow { key: ColorIdKey { - code: b'c', + block_height: block_height, color_id: deserialize_color_id(token_type, payload), } } diff --git a/src/rest.rs b/src/rest.rs index 100881e..770a25d 100644 --- a/src/rest.rs +++ b/src/rest.rs @@ -912,7 +912,7 @@ fn handle_request( let color_id: Option = last_seen_color_id.map_or(None,|hex| { ColorIdentifier::from_hex(hex).ok() }); - let colors: Vec = query.get_colors(color_id, COLOR_IDS_PER_PAGE); + let colors: Vec<(ColorIdentifier, u32)> = query.get_colors(color_id, COLOR_IDS_PER_PAGE); json_response(colors, TTL_SHORT) }