refactor: replace direct HTTP melt bypass with CDK-native meltProofs

This commit is contained in:
Evan Kaloudis
2026-06-01 06:06:31 +05:30
committed by Khushal
parent 754f7d4d11
commit cde80ddfd8
2 changed files with 29 additions and 147 deletions
@@ -955,18 +955,22 @@ class CashuDevKitModule(private val reactContext: ReactApplicationContext) :
}
@ReactMethod
fun meltPartial(mintUrl: String, quoteId: String, amount: Double, promise: Promise) {
fun meltPartial(mintUrl: String, bolt11: String, mppAmountMsat: Double, promise: Promise) {
val wallet = getInitializedWallet(promise) ?: return
scope.launch {
try {
val url = MintUrl(mintUrl)
val targetAmount = amount.toLong().toULong()
val mppAmount = Amount(mppAmountMsat.toLong().toULong())
val normalizedUrl = mintUrl.trimEnd('/')
// Get all proofs and find those for this mint
// Step 1: Create melt quote via CDK with MPP options
val options = MeltOptions.Mpp(mppAmount)
val quote = wallet!!.meltQuote(url, bolt11, options)
// Step 2: Get all proofs for this mint
val allProofs = wallet!!.listProofs()
var mintProofs = mutableListOf<org.cashudevkit.Proof>()
val mintProofs = mutableListOf<org.cashudevkit.Proof>()
for ((key, proofs) in allProofs) {
if (key.trimEnd('/') == normalizedUrl) {
mintProofs.addAll(proofs)
@@ -981,31 +985,8 @@ class CashuDevKitModule(private val reactContext: ReactApplicationContext) :
return@launch
}
// Sort proofs largest first for efficient selection
mintProofs.sortByDescending { it.amount.value }
// Select proofs that cover the target amount
val selected = mutableListOf<org.cashudevkit.Proof>()
var total = 0UL
for (proof in mintProofs) {
if (total >= targetAmount) break
selected.add(proof)
total += proof.amount.value
}
if (total < targetAmount) {
withContext(Dispatchers.Main) {
promise.reject("INSUFFICIENT_PROOFS",
"Selected $total sats but need $targetAmount")
}
return@launch
}
// Execute the melt with the selected proofs
val melted = wallet!!.meltProofs(url, quoteId, selected)
// Restore to sync CDK's proof database after direct melt
try { wallet!!.restore(url) } catch (_: Exception) {}
// Step 3: Try CDK's meltProofs (keeps proof DB in sync)
val melted = wallet!!.meltProofs(url, quote.id, mintProofs)
withContext(Dispatchers.Main) {
promise.resolve(encodeMelted(melted).toString())
+19 -118
View File
@@ -898,57 +898,21 @@ class CashuDevKitModule: RCTEventEmitter {
Task {
do {
let url = MintUrl(url: mintUrl)
let normalizedUrl = mintUrl.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
let mppAmount = Amount(value: mppAmountMsat.uint64Value)
// Step 1: Create melt quote directly with the mint to get actual fee
let quoteUrl = normalizedUrl + "/v1/melt/quote/bolt11"
guard let quoteRequestUrl = URL(string: quoteUrl) else {
reject("INVALID_URL", "Invalid mint URL", nil)
return
}
// Step 1: Create melt quote via CDK with MPP options
let options = MeltOptions.mpp(amount: mppAmount)
let quote = try await wallet.meltQuote(
mintUrl: url,
request: bolt11,
options: options
)
let quoteBody: [String: Any] = [
"request": bolt11,
"unit": "sat",
"options": ["mpp": ["amount": mppAmountMsat.uint64Value]]
]
var quoteRequest = URLRequest(url: quoteRequestUrl)
quoteRequest.httpMethod = "POST"
quoteRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
quoteRequest.httpBody = try JSONSerialization.data(withJSONObject: quoteBody)
let (quoteData, quoteResponse) = try await URLSession.shared.data(for: quoteRequest)
if let httpResp = quoteResponse as? HTTPURLResponse,
!(200...299).contains(httpResp.statusCode) {
let errorBody = String(data: quoteData, encoding: .utf8) ?? "Unknown"
reject("QUOTE_HTTP_ERROR", "Quote failed HTTP \(httpResp.statusCode): \(errorBody)", nil)
return
}
guard let quoteJson = try? JSONSerialization.jsonObject(with: quoteData) as? [String: Any],
let actualQuoteId = quoteJson["quote"] as? String else {
reject("PARSE_ERROR", "Failed to parse quote response", nil)
return
}
// JSONSerialization returns numbers as __NSCFNumber;
// cast to Int first then convert to UInt64
let actualAmount: UInt64 = {
if let n = quoteJson["amount"] as? Int { return UInt64(max(0, n)) }
if let n = quoteJson["amount"] as? Double { return UInt64(max(0, n)) }
return 0
}()
let actualFeeReserve: UInt64 = {
if let n = quoteJson["fee_reserve"] as? Int { return UInt64(max(0, n)) }
if let n = quoteJson["fee_reserve"] as? Double { return UInt64(max(0, n)) }
return 0
}()
let totalNeeded = actualAmount + actualFeeReserve
// Step 2: Get all proofs for this mint
// Step 2: Select proofs via CDK and execute melt
// Try meltProofs with all proofs from this mint
// the mint knows the MPP partial amount from the quote
let allProofs = try await wallet.listProofs()
let normalizedUrl = mintUrl.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
var mintProofs: [Proof] = []
for (key, proofs) in allProofs {
let normalizedKey = key.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
@@ -963,77 +927,14 @@ class CashuDevKitModule: RCTEventEmitter {
return
}
let totalProofs = mintProofs.reduce(UInt64(0)) { $0 + $1.amount.value }
guard totalProofs >= totalNeeded else {
reject("INSUFFICIENT_BALANCE",
"Mint balance \(totalProofs) < needed \(totalNeeded) (\(actualAmount) + \(actualFeeReserve) fee)",
nil)
return
}
// Step 3: Try CDK's meltProofs first (keeps proof DB in sync)
let melted = try await wallet.meltProofs(
mintUrl: url,
quoteId: quote.id,
proofs: mintProofs
)
// Step 3: Send all proofs to the mint's melt endpoint
var inputs: [[String: Any]] = []
for proof in mintProofs {
var p: [String: Any] = [
"amount": proof.amount.value,
"secret": proof.secret,
"C": proof.c,
"id": proof.keysetId
]
if let witness = proof.witness {
p["witness"] = witness
}
inputs.append(p)
}
let meltUrl = normalizedUrl + "/v1/melt/bolt11"
guard let meltRequestUrl = URL(string: meltUrl) else {
reject("INVALID_URL", "Invalid melt URL", nil)
return
}
let meltBody: [String: Any] = [
"quote": actualQuoteId,
"inputs": inputs
]
var meltRequest = URLRequest(url: meltRequestUrl)
meltRequest.httpMethod = "POST"
meltRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
meltRequest.httpBody = try JSONSerialization.data(withJSONObject: meltBody)
let (meltData, meltResponse) = try await URLSession.shared.data(for: meltRequest)
if let httpResp = meltResponse as? HTTPURLResponse,
!(200...299).contains(httpResp.statusCode) {
let errorBody = String(data: meltData, encoding: .utf8) ?? "Unknown"
reject("MELT_HTTP_ERROR", "Mint returned HTTP \(httpResp.statusCode): \(errorBody)", nil)
return
}
guard let json = try? JSONSerialization.jsonObject(with: meltData) as? [String: Any] else {
reject("PARSE_ERROR", "Failed to parse melt response", nil)
return
}
// Sync wallet state after direct melt restore
// re-derives proofs from seed and checks with the mint
// which ones are still valid, updating the CDK's database
try? await wallet.restore(mintUrl: url)
var result: [String: Any] = [
"state": (json["state"] as? String) ?? "PAID",
"amount": json["amount"] ?? actualAmount,
"fee_paid": json["fee_paid"] ?? json["fee_reserve"] ?? actualFeeReserve
]
if let preimage = json["payment_preimage"] as? String {
result["preimage"] = preimage
}
if let change = json["change"] as? [[String: Any]] {
result["change"] = change
}
resolve(encodeToJson(result))
resolve(encodeToJson(encodeMelted(melted)))
} catch {
reject("MELT_PARTIAL_ERROR", error.localizedDescription, error)
}