bark-rest 0.2.1

a REST server built on top of the bark-wallet crate
Documentation
use std::str::FromStr;

use axum::extract::{Query, State};
use axum::routing::get;
use axum::{debug_handler, Json, Router};
use anyhow::Context;
use bitcoin::Amount;
use utoipa::OpenApi;

use crate::ServerState;
use crate::error::{self, HandlerResult, ContextExt};

#[derive(OpenApi)]
#[openapi(
	paths(
		onchain_fee_rates,
		board_fee,
		send_onchain_fee,
		offboard_all_fee,
		lightning_send_fee,
		lightning_receive_fee,
	),
	components(schemas(
		bark_json::web::FeeEstimateQuery,
		bark_json::web::SendOnchainFeeEstimateQuery,
		bark_json::web::OffboardAllFeeEstimateQuery,
		bark_json::web::FeeEstimateResponse,
		bark_json::web::OnchainFeeRatesResponse,
	)),
	tags((name = "fees", description = "Estimate fees for wallet operations before executing them."))
)]
pub struct FeesApiDoc;

pub fn router() -> Router<ServerState> {
	Router::new()
		.route("/onchain", get(onchain_fee_rates))
		.route("/board", get(board_fee))
		.route("/send-onchain", get(send_onchain_fee))
		.route("/offboard-all", get(offboard_all_fee))
		.route("/lightning/pay", get(lightning_send_fee))
		.route("/lightning/receive", get(lightning_receive_fee))
}

#[utoipa::path(
	get,
	path = "/onchain",
	summary = "Get on-chain fee rates",
	responses(
		(status = 200, description = "Returns current mempool fee rates", body = bark_json::web::OnchainFeeRatesResponse),
		(status = 500, description = "Internal server error", body = error::InternalServerError)
	),
	description = "Returns the current mempool fee rates from the chain source at three \
		confirmation targets: fast (~1 block), regular (~3 blocks), and slow (~6 blocks). \
		Rates are in sat/vB, rounded up.",
	tag = "fees"
)]
#[debug_handler]
pub async fn onchain_fee_rates(
	State(state): State<ServerState>,
) -> HandlerResult<Json<bark_json::web::OnchainFeeRatesResponse>> {
	let wallet = state.require_wallet()?;

	let rates = wallet.chain().fee_rates().await;

	Ok(axum::Json(bark_json::web::OnchainFeeRatesResponse {
		fast_sat_per_vb: rates.fast.to_sat_per_vb_ceil(),
		regular_sat_per_vb: rates.regular.to_sat_per_vb_ceil(),
		slow_sat_per_vb: rates.slow.to_sat_per_vb_ceil(),
	}))
}

#[utoipa::path(
	get,
	path = "/board",
	summary = "Estimate board fee",
	params(
		("amount_sat" = u64, Query, description = "The amount in satoshis to board"),
	),
	responses(
		(status = 200, description = "Returns the fee estimate", body = bark_json::web::FeeEstimateResponse),
		(status = 400, description = "Invalid amount", body = error::BadRequestError),
		(status = 500, description = "Internal server error", body = error::InternalServerError)
	),
	description = "Estimates the Ark protocol fee for boarding the specified amount of on-chain \
		bitcoin. The net amount is what the user receives as a VTXO. Does not include the \
		on-chain transaction fee for the board anchor transaction.",
	tag = "fees"
)]
#[debug_handler]
pub async fn board_fee(
	State(state): State<ServerState>,
	Query(query): Query<bark_json::web::FeeEstimateQuery>,
) -> HandlerResult<Json<bark_json::web::FeeEstimateResponse>> {
	let wallet = state.require_wallet()?;

	let amount = Amount::from_sat(query.amount_sat);
	let estimate = wallet.estimate_board_offchain_fee(amount).await
		.context("Failed to estimate board fee")?;

	Ok(axum::Json(estimate.into()))
}

#[utoipa::path(
	get,
	path = "/send-onchain",
	summary = "Estimate send-onchain fee",
	params(
		("amount_sat" = u64, Query, description = "The amount in satoshis to send on-chain"),
		("address" = String, Query, description = "The destination Bitcoin address"),
	),
	responses(
		(status = 200, description = "Returns the fee estimate", body = bark_json::web::FeeEstimateResponse),
		(status = 400, description = "Invalid amount or address", body = error::BadRequestError),
		(status = 500, description = "Internal server error", body = error::InternalServerError)
	),
	description = "Estimates the total fee for sending bitcoin from the Ark wallet to an \
		on-chain address. The fee depends on the destination address type and current fee \
		rates. The gross amount is what the user pays (including VTXOs spent), and the net \
		amount is what the recipient receives on-chain.",
	tag = "fees"
)]
#[debug_handler]
pub async fn send_onchain_fee(
	State(state): State<ServerState>,
	Query(query): Query<bark_json::web::SendOnchainFeeEstimateQuery>,
) -> HandlerResult<Json<bark_json::web::FeeEstimateResponse>> {
	let wallet = state.require_wallet()?;

	let network = wallet.network().await?;
	let address = bitcoin::Address::from_str(&query.address)
		.badarg("Invalid destination address")?
		.require_network(network)
		.badarg("Address is not valid for configured network")?;

	let amount = Amount::from_sat(query.amount_sat);
	let estimate = wallet.estimate_send_onchain(&address, amount).await
		.context("Failed to estimate send-onchain fee")?;

	Ok(axum::Json(estimate.into()))
}

#[utoipa::path(
	get,
	path = "/offboard-all",
	summary = "Estimate offboard-all fee",
	params(
		("address" = String, Query, description = "The destination Bitcoin address"),
	),
	responses(
		(status = 200, description = "Returns the fee estimate", body = bark_json::web::FeeEstimateResponse),
		(status = 400, description = "Invalid address", body = error::BadRequestError),
		(status = 500, description = "Internal server error", body = error::InternalServerError)
	),
	description = "Estimates the fee for offboarding the entire Ark balance to the given \
		on-chain address. The gross amount is the total spendable balance, and the net \
		amount is what the user receives on-chain after fees. The fee depends on the \
		destination address type, current fee rates, and VTXO expiry.",
	tag = "fees"
)]
#[debug_handler]
pub async fn offboard_all_fee(
	State(state): State<ServerState>,
	Query(query): Query<bark_json::web::OffboardAllFeeEstimateQuery>,
) -> HandlerResult<Json<bark_json::web::FeeEstimateResponse>> {
	let wallet = state.require_wallet()?;

	let network = wallet.network().await?;
	let address = bitcoin::Address::from_str(&query.address)
		.badarg("Invalid destination address")?
		.require_network(network)
		.badarg("Address is not valid for configured network")?;

	let estimate = wallet.estimate_offboard_all(&address).await
		.context("Failed to estimate offboard-all fee")?;

	Ok(axum::Json(estimate.into()))
}

#[utoipa::path(
	get,
	path = "/lightning/pay",
	summary = "Estimate Lightning send fee",
	params(
		("amount_sat" = u64, Query, description = "The amount in satoshis to send over Lightning"),
	),
	responses(
		(status = 200, description = "Returns the fee estimate", body = bark_json::web::FeeEstimateResponse),
		(status = 400, description = "Invalid amount", body = error::BadRequestError),
		(status = 500, description = "Internal server error", body = error::InternalServerError)
	),
	description = "Estimates the fee for sending the specified amount over Lightning. The net \
		amount is what the recipient receives. The fee depends on the VTXOs selected and \
		their expiry. If the wallet has insufficient funds, returns a worst-case fee \
		estimate assuming the user acquires enough funds to cover the payment.",
	tag = "fees"
)]
#[debug_handler]
pub async fn lightning_send_fee(
	State(state): State<ServerState>,
	Query(query): Query<bark_json::web::FeeEstimateQuery>,
) -> HandlerResult<Json<bark_json::web::FeeEstimateResponse>> {
	let wallet = state.require_wallet()?;

	let amount = Amount::from_sat(query.amount_sat);
	let estimate = wallet.estimate_lightning_send_fee(amount).await
		.context("Failed to estimate lightning send fee")?;

	Ok(axum::Json(estimate.into()))
}

#[utoipa::path(
	get,
	path = "/lightning/receive",
	summary = "Estimate Lightning receive fee",
	params(
		("amount_sat" = u64, Query, description = "The amount in satoshis to receive over Lightning"),
	),
	responses(
		(status = 200, description = "Returns the fee estimate", body = bark_json::web::FeeEstimateResponse),
		(status = 400, description = "Invalid amount", body = error::BadRequestError),
		(status = 500, description = "Internal server error", body = error::InternalServerError)
	),
	description = "Estimates the fee for receiving the specified amount over Lightning. The \
		gross amount is the Lightning payment amount, and the net amount is what the user \
		receives as a VTXO after the Ark server deducts its fee.",
	tag = "fees"
)]
#[debug_handler]
pub async fn lightning_receive_fee(
	State(state): State<ServerState>,
	Query(query): Query<bark_json::web::FeeEstimateQuery>,
) -> HandlerResult<Json<bark_json::web::FeeEstimateResponse>> {
	let wallet = state.require_wallet()?;

	let amount = Amount::from_sat(query.amount_sat);
	let estimate = wallet.estimate_lightning_receive_fee(amount).await
		.context("Failed to estimate lightning receive fee")?;

	Ok(axum::Json(estimate.into()))
}