Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 2 additions & 4 deletions crates/contracts/src/programs/lending/scanners.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1052,10 +1052,8 @@ mod tests {
let protocol_after_supplied = 2_u64;
let lender_after_supplied = lender_before + (amount_to_repay - 1);

let lender_after = AssetAuthVault::new_active(
params.get_lender_vault_parameters(),
lender_after_supplied,
);
let lender_after =
AssetAuthVault::new_active(params.get_lender_vault_parameters(), lender_after_supplied);
let protocol_after = AssetAuthVault::new_active(
params.get_protocol_fee_vault_parameters(),
protocol_after_supplied,
Expand Down
6 changes: 6 additions & 0 deletions crates/indexer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -381,3 +381,9 @@ Overview sums use remaining state (`collateral_remaining`, `current_debt`) acros
| `GET` | `/offers` | Paginated short offer list | offer list filters (see above) |
| `GET` | `/offers/by-script` | Offer IDs (decimal strings) where `script_pubkey` matches an unspent participant UTXO (borrower or lender); response body is `["1", "2", …]` | `script_pubkey` (query param, hex) |
| `GET` | `/offers/{id}` | Full offer details with latest participant UTXOs and full offer UTXO history (spent + unspent); `{id}` is the numeric offer ID in the path | — |

### Vaults Endpoints

| Method | Endpoint | Description | Params / Body |
| :--- | :--- | :--- | :--- |
| `GET` | `/vaults/protocol-fee` | Unspent, finalized `protocol_fee` vaults for a given `principal_asset`, sorted by `amount` descending | `principal_asset` (query param, hex) |
2 changes: 2 additions & 0 deletions crates/indexer/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ mod query;
pub mod server;
mod state;
pub mod utils;
mod vaults;

pub use borrowers::dto::BorrowerOverview;
pub use dto::AssetAmount;
Expand All @@ -28,3 +29,4 @@ pub use offers::dto::{
pub use openapi::ApiDoc;
pub use params::*;
pub use state::AppState;
pub use vaults::dto::{ProtocolFeeVaultsQuery, ProtocolFeeVaultsResponse};
6 changes: 6 additions & 0 deletions crates/indexer/src/api/openapi/doc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ use crate::api::offers::dto::{
};
use crate::api::offers::handlers as offer_handlers;
use crate::api::params::{OfferSortBy, SortDir};
use crate::api::vaults::dto::ProtocolFeeVaultsResponse;
use crate::api::vaults::handlers as vault_handlers;
use crate::events::IndexerEvent;
use crate::models::{FactoryStatus, OfferStatus, ParticipantType, UtxoType, VaultType};

Expand Down Expand Up @@ -50,6 +52,7 @@ use super::schemas::{ErrorBody, ErrorResponse, OfferDetailsResponseSchema};
factory_handlers::get_by_id,
event_handlers::subscribe_events,
asset_handlers::get_domain_proof,
vault_handlers::list_protocol_fee_vaults,
health::health,
health::ready,
),
Expand Down Expand Up @@ -80,6 +83,7 @@ use super::schemas::{ErrorBody, ErrorResponse, OfferDetailsResponseSchema};
ParticipantDto,
ParticipantShort,
ParticipantType,
ProtocolFeeVaultsResponse,
SortDir,
UtxoType,
VaultType,
Expand All @@ -91,6 +95,7 @@ use super::schemas::{ErrorBody, ErrorResponse, OfferDetailsResponseSchema};
(name = "factories", description = "Issuance factory queries"),
(name = "events", description = "Server-Sent Events for indexer updates"),
(name = "assets", description = "ELIP-0100 asset domain proofs"),
(name = "vaults", description = "Protocol vault queries"),
(name = "health", description = "Liveness and readiness checks"),
)
)]
Expand Down Expand Up @@ -124,6 +129,7 @@ mod tests {
assert!(paths.contains_key("/factories/by-script"));
assert!(paths.contains_key("/factories/{id}"));
assert!(paths.contains_key("/.well-known/{proof_file}"));
assert!(paths.contains_key("/vaults/protocol-fee"));
assert!(paths.contains_key("/health"));
assert!(paths.contains_key("/ready"));
assert!(paths.contains_key("/events"));
Expand Down
4 changes: 3 additions & 1 deletion crates/indexer/src/api/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use crate::api::lenders;
use crate::api::offers;
use crate::api::openapi;
use crate::api::state::AppState;
use crate::api::vaults;

pub async fn run_server(listener: TcpListener, db_pool: PgPool) {
let events = EventBus::new();
Expand All @@ -33,7 +34,8 @@ pub async fn run_server(listener: TcpListener, db_pool: PgPool) {
.merge(lenders::routes())
.merge(factories::routes())
.merge(offers::routes())
.merge(assets::routes());
.merge(assets::routes())
.merge(vaults::routes());

#[cfg(feature = "swagger-ui")]
let app = app.merge(openapi::swagger_routes());
Expand Down
72 changes: 72 additions & 0 deletions crates/indexer/src/api/vaults/db.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
use sqlx::PgPool;

use crate::api::utils::{format_hex, format_offer_id, format_satoshis};

use super::dto::{ProtocolFeeVaultDto, ProtocolFeeVaultsResponse};

struct HarvestableProtocolFeeVaultRow {
offer_id: i64,
txid: Vec<u8>,
vout: i32,
amount: i64,
created_at_height: i64,
updated_at_height: i64,
borrower_nft_asset_id: Vec<u8>,
protocol_fee_keeper_asset_id: Vec<u8>,
}

impl From<HarvestableProtocolFeeVaultRow> for ProtocolFeeVaultDto {
fn from(row: HarvestableProtocolFeeVaultRow) -> Self {
Self {
offer_id: format_offer_id(row.offer_id),
txid: format_hex(row.txid),
vout: row.vout as u32,
amount: format_satoshis(row.amount),
borrower_nft_asset: format_hex(row.borrower_nft_asset_id),
protocol_fee_keeper_asset: format_hex(row.protocol_fee_keeper_asset_id),
created_at_height: row.created_at_height as u64,
updated_at_height: row.updated_at_height as u64,
}
}
}

#[tracing::instrument(name = "Fetching unspent protocol-fee vaults", skip(db))]
pub async fn fetch_unspent_protocol_fee_vaults(
db: &PgPool,
principal_asset_id: Vec<u8>,
) -> Result<ProtocolFeeVaultsResponse, sqlx::Error> {
let rows = sqlx::query_as!(
HarvestableProtocolFeeVaultRow,
r#"
SELECT
v.offer_id,
v.txid,
v.vout,
v.amount,
v.created_at_height,
v.updated_at_height,
o.borrower_nft_asset_id,
o.protocol_fee_keeper_asset_id
FROM offer_vaults v
JOIN offers o ON o.id = v.offer_id
Comment thread
zahorodnyi marked this conversation as resolved.
Outdated
WHERE v.vault_type = 'protocol_fee'
AND v.is_finalized = true
AND v.spent_txid IS NULL
AND o.principal_asset_id = $1
ORDER BY v.amount DESC, v.id DESC
"#,
principal_asset_id,
)
.fetch_all(db)
.await?;

let count = rows.len() as u64;
let total_amount: i64 = rows.iter().map(|row| row.amount).sum();
let items = rows.into_iter().map(ProtocolFeeVaultDto::from).collect();

Ok(ProtocolFeeVaultsResponse {
items,
count,
total_amount: format_satoshis(total_amount),
})
}
60 changes: 60 additions & 0 deletions crates/indexer/src/api/vaults/dto.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
use serde::Deserialize;
use utoipa::{IntoParams, ToSchema};

#[derive(Deserialize, IntoParams, ToSchema)]
#[into_params(parameter_in = Query)]
pub struct ProtocolFeeVaultsQuery {
#[param(example = "020202…")]
pub principal_asset: String,
}

#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, ToSchema)]
pub struct ProtocolFeeVaultDto {
#[schema(example = "1")]
pub offer_id: String,
pub txid: String,
pub vout: u32,
#[schema(example = "1000")]
pub amount: String,
pub borrower_nft_asset: String,
pub protocol_fee_keeper_asset: String,
pub created_at_height: u64,
pub updated_at_height: u64,
}

#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, ToSchema)]
pub struct ProtocolFeeVaultsResponse {
pub items: Vec<ProtocolFeeVaultDto>,
pub count: u64,
#[schema(example = "1500")]
pub total_amount: String,
}

#[cfg(test)]
mod tests {
use super::{ProtocolFeeVaultDto, ProtocolFeeVaultsResponse};

#[test]
fn protocol_fee_vaults_response_serializes_expected_shape() {
let response = ProtocolFeeVaultsResponse {
items: vec![ProtocolFeeVaultDto {
offer_id: "1".to_string(),
txid: "aabb".to_string(),
vout: 0,
amount: "1000".to_string(),
borrower_nft_asset: "0102".to_string(),
protocol_fee_keeper_asset: "0304".to_string(),
created_at_height: 10,
updated_at_height: 10,
}],
count: 1,
total_amount: "1000".to_string(),
};

let json = serde_json::to_value(&response).expect("serialize");
assert_eq!(json["count"], 1);
assert_eq!(json["total_amount"], "1000");
assert_eq!(json["items"][0]["borrower_nft_asset"], "0102");
assert_eq!(json["items"][0]["protocol_fee_keeper_asset"], "0304");
}
}
36 changes: 36 additions & 0 deletions crates/indexer/src/api/vaults/handlers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use std::sync::Arc;

use axum::{
Json,
extract::{Query, State},
};

use crate::api::openapi::ErrorResponse;
use crate::api::utils::parse_filter_hex;
use crate::api::{ApiError, AppState};

use super::dto::{ProtocolFeeVaultsQuery, ProtocolFeeVaultsResponse};

#[utoipa::path(
get,
path = "/vaults/protocol-fee",
tag = "vaults",
params(ProtocolFeeVaultsQuery),
responses(
(status = 200, description = "Unspent, finalized protocol-fee vaults for the asset, sorted by amount desc", body = ProtocolFeeVaultsResponse),
(status = 400, description = "Invalid principal_asset hex", body = ErrorResponse),
(status = 500, description = "Internal server error", body = ErrorResponse),
)
)]
#[tracing::instrument(name = "Getting unspent protocol-fee vaults", skip(state, query))]
pub async fn list_protocol_fee_vaults(
State(state): State<Arc<AppState>>,
Query(query): Query<ProtocolFeeVaultsQuery>,
) -> Result<Json<ProtocolFeeVaultsResponse>, ApiError> {
let asset_bytes = parse_filter_hex(&query.principal_asset)
.ok_or_else(|| ApiError::BadRequest("Invalid principal_asset hex".to_string()))?;

let response = super::db::fetch_unspent_protocol_fee_vaults(&state.db, asset_bytes).await?;

Ok(Json(response))
}
6 changes: 6 additions & 0 deletions crates/indexer/src/api/vaults/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pub(crate) mod db;
pub(crate) mod dto;
pub(crate) mod handlers;
mod routes;

pub use routes::routes;
14 changes: 14 additions & 0 deletions crates/indexer/src/api/vaults/routes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
use std::sync::Arc;

use axum::{Router, routing::get};

use crate::api::AppState;

use super::handlers;

pub fn routes() -> Router<Arc<AppState>> {
Router::new().nest(
"/vaults",
Router::new().route("/protocol-fee", get(handlers::list_protocol_fee_vaults)),
)
}
Loading
Loading