Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Signer endpoint WIP #1032

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions emily/cdk/lib/emily-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,21 @@ export class EmilyStack extends cdk.Stack {
persistentResourceRemovalPolicy,
);

const signerTableId: string = 'SignerTable';
const signerTableName: string = EmilyStackUtils.getResourceName(signerTableId, props);
const signerTable: dynamodb.Table = this.createOrUpdateSignerTable(
signerTableId,
signerTableName,
persistentResourceRemovalPolicy,
);

if (!EmilyStackUtils.isTablesOnly()) {
const operationLambda: lambda.Function = this.createOrUpdateOperationLambda(
depositTableName,
withdrawalTableName,
chainstateTableName,
limitTableName,
signerTableName,
props
);

Expand All @@ -81,6 +90,7 @@ export class EmilyStack extends cdk.Stack {
withdrawalTable.grantReadWriteData(operationLambda);
chainstateTable.grantReadWriteData(operationLambda);
limitTable.grantReadWriteData(operationLambda);
signerTable.grantReadWriteData(operationLambda);

const emilyApi: apig.SpecRestApi = this.createOrUpdateApi(operationLambda, props);
}
Expand Down Expand Up @@ -246,11 +256,42 @@ export class EmilyStack extends cdk.Stack {
});
}

/**
* Creates or updates a DynamoDB table for signer information.
* @param {string} tableId The id of the table AWS resource.
* @param {string} tableName The name of the DynamoDB table.
* @returns {dynamodb.Table} The created or updated DynamoDB table.
* @post A DynamoDB table is returned without additional configuration.
*/
createOrUpdateSignerTable(
tableId: string,
tableName: string,
removalPolicy: cdk.RemovalPolicy,
): dynamodb.Table {
// Create DynamoDB table to store the messages. Encrypted by default.
const table = new dynamodb.Table(this, tableId, {
tableName: tableName,
partitionKey: {
name: 'ApiKeyHash',
type: dynamodb.AttributeType.STRING,
},
sortKey: {
name: 'Timestamp',
type: dynamodb.AttributeType.NUMBER,
},
removalPolicy: removalPolicy,
});

return table;
}

/**
* Creates or updates the operation Lambda function.
* @param {string} depositTableName The name of the deposit DynamoDB table.
* @param {string} withdrawalTableName The name of the withdrawal DynamoDB table.
* @param {string} chainstateTableName The name of the chainstate DynamoDB table.
* @param {string} limitTableName The name of the limit DynamoDB table.
* @param {string} signerTableName The name of the signer DynamoDB table.
* @param {EmilyStackProps} props The stack properties.
* @returns {lambda.Function} The created or updated Lambda function.
* @post Lambda function with environment variables set and permissions for DynamoDB access is returned.
Expand All @@ -260,6 +301,7 @@ export class EmilyStack extends cdk.Stack {
withdrawalTableName: string,
chainstateTableName: string,
limitTableName: string,
signerTableName: string,
props: EmilyStackProps
): lambda.Function {

Expand All @@ -282,6 +324,7 @@ export class EmilyStack extends cdk.Stack {
WITHDRAWAL_TABLE_NAME: withdrawalTableName,
CHAINSTATE_TABLE_NAME: chainstateTableName,
LIMIT_TABLE_NAME: limitTableName,
SIGNER_TABLE_NAME: signerTableName,
// Declare an environment variable that will be overwritten in local SAM
// deployments the AWS stack. SAM can only set environment variables that are
// already expected to be present in the lambda.
Expand Down
126 changes: 126 additions & 0 deletions emily/handler/src/api/handlers/signer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
//! Handlers for Deposit endpoints.
use std::time::SystemTime;

use bitcoin::ScriptBuf;
use stacks_common::codec::StacksMessageCodec as _;
use warp::http::StatusCode;
use warp::reply::json;
use warp::reply::with_status;
use warp::reply::Reply;

use crate::api::models::common::Status;
use crate::api::models::deposit::requests::CreateDepositRequestBody;
use crate::api::models::deposit::requests::GetDepositsForTransactionQuery;
use crate::api::models::deposit::requests::GetDepositsQuery;
use crate::api::models::deposit::requests::UpdateDepositsRequestBody;
use crate::api::models::deposit::responses::GetDepositsForTransactionResponse;
use crate::api::models::deposit::responses::GetDepositsResponse;
use crate::api::models::deposit::responses::UpdateDepositsResponse;
use crate::api::models::deposit::Deposit;
use crate::api::models::deposit::DepositInfo;
use crate::api::models::signer::Signer;
use crate::api::models::signer::SignerInfo;
use crate::common::error::Error;
use crate::context::EmilyContext;
use crate::database::accessors;
use crate::database::entries::deposit::DepositEntry;
use crate::database::entries::deposit::DepositEntryKey;
use crate::database::entries::deposit::DepositEvent;
use crate::database::entries::deposit::DepositParametersEntry;
use crate::database::entries::deposit::ValidatedUpdateDepositsRequest;
use crate::database::entries::signers::SignerEntry;
use crate::database::entries::signers::SignerInfoEntry;
use crate::database::entries::StatusEntry;

const API_KEY: &str = "api_key";

/// The register signer handler.
#[utoipa::path(
post,
operation_id = "registerSigner",
path = "/signer",
request_body = RegisterSignerRequestBody,
tag = "signer",
responses(
// TODO(271): Add success body.
(status = 200, description = "Successfully registered signer", body = FullSigner),
(status = 400, description = "Invalid request body", body = ErrorResponse),
(status = 404, description = "Address not found", body = ErrorResponse),
(status = 405, description = "Method not allowed", body = ErrorResponse),
(status = 500, description = "Internal server error", body = ErrorResponse)
)
)]
pub async fn register_signer(
context: EmilyContext, request: Signer,
) -> impl warp::reply::Reply {
// Internal handler so `?` can be used correctly while still returning a
// reply.
async fn handler(
context: EmilyContext, full_signer: Signer,
) -> Result<impl warp::reply::Reply, Error> {
// Set variables.
let signer_entry = SignerEntry::from_full_signer(
API_KEY.to_string(),
full_signer,
SystemTime::now(),
);
// Set the signer.
accessors::set_signer_entry(&context, &signer_entry).await?;
// Get signer.
let full_signer: Signer = accessors::get_signer_entry_from_api_key(
&context,
API_KEY.to_string(),
)
.await?
.into();
// Respond.
Ok(with_status(json(&full_signer), StatusCode::OK))
}

// Handle and respond.
handler(context, request)
.await
.map_or_else(Reply::into_response, Reply::into_response)
}

/// The register signer handler.
#[utoipa::path(
post,
operation_id = "getSigner",
path = "/signer/{public_key}",
request_body = RegisterSignerRequestBody,
params(
("public_key" = String, Path, description = "The public key of the signer."),
),
tag = "signer",
responses(
// TODO(271): Add success body.
(status = 200, description = "Successfully got the signer infor", body = FullSigner),
(status = 400, description = "Invalid request body", body = ErrorResponse),
(status = 404, description = "Address not found", body = ErrorResponse),
(status = 405, description = "Method not allowed", body = ErrorResponse),
(status = 500, description = "Internal server error", body = ErrorResponse)
)
)]
pub async fn get_signer(
context: EmilyContext, public_key: String,
) -> impl warp::reply::Reply {
// Internal handler so `?` can be used correctly while still returning a
// reply.
async fn handler(
context: EmilyContext, public_key: String,
) -> Result<impl warp::reply::Reply, Error> {
// Get signer.
let signer: SignerInfo =
accessors::get_signer_entry_from_public_key(&context, public_key)
.await?
.into();
// Respond.
Ok(with_status(json(&signer), StatusCode::OK))
}

// Handle and respond.
handler(context, public_key)
.await
.map_or_else(Reply::into_response, Reply::into_response)
}
2 changes: 2 additions & 0 deletions emily/handler/src/api/models/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,7 @@ pub mod deposit;
pub mod health;
/// Api structures for limits.
pub mod limits;
/// Api structures for signers.
pub mod signer;
/// Api structures for withdrawals.
pub mod withdrawal;
88 changes: 88 additions & 0 deletions emily/handler/src/api/models/signer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
//! Request structures for signer api calls.

use serde::Deserialize;
use serde::Serialize;
use utoipa::ToResponse;
use utoipa::ToSchema;

/// The health of the signer.
#[derive(
Clone,
Default,
Debug,
Eq,
PartialEq,
PartialOrd,
Ord,
Hash,
Serialize,
Deserialize,
ToSchema,
ToResponse,
)]
pub enum SignerHealth {
/// The signer is inactive.
Healthy,
/// The signer is unhealthy.
Unhealthy(String),
/// The signer is dead.
Dead(String),
/// The status of the signer is unknown.
#[default]
Unknown,
}

/// The full information about the signer. This includes some private
/// information that only select users should have access to.
#[derive(
Clone,
Default,
Debug,
Eq,
PartialEq,
PartialOrd,
Ord,
Hash,
Serialize,
Deserialize,
ToSchema,
ToResponse,
)]
pub struct Signer {
/// The "name" of the signer being registered.
pub name: String,
/// Public key.
pub public_key: String,
/// Approximate location.
pub location: String,
/// Signer health.
pub health: SignerHealth,
/// Contact information for the signer. This is private information.
pub contact: String,
}

/// The representation of the signer.
#[derive(
Clone,
Default,
Debug,
Eq,
PartialEq,
PartialOrd,
Ord,
Hash,
Serialize,
Deserialize,
ToSchema,
ToResponse,
)]
pub struct SignerInfo {
/// The "name" of the signer being registered.
pub name: String,
/// Public key.
pub public_key: String,
/// Approximate location.
pub location: String,
/// Signer health.
pub health: SignerHealth,
}
37 changes: 37 additions & 0 deletions emily/handler/src/api/routes/signer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//! Route definitions for the chainstate endpoint.

use warp::Filter;

use super::handlers;
use crate::context::EmilyContext;

/// Chainstate routes.
pub fn routes(
context: EmilyContext,
) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
register_signer(context.clone())
.or(get_signer(context.clone()))
}

/// Register a specific signer.
fn register_signer(
context: EmilyContext,
) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
warp::any()
.map(move || context.clone())
.and(warp::path!("signer"))
.and(warp::post())
.and(warp::body::json())
.then(handlers::signer::register_signer)
}

/// Get chainstate at height endpoint.
fn get_signer(
context: EmilyContext,
) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
warp::any()
.map(move || context.clone())
.and(warp::path!("signer" / String))
.and(warp::get())
.then(handlers::signer::get_signer)
}
10 changes: 9 additions & 1 deletion emily/handler/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ pub struct Settings {
pub chainstate_table_name: String,
/// Limit table name.
pub limit_table_name: String,
/// Signer table name.
pub signer_table_name: String,
/// The default global limits for the system.
pub default_limits: AccountLimits,
}
Expand Down Expand Up @@ -67,6 +69,7 @@ impl Settings {
withdrawal_table_name: env::var("WITHDRAWAL_TABLE_NAME")?,
chainstate_table_name: env::var("CHAINSTATE_TABLE_NAME")?,
limit_table_name: env::var("LIMIT_TABLE_NAME")?,
signer_table_name: env::var("SIGNER_TABLE_NAME")?,
default_limits: AccountLimits {
peg_cap: env::var("DEFAULT_PEG_CAP")
.ok()
Expand Down Expand Up @@ -136,7 +139,8 @@ impl EmilyContext {
// Attempt to get all the tables by searching the output of the
// list tables operation.
let mut table_name_map: HashMap<&str, String> = HashMap::new();
let tables_to_find: Vec<&str> = vec!["Deposit", "Chainstate", "Withdrawal", "Limit"];
let tables_to_find: Vec<&str> =
vec!["Deposit", "Chainstate", "Withdrawal", "Limit", "Signer"];
for name in table_names {
for table_to_find in &tables_to_find {
if name.contains(table_to_find) {
Expand Down Expand Up @@ -165,6 +169,10 @@ impl EmilyContext {
.get("Limit")
.expect("Couldn't find valid limit table table in existing table list.")
.to_string(),
signer_table_name: table_name_map
.get("Signer")
.expect("Couldn't find valid signer table table in existing table list.")
.to_string(),
default_limits: AccountLimits::default(),
},
dynamodb_client,
Expand Down
Loading
Loading