diff --git a/cmd/bolt-agent/main.go b/cmd/bolt-agent/main.go index 1f6c595..90f6089 100644 --- a/cmd/bolt-agent/main.go +++ b/cmd/bolt-agent/main.go @@ -228,6 +228,11 @@ func getApp() *cli.App { Hidden: true, Value: 5 * time.Minute, }, + &cli.BoolFlag{ + Name: "smooth", + Usage: "smooth htlcs", + Hidden: true, + }, } app.Flags = append(app.Flags, entities.GlogFlags...) diff --git a/lightning/api.go b/lightning/api.go index bb6da59..c750cb6 100644 --- a/lightning/api.go +++ b/lightning/api.go @@ -572,7 +572,7 @@ type LightingAPICalls interface { GetOnChainFunds(ctx context.Context) (*Funds, error) SendToOnChainAddress(ctx context.Context, address string, sats int64, useUnconfirmed bool, urgency Urgency) (string, error) PayInvoice(ctx context.Context, paymentRequest string, sats int64, outgoingChanIds []uint64) (*PaymentResp, error) - GetPaymentStatus(ctx context.Context, paymentHash string) (*PaymentResp, error) + GetPaymentStatus(ctx context.Context, paymentRequest string) (*PaymentResp, error) CreateInvoice(ctx context.Context, sats int64, preimage string, memo string, expiry time.Duration) (*InvoiceResp, error) IsInvoicePaid(ctx context.Context, paymentHash string) (bool, error) diff --git a/lightning/api_clnraw.go b/lightning/api_clnraw.go index ea715e2..f7a3d5e 100644 --- a/lightning/api_clnraw.go +++ b/lightning/api_clnraw.go @@ -943,9 +943,28 @@ func (l *ClnRawLightningAPI) calculateExclusions(ctx context.Context, outgoingCh } // GetPaymentStatus - API call. -func (l *ClnRawLightningAPI) GetPaymentStatus(ctx context.Context, paymentHash string) (*PaymentResp, error) { +func (l *ClnRawLightningAPI) GetPaymentStatus(ctx context.Context, paymentRequest string) (*PaymentResp, error) { var reply ClnPaymentEntries - err := l.connection.Call(ctx, ListPays, []interface{}{nil, paymentHash}, &reply, DefaultDuration) + + if paymentRequest == "" { + return nil, fmt.Errorf("missing payment request") + } + + var ( + err error + paymentHash string + ) + + if strings.HasPrefix(paymentRequest, "ln") { + paymentHash = GetHashFromInvoice(paymentRequest) + if paymentHash == "" { + return nil, fmt.Errorf("bad payment request") + } + } else { + paymentHash = paymentRequest + } + + err = l.connection.Call(ctx, ListPays, []interface{}{nil, paymentHash}, &reply, DefaultDuration) if err != nil { return nil, err } diff --git a/lightning/api_clnsocket_test.go b/lightning/api_clnsocket_test.go index 3713db1..5c867c1 100644 --- a/lightning/api_clnsocket_test.go +++ b/lightning/api_clnsocket_test.go @@ -415,12 +415,27 @@ func TestClnGetPaymentStatus(t *testing.T) { ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(Deadline)) defer cancel() - resp, err := api.GetPaymentStatus(ctx, "d4b75c00becfb3d50b98e296a3f8f4f590af2093fddf870c01ad2e19c8e13994") + resp, err := api.GetPaymentStatus(ctx, "lnbc13370n1pjx3794pp56jm4cq97e7ea2zucu2t287857kg27gynlh0cwrqp45hpnj8p8x2qdqqcqzzsxqyz5vqsp57587usq4ph5axdvjgvxhfqh64p295utlarcn88tna6q73audza0q9qyyssq6584t5datgf0kgw3thk5ny85dd3wjgmckrqs9w7ja03l8pqd643rt6zdlhc5pf6ktc6m6mrgn2d3tjesw6ups2yr96g4jp0dm9s4tmgp359yyu") + assert.NoError(t, err) assert.NotEqual(t, 0, resp.Preimage) +} + +func TestClnGetPaymentStatusLegacy(t *testing.T) { + data := clnData(t, "cln_listpay") - _, err = api.GetPaymentStatus(ctx, "d4b75c00becfb3d50b98e296a3f8f4f590af2093fddf870c01ad2e19c8e13995") - assert.Error(t, err) + _, api, closer := clnCommon(t, func(c net.Conn) { + clnCannedResponse(t, c, "listpay", data) + }) + defer closer() + + ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(Deadline)) + defer cancel() + + resp, err := api.GetPaymentStatus(ctx, "d4b75c00becfb3d50b98e296a3f8f4f590af2093fddf870c01ad2e19c8e13994") + + assert.NoError(t, err) + assert.NotEqual(t, 0, resp.Preimage) } func TestClnCreateInvoice(t *testing.T) { diff --git a/lightning/api_grpc.go b/lightning/api_grpc.go index 34c1273..c4caf04 100644 --- a/lightning/api_grpc.go +++ b/lightning/api_grpc.go @@ -859,7 +859,7 @@ func GetMsatsFromInvoice(bolt11 string) uint64 { } firstNumber := strings.IndexAny(bolt11, "1234567890") - if firstNumber == -1 { + if firstNumber <= 2 { return 0 } @@ -880,6 +880,33 @@ func GetMsatsFromInvoice(bolt11 string) uint64 { return 0 } +func GetHashFromInvoice(bolt11 string) string { + if len(bolt11) < 2 { + return "" + } + + firstNumber := strings.IndexAny(bolt11, "1234567890") + if firstNumber <= 2 { + return "" + } + + chainPrefix := bolt11[2:firstNumber] + chain := &chaincfg.Params{ + Bech32HRPSegwit: chainPrefix, + } + + inv, err := zpay32.Decode(bolt11, chain) + if err != nil { + return "" + } + + if inv.PaymentHash == nil { + return "" + } + + return hex.EncodeToString(inv.PaymentHash[:]) +} + // PayInvoice API. func (l *LndGrpcLightningAPI) PayInvoice(ctx context.Context, paymentRequest string, sats int64, outgoingChanIds []uint64) (*PaymentResp, error) { req := &routerrpc.SendPaymentRequest{} @@ -909,14 +936,6 @@ func (l *LndGrpcLightningAPI) PayInvoice(ctx context.Context, paymentRequest str resp, err := l.RouterClient.SendPaymentV2(ctx, req) if err != nil { - // TODO: we don't know the hash here, else we could return l.GetPaymentStatus() - if strings.Contains(err.Error(), "invoice is already paid") { - return nil, nil - - } else if strings.Contains(err.Error(), "AlreadyExists desc = payment is in transition") { - return nil, nil - } - return nil, err } @@ -925,12 +944,11 @@ func (l *LndGrpcLightningAPI) PayInvoice(ctx context.Context, paymentRequest str return nil, ctx.Err() } event, err := resp.Recv() + if err == io.EOF { + break + } if err != nil { - if strings.Contains(err.Error(), "AlreadyExists desc = payment is in transition") { - return nil, nil - } - return nil, err } @@ -958,16 +976,37 @@ func (l *LndGrpcLightningAPI) PayInvoice(ctx context.Context, paymentRequest str } case lnrpc.Payment_FAILED: - return nil, fmt.Errorf("failed payment") + return &PaymentResp{ + Hash: event.PaymentHash, + Status: Failed, + }, nil } } + + return nil, fmt.Errorf("eof") } // GetPaymentStatus API. -func (l *LndGrpcLightningAPI) GetPaymentStatus(ctx context.Context, paymentHash string) (*PaymentResp, error) { +func (l *LndGrpcLightningAPI) GetPaymentStatus(ctx context.Context, paymentRequest string) (*PaymentResp, error) { var err error req := &routerrpc.TrackPaymentRequest{} - req.PaymentHash, err = hex.DecodeString(paymentHash) + if paymentRequest == "" { + return nil, fmt.Errorf("missing payment request") + } + + if strings.HasPrefix(paymentRequest, "ln") { + paymentHash := GetHashFromInvoice(paymentRequest) + if paymentHash == "" { + return nil, fmt.Errorf("bad payment request") + } + + req.PaymentHash, err = hex.DecodeString(paymentHash) + if err != nil { + return nil, fmt.Errorf("bad payment request") + } + } else { + req.PaymentHash = []byte(paymentRequest) + } if err != nil { return nil, err } @@ -985,6 +1024,9 @@ func (l *LndGrpcLightningAPI) GetPaymentStatus(ctx context.Context, paymentHash } event, err := resp.Recv() + if err == io.EOF { + break + } if err != nil { return nil, err @@ -1011,6 +1053,8 @@ func (l *LndGrpcLightningAPI) GetPaymentStatus(ctx context.Context, paymentHash }, nil } } + + return nil, fmt.Errorf("eof") } // CreateInvoice API. diff --git a/lightning/api_grpc_test.go b/lightning/api_grpc_test.go index e172cd5..db38e59 100644 --- a/lightning/api_grpc_test.go +++ b/lightning/api_grpc_test.go @@ -475,7 +475,8 @@ type PayInvoiceRespFunc func(resp *PaymentResp, err error) func TestPayInvoiceGrpc(t *testing.T) { for _, currentCase := range []Pair[string, PayInvoiceRespFunc]{ {First: "payinvoice_noroute", Second: func(resp *PaymentResp, err error) { - assert.Error(t, err) + assert.NoError(t, err) + assert.Equal(t, Failed, resp.Status) }}, {First: "payinvoice_inflight", Second: func(resp *PaymentResp, err error) { assert.NoError(t, err) @@ -543,7 +544,7 @@ func TestGetPaymentStatusGrpc(t *testing.T) { TrackPaymentV2(gomock.Any(), gomock.Any()). Return(c, nil) - resp, err := api.GetPaymentStatus(context.Background(), "12581685c1f79db4049d7757ba51fc6170c33eec2dd1cb19bae3c20036f86937") + resp, err := api.GetPaymentStatus(context.Background(), "lnbcrt13370n1p3lwhhnpp5zfvpdpwp77wmgpyawatm550uv9cvx0hv9hgukxd6u0pqqdhcdymsdqqcqzpgxqyz5vqsp5xsum9s4cpkvw27f6smk4daxqkpjgmah8xxhs8ty34fm4srmwfvjs9qyyssqlx5zk7j8lfeelzmpsk0mmwp3583tl52j8us2q9nt05vrmtp3sasxzc8wyjchrum67sllzr52gjz26rcrye6y4vrlpr6pyv9jhhlrp2cq52rxf5") assert.NoError(t, err) assert.Equal(t, Success, resp.Status) } @@ -665,4 +666,13 @@ func TestGetMsatsFromInvoice(t *testing.T) { assert.Equal(t, expected, GetMsatsFromInvoice("lntb1m1pjyxysnpp5x24d0yvj0c7atwhf88l8c7xu7l80xnymhhuaz7j5u4yp6dfcgqcqdqqcqzpgxqyz5vqsp5uq3c68sh2fqrdwl2l90ccxw7qpfn65cgs3n5lrd8nwu3f2hrxk8q9qyyssqzdr5gqj35u30fra74d02utsq3gljkhfj9zpvxp8ljhaclz49zkjpy2gxha53ejyu8am8m0q97ql7algt068tze8p8wwfquc4e7m3z8cp6a2mp9")) assert.Equal(t, expected, GetMsatsFromInvoice("lnbc1m1pjyxyw7pp5ftl92yhqxnp925ppl7tr7txze2px38ccgr5msekvgru0v9fthzcqdqqcqzpgxqyz5vqsp5xa4zr56xw6fkprpc9w75cyyv4zdv0hxw7em770qetxsuz9ywsy6s9qyyssqpuyv9ft83utw87wv0xlan4r4wju7rd4ktk6cyls9da6w0qjjagn94x5hjlaez0l5tfw9ttkhezh2jw9hgd0vpncfyrpspzatww57pvsq4cx0cg")) assert.Equal(t, uint64(0), GetMsatsFromInvoice("lnbc")) + + assert.Equal(t, uint64(806246000), GetMsatsFromInvoice("lnbc8062460n1pjxgfldpp5e96fqfeahqdlvse0mmmfl3vnv90huhl96cnuwxp7w90nrzdzhe2sdql2djkuepqw3hjqsj5gvsxzerywfjhxuccqzynxqrrsssp5ha03x05dx2s0gyf69dvk2rh0gma7xr009znr9m06tlnqwcf0xweq9qyyssqe5glksza37wee3lgtpste2yt3xgrjwj8en5au0sf28qfhpffmkprnppwk3vl2m6mcumxx9v84jh974a4jqs2a2a92g2mguwt22xpkgqq43qyfs")) +} + +func TestGetHashFromInvoice(t *testing.T) { + assert.Equal(t, "32aad791927e3dd5bae939fe7c78dcf7cef34c9bbdf9d17a54e5481d35384030", GetHashFromInvoice("lntb1m1pjyxysnpp5x24d0yvj0c7atwhf88l8c7xu7l80xnymhhuaz7j5u4yp6dfcgqcqdqqcqzpgxqyz5vqsp5uq3c68sh2fqrdwl2l90ccxw7qpfn65cgs3n5lrd8nwu3f2hrxk8q9qyyssqzdr5gqj35u30fra74d02utsq3gljkhfj9zpvxp8ljhaclz49zkjpy2gxha53ejyu8am8m0q97ql7algt068tze8p8wwfquc4e7m3z8cp6a2mp9")) + assert.Equal(t, "c97490273db81bf6432fdef69fc593615f7e5fe5d627c7183e715f3189a2be55", GetHashFromInvoice("lnbc8062460n1pjxgfldpp5e96fqfeahqdlvse0mmmfl3vnv90huhl96cnuwxp7w90nrzdzhe2sdql2djkuepqw3hjqsj5gvsxzerywfjhxuccqzynxqrrsssp5ha03x05dx2s0gyf69dvk2rh0gma7xr009znr9m06tlnqwcf0xweq9qyyssqe5glksza37wee3lgtpste2yt3xgrjwj8en5au0sf28qfhpffmkprnppwk3vl2m6mcumxx9v84jh974a4jqs2a2a92g2mguwt22xpkgqq43qyfs")) + assert.Equal(t, "", GetHashFromInvoice("l")) + assert.Equal(t, "", GetHashFromInvoice("lnbc")) } diff --git a/lightning/api_mock.go b/lightning/api_mock.go index 315ab64..416d39d 100644 --- a/lightning/api_mock.go +++ b/lightning/api_mock.go @@ -94,7 +94,7 @@ func (m *MockLightningAPI) PayInvoice(ctx context.Context, paymentRequest string panic("not implemented") } -func (m *MockLightningAPI) GetPaymentStatus(ctx context.Context, paymentHash string) (*PaymentResp, error) { +func (m *MockLightningAPI) GetPaymentStatus(ctx context.Context, paymentRequest string) (*PaymentResp, error) { panic("not implemented") } diff --git a/lightning/api_rest.go b/lightning/api_rest.go index 3756942..d0cf019 100644 --- a/lightning/api_rest.go +++ b/lightning/api_rest.go @@ -716,11 +716,25 @@ func (l *LndRestLightningAPI) PayInvoice(ctx context.Context, paymentRequest str } // GetPaymentStatus API. -func (l *LndRestLightningAPI) GetPaymentStatus(ctx context.Context, paymentHash string) (*PaymentResp, error) { +func (l *LndRestLightningAPI) GetPaymentStatus(ctx context.Context, paymentRequest string) (*PaymentResp, error) { req := &TrackPaymentRequestOverride{} - req.PaymentHash = paymentHash req.NoInflightUpdates = true + if paymentRequest == "" { + return nil, fmt.Errorf("missing payment request") + } + + if strings.HasPrefix(paymentRequest, "ln") { + paymentHash := GetHashFromInvoice(paymentRequest) + if paymentHash == "" { + return nil, fmt.Errorf("bad payment request") + } + + req.PaymentHash = paymentHash + } else { + req.PaymentHash = paymentRequest + } + resp, err := l.HTTPAPI.HTTPTrackPayment(ctx, l.Request, req) if err != nil { return nil, err @@ -730,19 +744,19 @@ func (l *LndRestLightningAPI) GetPaymentStatus(ctx context.Context, paymentHash case "succeeded": return &PaymentResp{ Preimage: resp.PaymentPreimage, - Hash: paymentHash, + Hash: req.PaymentHash, Status: Success, }, nil case "failed": return &PaymentResp{ Preimage: "", - Hash: paymentHash, + Hash: req.PaymentHash, Status: Failed, }, nil default: return &PaymentResp{ Preimage: "", - Hash: paymentHash, + Hash: req.PaymentHash, Status: Pending, }, nil } diff --git a/plugins/boltz/swap_test.go b/plugins/boltz/swap_test.go index 175f1c6..a8df282 100644 --- a/plugins/boltz/swap_test.go +++ b/plugins/boltz/swap_test.go @@ -313,18 +313,13 @@ func TestPayHodlInvoiceGrpc(t *testing.T) { api, err := ln() require.NoError(t, err) - request := "lnbcrt1200n1pjy9dsxpp52nwgn5e2z5cxkccwwlcsr53mn66elv6dum6xd346r5k3xrnwlqzsdqqcqzpgxqyz5vqsp5s7rndl282gtcqn86t5zv2rak4qe2ma0n0dn45saxwajszhkps36s9qyyssqudjn4pan0lrucj0rq82x8e48u9p7lh8nmgjtsn3vs3qeqcwqh7spdsd0tps9erthgydtrgpx8dg8yk5q3nwdp6dglh5c9823fj5tmlqq3rvctw" - channel := uint64(178120883765248) + request := "lnbcrt211pjxwcndpp5ns4kuz97pww5andpd4rvs3allwrtptt2tk0mzzct0rjpffn7lm0sdqqcqzpgxqyz5vqsp5j6ph6ql60y3xrpvdxytsh2hslrg2ztwghh22hjtce5vx7sqdlaus9qyyssq99smdrc625teq0mjsk3zp7dnrk4vejt7x6ayxy99eqxhednanhzptgktzhaayu34dy42jtgutvzehu8lmh3g2z0ezufzt8xrfgkpecqqy0ntpt" + //channel := uint64(178120883765248) - resp, err := api.PayInvoice(context.Background(), request, 0, []uint64{channel}) + resp, err := api.PayInvoice(context.Background(), request, 0, []uint64{}) assert.NoError(t, err) fmt.Printf("%+v\n", resp) - resp2, err := api.PayInvoice(context.Background(), request, 0, []uint64{}) - assert.NoError(t, err) - - fmt.Printf("%+v\n", resp2) - t.Fail() } diff --git a/plugins/boltz/swapmachine/swapmachine_reverse.go b/plugins/boltz/swapmachine/swapmachine_reverse.go index 82c4b13..40613e6 100644 --- a/plugins/boltz/swapmachine/swapmachine_reverse.go +++ b/plugins/boltz/swapmachine/swapmachine_reverse.go @@ -13,6 +13,7 @@ import ( "github.com/BoltzExchange/boltz-lnd/boltz" "github.com/bolt-observer/agent/entities" + "github.com/bolt-observer/agent/lightning" bapi "github.com/bolt-observer/agent/plugins/boltz/api" common "github.com/bolt-observer/agent/plugins/boltz/common" crypto "github.com/bolt-observer/agent/plugins/boltz/crypto" @@ -134,12 +135,48 @@ func (s *SwapMachine) FsmInitialReverse(in common.FsmIn) common.FsmOut { return common.FsmOut{NextState: common.ReverseSwapCreated} } +func ensureInvoiceIsPaid(ctx context.Context, ln lightning.LightingAPICalls, invoice string, chanIdsToUse []uint64, logger LogEntry, in common.FsmIn) bool { + log(in, fmt.Sprintf("Ensuring invoice %v is paid [%+v]", invoice, chanIdsToUse), + logger.Get("invoice", invoice)) + + status, err := ln.GetPaymentStatus(ctx, invoice) + if err != nil { + log(in, fmt.Sprintf("Failed to get payment status for %v - %v", invoice, err), + logger.Get("invoice", invoice, "error", err)) + // but still continue + } else { + if status != nil && (status.Status == lightning.Pending || status.Status == lightning.Success) { + log(in, fmt.Sprintf("Payment status for %v - %v", invoice, status), + logger.Get("invoice", invoice)) + + in.SwapData.CommitFees() + return true + } + } + + pay, err := ln.PayInvoice(ctx, invoice, 0, chanIdsToUse) + if err != nil { + log(in, fmt.Sprintf("Failed to pay invoice %v - %v", invoice, err), + logger.Get("invoice", invoice, "error", err)) + return false + } + + if pay != nil && (pay.Status == lightning.Pending || pay.Status == lightning.Success) { + log(in, fmt.Sprintf("Paid invoice %v - %v", invoice, status), + logger.Get("invoice", invoice)) + + in.SwapData.CommitFees() + return true + } + + return false +} + func (s *SwapMachine) FsmReverseSwapCreated(in common.FsmIn) common.FsmOut { const PayRetries = 5 ctx := context.Background() logger := NewLogEntry(in.SwapData) - paid := false SleepTime := s.GetSleepTimeFn(in) @@ -166,31 +203,17 @@ func (s *SwapMachine) FsmReverseSwapCreated(in common.FsmIn) common.FsmOut { continue } - if !paid { - payAttempt++ - log(in, fmt.Sprintf("Paying invoice %v %+v", in.SwapData.ReverseInvoice, in.SwapData.ChanIdsToUse), - logger.Get("invoice", in.SwapData.ReverseInvoice)) - - _, err = lnConnection.PayInvoice(ctx, in.SwapData.ReverseInvoice, 0, in.SwapData.ChanIdsToUse) - if err != nil { - log(in, fmt.Sprintf("Failed paying invoice %v due to %v", in.SwapData.ReverseInvoice, err), logger.Get("error", err.Error())) - if in.SwapData.ReverseChannelId == 0 { - // this means node level liquidity - if the hints worked that would be nice, but try without them too - - _, err = lnConnection.PayInvoice(ctx, in.SwapData.ReverseInvoice, 0, nil) - if err == nil { - paid = true - in.SwapData.CommitFees() - } - } + if payAttempt < PayRetries*1 { + ensureInvoiceIsPaid(ctx, lnConnection, in.SwapData.ReverseInvoice, in.SwapData.ChanIdsToUse, logger, in) + } else if payAttempt >= PayRetries*1 { + if in.SwapData.ReverseChannelId == 0 { + // this means node level liquidity - if the hints worked that would be nice, but try without them too + ensureInvoiceIsPaid(ctx, lnConnection, in.SwapData.ReverseInvoice, nil, logger, in) } else { - paid = true - in.SwapData.CommitFees() - } - - if !paid && payAttempt > PayRetries { - return common.FsmOut{NextState: common.SwapInvoiceCouldNotBePaid} + ensureInvoiceIsPaid(ctx, lnConnection, in.SwapData.ReverseInvoice, in.SwapData.ChanIdsToUse, logger, in) } + } else if payAttempt > PayRetries*2 { + return common.FsmOut{NextState: common.SwapInvoiceCouldNotBePaid} } s, err := s.BoltzAPI.SwapStatus(in.SwapData.BoltzID)