looprpc: add new method SweepHtlc

This commit is contained in:
Boris Nagaev 2026-01-14 18:08:48 -05:00
parent 1f1f668c5c
commit 9ee7a36f37
No known key found for this signature in database
8 changed files with 1055 additions and 364 deletions

File diff suppressed because it is too large Load diff

View file

@ -119,6 +119,32 @@ func local_request_SwapClient_ListSwaps_0(ctx context.Context, marshaler runtime
}
func request_SwapClient_SweepHtlc_0(ctx context.Context, marshaler runtime.Marshaler, client SwapClientClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq SweepHtlcRequest
var metadata runtime.ServerMetadata
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.SweepHtlc(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_SwapClient_SweepHtlc_0(ctx context.Context, marshaler runtime.Marshaler, server SwapClientServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq SweepHtlcRequest
var metadata runtime.ServerMetadata
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.SweepHtlc(ctx, &protoReq)
return msg, metadata, err
}
func request_SwapClient_SwapInfo_0(ctx context.Context, marshaler runtime.Marshaler, client SwapClientClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq SwapInfoRequest
var metadata runtime.ServerMetadata
@ -924,6 +950,31 @@ func RegisterSwapClientHandlerServer(ctx context.Context, mux *runtime.ServeMux,
})
mux.Handle("POST", pattern_SwapClient_SweepHtlc_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/looprpc.SwapClient/SweepHtlc", runtime.WithHTTPPathPattern("/v1/loop/out/sweephtlc"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_SwapClient_SweepHtlc_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_SwapClient_SweepHtlc_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_SwapClient_SwapInfo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
@ -1656,6 +1707,28 @@ func RegisterSwapClientHandlerClient(ctx context.Context, mux *runtime.ServeMux,
})
mux.Handle("POST", pattern_SwapClient_SweepHtlc_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/looprpc.SwapClient/SweepHtlc", runtime.WithHTTPPathPattern("/v1/loop/out/sweephtlc"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_SwapClient_SweepHtlc_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_SwapClient_SweepHtlc_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_SwapClient_SwapInfo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
@ -2216,6 +2289,8 @@ var (
pattern_SwapClient_ListSwaps_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "loop", "swaps"}, ""))
pattern_SwapClient_SweepHtlc_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v1", "loop", "out", "sweephtlc"}, ""))
pattern_SwapClient_SwapInfo_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"v1", "loop", "swap", "id"}, ""))
pattern_SwapClient_LoopOutTerms_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v1", "loop", "out", "terms"}, ""))
@ -2274,6 +2349,8 @@ var (
forward_SwapClient_ListSwaps_0 = runtime.ForwardResponseMessage
forward_SwapClient_SweepHtlc_0 = runtime.ForwardResponseMessage
forward_SwapClient_SwapInfo_0 = runtime.ForwardResponseMessage
forward_SwapClient_LoopOutTerms_0 = runtime.ForwardResponseMessage

View file

@ -38,6 +38,12 @@ service SwapClient {
*/
rpc ListSwaps (ListSwapsRequest) returns (ListSwapsResponse);
/* loop: `sweephtlc`
SweepHtlc spends a swap HTLC output via the preimage (success) path using
the swap's known preimage or an optionally supplied one.
*/
rpc SweepHtlc (SweepHtlcRequest) returns (SweepHtlcResponse);
/* loop: `swapinfo`
SwapInfo returns all known details about a single swap.
*/
@ -739,6 +745,61 @@ message ListSwapsResponse {
int64 next_start_time = 2;
}
// SweepHtlcRequest instructs loopd to sweep a swap HTLC via its success path.
message SweepHtlcRequest {
// Optional override for the sweep destination; defaults to a new address
// derived from the connected lnd wallet.
string dest_address = 1;
// Fee rate used for the sweep transaction in sat/vByte.
uint32 sat_per_vbyte = 2;
// HTLC outpoint to sweep, formatted as "txid:vout".
string outpoint = 3;
// Optional override for the stored swap preimage.
bytes preimage = 4;
// If true, publish the sweep transaction immediately.
bool publish = 5;
// The HTLC address whose output is being swept; used to derive the
// expected pkScript.
string htlc_address = 6;
}
// SweepHtlcResponse returns the broadcast sweep transaction.
message SweepHtlcResponse {
// Raw sweep transaction bytes.
bytes sweep_tx = 1;
// Miner fee paid by the sweep transaction.
uint64 fee_sats = 2;
// Publish outcome.
oneof publish {
PublishNotRequested not_requested = 3;
PublishSucceeded published = 4;
PublishFailed failed = 5;
}
}
// PublishNotRequested is returned by SweepHtlc if publishing was not requested
// in SweepHtlcRequest.
message PublishNotRequested {
}
// PublishSucceeded is returned by SweepHtlc if publishing was requested in
// SweepHtlcRequest and it succeeded.
message PublishSucceeded {
}
// PublishFailed is returned by SweepHtlc if publishing was requested in
// SweepHtlcRequest, but failed. It includes the error message.
message PublishFailed {
string error = 1;
}
message SwapInfoRequest {
/*
The swap identifier which currently is the hash that locks the HTLCs. When

View file

@ -660,6 +660,40 @@
]
}
},
"/v1/loop/out/sweephtlc": {
"post": {
"summary": "loop: `sweephtlc`\nSweepHtlc spends a swap HTLC output via the preimage (success) path using\nthe swap's known preimage or an optionally supplied one.",
"operationId": "SwapClient_SweepHtlc",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/looprpcSweepHtlcResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "body",
"description": "SweepHtlcRequest instructs loopd to sweep a swap HTLC via its success path.",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/looprpcSweepHtlcRequest"
}
}
],
"tags": [
"SwapClient"
]
}
},
"/v1/loop/out/terms": {
"get": {
"summary": "loop: `terms`\nLoopOutTerms returns the terms that the server enforces for a loop out swap.",
@ -2277,6 +2311,23 @@
"looprpcProbeResponse": {
"type": "object"
},
"looprpcPublishFailed": {
"type": "object",
"properties": {
"error": {
"type": "string"
}
},
"description": "PublishFailed is returned by SweepHtlc if publishing was requested in\nSweepHtlcRequest, but failed. It includes the error message."
},
"looprpcPublishNotRequested": {
"type": "object",
"description": "PublishNotRequested is returned by SweepHtlc if publishing was not requested\nin SweepHtlcRequest."
},
"looprpcPublishSucceeded": {
"type": "object",
"description": "PublishSucceeded is returned by SweepHtlc if publishing was requested in\nSweepHtlcRequest and it succeeded."
},
"looprpcRouteHint": {
"type": "object",
"properties": {
@ -2750,6 +2801,63 @@
"default": "LOOP_OUT",
"title": "- LOOP_OUT: LOOP_OUT indicates an loop out swap (off-chain to on-chain)\n - LOOP_IN: LOOP_IN indicates a loop in swap (on-chain to off-chain)"
},
"looprpcSweepHtlcRequest": {
"type": "object",
"properties": {
"dest_address": {
"type": "string",
"description": "Optional override for the sweep destination; defaults to a new address\nderived from the connected lnd wallet."
},
"sat_per_vbyte": {
"type": "integer",
"format": "int64",
"description": "Fee rate used for the sweep transaction in sat/vByte."
},
"outpoint": {
"type": "string",
"description": "HTLC outpoint to sweep, formatted as \"txid:vout\"."
},
"preimage": {
"type": "string",
"format": "byte",
"description": "Optional override for the stored swap preimage."
},
"publish": {
"type": "boolean",
"description": "If true, publish the sweep transaction immediately."
},
"htlc_address": {
"type": "string",
"description": "The HTLC address whose output is being swept; used to derive the\nexpected pkScript."
}
},
"description": "SweepHtlcRequest instructs loopd to sweep a swap HTLC via its success path."
},
"looprpcSweepHtlcResponse": {
"type": "object",
"properties": {
"sweep_tx": {
"type": "string",
"format": "byte",
"description": "Raw sweep transaction bytes."
},
"fee_sats": {
"type": "string",
"format": "uint64",
"description": "Miner fee paid by the sweep transaction."
},
"not_requested": {
"$ref": "#/definitions/looprpcPublishNotRequested"
},
"published": {
"$ref": "#/definitions/looprpcPublishSucceeded"
},
"failed": {
"$ref": "#/definitions/looprpcPublishFailed"
}
},
"description": "SweepHtlcResponse returns the broadcast sweep transaction."
},
"looprpcTokensResponse": {
"type": "object",
"properties": {

View file

@ -18,6 +18,9 @@ http:
get: "/v1/loop/out/terms"
- selector: looprpc.SwapClient.LoopOutQuote
get: "/v1/loop/out/quote/{amt}"
- selector: looprpc.SwapClient.SweepHtlc
post: "/v1/loop/out/sweephtlc"
body: "*"
- selector: looprpc.SwapClient.GetLoopInTerms
get: "/v1/loop/in/terms"
- selector: looprpc.SwapClient.GetLoopInQuote

View file

@ -37,6 +37,10 @@ type SwapClientClient interface {
// ListSwaps returns a list of all currently known swaps and their current
// status.
ListSwaps(ctx context.Context, in *ListSwapsRequest, opts ...grpc.CallOption) (*ListSwapsResponse, error)
// loop: `sweephtlc`
// SweepHtlc spends a swap HTLC output via the preimage (success) path using
// the swap's known preimage or an optionally supplied one.
SweepHtlc(ctx context.Context, in *SweepHtlcRequest, opts ...grpc.CallOption) (*SweepHtlcResponse, error)
// loop: `swapinfo`
// SwapInfo returns all known details about a single swap.
SwapInfo(ctx context.Context, in *SwapInfoRequest, opts ...grpc.CallOption) (*SwapStatus, error)
@ -205,6 +209,15 @@ func (c *swapClientClient) ListSwaps(ctx context.Context, in *ListSwapsRequest,
return out, nil
}
func (c *swapClientClient) SweepHtlc(ctx context.Context, in *SweepHtlcRequest, opts ...grpc.CallOption) (*SweepHtlcResponse, error) {
out := new(SweepHtlcResponse)
err := c.cc.Invoke(ctx, "/looprpc.SwapClient/SweepHtlc", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *swapClientClient) SwapInfo(ctx context.Context, in *SwapInfoRequest, opts ...grpc.CallOption) (*SwapStatus, error) {
out := new(SwapStatus)
err := c.cc.Invoke(ctx, "/looprpc.SwapClient/SwapInfo", in, out, opts...)
@ -471,6 +484,10 @@ type SwapClientServer interface {
// ListSwaps returns a list of all currently known swaps and their current
// status.
ListSwaps(context.Context, *ListSwapsRequest) (*ListSwapsResponse, error)
// loop: `sweephtlc`
// SweepHtlc spends a swap HTLC output via the preimage (success) path using
// the swap's known preimage or an optionally supplied one.
SweepHtlc(context.Context, *SweepHtlcRequest) (*SweepHtlcResponse, error)
// loop: `swapinfo`
// SwapInfo returns all known details about a single swap.
SwapInfo(context.Context, *SwapInfoRequest) (*SwapStatus, error)
@ -589,6 +606,9 @@ func (UnimplementedSwapClientServer) Monitor(*MonitorRequest, SwapClient_Monitor
func (UnimplementedSwapClientServer) ListSwaps(context.Context, *ListSwapsRequest) (*ListSwapsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListSwaps not implemented")
}
func (UnimplementedSwapClientServer) SweepHtlc(context.Context, *SweepHtlcRequest) (*SweepHtlcResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method SweepHtlc not implemented")
}
func (UnimplementedSwapClientServer) SwapInfo(context.Context, *SwapInfoRequest) (*SwapStatus, error) {
return nil, status.Errorf(codes.Unimplemented, "method SwapInfo not implemented")
}
@ -758,6 +778,24 @@ func _SwapClient_ListSwaps_Handler(srv interface{}, ctx context.Context, dec fun
return interceptor(ctx, in, info, handler)
}
func _SwapClient_SweepHtlc_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SweepHtlcRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(SwapClientServer).SweepHtlc(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/looprpc.SwapClient/SweepHtlc",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(SwapClientServer).SweepHtlc(ctx, req.(*SweepHtlcRequest))
}
return interceptor(ctx, in, info, handler)
}
func _SwapClient_SwapInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SwapInfoRequest)
if err := dec(in); err != nil {
@ -1263,6 +1301,10 @@ var SwapClient_ServiceDesc = grpc.ServiceDesc{
MethodName: "ListSwaps",
Handler: _SwapClient_ListSwaps_Handler,
},
{
MethodName: "SweepHtlc",
Handler: _SwapClient_SweepHtlc_Handler,
},
{
MethodName: "SwapInfo",
Handler: _SwapClient_SwapInfo_Handler,

View file

@ -55,6 +55,13 @@ var RequiredPermissions = map[string][]bakery.Op{
Entity: "loop",
Action: "out",
}},
"/looprpc.SwapClient/SweepHtlc": {{
Entity: "swap",
Action: "read",
}, {
Entity: "loop",
Action: "out",
}},
"/looprpc.SwapClient/GetLoopInTerms": {{
Entity: "terms",
Action: "read",

View file

@ -138,6 +138,31 @@ func RegisterSwapClientJSONCallbacks(registry map[string]func(ctx context.Contex
callback(string(respBytes), nil)
}
registry["looprpc.SwapClient.SweepHtlc"] = func(ctx context.Context,
conn *grpc.ClientConn, reqJSON string, callback func(string, error)) {
req := &SweepHtlcRequest{}
err := marshaler.Unmarshal([]byte(reqJSON), req)
if err != nil {
callback("", err)
return
}
client := NewSwapClientClient(conn)
resp, err := client.SweepHtlc(ctx, req)
if err != nil {
callback("", err)
return
}
respBytes, err := marshaler.Marshal(resp)
if err != nil {
callback("", err)
return
}
callback(string(respBytes), nil)
}
registry["looprpc.SwapClient.SwapInfo"] = func(ctx context.Context,
conn *grpc.ClientConn, reqJSON string, callback func(string, error)) {