router_handlers.rs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  1. use std::str::FromStr;
  2. use anyhow::Result;
  3. use axum::extract::{Json, Path, State};
  4. use axum::http::StatusCode;
  5. use axum::response::{IntoResponse, Response};
  6. use cdk::amount::Amount;
  7. use cdk::cdk_lightning::to_unit;
  8. use cdk::error::{Error, ErrorResponse};
  9. use cdk::nuts::nut05::MeltBolt11Response;
  10. use cdk::nuts::{
  11. CheckStateRequest, CheckStateResponse, CurrencyUnit, Id, KeysResponse, KeysetResponse,
  12. MeltBolt11Request, MeltQuoteBolt11Request, MeltQuoteBolt11Response, MintBolt11Request,
  13. MintBolt11Response, MintInfo, MintQuoteBolt11Request, MintQuoteBolt11Response, MintQuoteState,
  14. PaymentMethod, RestoreRequest, RestoreResponse, SwapRequest, SwapResponse,
  15. };
  16. use cdk::util::unix_time;
  17. use cdk::Bolt11Invoice;
  18. use crate::{LnKey, MintState};
  19. pub async fn get_keys(State(state): State<MintState>) -> Result<Json<KeysResponse>, Response> {
  20. let pubkeys = state.mint.pubkeys().await.map_err(|err| {
  21. tracing::error!("Could not get keys: {}", err);
  22. into_response(err)
  23. })?;
  24. Ok(Json(pubkeys))
  25. }
  26. pub async fn get_keyset_pubkeys(
  27. State(state): State<MintState>,
  28. Path(keyset_id): Path<Id>,
  29. ) -> Result<Json<KeysResponse>, Response> {
  30. let pubkeys = state.mint.keyset_pubkeys(&keyset_id).await.map_err(|err| {
  31. tracing::error!("Could not get keyset pubkeys: {}", err);
  32. into_response(err)
  33. })?;
  34. Ok(Json(pubkeys))
  35. }
  36. pub async fn get_keysets(State(state): State<MintState>) -> Result<Json<KeysetResponse>, Response> {
  37. let mint = state.mint.keysets().await.map_err(|err| {
  38. tracing::error!("Could not get keyset: {}", err);
  39. into_response(err)
  40. })?;
  41. Ok(Json(mint))
  42. }
  43. pub async fn get_mint_bolt11_quote(
  44. State(state): State<MintState>,
  45. Json(payload): Json<MintQuoteBolt11Request>,
  46. ) -> Result<Json<MintQuoteBolt11Response>, Response> {
  47. let ln = state
  48. .ln
  49. .get(&LnKey::new(payload.unit, PaymentMethod::Bolt11))
  50. .ok_or_else(|| {
  51. tracing::info!("Bolt11 mint request for unsupported unit");
  52. into_response(Error::UnitUnsupported)
  53. })?;
  54. let amount =
  55. to_unit(payload.amount, &payload.unit, &ln.get_settings().unit).map_err(|err| {
  56. tracing::error!("Backed does not support unit: {}", err);
  57. into_response(Error::UnitUnsupported)
  58. })?;
  59. let quote_expiry = unix_time() + state.quote_ttl;
  60. let create_invoice_response = ln
  61. .create_invoice(amount, &payload.unit, "".to_string(), quote_expiry)
  62. .await
  63. .map_err(|err| {
  64. tracing::error!("Could not create invoice: {}", err);
  65. into_response(Error::InvalidPaymentRequest)
  66. })?;
  67. let quote = state
  68. .mint
  69. .new_mint_quote(
  70. state.mint_url,
  71. create_invoice_response.request.to_string(),
  72. payload.unit,
  73. payload.amount,
  74. create_invoice_response.expiry.unwrap_or(0),
  75. create_invoice_response.request_lookup_id,
  76. )
  77. .await
  78. .map_err(|err| {
  79. tracing::error!("Could not create new mint quote: {}", err);
  80. into_response(err)
  81. })?;
  82. Ok(Json(quote.into()))
  83. }
  84. pub async fn get_check_mint_bolt11_quote(
  85. State(state): State<MintState>,
  86. Path(quote_id): Path<String>,
  87. ) -> Result<Json<MintQuoteBolt11Response>, Response> {
  88. let quote = state
  89. .mint
  90. .check_mint_quote(&quote_id)
  91. .await
  92. .map_err(|err| {
  93. tracing::error!("Could not check mint quote {}: {}", quote_id, err);
  94. into_response(err)
  95. })?;
  96. Ok(Json(quote))
  97. }
  98. pub async fn post_mint_bolt11(
  99. State(state): State<MintState>,
  100. Json(payload): Json<MintBolt11Request>,
  101. ) -> Result<Json<MintBolt11Response>, Response> {
  102. let res = state
  103. .mint
  104. .process_mint_request(payload)
  105. .await
  106. .map_err(|err| {
  107. tracing::error!("Could not process mint: {}", err);
  108. into_response(err)
  109. })?;
  110. Ok(Json(res))
  111. }
  112. pub async fn get_melt_bolt11_quote(
  113. State(state): State<MintState>,
  114. Json(payload): Json<MeltQuoteBolt11Request>,
  115. ) -> Result<Json<MeltQuoteBolt11Response>, Response> {
  116. let ln = state
  117. .ln
  118. .get(&LnKey::new(payload.unit, PaymentMethod::Bolt11))
  119. .ok_or_else(|| {
  120. tracing::info!("Could not get ln backend for {}, bolt11 ", payload.unit);
  121. into_response(Error::UnitUnsupported)
  122. })?;
  123. let payment_quote = ln.get_payment_quote(&payload).await.map_err(|err| {
  124. tracing::error!(
  125. "Could not get payment quote for mint quote, {} bolt11, {}",
  126. payload.unit,
  127. err
  128. );
  129. into_response(Error::UnitUnsupported)
  130. })?;
  131. let quote = state
  132. .mint
  133. .new_melt_quote(
  134. payload.request.to_string(),
  135. payload.unit,
  136. payment_quote.amount,
  137. payment_quote.fee,
  138. unix_time() + state.quote_ttl,
  139. payment_quote.request_lookup_id,
  140. )
  141. .await
  142. .map_err(|err| {
  143. tracing::error!("Could not create melt quote: {}", err);
  144. into_response(err)
  145. })?;
  146. Ok(Json(quote.into()))
  147. }
  148. pub async fn get_check_melt_bolt11_quote(
  149. State(state): State<MintState>,
  150. Path(quote_id): Path<String>,
  151. ) -> Result<Json<MeltQuoteBolt11Response>, Response> {
  152. let quote = state
  153. .mint
  154. .check_melt_quote(&quote_id)
  155. .await
  156. .map_err(|err| {
  157. tracing::error!("Could not check melt quote: {}", err);
  158. into_response(err)
  159. })?;
  160. Ok(Json(quote))
  161. }
  162. pub async fn post_melt_bolt11(
  163. State(state): State<MintState>,
  164. Json(payload): Json<MeltBolt11Request>,
  165. ) -> Result<Json<MeltBolt11Response>, Response> {
  166. let quote = match state.mint.verify_melt_request(&payload).await {
  167. Ok(quote) => quote,
  168. Err(err) => {
  169. tracing::debug!("Error attempting to verify melt quote: {}", err);
  170. if let Err(err) = state.mint.process_unpaid_melt(&payload).await {
  171. tracing::error!("Could not reset melt quote state: {}", err);
  172. }
  173. return Err(into_response(Error::UnitUnsupported));
  174. }
  175. };
  176. // Check to see if there is a corresponding mint quote for a melt.
  177. // In this case the mint can settle the payment internally and no ln payment is
  178. // needed
  179. let mint_quote = match state
  180. .mint
  181. .localstore
  182. .get_mint_quote_by_request(&quote.request)
  183. .await
  184. {
  185. Ok(mint_quote) => mint_quote,
  186. Err(err) => {
  187. tracing::debug!("Error attempting to get mint quote: {}", err);
  188. if let Err(err) = state.mint.process_unpaid_melt(&payload).await {
  189. tracing::error!("Could not reset melt quote state: {}", err);
  190. }
  191. return Err(into_response(Error::Internal));
  192. }
  193. };
  194. let inputs_amount_quote_unit = payload.proofs_amount().map_err(|_| {
  195. tracing::error!("Proof inputs in melt quote overflowed");
  196. into_response(Error::AmountOverflow)
  197. })?;
  198. let (preimage, amount_spent_quote_unit) = match mint_quote {
  199. Some(mint_quote) => {
  200. if mint_quote.state == MintQuoteState::Issued
  201. || mint_quote.state == MintQuoteState::Paid
  202. {
  203. return Err(into_response(Error::RequestAlreadyPaid));
  204. }
  205. let mut mint_quote = mint_quote;
  206. if mint_quote.amount > inputs_amount_quote_unit {
  207. tracing::debug!(
  208. "Not enough inuts provided: {} needed {}",
  209. inputs_amount_quote_unit,
  210. mint_quote.amount
  211. );
  212. if let Err(err) = state.mint.process_unpaid_melt(&payload).await {
  213. tracing::error!("Could not reset melt quote state: {}", err);
  214. }
  215. return Err(into_response(Error::InsufficientFunds));
  216. }
  217. mint_quote.state = MintQuoteState::Paid;
  218. let amount = quote.amount;
  219. if let Err(_err) = state.mint.update_mint_quote(mint_quote).await {
  220. if let Err(err) = state.mint.process_unpaid_melt(&payload).await {
  221. tracing::error!("Could not reset melt quote state: {}", err);
  222. }
  223. return Err(into_response(Error::Internal));
  224. }
  225. (None, amount)
  226. }
  227. None => {
  228. let invoice = match Bolt11Invoice::from_str(&quote.request) {
  229. Ok(bolt11) => bolt11,
  230. Err(_) => {
  231. tracing::error!("Melt quote has invalid payment request");
  232. if let Err(err) = state.mint.process_unpaid_melt(&payload).await {
  233. tracing::error!("Could not reset melt quote state: {}", err);
  234. }
  235. return Err(into_response(Error::InvalidPaymentRequest));
  236. }
  237. };
  238. let mut partial_amount = None;
  239. // If the quote unit is SAT or MSAT we can check that the expected fees are
  240. // provided. We also check if the quote is less then the invoice
  241. // amount in the case that it is a mmp However, if the quote id not
  242. // of a bitcoin unit we cannot do these checks as the mint
  243. // is unaware of a conversion rate. In this case it is assumed that the quote is
  244. // correct and the mint should pay the full invoice amount if inputs
  245. // > then quote.amount are included. This is checked in the
  246. // verify_melt method.
  247. if quote.unit == CurrencyUnit::Msat || quote.unit == CurrencyUnit::Sat {
  248. let quote_msats = to_unit(quote.amount, &quote.unit, &CurrencyUnit::Msat)
  249. .expect("Quote unit is checked above that it can convert to msat");
  250. let invoice_amount_msats: Amount = match invoice.amount_milli_satoshis() {
  251. Some(amount) => amount.into(),
  252. None => {
  253. if let Err(err) = state.mint.process_unpaid_melt(&payload).await {
  254. tracing::error!("Could not reset melt quote state: {}", err);
  255. }
  256. return Err(into_response(Error::InvoiceAmountUndefined));
  257. }
  258. };
  259. partial_amount = match invoice_amount_msats > quote_msats {
  260. true => {
  261. let partial_msats = invoice_amount_msats - quote_msats;
  262. Some(
  263. to_unit(partial_msats, &CurrencyUnit::Msat, &quote.unit)
  264. .map_err(|_| into_response(Error::UnitUnsupported))?,
  265. )
  266. }
  267. false => None,
  268. };
  269. let amount_to_pay = match partial_amount {
  270. Some(amount_to_pay) => amount_to_pay,
  271. None => to_unit(invoice_amount_msats, &CurrencyUnit::Msat, &quote.unit)
  272. .map_err(|_| into_response(Error::UnitUnsupported))?,
  273. };
  274. if amount_to_pay + quote.fee_reserve > inputs_amount_quote_unit {
  275. tracing::debug!(
  276. "Not enough inuts provided: {} msats needed {} msats",
  277. inputs_amount_quote_unit,
  278. amount_to_pay
  279. );
  280. if let Err(err) = state.mint.process_unpaid_melt(&payload).await {
  281. tracing::error!("Could not reset melt quote state: {}", err);
  282. }
  283. return Err(into_response(Error::InsufficientInputs(
  284. inputs_amount_quote_unit.into(),
  285. amount_to_pay.into(),
  286. quote.fee_reserve.into(),
  287. )));
  288. }
  289. }
  290. let ln = match state.ln.get(&LnKey::new(quote.unit, PaymentMethod::Bolt11)) {
  291. Some(ln) => ln,
  292. None => {
  293. tracing::info!("Could not get ln backend for {}, bolt11 ", quote.unit);
  294. if let Err(err) = state.mint.process_unpaid_melt(&payload).await {
  295. tracing::error!("Could not reset melt quote state: {}", err);
  296. }
  297. return Err(into_response(Error::UnitUnsupported));
  298. }
  299. };
  300. let pre = match ln
  301. .pay_invoice(quote.clone(), partial_amount, Some(quote.fee_reserve))
  302. .await
  303. {
  304. Ok(pay) => pay,
  305. Err(err) => {
  306. tracing::error!("Could not pay invoice: {}", err);
  307. if let Err(err) = state.mint.process_unpaid_melt(&payload).await {
  308. tracing::error!("Could not reset melt quote state: {}", err);
  309. }
  310. let err = match err {
  311. cdk::cdk_lightning::Error::InvoiceAlreadyPaid => Error::RequestAlreadyPaid,
  312. _ => Error::PaymentFailed,
  313. };
  314. return Err(into_response(err));
  315. }
  316. };
  317. let amount_spent = to_unit(pre.total_spent, &ln.get_settings().unit, &quote.unit)
  318. .map_err(|_| into_response(Error::UnitUnsupported))?;
  319. (pre.payment_preimage, amount_spent)
  320. }
  321. };
  322. let res = state
  323. .mint
  324. .process_melt_request(&payload, preimage, amount_spent_quote_unit)
  325. .await
  326. .map_err(|err| {
  327. tracing::error!("Could not process melt request: {}", err);
  328. into_response(err)
  329. })?;
  330. Ok(Json(res.into()))
  331. }
  332. pub async fn post_check(
  333. State(state): State<MintState>,
  334. Json(payload): Json<CheckStateRequest>,
  335. ) -> Result<Json<CheckStateResponse>, Response> {
  336. let state = state.mint.check_state(&payload).await.map_err(|err| {
  337. tracing::error!("Could not check state of proofs");
  338. into_response(err)
  339. })?;
  340. Ok(Json(state))
  341. }
  342. pub async fn get_mint_info(State(state): State<MintState>) -> Result<Json<MintInfo>, Response> {
  343. Ok(Json(state.mint.mint_info().clone().time(unix_time())))
  344. }
  345. pub async fn post_swap(
  346. State(state): State<MintState>,
  347. Json(payload): Json<SwapRequest>,
  348. ) -> Result<Json<SwapResponse>, Response> {
  349. let swap_response = state
  350. .mint
  351. .process_swap_request(payload)
  352. .await
  353. .map_err(|err| {
  354. tracing::error!("Could not process swap request: {}", err);
  355. into_response(err)
  356. })?;
  357. Ok(Json(swap_response))
  358. }
  359. pub async fn post_restore(
  360. State(state): State<MintState>,
  361. Json(payload): Json<RestoreRequest>,
  362. ) -> Result<Json<RestoreResponse>, Response> {
  363. let restore_response = state.mint.restore(payload).await.map_err(|err| {
  364. tracing::error!("Could not process restore: {}", err);
  365. into_response(err)
  366. })?;
  367. Ok(Json(restore_response))
  368. }
  369. pub fn into_response<T>(error: T) -> Response
  370. where
  371. T: Into<ErrorResponse>,
  372. {
  373. (
  374. StatusCode::INTERNAL_SERVER_ERROR,
  375. Json::<ErrorResponse>(error.into()),
  376. )
  377. .into_response()
  378. }