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

Bolt12 with support for multiple payments per mint quote #404

Draft
wants to merge 40 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift click to select a range
b3f892f
feat: bolt12
thesimplekid Sep 27, 2024
8558e94
feat: bolt12 mints
thesimplekid Oct 25, 2024
c9cfb03
chore: clippy
thesimplekid Oct 26, 2024
cf0b4ce
feat: check mint bolt12
thesimplekid Oct 26, 2024
9ded476
fix: mint nut04
thesimplekid Oct 26, 2024
c322bb6
feat: wait invoice response
thesimplekid Oct 26, 2024
aa31ad3
fix: wallet bolt12 mint
thesimplekid Oct 26, 2024
22faa1d
chore: clippy
thesimplekid Oct 30, 2024
05b00c1
feat: only enable bolt12 if configured
thesimplekid Oct 30, 2024
d51526b
feat: swagger for bolt12
thesimplekid Oct 30, 2024
6410d2b
feat: return error for lnd bolt12
thesimplekid Oct 30, 2024
3c56b37
chore: bolt 12 request updates
thesimplekid Oct 31, 2024
9780b38
fix: uuid as isser
thesimplekid Oct 31, 2024
6fa9b41
feat: cli bolt12 flags
thesimplekid Oct 31, 2024
3764dae
feat: bolt12 trait
thesimplekid Oct 31, 2024
12e89e3
Merge main into multuple bolt12
thesimplekid Nov 5, 2024
fbdd3d6
fix: flake
thesimplekid Nov 5, 2024
5fe70de
chore: clippy
thesimplekid Nov 5, 2024
61200fa
chore: Merge main
thesimplekid Nov 5, 2024
a0e0899
Merge branch 'main' into multiple_bolt12
thesimplekid Nov 6, 2024
05b8bb1
feat: mint builder to cdk
thesimplekid Nov 6, 2024
523569c
Merge main into multiple_bolt12
thesimplekid Nov 6, 2024
5963580
chore: Merge main into multiple_bolt12
thesimplekid Nov 6, 2024
f656bf1
chore: nix flake stable
thesimplekid Nov 6, 2024
9c1341d
feat: nut19 signature on mint witness
thesimplekid Nov 8, 2024
311aa23
feat: use utf-8 bytes
thesimplekid Nov 16, 2024
f427ba6
feat: wait invoice payment id
thesimplekid Nov 17, 2024
c488db2
Merge main into multiple_bolt12
thesimplekid Nov 17, 2024
b056e86
chore: fmt, clippy, typos
thesimplekid Nov 17, 2024
c4ac11d
feat: sql bolt12
thesimplekid Nov 17, 2024
a4789ea
feat: wait any invoice for bolt12
thesimplekid Nov 17, 2024
fa2fb58
feat: bolt12 mint builder
thesimplekid Nov 17, 2024
98e3158
feat: add settings to bolt12
thesimplekid Nov 18, 2024
0afa42c
feat: mint check into cdk
thesimplekid Nov 18, 2024
524f2c3
feat: melt startup check into cdk
thesimplekid Nov 18, 2024
a04fb56
Merge: main into bolt12
thesimplekid Nov 18, 2024
558aeda
feat: rename to 20 and 21
thesimplekid Nov 18, 2024
0af8b0d
chore: merge NUT19 into 12
thesimplekid Nov 18, 2024
1a75215
feat: pubkey on bolt12
thesimplekid Nov 18, 2024
cd67f63
feat: bolt12 on fake wallet
thesimplekid Nov 18, 2024
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
12 changes: 10 additions & 2 deletions bindings/cdk-js/src/nuts/nut04.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 88,18 @@ impl From<MintBolt11Request> for JsMintBolt11Request {
impl JsMintBolt11Request {
/// Try From Base 64 String
#[wasm_bindgen(constructor)]
pub fn new(quote: String, outputs: JsValue) -> Result<JsMintBolt11Request> {
pub fn new(
quote: String,
outputs: JsValue,
witness: Option<String>,
) -> Result<JsMintBolt11Request> {
let outputs = serde_wasm_bindgen::from_value(outputs).map_err(into_err)?;
Ok(JsMintBolt11Request {
inner: MintBolt11Request { quote, outputs },
inner: MintBolt11Request {
quote,
outputs,
witness,
},
})
}

Expand Down
4 changes: 2 additions & 2 deletions bindings/cdk-js/src/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 93,7 @@ impl JsWallet {
) -> Result<JsMintQuote> {
let quote = self
.inner
.mint_quote(amount.into(), description)
.mint_quote(amount.into(), description, None)
.await
.map_err(into_err)?;

Expand Down Expand Up @@ -142,7 142,7 @@ impl JsWallet {

Ok(self
.inner
.mint(&quote_id, target, conditions)
.mint(&quote_id, target, conditions, None)
.await
.map_err(into_err)?
.into())
Expand Down
129 changes: 129 additions & 0 deletions crates/cdk-axum/src/bolt12_router.rs
Original file line number Diff line number Diff line change
@@ -0,0 1,129 @@
use anyhow::Result;
use axum::extract::{Json, Path, State};
use axum::response::Response;
use cdk::nuts::{
MeltBolt12Request, MeltQuoteBolt11Response, MeltQuoteBolt12Request, MintBolt11Request,
MintBolt11Response, MintQuoteBolt12Request, MintQuoteBolt12Response,
};

use crate::{into_response, MintState};

#[cfg_attr(feature = "swagger", utoipa::path(
get,
context_path = "/v1",
path = "/mint/quote/bolt12",
responses(
(status = 200, description = "Successful response", body = MintQuoteBolt12Response, content_type = "application/json")
)
))]
/// Get mint bolt12 quote
pub async fn get_mint_bolt12_quote(
State(state): State<MintState>,
Json(payload): Json<MintQuoteBolt12Request>,
) -> Result<Json<MintQuoteBolt12Response>, Response> {
let quote = state
.mint
.get_mint_bolt12_quote(payload)
.await
.map_err(into_response)?;

Ok(Json(quote))
}

#[cfg_attr(feature = "swagger", utoipa::path(
get,
context_path = "/v1",
path = "/mint/quote/bolt12/{quote_id}",
params(
("quote_id" = String, description = "The quote ID"),
),
responses(
(status = 200, description = "Successful response", body = MintQuoteBolt12Response, content_type = "application/json"),
(status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
)
))]
/// Get mint bolt12 quote
pub async fn get_check_mint_bolt12_quote(
State(state): State<MintState>,
Path(quote_id): Path<String>,
) -> Result<Json<MintQuoteBolt12Response>, Response> {
let quote = state
.mint
.check_mint_bolt12_quote(&quote_id)
.await
.map_err(into_response)?;

Ok(Json(quote))
}

#[cfg_attr(feature = "swagger", utoipa::path(
post,
context_path = "/v1",
path = "/mint/bolt12",
request_body(content = MintBolt11Request, description = "Request params", content_type = "application/json"),
responses(
(status = 200, description = "Successful response", body = MintBolt11Response, content_type = "application/json"),
(status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
)
))]
/// Request a quote for melting tokens
pub async fn post_mint_bolt12(
State(state): State<MintState>,
Json(payload): Json<MintBolt11Request>,
) -> Result<Json<MintBolt11Response>, Response> {
let res = state
.mint
.process_mint_request(payload)
.await
.map_err(|err| {
tracing::error!("Could not process mint: {}", err);
into_response(err)
})?;

Ok(Json(res))
}

#[cfg_attr(feature = "swagger", utoipa::path(
post,
context_path = "/v1",
path = "/melt/quote/bolt12",
request_body(content = MeltQuoteBolt12Request, description = "Quote params", content_type = "application/json"),
responses(
(status = 200, description = "Successful response", body = MeltQuoteBolt11Response, content_type = "application/json"),
(status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
)
))]
pub async fn get_melt_bolt12_quote(
State(state): State<MintState>,
Json(payload): Json<MeltQuoteBolt12Request>,
) -> Result<Json<MeltQuoteBolt11Response>, Response> {
let quote = state
.mint
.get_melt_bolt12_quote(&payload)
.await
.map_err(into_response)?;

Ok(Json(quote))
}

#[cfg_attr(feature = "swagger", utoipa::path(
post,
context_path = "/v1",
path = "/melt/bolt12",
request_body(content = MeltBolt12Request, description = "Melt params", content_type = "application/json"),
responses(
(status = 200, description = "Successful response", body = MeltQuoteBolt11Response, content_type = "application/json"),
(status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
)
))]
/// Melt tokens for a Bitcoin payment that the mint will make for the user in exchange
///
/// Requests tokens to be destroyed and sent out via Lightning.
pub async fn post_melt_bolt12(
State(state): State<MintState>,
Json(payload): Json<MeltBolt12Request>,
) -> Result<Json<MeltQuoteBolt11Response>, Response> {
let res = state.mint.melt(&payload).await.map_err(into_response)?;

Ok(Json(res))
}
39 changes: 37 additions & 2 deletions crates/cdk-axum/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 9,15 @@ use std::time::Duration;
use anyhow::Result;
use axum::routing::{get, post};
use axum::Router;
use bolt12_router::{
get_check_mint_bolt12_quote, get_melt_bolt12_quote, get_mint_bolt12_quote, post_melt_bolt12,
post_mint_bolt12,
};
use cdk::mint::Mint;
use moka::future::Cache;
use router_handlers::*;

mod bolt12_router;
mod router_handlers;
mod ws;

Expand Down Expand Up @@ -130,7 135,12 @@ pub struct MintState {
pub struct ApiDocV1;

/// Create mint [`Router`] with required endpoints for cashu mint
pub async fn create_mint_router(mint: Arc<Mint>, cache_ttl: u64, cache_tti: u64) -> Result<Router> {
pub async fn create_mint_router(
mint: Arc<Mint>,
cache_ttl: u64,
cache_tti: u64,
include_bolt12: bool,
) -> Result<Router> {
let state = MintState {
mint,
cache: Cache::builder()
Expand All @@ -140,7 150,7 @@ pub async fn create_mint_router(mint: Arc<Mint>, cache_ttl: u64, cache_tti: u64)
.build(),
};

let v1_router = Router::new()
let mut v1_router = Router::new()
.route("/keys", get(get_keys))
.route("/keysets", get(get_keysets))
.route("/keys/:keyset_id", get(get_keyset_pubkeys))
Expand All @@ -162,7 172,32 @@ pub async fn create_mint_router(mint: Arc<Mint>, cache_ttl: u64, cache_tti: u64)
.route("/info", get(get_mint_info))
.route("/restore", post(post_restore));

// Conditionally create and merge bolt12_router
if include_bolt12 {
let bolt12_router = create_bolt12_router(state.clone());
//v1_router = bolt12_router.merge(v1_router);
v1_router = v1_router.merge(bolt12_router);
}

// Nest the combined router under "/v1"
let mint_router = Router::new().nest("/v1", v1_router).with_state(state);

Ok(mint_router)
}

fn create_bolt12_router(state: MintState) -> Router<MintState> {
Router::new()
.route("/melt/quote/bolt12", post(get_melt_bolt12_quote))
.route(
"/melt/quote/bolt12/:quote_id",
get(get_check_melt_bolt11_quote),
)
.route("/melt/bolt12", post(post_melt_bolt12))
.route("/mint/quote/bolt12", post(get_mint_bolt12_quote))
.route(
"/mint/quote/bolt12/:quote_id",
get(get_check_mint_bolt12_quote),
)
.route("/mint/bolt12", post(post_mint_bolt12))
.with_state(state)
}
8 changes: 1 addition & 7 deletions crates/cdk-axum/src/router_handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 157,6 @@ pub async fn post_mint_bolt11_quote(
(status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
)
))]
/// Get mint quote by ID
///
/// Get mint quote state.
pub async fn get_check_mint_bolt11_quote(
State(state): State<MintState>,
Expand Down Expand Up @@ -283,11 281,7 @@ pub async fn post_melt_bolt11(
State(state): State<MintState>,
Json(payload): Json<MeltBolt11Request>,
) -> Result<Json<MeltQuoteBolt11Response>, Response> {
let res = state
.mint
.melt_bolt11(&payload)
.await
.map_err(into_response)?;
let res = state.mint.melt(&payload).await.map_err(into_response)?;

Ok(Json(res))
}
Expand Down
13 changes: 12 additions & 1 deletion crates/cdk-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 25,7 @@ const DEFAULT_WORK_DIR: &str = ".cdk-cli";
#[derive(Parser)]
#[command(name = "cashu-tool")]
#[command(author = "thesimplekid <[email protected]>")]
#[command(version = "0.1.0")]
#[command(version = "0.4.0")]
#[command(author, version, about, long_about = None)]
struct Cli {
/// Database engine to use (sqlite/redb)
Expand Down Expand Up @@ -64,6 64,8 @@ enum Commands {
MintInfo(sub_commands::mint_info::MintInfoSubcommand),
/// Mint proofs via bolt11
Mint(sub_commands::mint::MintSubCommand),
/// Remint
ReMint(sub_commands::remint_bolt12::ReMintSubCommand),
/// Burn Spent tokens
Burn(sub_commands::burn::BurnSubCommand),
/// Restore proofs from seed
Expand Down Expand Up @@ -219,5 221,14 @@ async fn main() -> Result<()> {
Commands::CreateRequest(sub_command_args) => {
sub_commands::create_request::create_request(&multi_mint_wallet, sub_command_args).await
}
Commands::ReMint(sub_command_args) => {
sub_commands::remint_bolt12::remint(
&multi_mint_wallet,
&mnemonic.to_seed_normalized(""),
localstore,
sub_command_args,
)
.await
}
}
}
48 changes: 34 additions & 14 deletions crates/cdk-cli/src/sub_commands/melt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 3,9 @@ use std::io::Write;
use std::str::FromStr;

use anyhow::{bail, Result};
use cdk::nuts::CurrencyUnit;
use cdk::amount::Amount;
use cdk::nuts::{CurrencyUnit, PaymentMethod};
use cdk::wallet::multi_mint_wallet::{MultiMintWallet, WalletKey};
use cdk::Bolt11Invoice;
use clap::Args;

use crate::sub_commands::balance::mint_balances;
Expand All @@ -15,13 15,19 @@ pub struct MeltSubCommand {
/// Currency unit e.g. sat
#[arg(default_value = "sat")]
unit: String,
/// Payment method
#[arg(short, long, default_value = "bolt11")]
method: String,
/// Amount
#[arg(short, long)]
amount: Option<u64>,
}

pub async fn pay(
multi_mint_wallet: &MultiMintWallet,
sub_command_args: &MeltSubCommand,
) -> Result<()> {
let unit = CurrencyUnit::from_str(&sub_command_args.unit)?;
let unit = CurrencyUnit::from_str(&sub_command_args.unit).unwrap();
let mints_amounts = mint_balances(multi_mint_wallet, &unit).await?;

println!("Enter mint number to melt from");
Expand All @@ -44,22 50,36 @@ pub async fn pay(
.await
.expect("Known wallet");

println!("Enter bolt11 invoice request");
let method = PaymentMethod::from_str(&sub_command_args.method)?;
match method {
PaymentMethod::Bolt11 => {
println!("Enter bolt11 invoice request");
}
PaymentMethod::Bolt12 => {
println!("Enter bolt12 invoice request");
}
_ => panic!("Unknown payment method"),
}

let mut user_input = String::new();
let stdin = io::stdin();
io::stdout().flush().unwrap();
stdin.read_line(&mut user_input)?;
let bolt11 = Bolt11Invoice::from_str(user_input.trim())?;

if bolt11
.amount_milli_satoshis()
.unwrap()
.gt(&(<cdk::Amount as Into<u64>>::into(mints_amounts[mint_number].1) * 1000_u64))
{
bail!("Not enough funds");
}
let quote = wallet.melt_quote(bolt11.to_string(), None).await?;

let quote = match method {
PaymentMethod::Bolt11 => {
wallet
.melt_quote(user_input.trim().to_string(), None)
.await?
}
PaymentMethod::Bolt12 => {
let amount = sub_command_args.amount.map(Amount::from);
wallet
.melt_bolt12_quote(user_input.trim().to_string(), amount)
.await?
}
_ => panic!("Unsupported payment methof"),
};

println!("{:?}", quote);

Expand Down
Loading
Loading