use chia_protocol::Bytes32;
use crate::coin_store::CoinStore;
use crate::error::CoinStoreError;
use crate::storage::schema;
use crate::types::{CoinId, CoinRecord};
impl CoinStore {
pub fn get_coin_record(&self, coin_id: &CoinId) -> Result<Option<CoinRecord>, CoinStoreError> {
let key = schema::coin_key(coin_id);
match self.backend.get(schema::CF_COIN_RECORDS, &key)? {
None => Ok(None),
Some(bytes) => Ok(Self::decode_coin_record_bytes(&bytes)),
}
}
pub fn get_coin_records(&self, coin_ids: &[CoinId]) -> Result<Vec<CoinRecord>, CoinStoreError> {
let mut results = Vec::with_capacity(coin_ids.len());
for coin_id in coin_ids {
if let Some(rec) = self.get_coin_record(coin_id)? {
results.push(rec);
}
}
Ok(results)
}
pub fn get_coin_records_by_puzzle_hash(
&self,
include_spent: bool,
puzzle_hash: &Bytes32,
start_height: u64,
end_height: u64,
) -> Result<Vec<CoinRecord>, CoinStoreError> {
let entries = self
.backend
.prefix_scan(schema::CF_COIN_BY_PUZZLE_HASH, puzzle_hash.as_ref())?;
let mut results = Vec::new();
for (key, _value) in entries {
if key.len() < 64 {
continue;
}
let coin_id = schema::coin_id_from_key(&key[32..64]);
if let Some(rec) = self.get_coin_record(&coin_id)? {
if !include_spent && rec.is_spent() {
continue;
}
if rec.confirmed_height < start_height || rec.confirmed_height > end_height {
continue;
}
results.push(rec);
}
}
Ok(results)
}
pub fn get_coin_records_by_puzzle_hashes(
&self,
include_spent: bool,
puzzle_hashes: &[Bytes32],
start_height: u64,
end_height: u64,
) -> Result<Vec<CoinRecord>, CoinStoreError> {
let mut results = Vec::new();
for ph in puzzle_hashes {
let batch =
self.get_coin_records_by_puzzle_hash(include_spent, ph, start_height, end_height)?;
results.extend(batch);
}
Ok(results)
}
pub fn get_coins_added_at_height(
&self,
height: u64,
) -> Result<Vec<CoinRecord>, CoinStoreError> {
let prefix = height.to_be_bytes();
let entries = self
.backend
.prefix_scan(schema::CF_COIN_BY_CONFIRMED_HEIGHT, &prefix)?;
let mut results = Vec::new();
for (key, _value) in entries {
if key.len() < 40 {
continue;
}
let coin_id = schema::coin_id_from_key(&key[8..40]);
if let Some(rec) = self.get_coin_record(&coin_id)? {
results.push(rec);
}
}
Ok(results)
}
pub fn get_coins_removed_at_height(
&self,
height: u64,
) -> Result<Vec<CoinRecord>, CoinStoreError> {
if height == 0 {
return Ok(Vec::new());
}
let prefix = height.to_be_bytes();
let entries = self
.backend
.prefix_scan(schema::CF_COIN_BY_SPENT_HEIGHT, &prefix)?;
let mut results = Vec::new();
for (key, _value) in entries {
if key.len() < 40 {
continue;
}
let coin_id = schema::coin_id_from_key(&key[8..40]);
if let Some(rec) = self.get_coin_record(&coin_id)? {
results.push(rec);
}
}
Ok(results)
}
pub fn get_coin_records_by_parent_ids(
&self,
include_spent: bool,
parent_ids: &[CoinId],
start_height: u64,
end_height: u64,
) -> Result<Vec<CoinRecord>, CoinStoreError> {
let mut results = Vec::new();
for parent_id in parent_ids {
let entries = self
.backend
.prefix_scan(schema::CF_COIN_BY_PARENT, parent_id.as_ref())?;
for (key, _value) in entries {
if key.len() < 64 {
continue;
}
let coin_id = schema::coin_id_from_key(&key[32..64]);
if let Some(rec) = self.get_coin_record(&coin_id)? {
if !include_spent && rec.is_spent() {
continue;
}
if rec.confirmed_height < start_height || rec.confirmed_height > end_height {
continue;
}
results.push(rec);
}
}
}
Ok(results)
}
pub fn get_coin_records_by_names(
&self,
include_spent: bool,
names: &[CoinId],
start_height: u64,
end_height: u64,
) -> Result<Vec<CoinRecord>, CoinStoreError> {
let mut results = Vec::new();
for coin_id in names {
if let Some(rec) = self.get_coin_record(coin_id)? {
if !include_spent && rec.is_spent() {
continue;
}
if rec.confirmed_height < start_height || rec.confirmed_height > end_height {
continue;
}
results.push(rec);
}
}
Ok(results)
}
pub fn get_coin_states_by_ids(
&self,
include_spent: bool,
coin_ids: &[CoinId],
min_height: u64,
max_height: u64,
max_items: usize,
) -> Result<Vec<crate::CoinState>, CoinStoreError> {
let mut results = Vec::new();
for coin_id in coin_ids {
if results.len() >= max_items {
break;
}
if let Some(rec) = self.get_coin_record(coin_id)? {
if !include_spent && rec.is_spent() {
continue;
}
if rec.confirmed_height < min_height || rec.confirmed_height > max_height {
continue;
}
results.push(rec.to_coin_state());
}
}
Ok(results)
}
pub fn get_coin_states_by_puzzle_hashes(
&self,
include_spent: bool,
puzzle_hashes: &[Bytes32],
min_height: u64,
max_items: usize,
) -> Result<Vec<crate::CoinState>, CoinStoreError> {
let mut results = Vec::new();
for ph in puzzle_hashes {
if results.len() >= max_items {
break;
}
let recs =
self.get_coin_records_by_puzzle_hash(include_spent, ph, min_height, u64::MAX)?;
for rec in recs {
if results.len() >= max_items {
break;
}
results.push(rec.to_coin_state());
}
}
Ok(results)
}
pub fn num_unspent(&self) -> Result<u64, CoinStoreError> {
let entries = self.backend.prefix_scan(schema::CF_COIN_RECORDS, &[])?;
let mut count = 0u64;
for (_key, value) in entries {
if let Some(rec) = Self::decode_coin_record_bytes(&value) {
if !rec.is_spent() {
count += 1;
}
}
}
Ok(count)
}
pub fn total_unspent_value(&self) -> Result<u128, CoinStoreError> {
let entries = self.backend.prefix_scan(schema::CF_COIN_RECORDS, &[])?;
let mut total = 0u128;
for (_key, value) in entries {
if let Some(rec) = Self::decode_coin_record_bytes(&value) {
if !rec.is_spent() {
total += rec.coin.amount as u128;
}
}
}
Ok(total)
}
pub fn aggregate_unspent_by_puzzle_hash(
&self,
) -> Result<std::collections::HashMap<Bytes32, (u64, usize)>, CoinStoreError> {
let entries = self.backend.prefix_scan(schema::CF_COIN_RECORDS, &[])?;
let mut agg: std::collections::HashMap<Bytes32, (u64, usize)> =
std::collections::HashMap::new();
for (_key, value) in entries {
if let Some(rec) = Self::decode_coin_record_bytes(&value) {
if !rec.is_spent() {
let entry = agg.entry(rec.coin.puzzle_hash).or_insert((0, 0));
entry.0 += rec.coin.amount;
entry.1 += 1;
}
}
}
Ok(agg)
}
pub fn num_total(&self) -> Result<u64, CoinStoreError> {
let entries = self.backend.prefix_scan(schema::CF_COIN_RECORDS, &[])?;
Ok(entries.len() as u64)
}
pub fn batch_coin_states_by_puzzle_hashes(
&self,
puzzle_hashes: &[Bytes32],
min_height: u64,
filters: crate::CoinStateFilters,
max_items: usize,
) -> Result<(Vec<crate::CoinState>, Option<u64>), CoinStoreError> {
const MAX_PUZZLE_HASH_BATCH_SIZE: usize = 990;
if puzzle_hashes.len() > MAX_PUZZLE_HASH_BATCH_SIZE {
return Err(CoinStoreError::PuzzleHashBatchTooLarge {
size: puzzle_hashes.len(),
max: MAX_PUZZLE_HASH_BATCH_SIZE,
});
}
let mut seen = std::collections::HashSet::new();
let mut candidates: Vec<CoinRecord> = Vec::new();
for ph in puzzle_hashes {
let entries = self
.backend
.prefix_scan(schema::CF_COIN_BY_PUZZLE_HASH, ph.as_ref())?;
for (key, _) in entries {
if key.len() < 64 {
continue;
}
let coin_id = schema::coin_id_from_key(&key[32..64]);
if seen.insert(coin_id) {
if let Some(rec) = self.get_coin_record(&coin_id)? {
candidates.push(rec);
}
}
}
}
if filters.include_hinted {
for ph in puzzle_hashes {
let hint_entries = self
.backend
.prefix_scan(schema::CF_HINTS_BY_VALUE, ph.as_ref())?;
for (key, _) in hint_entries {
if key.len() < 64 {
continue;
}
let coin_id = schema::coin_id_from_key(&key[32..64]);
if seen.insert(coin_id) {
if let Some(rec) = self.get_coin_record(&coin_id)? {
candidates.push(rec);
}
}
}
}
}
candidates.retain(|rec| {
if rec.is_spent() && !filters.include_spent {
return false;
}
if !rec.is_spent() && !filters.include_unspent {
return false;
}
if rec.confirmed_height < min_height {
return false;
}
if rec.coin.amount < filters.min_amount {
return false;
}
true
});
candidates.sort_by(|a, b| {
let a_max = std::cmp::max(a.confirmed_height, a.spent_height.unwrap_or(0));
let b_max = std::cmp::max(b.confirmed_height, b.spent_height.unwrap_or(0));
a_max
.cmp(&b_max)
.then_with(|| a.coin_id().cmp(&b.coin_id()))
});
if candidates.len() <= max_items {
let states: Vec<crate::CoinState> =
candidates.iter().map(|r| r.to_coin_state()).collect();
return Ok((states, None));
}
let overflow_rec = &candidates[max_items];
let overflow_height = std::cmp::max(
overflow_rec.confirmed_height,
overflow_rec.spent_height.unwrap_or(0),
);
let mut end = max_items;
while end > 0 {
let rec = &candidates[end - 1];
let rec_height = std::cmp::max(rec.confirmed_height, rec.spent_height.unwrap_or(0));
if rec_height < overflow_height {
break;
}
end -= 1;
}
let states: Vec<crate::CoinState> = candidates[..end]
.iter()
.map(|r| r.to_coin_state())
.collect();
Ok((states, Some(overflow_height)))
}
pub fn get_unspent_lineage_info_for_puzzle_hash(
&self,
puzzle_hash: &Bytes32,
) -> Result<Option<crate::types::UnspentLineageInfo>, CoinStoreError> {
let entries = self
.backend
.prefix_scan(schema::CF_UNSPENT_BY_PUZZLE_HASH, puzzle_hash.as_ref())?;
if entries.len() != 1 {
return Ok(None);
}
let key = &entries[0].0;
if key.len() < 64 {
return Ok(None);
}
let coin_id = schema::coin_id_from_key(&key[32..64]);
let rec = match self.get_coin_record(&coin_id)? {
Some(r) => r,
None => return Ok(None),
};
let parent_id = rec.coin.parent_coin_info;
let parent_parent_id = match self.get_coin_record(&parent_id)? {
Some(parent_rec) => parent_rec.coin.parent_coin_info,
None => Bytes32::from([0u8; 32]),
};
Ok(Some(crate::types::UnspentLineageInfo {
coin_id,
parent_id,
parent_parent_id,
}))
}
}