imbibe-persistence 0.0.1

details the database model of the information extracted from a block and its constituent transactions
Documentation
use bigdecimal::{BigDecimal, ToPrimitive};
use bon::Builder;
use chrono::{DateTime, Utc};
use cosmrs::{
	Any, Coin,
	tx::{SignatureBytes, SignerPublicKey},
};
use diesel::prelude::Queryable;
use imbibe_domain::{
	Address, NonEmptyBz, Sha256,
	block::{AppHash, Block, BlockData, Header},
	tx::{Codespace, Fees, Memo, Tx},
};
use serde_json::Value;

use super::error::InvalidValueError;

#[derive(Debug, Queryable)]
pub struct BlockWithDataRecord {
	block: BlockRecord,
	data: Vec<Vec<u8>>,
}

#[derive(Debug, Queryable)]
pub struct BlockRecord {
	height: i64,
	block_hash: Vec<u8>,
	chain_id: String,
	time: DateTime<Utc>,
	app_hash: Vec<u8>,
	validators_hash: Vec<u8>,
	next_validators_hash: Vec<u8>,
	consensus_hash: Vec<u8>,
	proposer: Vec<u8>,
	gas_used: i64,
	last_commit_hash: Option<Vec<u8>>,
	data_hash: Option<Vec<u8>>,
	last_results_hash: Option<Vec<u8>>,
	evidence_hash: Option<Vec<u8>>,
}

#[derive(Debug, Builder)]
pub struct TxWithDetailsRecord {
	tx: TxRecord,
	signatures: Vec<SignatureBytes>,
	fees: Vec<Coin>,
	msgs: Vec<Any>,
}

#[derive(Debug, Queryable)]
pub struct TxRecord {
	block_height: i64,
	tx_idx_in_block: i64,
	tx_hash: Vec<u8>,
	memo: Option<String>,
	timeout_height: Option<i64>,
	signers: Value,
	payer: Vec<u8>,
	granter: Option<Vec<u8>>,
	gas_limit: i64,
	gas_wanted: i64,
	gas_used: i64,
	code: i32,
	codespace: Option<String>,
	data_bz: Option<Vec<u8>>,
	tx_bz: Vec<u8>,
}

#[derive(Debug, Queryable)]
pub struct SignatureRecord {
	bz: Vec<u8>,
}

#[derive(Debug, Queryable)]
pub struct FeeRecord {
	amount: BigDecimal,
	denom: String,
}

#[derive(Debug, Queryable)]
pub struct MsgRecord {
	type_url: String,
	value: Vec<u8>,
}

impl TxRecord {
	pub fn block_height(&self) -> i64 {
		self.block_height
	}

	pub fn tx_idx_in_block(&self) -> i64 {
		self.tx_idx_in_block
	}
}

impl SignatureRecord {
	pub fn into_bytes(self) -> SignatureBytes {
		self.bz
	}
}

impl TryFrom<BlockWithDataRecord> for Block {
	type Error = InvalidValueError;

	fn try_from(record: BlockWithDataRecord) -> Result<Self, Self::Error> {
		let block_record = record.block;

		let header = Header::builder()
			.chain_id(block_record.chain_id)
			.height(block_record.height.try_into()?)
			.time(super::chrono_to_jiff(&block_record.time))
			.validators_hash(Sha256::new(
				block_record.validators_hash.as_slice().try_into()?,
			))
			.next_validators_hash(Sha256::new(
				block_record.next_validators_hash.as_slice().try_into()?,
			))
			.consensus_hash(Sha256::new(
				block_record.consensus_hash.as_slice().try_into()?,
			))
			.app_hash(AppHash::new(block_record.app_hash))
			.proposer(Address::new(block_record.proposer.as_slice().try_into()?))
			.maybe_last_commit_hash(
				block_record
					.last_commit_hash
					.map(|h| h.as_slice().try_into().map(Sha256::new))
					.transpose()?,
			)
			.maybe_data_hash(
				block_record
					.data_hash
					.map(|h| h.as_slice().try_into().map(Sha256::new))
					.transpose()?,
			)
			.maybe_last_results_hash(
				block_record
					.last_results_hash
					.map(|h| h.as_slice().try_into().map(Sha256::new))
					.transpose()?,
			)
			.maybe_evidence_hash(
				block_record
					.evidence_hash
					.map(|h| h.as_slice().try_into().map(Sha256::new))
					.transpose()?,
			)
			.build();

		let block = Block::builder()
			.header(header)
			.hash(Sha256::new(block_record.block_hash.as_slice().try_into()?))
			.gas_used(block_record.gas_used.try_into()?)
			.data(
				record
					.data
					.into_iter()
					.map(From::from)
					.map(NonEmptyBz::new)
					.collect::<Option<_>>()
					.and_then(BlockData::new)
					.ok_or(InvalidValueError::Empty)?,
			)
			.build();

		Ok(block)
	}
}

impl TryFrom<TxWithDetailsRecord> for Tx {
	type Error = InvalidValueError;

	fn try_from(record: TxWithDetailsRecord) -> Result<Self, Self::Error> {
		let txr = record.tx;
		let tx = Tx::builder()
			.block_height(u64::try_from(txr.block_height).and_then(|h| h.try_into())?)
			.tx_idx_in_block(txr.tx_idx_in_block.try_into()?)
			.tx_hash(
				txr.tx_hash
					.as_slice()
					.try_into()
					.map(Sha256::new)
					.map_err(InvalidValueError::from)?,
			)
			.msgs(imbibe_domain::tx::Msgs::new(record.msgs).ok_or(InvalidValueError::Empty)?)
			.maybe_memo(txr.memo.and_then(Memo::new))
			.maybe_timeout_height(
				txr.timeout_height
					.map(u64::try_from)
					.transpose()?
					.map(TryFrom::try_from)
					.transpose()?,
			)
			.signatures(record.signatures)
			.maybe_fees(Fees::new(record.fees))
			.signers(json_to_signer_keys(txr.signers)?)
			.payer(txr.payer.as_slice().try_into().map(Address::new)?)
			.maybe_granter(
				txr.granter.as_deref().map(TryFrom::try_from).transpose()?.map(Address::new),
			)
			.code(u32::try_from(txr.code)?.into())
			.maybe_codespace(txr.codespace.and_then(Codespace::new))
			.gas_limit(txr.gas_limit.try_into()?)
			.gas_wanted(txr.gas_wanted.try_into()?)
			.gas_used(txr.gas_used.try_into()?)
			.maybe_data_bz(txr.data_bz.map(From::from).and_then(NonEmptyBz::new))
			.tx_bz(NonEmptyBz::new(txr.tx_bz.into()).ok_or(InvalidValueError::Empty)?)
			.build();

		Ok(tx)
	}
}

impl TryFrom<&FeeRecord> for Coin {
	type Error = InvalidValueError;

	fn try_from(FeeRecord { amount, denom, .. }: &FeeRecord) -> Result<Self, Self::Error> {
		amount
			.to_u128()
			.ok_or(InvalidValueError::AmountError)
			.and_then(|amount| Coin::new(amount, denom).map_err(From::from))
	}
}

impl From<MsgRecord> for Any {
	fn from(MsgRecord { type_url, value, .. }: MsgRecord) -> Self {
		Any { type_url, value }
	}
}

fn json_to_signer_keys(json: Value) -> Result<Vec<SignerPublicKey>, InvalidValueError> {
	match json {
		Value::Array(keys) => keys
			.into_iter()
			.map(serde_json::from_value::<Any>)
			.map(|any| any.map(SignerPublicKey::try_from).map_err(From::from))
			.map(|spk| spk.and_then(|spk| spk.map_err(InvalidValueError::from)))
			.collect(),
		_ => Err(InvalidValueError::Other(
			"signer keys must be array of keys".into(),
		))?,
	}
}