Skip to content

Expose BOLT 12 refunds over gRPC - #262

Merged
jkczyz merged 2 commits into
lightningdevkit:mainfrom
benthecarman:2026-08-pruned-bitcoind-docs
Aug 28, 2026
Merged

Expose BOLT 12 refunds over gRPC#262
jkczyz merged 2 commits into
lightningdevkit:mainfrom
benthecarman:2026-08-pruned-bitcoind-docs

Conversation

@benthecarman

Copy link
Copy Markdown
Collaborator

Clients need both parts of the BOLT 12 refund flow. Add RPCs for creating a refund and requesting its payment, and expose them through the Rust client and CLI.

Add end-to-end coverage for the reverse payment flow and document the new endpoints.

@ldk-reviews-bot

ldk-reviews-bot commented Aug 27, 2026

Copy link
Copy Markdown

👋 Thanks for assigning @jkczyz as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@benthecarman
benthecarman requested review from jkczyz and removed request for wpaulino August 27, 2026 05:37
Comment thread ldk-server/src/api/bolt12_refund.rs Outdated
Comment on lines +43 to +46
let invoice = context.node.bolt12_payment().request_refund_payment(&refund)?;
let payment_id = invoice.payment_hash().to_string();

Ok(Bolt12RequestRefundResponse { payment_id })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm... we can't do this unfortunately as LDK Node was updated to decouple PaymentId from PaymentHash in lightningdevkit/ldk-node#948. Even more unfortunately, we rely on LDK for generating the PaymentId when the payment is received, so LDK Node will only know it when it processes the PaymentClaimable event.

We'll likely want to model Bolt11ReceiveResponse, which returns the PaymentHash, since they are both inbound payments. So in practice we just need to rename the field in the response and update the docs.

@benthecarman benthecarman Aug 28, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made Bolt12ReceiveRefundResponse return payment_hash

let route_parameters = build_route_parameters_config_from_proto(request.route_parameters)?;
let refund = context.node.bolt12_payment().initiate_refund(
request.amount_msat,
request.expiry_secs,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we reject a 0-expiry? The CLI will give a more reasonable default if left unset, but other clients may forget to set it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added

Comment thread docs/api-guide.md Outdated
|------------------------|-------------------------------------------------------------------------|
| `Bolt12Receive` | Create a BOLT12 offer (fixed or variable amount) |
| `Bolt12Send` | Pay a BOLT12 offer (with optional quantity, payer note, routing config) |
| `Bolt12InitiateRefund` | Create a BOLT12 refund |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra space

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Comment thread ldk-server-grpc/src/proto/api.proto Outdated
Comment on lines +404 to +408
message Bolt12InitiateRefundResponse {

// A BOLT12 refund that the recipient can use to request the refund payment.
string refund = 1;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is equivalent to an outbound payment, we should have a payment_id here, but LDK Node doesn't return it. We'll want to do so upstream, IIUC, so we can return it here.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, seems more systemic than this PR tho, can defer for now?

Comment thread ldk-server-grpc/src/proto/api.proto Outdated
Comment on lines +1020 to +1023
// Return a BOLT12 refund.
rpc Bolt12InitiateRefund(Bolt12InitiateRefundRequest) returns (Bolt12InitiateRefundResponse);
// Request payment for a BOLT12 refund.
rpc Bolt12RequestRefund(Bolt12RequestRefundRequest) returns (Bolt12RequestRefundResponse);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may want to break with the LDK Node naming convention and call these Bolt12SendRefund and Bolt12ReceiveRefund, though I don't have a strong opinion. The double "request" is just a bit icky.

@benthecarman benthecarman Aug 28, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah this is better, fixed

Comment thread ldk-server-grpc/src/proto/api.proto Outdated
Comment on lines +1020 to +1023
// Return a BOLT12 refund.
rpc Bolt12InitiateRefund(Bolt12InitiateRefundRequest) returns (Bolt12InitiateRefundResponse);
// Request payment for a BOLT12 refund.
rpc Bolt12RequestRefund(Bolt12RequestRefundRequest) returns (Bolt12RequestRefundResponse);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also need MCP handlers for these.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@benthecarman
benthecarman force-pushed the 2026-08-pruned-bitcoind-docs branch from f6eb35b to 5d630e7 Compare August 28, 2026 08:08
Comment on lines +387 to +389
"type": "integer",
"minimum": 1,
"description": "Amount in millisatoshis to refund"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we do this elsewhere, too, for consistency?

@benthecarman benthecarman Aug 28, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed so we're consistent wit the rest for now

Comment on lines +257 to +259
if request.expiry_secs == 0 {
request.expiry_secs = DEFAULT_EXPIRY_SECS;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The schema as "minimum": 1,, so this will never be hit? Probably better to remove the minimum from the schema.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

Comment thread ldk-server-grpc/src/proto/api.proto Outdated
Comment on lines +391 to +392
// Refund expiry time in seconds.
uint32 expiry_secs = 2;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should note that zero is rejected.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added

Comment thread e2e-tests/tests/e2e.rs Outdated
assert_eq!(refund.payer_note().unwrap().to_string(), "test refund");

let output = run_cli(&server_a, &["bolt12-receive-refund", refund_str]);
assert!(!output["payment_hash"].as_str().unwrap().is_empty());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we check the payment hash now?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added, also added mcp tests too

@benthecarman
benthecarman force-pushed the 2026-08-pruned-bitcoind-docs branch from 5d630e7 to dd43c20 Compare August 28, 2026 18:52
Comment thread e2e-tests/tests/mcp.rs
Comment on lines +39 to +42
fn tool_result_json(response: &Value) -> Value {
let text = response["result"]["content"][0]["text"].as_str().unwrap();
serde_json::from_str(text).unwrap()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be used in a few other pre-existing places. Consider adding another commit to do so.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added

Comment thread e2e-tests/tests/mcp.rs Outdated
Comment on lines +26 to +37
async fn wait_for_event(events: &mut EventStream, pred: impl Fn(&Event) -> bool) -> EventEnvelope {
tokio::time::timeout(EVENT_TIMEOUT, async {
while let Some(Ok(event)) = events.next_message().await {
if event.event.as_ref().is_some_and(&pred) {
return event;
}
}
panic!("Event stream ended without matching event");
})
.await
.expect("Timed out waiting for event")
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is copied from e2e.rs. Could you refactor them into e2e-tests/src/lib.rs?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread ldk-server-mcp/src/tools/schema.rs Outdated
},
"expiry_secs": {
"type": "integer",
"description": "Refund expiry time in seconds"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add "(defaults to 86400 if omitted or 0)" like a similar description in bolt11_receive_schema?

@benthecarman benthecarman Aug 28, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added

Share event waiting across test suites and centralize MCP tool response
parsing so existing and future tests use consistent behavior.

Created with Codex, an AI coding assistant.
Clients need both parts of the BOLT 12 refund flow. Add RPCs for
creating a refund and requesting its payment, and expose them through
the Rust client and CLI.

Add end-to-end coverage for the reverse payment flow and document the
new endpoints.

This change was developed with OpenAI Codex assistance.
@benthecarman
benthecarman force-pushed the 2026-08-pruned-bitcoind-docs branch from dd43c20 to b661d5f Compare August 28, 2026 20:04
@jkczyz
jkczyz merged commit db32dda into lightningdevkit:main Aug 28, 2026
9 checks passed
@benthecarman
benthecarman deleted the 2026-08-pruned-bitcoind-docs branch August 28, 2026 21:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants