|
|
|
package lndclient
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"crypto/rand"
|
|
|
|
"encoding/hex"
|
|
|
|
"fmt"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/btcsuite/btcutil"
|
|
|
|
"github.com/lightningnetwork/lnd/channeldb"
|
|
|
|
"github.com/lightningnetwork/lnd/lnrpc"
|
|
|
|
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
|
|
|
"github.com/lightningnetwork/lnd/lntypes"
|
|
|
|
"github.com/lightningnetwork/lnd/lnwire"
|
|
|
|
"github.com/lightningnetwork/lnd/record"
|
|
|
|
"github.com/lightningnetwork/lnd/routing/route"
|
|
|
|
"github.com/lightningnetwork/lnd/zpay32"
|
|
|
|
"google.golang.org/grpc"
|
|
|
|
"google.golang.org/grpc/codes"
|
|
|
|
"google.golang.org/grpc/status"
|
|
|
|
)
|
|
|
|
|
|
|
|
// RouterClient exposes payment functionality.
|
|
|
|
type RouterClient interface {
|
|
|
|
// SendPayment attempts to route a payment to the final destination. The
|
|
|
|
// call returns a payment update stream and an error stream.
|
|
|
|
SendPayment(ctx context.Context, request SendPaymentRequest) (
|
|
|
|
chan PaymentStatus, chan error, error)
|
|
|
|
|
|
|
|
// TrackPayment picks up a previously started payment and returns a
|
|
|
|
// payment update stream and an error stream.
|
|
|
|
TrackPayment(ctx context.Context, hash lntypes.Hash) (
|
|
|
|
chan PaymentStatus, chan error, error)
|
|
|
|
}
|
|
|
|
|
|
|
|
// PaymentStatus describe the state of a payment.
|
|
|
|
type PaymentStatus struct {
|
|
|
|
State lnrpc.Payment_PaymentStatus
|
|
|
|
|
|
|
|
// FailureReason is the reason why the payment failed. Only set when
|
|
|
|
// State is Failed.
|
|
|
|
FailureReason lnrpc.PaymentFailureReason
|
|
|
|
|
|
|
|
Preimage lntypes.Preimage
|
|
|
|
Fee lnwire.MilliSatoshi
|
|
|
|
Value lnwire.MilliSatoshi
|
|
|
|
InFlightAmt lnwire.MilliSatoshi
|
|
|
|
InFlightHtlcs int
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p PaymentStatus) String() string {
|
|
|
|
text := fmt.Sprintf("state=%v", p.State)
|
|
|
|
if p.State == lnrpc.Payment_IN_FLIGHT {
|
|
|
|
text += fmt.Sprintf(", inflight_htlcs=%v, inflight_amt=%v",
|
|
|
|
p.InFlightHtlcs, p.InFlightAmt)
|
|
|
|
}
|
|
|
|
|
|
|
|
return text
|
|
|
|
}
|
|
|
|
|
|
|
|
// SendPaymentRequest defines the payment parameters for a new payment.
|
|
|
|
type SendPaymentRequest struct {
|
|
|
|
// Invoice is an encoded payment request. The individual payment
|
|
|
|
// parameters Target, Amount, PaymentHash, FinalCLTVDelta and RouteHints
|
|
|
|
// are only processed when the Invoice field is empty.
|
|
|
|
Invoice string
|
|
|
|
|
|
|
|
// MaxFee is the fee limit for this payment.
|
|
|
|
MaxFee btcutil.Amount
|
|
|
|
|
|
|
|
// MaxCltv is the maximum timelock for this payment. If nil, there is no
|
|
|
|
// maximum.
|
|
|
|
MaxCltv *int32
|
|
|
|
|
|
|
|
// OutgoingChanIds is a restriction on the set of possible outgoing
|
|
|
|
// channels. If nil or empty, there is no restriction.
|
|
|
|
OutgoingChanIds []uint64
|
|
|
|
|
|
|
|
// Timeout is the payment loop timeout. After this time, no new payment
|
|
|
|
// attempts will be started.
|
|
|
|
Timeout time.Duration
|
|
|
|
|
|
|
|
// Target is the node in which the payment should be routed towards.
|
|
|
|
Target route.Vertex
|
|
|
|
|
|
|
|
// Amount is the value of the payment to send through the network in
|
|
|
|
// satoshis.
|
|
|
|
Amount btcutil.Amount
|
|
|
|
|
|
|
|
// PaymentHash is the r-hash value to use within the HTLC extended to
|
|
|
|
// the first hop.
|
|
|
|
PaymentHash *lntypes.Hash
|
|
|
|
|
|
|
|
// FinalCLTVDelta is the CTLV expiry delta to use for the _final_ hop
|
|
|
|
// in the route. This means that the final hop will have a CLTV delta
|
|
|
|
// of at least: currentHeight + FinalCLTVDelta.
|
|
|
|
FinalCLTVDelta uint16
|
|
|
|
|
|
|
|
// RouteHints represents the different routing hints that can be used to
|
|
|
|
// assist a payment in reaching its destination successfully. These
|
|
|
|
// hints will act as intermediate hops along the route.
|
|
|
|
//
|
|
|
|
// NOTE: This is optional unless required by the payment. When providing
|
|
|
|
// multiple routes, ensure the hop hints within each route are chained
|
|
|
|
// together and sorted in forward order in order to reach the
|
|
|
|
// destination successfully.
|
|
|
|
RouteHints [][]zpay32.HopHint
|
|
|
|
|
|
|
|
// LastHopPubkey is the pubkey of the last hop of the route taken
|
|
|
|
// for this payment. If empty, any hop may be used.
|
|
|
|
LastHopPubkey *route.Vertex
|
|
|
|
|
|
|
|
// MaxParts is the maximum number of partial payments that may be used
|
|
|
|
// to complete the full amount.
|
|
|
|
MaxParts uint32
|
|
|
|
|
|
|
|
// KeySend is set to true if the tlv payload will include the preimage.
|
|
|
|
KeySend bool
|
|
|
|
|
|
|
|
// CustomRecords holds the custom TLV records that will be added to the
|
|
|
|
// payment.
|
|
|
|
CustomRecords map[uint64][]byte
|
|
|
|
}
|
|
|
|
|
|
|
|
// routerClient is a wrapper around the generated routerrpc proxy.
|
|
|
|
type routerClient struct {
|
|
|
|
client routerrpc.RouterClient
|
|
|
|
routerKitMac serializedMacaroon
|
|
|
|
}
|
|
|
|
|
|
|
|
func newRouterClient(conn *grpc.ClientConn,
|
|
|
|
routerKitMac serializedMacaroon) *routerClient {
|
|
|
|
|
|
|
|
return &routerClient{
|
|
|
|
client: routerrpc.NewRouterClient(conn),
|
|
|
|
routerKitMac: routerKitMac,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// SendPayment attempts to route a payment to the final destination. The call
|
|
|
|
// returns a payment update stream and an error stream.
|
|
|
|
func (r *routerClient) SendPayment(ctx context.Context,
|
|
|
|
request SendPaymentRequest) (chan PaymentStatus, chan error, error) {
|
|
|
|
|
|
|
|
rpcCtx := r.routerKitMac.WithMacaroonAuth(ctx)
|
|
|
|
rpcReq := &routerrpc.SendPaymentRequest{
|
|
|
|
FeeLimitSat: int64(request.MaxFee),
|
|
|
|
PaymentRequest: request.Invoice,
|
|
|
|
TimeoutSeconds: int32(request.Timeout.Seconds()),
|
|
|
|
MaxParts: request.MaxParts,
|
|
|
|
OutgoingChanIds: request.OutgoingChanIds,
|
|
|
|
}
|
|
|
|
if request.MaxCltv != nil {
|
|
|
|
rpcReq.CltvLimit = *request.MaxCltv
|
|
|
|
}
|
|
|
|
|
|
|
|
if request.LastHopPubkey != nil {
|
|
|
|
rpcReq.LastHopPubkey = request.LastHopPubkey[:]
|
|
|
|
}
|
|
|
|
|
|
|
|
rpcReq.DestCustomRecords = request.CustomRecords
|
|
|
|
|
|
|
|
if request.KeySend {
|
|
|
|
if request.PaymentHash != nil {
|
|
|
|
return nil, nil, fmt.Errorf(
|
|
|
|
"keysend payment must not include a preset payment hash")
|
|
|
|
}
|
|
|
|
|
|
|
|
var preimage lntypes.Preimage
|
|
|
|
if _, err := rand.Read(preimage[:]); err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if rpcReq.DestCustomRecords == nil {
|
|
|
|
rpcReq.DestCustomRecords = make(map[uint64][]byte)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Override the payment hash.
|
|
|
|
rpcReq.DestCustomRecords[record.KeySendType] = preimage[:]
|
|
|
|
hash := preimage.Hash()
|
|
|
|
request.PaymentHash = &hash
|
|
|
|
}
|
|
|
|
|
|
|
|
// Only if there is no payment request set, we will parse the individual
|
|
|
|
// payment parameters.
|
|
|
|
if request.Invoice == "" {
|
|
|
|
rpcReq.Dest = request.Target[:]
|
|
|
|
rpcReq.Amt = int64(request.Amount)
|
|
|
|
rpcReq.PaymentHash = request.PaymentHash[:]
|
|
|
|
rpcReq.FinalCltvDelta = int32(request.FinalCLTVDelta)
|
|
|
|
|
|
|
|
routeHints, err := marshallRouteHints(request.RouteHints)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
rpcReq.RouteHints = routeHints
|
|
|
|
}
|
|
|
|
|
|
|
|
stream, err := r.client.SendPaymentV2(rpcCtx, rpcReq)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return r.trackPayment(ctx, stream)
|
|
|
|
}
|
|
|
|
|
|
|
|
// TrackPayment picks up a previously started payment and returns a payment
|
|
|
|
// update stream and an error stream.
|
|
|
|
func (r *routerClient) TrackPayment(ctx context.Context,
|
|
|
|
hash lntypes.Hash) (chan PaymentStatus, chan error, error) {
|
|
|
|
|
|
|
|
ctx = r.routerKitMac.WithMacaroonAuth(ctx)
|
|
|
|
stream, err := r.client.TrackPaymentV2(
|
|
|
|
ctx, &routerrpc.TrackPaymentRequest{
|
|
|
|
PaymentHash: hash[:],
|
|
|
|
},
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return r.trackPayment(ctx, stream)
|
|
|
|
}
|
|
|
|
|
|
|
|
// trackPayment takes an update stream from either a SendPayment or a
|
|
|
|
// TrackPayment rpc call and converts it into distinct update and error streams.
|
|
|
|
func (r *routerClient) trackPayment(ctx context.Context,
|
|
|
|
stream routerrpc.Router_TrackPaymentV2Client) (chan PaymentStatus,
|
|
|
|
chan error, error) {
|
|
|
|
|
|
|
|
statusChan := make(chan PaymentStatus)
|
|
|
|
errorChan := make(chan error, 1)
|
|
|
|
go func() {
|
|
|
|
for {
|
|
|
|
payment, err := stream.Recv()
|
|
|
|
if err != nil {
|
|
|
|
switch status.Convert(err).Code() {
|
|
|
|
|
|
|
|
// NotFound is only expected as a response to
|
|
|
|
// TrackPayment.
|
|
|
|
case codes.NotFound:
|
|
|
|
err = channeldb.ErrPaymentNotInitiated
|
|
|
|
|
|
|
|
// NotFound is only expected as a response to
|
|
|
|
// SendPayment.
|
|
|
|
case codes.AlreadyExists:
|
|
|
|
err = channeldb.ErrAlreadyPaid
|
|
|
|
}
|
|
|
|
|
|
|
|
errorChan <- err
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
status, err := unmarshallPaymentStatus(payment)
|
|
|
|
if err != nil {
|
|
|
|
errorChan <- err
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
select {
|
|
|
|
case statusChan <- *status:
|
|
|
|
case <-ctx.Done():
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
return statusChan, errorChan, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// unmarshallPaymentStatus converts an rpc status update to the PaymentStatus
|
|
|
|
// type that is used throughout the application.
|
|
|
|
func unmarshallPaymentStatus(rpcPayment *lnrpc.Payment) (
|
|
|
|
*PaymentStatus, error) {
|
|
|
|
|
|
|
|
status := PaymentStatus{
|
|
|
|
State: rpcPayment.Status,
|
|
|
|
}
|
|
|
|
|
|
|
|
switch status.State {
|
|
|
|
case lnrpc.Payment_SUCCEEDED:
|
|
|
|
preimage, err := lntypes.MakePreimageFromStr(
|
|
|
|
rpcPayment.PaymentPreimage,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
status.Preimage = preimage
|
|
|
|
status.Fee = lnwire.MilliSatoshi(rpcPayment.FeeMsat)
|
|
|
|
status.Value = lnwire.MilliSatoshi(rpcPayment.ValueMsat)
|
|
|
|
|
|
|
|
case lnrpc.Payment_FAILED:
|
|
|
|
status.FailureReason = rpcPayment.FailureReason
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, htlc := range rpcPayment.Htlcs {
|
|
|
|
if htlc.Status != lnrpc.HTLCAttempt_IN_FLIGHT {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
status.InFlightHtlcs++
|
|
|
|
|
|
|
|
lastHop := htlc.Route.Hops[len(htlc.Route.Hops)-1]
|
|
|
|
status.InFlightAmt += lnwire.MilliSatoshi(
|
|
|
|
lastHop.AmtToForwardMsat,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
return &status, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// unmarshallRoute unmarshalls an rpc route.
|
|
|
|
func unmarshallRoute(rpcroute *lnrpc.Route) (
|
|
|
|
*route.Route, error) {
|
|
|
|
|
|
|
|
hops := make([]*route.Hop, len(rpcroute.Hops))
|
|
|
|
for i, hop := range rpcroute.Hops {
|
|
|
|
routeHop, err := unmarshallHop(hop)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
hops[i] = routeHop
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO(joostjager): Fetch self node from lnd.
|
|
|
|
selfNode := route.Vertex{}
|
|
|
|
|
|
|
|
route, err := route.NewRouteFromHops(
|
|
|
|
lnwire.MilliSatoshi(rpcroute.TotalAmtMsat),
|
|
|
|
rpcroute.TotalTimeLock,
|
|
|
|
selfNode,
|
|
|
|
hops,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return route, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// unmarshallKnownPubkeyHop unmarshalls an rpc hop.
|
|
|
|
func unmarshallHop(hop *lnrpc.Hop) (*route.Hop, error) {
|
|
|
|
pubKey, err := hex.DecodeString(hop.PubKey)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("cannot decode pubkey %s", hop.PubKey)
|
|
|
|
}
|
|
|
|
|
|
|
|
var pubKeyBytes [33]byte
|
|
|
|
copy(pubKeyBytes[:], pubKey)
|
|
|
|
|
|
|
|
return &route.Hop{
|
|
|
|
OutgoingTimeLock: hop.Expiry,
|
|
|
|
AmtToForward: lnwire.MilliSatoshi(hop.AmtToForwardMsat),
|
|
|
|
PubKeyBytes: pubKeyBytes,
|
|
|
|
ChannelID: hop.ChanId,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// marshallRouteHints marshalls a list of route hints.
|
|
|
|
func marshallRouteHints(routeHints [][]zpay32.HopHint) (
|
|
|
|
[]*lnrpc.RouteHint, error) {
|
|
|
|
|
|
|
|
rpcRouteHints := make([]*lnrpc.RouteHint, 0, len(routeHints))
|
|
|
|
for _, routeHint := range routeHints {
|
|
|
|
rpcRouteHint := make(
|
|
|
|
[]*lnrpc.HopHint, 0, len(routeHint),
|
|
|
|
)
|
|
|
|
for _, hint := range routeHint {
|
|
|
|
rpcHint, err := marshallHopHint(hint)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
rpcRouteHint = append(rpcRouteHint, rpcHint)
|
|
|
|
}
|
|
|
|
rpcRouteHints = append(rpcRouteHints, &lnrpc.RouteHint{
|
|
|
|
HopHints: rpcRouteHint,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
return rpcRouteHints, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// marshallHopHint marshalls a single hop hint.
|
|
|
|
func marshallHopHint(hint zpay32.HopHint) (*lnrpc.HopHint, error) {
|
|
|
|
nodeID, err := route.NewVertexFromBytes(
|
|
|
|
hint.NodeID.SerializeCompressed(),
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &lnrpc.HopHint{
|
|
|
|
ChanId: hint.ChannelID,
|
|
|
|
CltvExpiryDelta: uint32(hint.CLTVExpiryDelta),
|
|
|
|
FeeBaseMsat: hint.FeeBaseMSat,
|
|
|
|
FeeProportionalMillionths: hint.FeeProportionalMillionths,
|
|
|
|
NodeId: nodeID.String(),
|
|
|
|
}, nil
|
|
|
|
}
|