Skip to content

Commit

Permalink
feat: Query By Recipient without tests
Browse files Browse the repository at this point in the history
  • Loading branch information
AshtonStephens committed Dec 13, 2024
1 parent 9dd76d9 commit 3e3ac32
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 14 deletions.
27 changes: 25 additions & 2 deletions emily/cdk/lib/emily-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,9 @@ export class EmilyStack extends cdk.Stack {
pointInTimeRecovery: pointInTimeRecovery,
});

const indexName: string = "DepositStatus";
const byStatusIndexName: string = "DepositStatus";
table.addGlobalSecondaryIndex({
indexName: indexName,
indexName: byStatusIndexName,
partitionKey: {
name: 'OpStatus',
type: dynamodb.AttributeType.STRING
Expand All @@ -158,6 +158,29 @@ export class EmilyStack extends cdk.Stack {
]
});

const byRecipientIndexName: string = "DepositRecipient";
table.addGlobalSecondaryIndex({
indexName: byRecipientIndexName,
partitionKey: {
name: 'Recipient',
type: dynamodb.AttributeType.STRING
},
sortKey: {
name: 'LastUpdateHeight',
type: dynamodb.AttributeType.NUMBER
},
projectionType: dynamodb.ProjectionType.INCLUDE,
nonKeyAttributes: [
"BitcoinTxid",
"BitcoinTxOutputIndex",
"OpStatus",
"Amount",
"LastUpdateBlockHash",
"ReclaimScript",
"DepositScript",
]
});

// TODO(388): Add an additional GSI for querying by user; not required for MVP.
return table;
}
Expand Down
56 changes: 46 additions & 10 deletions emily/handler/src/api/handlers/deposit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,16 +166,52 @@ pub async fn get_deposits(
context: EmilyContext,
query: GetDepositsQuery,
) -> Result<impl warp::reply::Reply, Error> {
// Deserialize next token into the exclusive start key if present/
let (entries, next_token) = accessors::get_deposit_entries(
&context,
&query.status,
query.next_token,
query.page_size,
)
.await?;
// Convert data into resource types.
let deposits: Vec<DepositInfo> = entries.into_iter().map(|entry| entry.into()).collect();


let (deposits, next_token) = match (query.status, query.recipient) {
(Some(_), Some(_)) => {
return Err(Error::BadRequest(
"Only one of recipient and status can be specified".to_string(),
));
},
(None, None) => {
return Err(Error::BadRequest(
"At least one of recipient and status must be specified".to_string(),
));
},
(None, Some(recipient)) => {
// Make the query.
let (entries, next_token) = accessors::get_deposit_entries_by_recipient(
&context,
&recipient,
query.next_token,
query.page_size,
)
.await?;
// Convert the entries into the right type.
let deposit_infos: Vec<DepositInfo> = entries
.into_iter()
.map(|entry| entry.into())
.collect();
(deposit_infos, next_token)
},
(Some(status), None) => {
// Make the query.
let (entries, next_token) = accessors::get_deposit_entries(
&context,
&status,
query.next_token,
query.page_size,
)
.await?;
// Convert the entries into the right type.
let deposit_infos: Vec<DepositInfo> = entries
.into_iter()
.map(|entry| entry.into())
.collect();
(deposit_infos, next_token)
},
};
// Create response.
let response = GetDepositsResponse { deposits, next_token };
// Respond.
Expand Down
6 changes: 5 additions & 1 deletion emily/handler/src/api/models/deposit/requests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ pub struct GetDepositsForTransactionQuery {
#[serde(rename_all = "camelCase")]
pub struct GetDepositsQuery {
/// Operation status.
pub status: Status,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<Status>,
/// Operation recipient.
#[serde(skip_serializing_if = "Option::is_none")]
pub recipient: Option<String>,
/// Next token for the search.
#[serde(skip_serializing_if = "Option::is_none")]
pub next_token: Option<String>,
Expand Down
5 changes: 5 additions & 0 deletions emily/handler/src/common/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ pub enum Error {
/// in an update not being performed.
#[error("Version conflict")]
VersionConflict,

/// Bad request
#[error("Bad request {0}")]
BadRequest(String),
}

/// Error implementation.
Expand All @@ -129,6 +133,7 @@ impl Error {
Error::TooManyInternalRetries => StatusCode::INTERNAL_SERVER_ERROR,
Error::InconsistentState(_) => StatusCode::INTERNAL_SERVER_ERROR,
Error::Reorganzing(_) => StatusCode::INTERNAL_SERVER_ERROR,
Error::BadRequest(_) => StatusCode::BAD_REQUEST,
Error::VersionConflict => StatusCode::INTERNAL_SERVER_ERROR,
}
}
Expand Down
18 changes: 17 additions & 1 deletion emily/handler/src/database/accessors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crate::common::error::{Error, Inconsistency};

use crate::{api::models::common::Status, context::EmilyContext};

use super::entries::deposit::ValidatedDepositUpdate;
use super::entries::deposit::{DepositInfoByRecipientEntry, DepositTableByRecipientSecondaryIndex, ValidatedDepositUpdate};
use super::entries::limits::{
LimitEntry, LimitEntryKey, LimitTablePrimaryIndex, GLOBAL_CAP_ACCOUNT,
};
Expand Down Expand Up @@ -77,6 +77,22 @@ pub async fn get_deposit_entries(
.await
}

/// Get deposit entries by recipient.
pub async fn get_deposit_entries_by_recipient(
context: &EmilyContext,
recipient: &String,
maybe_next_token: Option<String>,
maybe_page_size: Option<i32>,
) -> Result<(Vec<DepositInfoByRecipientEntry>, Option<String>), Error> {
query_with_partition_key::<DepositTableByRecipientSecondaryIndex>(
context,
recipient,
maybe_next_token,
maybe_page_size,
)
.await
}

/// Hacky exhasutive list of all statuses that we will iterate over in order to
/// get every deposit present.
const ALL_STATUSES: &[Status] = &[
Expand Down
106 changes: 106 additions & 0 deletions emily/handler/src/database/entries/deposit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,112 @@ impl From<DepositInfoEntry> for DepositInfo {
}
}

// Deposit info by recipient entry ---------------------------------------------

/// Search token for GSI.
#[derive(Clone, Default, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct DepositInfoByRecipientEntrySearchToken {
/// Primary index key.
#[serde(flatten)]
pub primary_index_key: DepositEntryKey,
/// Global secondary index key.
#[serde(flatten)]
pub secondary_index_key: DepositInfoByRecipientEntryKey,
}

/// Key for deposit info entry.
#[derive(Clone, Default, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct DepositInfoByRecipientEntryKey {
/// The recipient of the deposit.
pub recipient: String,
/// The most recent Stacks block height the API was aware of when the deposit was last
/// updated. If the most recent update is tied to an artifact on the Stacks blockchain
/// then this height is the Stacks block height that contains that artifact.
pub last_update_height: u64,
}

/// Reduced version of the deposit data.
#[derive(Clone, Default, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct DepositInfoByRecipientEntry {
/// Gsi key data.
#[serde(flatten)]
pub key: DepositInfoByRecipientEntryKey,
/// Primary index key data.
#[serde(flatten)]
pub primary_index_key: DepositEntryKey,
/// The status of the entry.
#[serde(rename = "OpStatus")]
pub status: Status,
/// Amount of BTC being deposited in satoshis.
pub amount: u64,
/// The raw reclaim script.
pub reclaim_script: String,
/// The raw deposit script.
pub deposit_script: String,
/// The most recent Stacks block hash the API was aware of when the deposit was last
/// updated. If the most recent update is tied to an artifact on the Stacks blockchain
/// then this hash is the Stacks block hash that contains that artifact.
pub last_update_block_hash: String,
}

/// Implements the key trait for the deposit entry key.
impl KeyTrait for DepositInfoByRecipientEntryKey{
/// The type of the partition key.
type PartitionKey = String;
/// the type of the sort key.
type SortKey = u64;
/// The table field name of the partition key.
const PARTITION_KEY_NAME: &'static str = "Recipient";
/// The table field name of the sort key.
const SORT_KEY_NAME: &'static str = "LastUpdateHeight";
}

/// Implements the entry trait for the deposit entry.
impl EntryTrait for DepositInfoByRecipientEntry {
/// The type of the key for this entry type.
type Key = DepositInfoByRecipientEntryKey;
/// Extract the key from the deposit info entry.
fn key(&self) -> Self::Key {
DepositInfoByRecipientEntryKey {
recipient: self.key.recipient.clone(),
last_update_height: self.key.last_update_height,
}
}
}

/// Primary index struct.
pub struct DepositTableByRecipientSecondaryIndexInner;
/// Deposit table primary index type.
pub type DepositTableByRecipientSecondaryIndex = SecondaryIndex<DepositTableByRecipientSecondaryIndexInner>;
/// Definition of Primary index trait.
impl SecondaryIndexTrait for DepositTableByRecipientSecondaryIndexInner {
type PrimaryIndex = DepositTablePrimaryIndex;
type Entry = DepositInfoByRecipientEntry;
const INDEX_NAME: &'static str = "DepositStatus";
}

impl From<DepositInfoByRecipientEntry> for DepositInfo {
fn from(deposit_info_entry: DepositInfoByRecipientEntry) -> Self {
// Create deposit info resource from deposit info table entry.
DepositInfo {
bitcoin_txid: deposit_info_entry.primary_index_key.bitcoin_txid,
bitcoin_tx_output_index: deposit_info_entry.primary_index_key.bitcoin_tx_output_index,
recipient: deposit_info_entry.key.recipient,
amount: deposit_info_entry.amount,
last_update_height: deposit_info_entry.key.last_update_height,
last_update_block_hash: deposit_info_entry.last_update_block_hash,
status: deposit_info_entry.status,
reclaim_script: deposit_info_entry.reclaim_script,
deposit_script: deposit_info_entry.deposit_script,
}
}
}

// -----------------------------------------------------------------------------

/// Validated version of the update deposit request.
#[derive(Clone, Default, Debug, Eq, PartialEq, Hash)]
pub struct ValidatedUpdateDepositsRequest {
Expand Down

0 comments on commit 3e3ac32

Please sign in to comment.