spacetimedb_client_api/routes/
energy.rs1use axum::extract::{Path, Query, State};
2use axum::response::IntoResponse;
3use http::StatusCode;
4use serde::{Deserialize, Serialize};
5
6use spacetimedb::energy::EnergyQuanta;
7use spacetimedb_lib::Identity;
8
9use crate::auth::SpacetimeAuthRequired;
10use crate::{log_and_500, ControlStateDelegate, NodeDelegate};
11
12use super::identity::IdentityForUrl;
13
14#[derive(Deserialize)]
15pub struct IdentityParams {
16 identity: IdentityForUrl,
17}
18
19pub async fn get_energy_balance<S: ControlStateDelegate>(
21 State(ctx): State<S>,
22 Path(IdentityParams { identity }): Path<IdentityParams>,
23) -> axum::response::Result<impl IntoResponse> {
24 let identity = Identity::from(identity);
25 get_budget_inner(ctx, &identity)
26}
27
28#[serde_with::serde_as]
29#[derive(Serialize)]
30struct BalanceResponse {
31 #[serde_as(as = "serde_with::DisplayFromStr")]
33 balance: i128,
34}
35
36#[derive(Deserialize)]
37pub struct AddEnergyQueryParams {
38 amount: Option<String>,
39}
40pub async fn add_energy<S: ControlStateDelegate>(
41 State(ctx): State<S>,
42 Query(AddEnergyQueryParams { amount }): Query<AddEnergyQueryParams>,
43 SpacetimeAuthRequired(auth): SpacetimeAuthRequired,
44) -> axum::response::Result<impl IntoResponse> {
45 let amount = amount.map(|s| s.parse::<u128>()).transpose().map_err(|e| {
47 log::error!("Failed to parse amount: {e:?}");
48 StatusCode::BAD_REQUEST
49 })?;
50
51 if let Some(satoshi) = amount {
52 ctx.add_energy(&auth.identity, EnergyQuanta::new(satoshi))
53 .await
54 .map_err(log_and_500)?;
55 }
56
57 let balance = ctx
59 .get_energy_balance(&auth.identity)
60 .map_err(log_and_500)?
61 .map_or(0, |quanta| quanta.get());
62
63 Ok(axum::Json(BalanceResponse { balance }))
64}
65
66fn get_budget_inner(ctx: impl ControlStateDelegate, identity: &Identity) -> axum::response::Result<impl IntoResponse> {
67 let balance = ctx
68 .get_energy_balance(identity)
69 .map_err(log_and_500)?
70 .map_or(0, |quanta| quanta.get());
71
72 Ok(axum::Json(BalanceResponse { balance }))
73}
74
75#[derive(Deserialize)]
76pub struct SetEnergyBalanceQueryParams {
77 balance: Option<String>,
78}
79pub async fn set_energy_balance<S: ControlStateDelegate>(
80 State(ctx): State<S>,
81 Path(IdentityParams { identity }): Path<IdentityParams>,
82 Query(SetEnergyBalanceQueryParams { balance }): Query<SetEnergyBalanceQueryParams>,
83 SpacetimeAuthRequired(auth): SpacetimeAuthRequired,
84) -> axum::response::Result<impl IntoResponse> {
85 if auth.identity != Identity::__dummy() {
91 return Err(StatusCode::UNAUTHORIZED.into());
92 }
93
94 let identity = Identity::from(identity);
95
96 let desired_balance = balance
97 .map(|balance| balance.parse::<i128>())
98 .transpose()
99 .map_err(|err| {
100 log::error!("Failed to parse balance: {err:?}");
101 StatusCode::BAD_REQUEST
102 })?
103 .unwrap_or(0);
104 let current_balance = ctx
105 .get_energy_balance(&identity)
106 .map_err(log_and_500)?
107 .map_or(0, |quanta| quanta.get());
108
109 let delta = EnergyQuanta::new(desired_balance.abs_diff(current_balance));
111 if desired_balance > current_balance {
112 ctx.add_energy(&identity, delta).await.map_err(log_and_500)?;
113 } else {
114 ctx.withdraw_energy(&identity, delta).await.map_err(log_and_500)?;
115 }
116
117 Ok(axum::Json(BalanceResponse {
118 balance: desired_balance,
119 }))
120}
121
122pub fn router<S>() -> axum::Router<S>
123where
124 S: NodeDelegate + ControlStateDelegate + Clone + 'static,
125{
126 use axum::routing::get;
127 axum::Router::new().route(
128 "/:identity",
129 get(get_energy_balance::<S>)
130 .put(set_energy_balance::<S>)
131 .post(add_energy::<S>),
132 )
133}