2019-03-11 11:56:05 +03:00
|
|
|
package routerrpc
|
|
|
|
|
|
|
|
import (
|
2019-09-29 02:06:34 +03:00
|
|
|
"context"
|
2019-03-11 11:56:05 +03:00
|
|
|
"encoding/hex"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
2019-10-11 22:47:15 +03:00
|
|
|
math "math"
|
2019-04-18 10:34:28 +03:00
|
|
|
"time"
|
2019-03-11 11:56:05 +03:00
|
|
|
|
2019-06-18 11:36:29 +03:00
|
|
|
"github.com/btcsuite/btcd/btcec"
|
|
|
|
|
2019-04-18 10:34:28 +03:00
|
|
|
"github.com/btcsuite/btcd/chaincfg"
|
2019-03-11 11:56:05 +03:00
|
|
|
"github.com/btcsuite/btcutil"
|
2019-11-20 07:41:44 +03:00
|
|
|
"github.com/lightningnetwork/lnd/channeldb"
|
2019-03-11 11:56:05 +03:00
|
|
|
"github.com/lightningnetwork/lnd/lnrpc"
|
2019-11-05 02:11:23 +03:00
|
|
|
"github.com/lightningnetwork/lnd/lntypes"
|
2019-03-11 11:56:05 +03:00
|
|
|
"github.com/lightningnetwork/lnd/lnwire"
|
2019-11-05 02:11:23 +03:00
|
|
|
"github.com/lightningnetwork/lnd/record"
|
2019-03-11 11:56:05 +03:00
|
|
|
"github.com/lightningnetwork/lnd/routing"
|
2019-04-05 18:36:11 +03:00
|
|
|
"github.com/lightningnetwork/lnd/routing/route"
|
2019-04-18 10:34:28 +03:00
|
|
|
"github.com/lightningnetwork/lnd/zpay32"
|
2019-03-11 11:56:05 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
// RouterBackend contains the backend implementation of the router rpc sub
|
|
|
|
// server calls.
|
|
|
|
type RouterBackend struct {
|
2019-03-13 07:20:21 +03:00
|
|
|
// MaxPaymentMSat is the largest payment permitted by the backend.
|
2019-03-11 11:56:05 +03:00
|
|
|
MaxPaymentMSat lnwire.MilliSatoshi
|
|
|
|
|
2019-03-13 07:20:21 +03:00
|
|
|
// SelfNode is the vertex of the node sending the payment.
|
2019-04-05 18:36:11 +03:00
|
|
|
SelfNode route.Vertex
|
2019-03-13 07:20:21 +03:00
|
|
|
|
|
|
|
// FetchChannelCapacity is a closure that we'll use the fetch the total
|
|
|
|
// capacity of a channel to populate in responses.
|
2019-03-11 11:56:05 +03:00
|
|
|
FetchChannelCapacity func(chanID uint64) (btcutil.Amount, error)
|
|
|
|
|
2019-03-14 17:13:45 +03:00
|
|
|
// FetchChannelEndpoints returns the pubkeys of both endpoints of the
|
|
|
|
// given channel id.
|
|
|
|
FetchChannelEndpoints func(chanID uint64) (route.Vertex,
|
|
|
|
route.Vertex, error)
|
|
|
|
|
2019-03-13 07:20:21 +03:00
|
|
|
// FindRoutes is a closure that abstracts away how we locate/query for
|
|
|
|
// routes.
|
2019-05-07 18:01:01 +03:00
|
|
|
FindRoute func(source, target route.Vertex,
|
2019-03-11 11:56:05 +03:00
|
|
|
amt lnwire.MilliSatoshi, restrictions *routing.RestrictParams,
|
2019-12-11 12:52:27 +03:00
|
|
|
destCustomRecords record.CustomSet,
|
2019-05-07 18:01:01 +03:00
|
|
|
finalExpiry ...uint16) (*route.Route, error)
|
2019-05-10 19:00:15 +03:00
|
|
|
|
2019-07-05 10:34:45 +03:00
|
|
|
MissionControl MissionControl
|
2019-04-18 10:34:28 +03:00
|
|
|
|
|
|
|
// ActiveNetParams are the network parameters of the primary network
|
|
|
|
// that the route is operating on. This is necessary so we can ensure
|
|
|
|
// that we receive payment requests that send to destinations on our
|
|
|
|
// network.
|
|
|
|
ActiveNetParams *chaincfg.Params
|
2019-03-22 12:21:25 +03:00
|
|
|
|
|
|
|
// Tower is the ControlTower instance that is used to track pending
|
|
|
|
// payments.
|
|
|
|
Tower routing.ControlTower
|
2019-10-11 22:46:10 +03:00
|
|
|
|
|
|
|
// MaxTotalTimelock is the maximum total time lock a route is allowed to
|
|
|
|
// have.
|
|
|
|
MaxTotalTimelock uint32
|
2019-03-11 11:56:05 +03:00
|
|
|
}
|
|
|
|
|
2019-07-05 10:34:45 +03:00
|
|
|
// MissionControl defines the mission control dependencies of routerrpc.
|
|
|
|
type MissionControl interface {
|
2019-07-29 16:10:58 +03:00
|
|
|
// GetProbability is expected to return the success probability of a
|
|
|
|
// payment from fromNode to toNode.
|
|
|
|
GetProbability(fromNode, toNode route.Vertex,
|
|
|
|
amt lnwire.MilliSatoshi) float64
|
2019-07-05 10:34:45 +03:00
|
|
|
|
2019-08-05 12:49:46 +03:00
|
|
|
// ResetHistory resets the history of MissionControl returning it to a
|
|
|
|
// state as if no payment attempts have been made.
|
2019-06-26 14:00:35 +03:00
|
|
|
ResetHistory() error
|
2019-07-05 10:34:45 +03:00
|
|
|
|
2019-08-05 12:49:46 +03:00
|
|
|
// GetHistorySnapshot takes a snapshot from the current mission control
|
|
|
|
// state and actual probability estimates.
|
2019-07-05 10:34:45 +03:00
|
|
|
GetHistorySnapshot() *routing.MissionControlSnapshot
|
2019-09-27 12:43:12 +03:00
|
|
|
|
|
|
|
// GetPairHistorySnapshot returns the stored history for a given node
|
|
|
|
// pair.
|
|
|
|
GetPairHistorySnapshot(fromNode,
|
|
|
|
toNode route.Vertex) routing.TimedPairResult
|
2019-07-05 10:34:45 +03:00
|
|
|
}
|
|
|
|
|
2019-03-11 11:56:05 +03:00
|
|
|
// QueryRoutes attempts to query the daemons' Channel Router for a possible
|
|
|
|
// route to a target destination capable of carrying a specific amount of
|
|
|
|
// satoshis within the route's flow. The retuned route contains the full
|
|
|
|
// details required to craft and send an HTLC, also including the necessary
|
|
|
|
// information that should be present within the Sphinx packet encapsulated
|
|
|
|
// within the HTLC.
|
|
|
|
//
|
2019-03-13 07:20:21 +03:00
|
|
|
// TODO(roasbeef): should return a slice of routes in reality * create separate
|
|
|
|
// PR to send based on well formatted route
|
2019-03-11 11:56:05 +03:00
|
|
|
func (r *RouterBackend) QueryRoutes(ctx context.Context,
|
|
|
|
in *lnrpc.QueryRoutesRequest) (*lnrpc.QueryRoutesResponse, error) {
|
|
|
|
|
2019-04-05 18:36:11 +03:00
|
|
|
parsePubKey := func(key string) (route.Vertex, error) {
|
2019-03-11 11:56:05 +03:00
|
|
|
pubKeyBytes, err := hex.DecodeString(key)
|
|
|
|
if err != nil {
|
2019-04-05 18:36:11 +03:00
|
|
|
return route.Vertex{}, err
|
2019-03-11 11:56:05 +03:00
|
|
|
}
|
|
|
|
|
2019-08-07 14:09:27 +03:00
|
|
|
return route.NewVertexFromBytes(pubKeyBytes)
|
2019-03-11 11:56:05 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// Parse the hex-encoded source and target public keys into full public
|
|
|
|
// key objects we can properly manipulate.
|
|
|
|
targetPubKey, err := parsePubKey(in.PubKey)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2019-04-05 18:36:11 +03:00
|
|
|
var sourcePubKey route.Vertex
|
2019-03-11 11:56:05 +03:00
|
|
|
if in.SourcePubKey != "" {
|
|
|
|
var err error
|
|
|
|
sourcePubKey, err = parsePubKey(in.SourcePubKey)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// If no source is specified, use self.
|
|
|
|
sourcePubKey = r.SelfNode
|
|
|
|
}
|
|
|
|
|
|
|
|
// Currently, within the bootstrap phase of the network, we limit the
|
|
|
|
// largest payment size allotted to (2^32) - 1 mSAT or 4.29 million
|
|
|
|
// satoshis.
|
2019-11-11 18:44:08 +03:00
|
|
|
amt, err := lnrpc.UnmarshallAmt(in.Amt, in.AmtMsat)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if amt > r.MaxPaymentMSat {
|
2019-03-11 11:56:05 +03:00
|
|
|
return nil, fmt.Errorf("payment of %v is too large, max payment "+
|
|
|
|
"allowed is %v", amt, r.MaxPaymentMSat.ToSatoshis())
|
|
|
|
}
|
|
|
|
|
|
|
|
// Unmarshall restrictions from request.
|
2019-11-11 18:44:08 +03:00
|
|
|
feeLimit := lnrpc.CalculateFeeLimit(in.FeeLimit, amt)
|
2019-03-11 11:56:05 +03:00
|
|
|
|
2019-04-05 18:36:11 +03:00
|
|
|
ignoredNodes := make(map[route.Vertex]struct{})
|
2019-03-11 11:56:05 +03:00
|
|
|
for _, ignorePubKey := range in.IgnoredNodes {
|
2019-08-07 14:09:27 +03:00
|
|
|
ignoreVertex, err := route.NewVertexFromBytes(ignorePubKey)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2019-03-11 11:56:05 +03:00
|
|
|
}
|
|
|
|
ignoredNodes[ignoreVertex] = struct{}{}
|
|
|
|
}
|
|
|
|
|
2019-07-29 16:10:58 +03:00
|
|
|
ignoredPairs := make(map[routing.DirectedNodePair]struct{})
|
|
|
|
|
|
|
|
// Convert deprecated ignoredEdges to pairs.
|
2019-03-11 11:56:05 +03:00
|
|
|
for _, ignoredEdge := range in.IgnoredEdges {
|
2019-07-29 16:10:58 +03:00
|
|
|
pair, err := r.rpcEdgeToPair(ignoredEdge)
|
|
|
|
if err != nil {
|
|
|
|
log.Warnf("Ignore channel %v skipped: %v",
|
|
|
|
ignoredEdge.ChannelId, err)
|
|
|
|
|
|
|
|
continue
|
2019-03-11 11:56:05 +03:00
|
|
|
}
|
2019-07-29 16:10:58 +03:00
|
|
|
ignoredPairs[pair] = struct{}{}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add ignored pairs to set.
|
|
|
|
for _, ignorePair := range in.IgnoredPairs {
|
|
|
|
from, err := route.NewVertexFromBytes(ignorePair.From)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
to, err := route.NewVertexFromBytes(ignorePair.To)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2019-03-11 11:56:05 +03:00
|
|
|
}
|
2019-07-29 16:10:58 +03:00
|
|
|
|
|
|
|
pair := routing.NewDirectedNodePair(from, to)
|
|
|
|
ignoredPairs[pair] = struct{}{}
|
2019-03-11 11:56:05 +03:00
|
|
|
}
|
|
|
|
|
2019-10-11 22:47:15 +03:00
|
|
|
// Since QueryRoutes allows having a different source other than
|
|
|
|
// ourselves, we'll only apply our max time lock if we are the source.
|
|
|
|
maxTotalTimelock := r.MaxTotalTimelock
|
|
|
|
if sourcePubKey != r.SelfNode {
|
|
|
|
maxTotalTimelock = math.MaxUint32
|
|
|
|
}
|
|
|
|
cltvLimit, err := ValidateCLTVLimit(in.CltvLimit, maxTotalTimelock)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// We need to subtract the final delta before passing it into path
|
|
|
|
// finding. The optimal path is independent of the final cltv delta and
|
|
|
|
// the path finding algorithm is unaware of this value.
|
|
|
|
finalCLTVDelta := uint16(zpay32.DefaultFinalCLTVDelta)
|
|
|
|
if in.FinalCltvDelta != 0 {
|
|
|
|
finalCLTVDelta = uint16(in.FinalCltvDelta)
|
|
|
|
}
|
|
|
|
cltvLimit -= uint32(finalCLTVDelta)
|
|
|
|
|
2019-03-11 11:56:05 +03:00
|
|
|
restrictions := &routing.RestrictParams{
|
2019-03-19 13:45:10 +03:00
|
|
|
FeeLimit: feeLimit,
|
2019-07-29 16:10:58 +03:00
|
|
|
ProbabilitySource: func(fromNode, toNode route.Vertex,
|
2019-03-19 19:09:27 +03:00
|
|
|
amt lnwire.MilliSatoshi) float64 {
|
2019-03-19 13:45:10 +03:00
|
|
|
|
2019-07-29 16:10:58 +03:00
|
|
|
if _, ok := ignoredNodes[fromNode]; ok {
|
2019-03-19 13:45:10 +03:00
|
|
|
return 0
|
|
|
|
}
|
|
|
|
|
2019-07-29 16:10:58 +03:00
|
|
|
pair := routing.DirectedNodePair{
|
|
|
|
From: fromNode,
|
|
|
|
To: toNode,
|
|
|
|
}
|
|
|
|
if _, ok := ignoredPairs[pair]; ok {
|
2019-03-19 13:45:10 +03:00
|
|
|
return 0
|
|
|
|
}
|
|
|
|
|
2019-06-19 09:29:44 +03:00
|
|
|
if !in.UseMissionControl {
|
|
|
|
return 1
|
|
|
|
}
|
|
|
|
|
2019-07-29 16:10:58 +03:00
|
|
|
return r.MissionControl.GetProbability(
|
|
|
|
fromNode, toNode, amt,
|
2019-06-19 09:29:44 +03:00
|
|
|
)
|
2019-03-19 13:45:10 +03:00
|
|
|
},
|
2019-12-11 13:49:03 +03:00
|
|
|
DestCustomRecords: record.CustomSet(in.DestCustomRecords),
|
|
|
|
CltvLimit: cltvLimit,
|
2019-07-31 07:44:02 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// If we have any TLV records destined for the final hop, then we'll
|
|
|
|
// attempt to decode them now into a form that the router can more
|
|
|
|
// easily manipulate.
|
2019-12-12 16:43:36 +03:00
|
|
|
customRecords := record.CustomSet(in.DestCustomRecords)
|
|
|
|
if err := customRecords.Validate(); err != nil {
|
2019-11-20 14:00:25 +03:00
|
|
|
return nil, err
|
|
|
|
}
|
2019-03-11 11:56:05 +03:00
|
|
|
|
|
|
|
// Query the channel router for a possible path to the destination that
|
|
|
|
// can carry `in.Amt` satoshis _including_ the total fee required on
|
|
|
|
// the route.
|
2019-10-11 22:47:15 +03:00
|
|
|
route, err := r.FindRoute(
|
2019-11-11 18:44:08 +03:00
|
|
|
sourcePubKey, targetPubKey, amt, restrictions,
|
2019-12-12 16:43:36 +03:00
|
|
|
customRecords, finalCLTVDelta,
|
2019-03-11 11:56:05 +03:00
|
|
|
)
|
2019-10-11 22:47:15 +03:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2019-03-11 11:56:05 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// For each valid route, we'll convert the result into the format
|
|
|
|
// required by the RPC system.
|
2019-07-31 07:44:02 +03:00
|
|
|
rpcRoute, err := r.MarshallRoute(route)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2019-05-07 18:01:01 +03:00
|
|
|
|
2019-09-03 09:58:14 +03:00
|
|
|
// Calculate route success probability. Do not rely on a probability
|
|
|
|
// that could have been returned from path finding, because mission
|
|
|
|
// control may have been disabled in the provided ProbabilitySource.
|
|
|
|
successProb := r.getSuccessProbability(route)
|
|
|
|
|
2019-03-11 11:56:05 +03:00
|
|
|
routeResp := &lnrpc.QueryRoutesResponse{
|
2019-09-03 09:58:14 +03:00
|
|
|
Routes: []*lnrpc.Route{rpcRoute},
|
|
|
|
SuccessProb: successProb,
|
2019-03-11 11:56:05 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
return routeResp, nil
|
|
|
|
}
|
|
|
|
|
2019-09-03 09:58:14 +03:00
|
|
|
// getSuccessProbability returns the success probability for the given route
|
|
|
|
// based on the current state of mission control.
|
|
|
|
func (r *RouterBackend) getSuccessProbability(rt *route.Route) float64 {
|
|
|
|
fromNode := rt.SourcePubKey
|
|
|
|
amtToFwd := rt.TotalAmount
|
|
|
|
successProb := 1.0
|
|
|
|
for _, hop := range rt.Hops {
|
|
|
|
toNode := hop.PubKeyBytes
|
|
|
|
|
|
|
|
probability := r.MissionControl.GetProbability(
|
|
|
|
fromNode, toNode, amtToFwd,
|
|
|
|
)
|
|
|
|
|
|
|
|
successProb *= probability
|
|
|
|
|
|
|
|
amtToFwd = hop.AmtToForward
|
|
|
|
fromNode = toNode
|
|
|
|
}
|
|
|
|
|
|
|
|
return successProb
|
|
|
|
}
|
|
|
|
|
2019-07-29 16:10:58 +03:00
|
|
|
// rpcEdgeToPair looks up the provided channel and returns the channel endpoints
|
|
|
|
// as a directed pair.
|
|
|
|
func (r *RouterBackend) rpcEdgeToPair(e *lnrpc.EdgeLocator) (
|
|
|
|
routing.DirectedNodePair, error) {
|
|
|
|
|
|
|
|
a, b, err := r.FetchChannelEndpoints(e.ChannelId)
|
|
|
|
if err != nil {
|
|
|
|
return routing.DirectedNodePair{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var pair routing.DirectedNodePair
|
|
|
|
if e.DirectionReverse {
|
|
|
|
pair.From, pair.To = b, a
|
|
|
|
} else {
|
|
|
|
pair.From, pair.To = a, b
|
|
|
|
}
|
|
|
|
|
|
|
|
return pair, nil
|
|
|
|
}
|
|
|
|
|
2019-03-11 11:56:05 +03:00
|
|
|
// MarshallRoute marshalls an internal route to an rpc route struct.
|
2019-07-31 07:44:02 +03:00
|
|
|
func (r *RouterBackend) MarshallRoute(route *route.Route) (*lnrpc.Route, error) {
|
2019-03-11 11:56:05 +03:00
|
|
|
resp := &lnrpc.Route{
|
|
|
|
TotalTimeLock: route.TotalTimeLock,
|
2019-05-15 15:37:58 +03:00
|
|
|
TotalFees: int64(route.TotalFees().ToSatoshis()),
|
|
|
|
TotalFeesMsat: int64(route.TotalFees()),
|
2019-03-11 11:56:05 +03:00
|
|
|
TotalAmt: int64(route.TotalAmount.ToSatoshis()),
|
|
|
|
TotalAmtMsat: int64(route.TotalAmount),
|
|
|
|
Hops: make([]*lnrpc.Hop, len(route.Hops)),
|
|
|
|
}
|
|
|
|
incomingAmt := route.TotalAmount
|
|
|
|
for i, hop := range route.Hops {
|
|
|
|
fee := route.HopFee(i)
|
|
|
|
|
|
|
|
// Channel capacity is not a defining property of a route. For
|
|
|
|
// backwards RPC compatibility, we retrieve it here from the
|
|
|
|
// graph.
|
|
|
|
chanCapacity, err := r.FetchChannelCapacity(hop.ChannelID)
|
|
|
|
if err != nil {
|
|
|
|
// If capacity cannot be retrieved, this may be a
|
|
|
|
// not-yet-received or private channel. Then report
|
|
|
|
// amount that is sent through the channel as capacity.
|
|
|
|
chanCapacity = incomingAmt.ToSatoshis()
|
|
|
|
}
|
|
|
|
|
2019-11-05 02:11:23 +03:00
|
|
|
// Extract the MPP fields if present on this hop.
|
|
|
|
var mpp *lnrpc.MPPRecord
|
|
|
|
if hop.MPP != nil {
|
|
|
|
addr := hop.MPP.PaymentAddr()
|
|
|
|
|
|
|
|
mpp = &lnrpc.MPPRecord{
|
|
|
|
PaymentAddr: addr[:],
|
|
|
|
TotalAmtMsat: int64(hop.MPP.TotalMsat()),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-11 11:56:05 +03:00
|
|
|
resp.Hops[i] = &lnrpc.Hop{
|
|
|
|
ChanId: hop.ChannelID,
|
|
|
|
ChanCapacity: int64(chanCapacity),
|
|
|
|
AmtToForward: int64(hop.AmtToForward.ToSatoshis()),
|
|
|
|
AmtToForwardMsat: int64(hop.AmtToForward),
|
|
|
|
Fee: int64(fee.ToSatoshis()),
|
|
|
|
FeeMsat: int64(fee),
|
|
|
|
Expiry: uint32(hop.OutgoingTimeLock),
|
|
|
|
PubKey: hex.EncodeToString(
|
2019-03-13 07:20:21 +03:00
|
|
|
hop.PubKeyBytes[:],
|
|
|
|
),
|
2019-12-11 12:52:27 +03:00
|
|
|
CustomRecords: hop.CustomRecords,
|
2019-11-20 14:00:25 +03:00
|
|
|
TlvPayload: !hop.LegacyPayload,
|
|
|
|
MppRecord: mpp,
|
2019-03-11 11:56:05 +03:00
|
|
|
}
|
|
|
|
incomingAmt = hop.AmtToForward
|
|
|
|
}
|
|
|
|
|
2019-07-31 07:44:02 +03:00
|
|
|
return resp, nil
|
2019-03-11 11:56:05 +03:00
|
|
|
}
|
2019-03-14 16:35:50 +03:00
|
|
|
|
2019-11-22 13:38:29 +03:00
|
|
|
// UnmarshallHopWithPubkey unmarshalls an rpc hop for which the pubkey has
|
|
|
|
// already been extracted.
|
|
|
|
func UnmarshallHopWithPubkey(rpcHop *lnrpc.Hop, pubkey route.Vertex) (*route.Hop,
|
|
|
|
error) {
|
2019-03-14 16:35:50 +03:00
|
|
|
|
2019-12-12 16:43:36 +03:00
|
|
|
customRecords := record.CustomSet(rpcHop.CustomRecords)
|
|
|
|
if err := customRecords.Validate(); err != nil {
|
2019-11-20 14:00:25 +03:00
|
|
|
return nil, err
|
|
|
|
}
|
2019-07-31 07:44:02 +03:00
|
|
|
|
2019-11-22 13:38:29 +03:00
|
|
|
mpp, err := UnmarshalMPP(rpcHop.MppRecord)
|
2019-11-05 02:11:23 +03:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2019-03-14 16:35:50 +03:00
|
|
|
return &route.Hop{
|
2019-11-22 13:38:29 +03:00
|
|
|
OutgoingTimeLock: rpcHop.Expiry,
|
|
|
|
AmtToForward: lnwire.MilliSatoshi(rpcHop.AmtToForwardMsat),
|
|
|
|
PubKeyBytes: pubkey,
|
|
|
|
ChannelID: rpcHop.ChanId,
|
2019-12-12 16:43:36 +03:00
|
|
|
CustomRecords: customRecords,
|
2019-11-22 13:38:29 +03:00
|
|
|
LegacyPayload: !rpcHop.TlvPayload,
|
2019-11-05 02:11:23 +03:00
|
|
|
MPP: mpp,
|
2019-03-14 16:35:50 +03:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// UnmarshallHop unmarshalls an rpc hop that may or may not contain a node
|
|
|
|
// pubkey.
|
2019-11-22 13:38:29 +03:00
|
|
|
func (r *RouterBackend) UnmarshallHop(rpcHop *lnrpc.Hop,
|
2019-03-14 16:35:50 +03:00
|
|
|
prevNodePubKey [33]byte) (*route.Hop, error) {
|
|
|
|
|
2019-11-22 13:38:29 +03:00
|
|
|
var pubKeyBytes [33]byte
|
|
|
|
if rpcHop.PubKey != "" {
|
|
|
|
// Unmarshall the provided hop pubkey.
|
|
|
|
pubKey, err := hex.DecodeString(rpcHop.PubKey)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("cannot decode pubkey %s",
|
|
|
|
rpcHop.PubKey)
|
|
|
|
}
|
|
|
|
copy(pubKeyBytes[:], pubKey)
|
|
|
|
} else {
|
|
|
|
// If no pub key is given of the hop, the local channel graph
|
|
|
|
// needs to be queried to complete the information necessary for
|
|
|
|
// routing. Discard edge policies, because they may be nil.
|
|
|
|
node1, node2, err := r.FetchChannelEndpoints(rpcHop.ChanId)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
switch {
|
|
|
|
case prevNodePubKey == node1:
|
|
|
|
pubKeyBytes = node2
|
|
|
|
case prevNodePubKey == node2:
|
|
|
|
pubKeyBytes = node1
|
|
|
|
default:
|
|
|
|
return nil, fmt.Errorf("channel edge does not match " +
|
|
|
|
"expected node")
|
|
|
|
}
|
2019-03-14 16:35:50 +03:00
|
|
|
}
|
|
|
|
|
2019-11-22 13:38:29 +03:00
|
|
|
return UnmarshallHopWithPubkey(rpcHop, pubKeyBytes)
|
2019-03-14 16:35:50 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// UnmarshallRoute unmarshalls an rpc route. For hops that don't specify a
|
|
|
|
// pubkey, the channel graph is queried.
|
2019-03-14 17:13:45 +03:00
|
|
|
func (r *RouterBackend) UnmarshallRoute(rpcroute *lnrpc.Route) (
|
|
|
|
*route.Route, error) {
|
2019-03-14 16:35:50 +03:00
|
|
|
|
2019-03-14 17:13:45 +03:00
|
|
|
prevNodePubKey := r.SelfNode
|
2019-03-14 16:35:50 +03:00
|
|
|
|
|
|
|
hops := make([]*route.Hop, len(rpcroute.Hops))
|
|
|
|
for i, hop := range rpcroute.Hops {
|
2019-03-14 17:13:45 +03:00
|
|
|
routeHop, err := r.UnmarshallHop(hop, prevNodePubKey)
|
2019-03-14 16:35:50 +03:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2019-12-05 01:35:43 +03:00
|
|
|
if routeHop.AmtToForward > r.MaxPaymentMSat {
|
|
|
|
return nil, fmt.Errorf("payment of %v is too large, "+
|
|
|
|
"max payment allowed is %v",
|
|
|
|
routeHop.AmtToForward,
|
|
|
|
r.MaxPaymentMSat.ToSatoshis())
|
|
|
|
}
|
|
|
|
|
2019-03-14 16:35:50 +03:00
|
|
|
hops[i] = routeHop
|
|
|
|
|
|
|
|
prevNodePubKey = routeHop.PubKeyBytes
|
|
|
|
}
|
|
|
|
|
|
|
|
route, err := route.NewRouteFromHops(
|
|
|
|
lnwire.MilliSatoshi(rpcroute.TotalAmtMsat),
|
|
|
|
rpcroute.TotalTimeLock,
|
2019-03-14 17:13:45 +03:00
|
|
|
r.SelfNode,
|
2019-03-14 16:35:50 +03:00
|
|
|
hops,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return route, nil
|
|
|
|
}
|
2019-04-18 10:34:28 +03:00
|
|
|
|
|
|
|
// extractIntentFromSendRequest attempts to parse the SendRequest details
|
|
|
|
// required to dispatch a client from the information presented by an RPC
|
|
|
|
// client.
|
2019-03-22 12:21:25 +03:00
|
|
|
func (r *RouterBackend) extractIntentFromSendRequest(
|
|
|
|
rpcPayReq *SendPaymentRequest) (*routing.LightningPayment, error) {
|
2019-04-18 10:34:28 +03:00
|
|
|
|
|
|
|
payIntent := &routing.LightningPayment{}
|
|
|
|
|
|
|
|
// Pass along an outgoing channel restriction if specified.
|
|
|
|
if rpcPayReq.OutgoingChanId != 0 {
|
|
|
|
payIntent.OutgoingChannelID = &rpcPayReq.OutgoingChanId
|
|
|
|
}
|
|
|
|
|
2019-11-18 14:08:42 +03:00
|
|
|
// Pass along a last hop restriction if specified.
|
|
|
|
if len(rpcPayReq.LastHopPubkey) > 0 {
|
|
|
|
lastHop, err := route.NewVertexFromBytes(
|
|
|
|
rpcPayReq.LastHopPubkey,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
payIntent.LastHop = &lastHop
|
|
|
|
}
|
|
|
|
|
2019-10-11 22:46:10 +03:00
|
|
|
// Take the CLTV limit from the request if set, otherwise use the max.
|
|
|
|
cltvLimit, err := ValidateCLTVLimit(
|
|
|
|
uint32(rpcPayReq.CltvLimit), r.MaxTotalTimelock,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2019-04-18 10:34:28 +03:00
|
|
|
}
|
2019-10-11 22:46:10 +03:00
|
|
|
payIntent.CltvLimit = cltvLimit
|
2019-04-18 10:34:28 +03:00
|
|
|
|
|
|
|
// Take fee limit from request.
|
2019-11-11 18:44:08 +03:00
|
|
|
payIntent.FeeLimit, err = lnrpc.UnmarshallAmt(
|
|
|
|
rpcPayReq.FeeLimitSat, rpcPayReq.FeeLimitMsat,
|
2019-04-18 10:34:28 +03:00
|
|
|
)
|
2019-11-11 18:44:08 +03:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2019-04-18 10:34:28 +03:00
|
|
|
|
|
|
|
// Set payment attempt timeout.
|
|
|
|
if rpcPayReq.TimeoutSeconds == 0 {
|
|
|
|
return nil, errors.New("timeout_seconds must be specified")
|
|
|
|
}
|
|
|
|
|
2019-12-12 16:43:36 +03:00
|
|
|
customRecords := record.CustomSet(rpcPayReq.DestCustomRecords)
|
|
|
|
if err := customRecords.Validate(); err != nil {
|
2019-11-20 14:00:25 +03:00
|
|
|
return nil, err
|
2019-07-31 07:44:02 +03:00
|
|
|
}
|
2019-12-12 16:43:36 +03:00
|
|
|
payIntent.DestCustomRecords = customRecords
|
2019-07-31 07:44:02 +03:00
|
|
|
|
2019-04-18 10:34:28 +03:00
|
|
|
payIntent.PayAttemptTimeout = time.Second *
|
|
|
|
time.Duration(rpcPayReq.TimeoutSeconds)
|
|
|
|
|
2019-06-18 11:36:29 +03:00
|
|
|
// Route hints.
|
|
|
|
routeHints, err := unmarshallRouteHints(
|
|
|
|
rpcPayReq.RouteHints,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
payIntent.RouteHints = routeHints
|
|
|
|
|
2019-11-11 18:44:08 +03:00
|
|
|
// Unmarshall either sat or msat amount from request.
|
|
|
|
reqAmt, err := lnrpc.UnmarshallAmt(
|
|
|
|
rpcPayReq.Amt, rpcPayReq.AmtMsat,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2019-04-18 10:34:28 +03:00
|
|
|
// If the payment request field isn't blank, then the details of the
|
|
|
|
// invoice are encoded entirely within the encoded payReq. So we'll
|
|
|
|
// attempt to decode it, populating the payment accordingly.
|
|
|
|
if rpcPayReq.PaymentRequest != "" {
|
|
|
|
switch {
|
|
|
|
|
|
|
|
case len(rpcPayReq.Dest) > 0:
|
|
|
|
return nil, errors.New("dest and payment_request " +
|
|
|
|
"cannot appear together")
|
|
|
|
|
|
|
|
case len(rpcPayReq.PaymentHash) > 0:
|
|
|
|
return nil, errors.New("dest and payment_hash " +
|
|
|
|
"cannot appear together")
|
|
|
|
|
|
|
|
case rpcPayReq.FinalCltvDelta != 0:
|
|
|
|
return nil, errors.New("dest and final_cltv_delta " +
|
|
|
|
"cannot appear together")
|
|
|
|
}
|
|
|
|
|
|
|
|
payReq, err := zpay32.Decode(
|
|
|
|
rpcPayReq.PaymentRequest, r.ActiveNetParams,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Next, we'll ensure that this payreq hasn't already expired.
|
|
|
|
err = ValidatePayReqExpiry(payReq)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the amount was not included in the invoice, then we let
|
|
|
|
// the payee specify the amount of satoshis they wish to send.
|
|
|
|
// We override the amount to pay with the amount provided from
|
|
|
|
// the payment request.
|
|
|
|
if payReq.MilliSat == nil {
|
2019-11-11 18:44:08 +03:00
|
|
|
if reqAmt == 0 {
|
2019-04-18 10:34:28 +03:00
|
|
|
return nil, errors.New("amount must be " +
|
|
|
|
"specified when paying a zero amount " +
|
|
|
|
"invoice")
|
|
|
|
}
|
|
|
|
|
2019-11-11 18:44:08 +03:00
|
|
|
payIntent.Amount = reqAmt
|
2019-04-18 10:34:28 +03:00
|
|
|
} else {
|
2019-11-11 18:44:08 +03:00
|
|
|
if reqAmt != 0 {
|
2019-04-18 10:34:28 +03:00
|
|
|
return nil, errors.New("amount must not be " +
|
|
|
|
"specified when paying a non-zero " +
|
|
|
|
" amount invoice")
|
|
|
|
}
|
|
|
|
|
|
|
|
payIntent.Amount = *payReq.MilliSat
|
|
|
|
}
|
|
|
|
|
|
|
|
copy(payIntent.PaymentHash[:], payReq.PaymentHash[:])
|
|
|
|
destKey := payReq.Destination.SerializeCompressed()
|
|
|
|
copy(payIntent.Target[:], destKey)
|
|
|
|
|
|
|
|
payIntent.FinalCLTVDelta = uint16(payReq.MinFinalCLTVExpiry())
|
2019-06-18 11:36:29 +03:00
|
|
|
payIntent.RouteHints = append(
|
|
|
|
payIntent.RouteHints, payReq.RouteHints...,
|
|
|
|
)
|
2019-12-19 10:56:59 +03:00
|
|
|
payIntent.DestFeatures = payReq.Features
|
|
|
|
payIntent.PaymentAddr = payReq.PaymentAddr
|
2019-04-18 10:34:28 +03:00
|
|
|
} else {
|
|
|
|
// Otherwise, If the payment request field was not specified
|
|
|
|
// (and a custom route wasn't specified), construct the payment
|
|
|
|
// from the other fields.
|
|
|
|
|
|
|
|
// Payment destination.
|
2019-08-07 14:09:27 +03:00
|
|
|
target, err := route.NewVertexFromBytes(rpcPayReq.Dest)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2019-04-18 10:34:28 +03:00
|
|
|
}
|
2019-08-07 14:09:27 +03:00
|
|
|
payIntent.Target = target
|
2019-04-18 10:34:28 +03:00
|
|
|
|
|
|
|
// Final payment CLTV delta.
|
|
|
|
if rpcPayReq.FinalCltvDelta != 0 {
|
|
|
|
payIntent.FinalCLTVDelta =
|
|
|
|
uint16(rpcPayReq.FinalCltvDelta)
|
|
|
|
} else {
|
|
|
|
payIntent.FinalCLTVDelta = zpay32.DefaultFinalCLTVDelta
|
|
|
|
}
|
|
|
|
|
|
|
|
// Amount.
|
2019-11-11 18:44:08 +03:00
|
|
|
if reqAmt == 0 {
|
2019-04-18 10:34:28 +03:00
|
|
|
return nil, errors.New("amount must be specified")
|
|
|
|
}
|
|
|
|
|
2019-11-11 18:44:08 +03:00
|
|
|
payIntent.Amount = reqAmt
|
2019-04-18 10:34:28 +03:00
|
|
|
|
|
|
|
// Payment hash.
|
|
|
|
copy(payIntent.PaymentHash[:], rpcPayReq.PaymentHash)
|
2019-12-19 10:57:38 +03:00
|
|
|
|
|
|
|
// Parse destination feature bits.
|
|
|
|
features, err := UnmarshalFeatures(rpcPayReq.DestFeatures)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
payIntent.DestFeatures = features
|
2019-04-18 10:34:28 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// Currently, within the bootstrap phase of the network, we limit the
|
|
|
|
// largest payment size allotted to (2^32) - 1 mSAT or 4.29 million
|
|
|
|
// satoshis.
|
|
|
|
if payIntent.Amount > r.MaxPaymentMSat {
|
|
|
|
// In this case, we'll send an error to the caller, but
|
|
|
|
// continue our loop for the next payment.
|
|
|
|
return payIntent, fmt.Errorf("payment of %v is too large, "+
|
|
|
|
"max payment allowed is %v", payIntent.Amount,
|
|
|
|
r.MaxPaymentMSat)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2019-11-25 16:13:21 +03:00
|
|
|
// Check for disallowed payments to self.
|
|
|
|
if !rpcPayReq.AllowSelfPayment && payIntent.Target == r.SelfNode {
|
|
|
|
return nil, errors.New("self-payments not allowed")
|
|
|
|
}
|
|
|
|
|
2019-04-18 10:34:28 +03:00
|
|
|
return payIntent, nil
|
|
|
|
}
|
|
|
|
|
2019-06-18 11:36:29 +03:00
|
|
|
// unmarshallRouteHints unmarshalls a list of route hints.
|
|
|
|
func unmarshallRouteHints(rpcRouteHints []*lnrpc.RouteHint) (
|
|
|
|
[][]zpay32.HopHint, error) {
|
|
|
|
|
|
|
|
routeHints := make([][]zpay32.HopHint, 0, len(rpcRouteHints))
|
|
|
|
for _, rpcRouteHint := range rpcRouteHints {
|
|
|
|
routeHint := make(
|
|
|
|
[]zpay32.HopHint, 0, len(rpcRouteHint.HopHints),
|
|
|
|
)
|
|
|
|
for _, rpcHint := range rpcRouteHint.HopHints {
|
|
|
|
hint, err := unmarshallHopHint(rpcHint)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
routeHint = append(routeHint, hint)
|
|
|
|
}
|
|
|
|
routeHints = append(routeHints, routeHint)
|
|
|
|
}
|
|
|
|
|
|
|
|
return routeHints, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// unmarshallHopHint unmarshalls a single hop hint.
|
|
|
|
func unmarshallHopHint(rpcHint *lnrpc.HopHint) (zpay32.HopHint, error) {
|
|
|
|
pubBytes, err := hex.DecodeString(rpcHint.NodeId)
|
|
|
|
if err != nil {
|
|
|
|
return zpay32.HopHint{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
pubkey, err := btcec.ParsePubKey(pubBytes, btcec.S256())
|
|
|
|
if err != nil {
|
|
|
|
return zpay32.HopHint{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return zpay32.HopHint{
|
|
|
|
NodeID: pubkey,
|
|
|
|
ChannelID: rpcHint.ChanId,
|
|
|
|
FeeBaseMSat: rpcHint.FeeBaseMsat,
|
|
|
|
FeeProportionalMillionths: rpcHint.FeeProportionalMillionths,
|
|
|
|
CLTVExpiryDelta: uint16(rpcHint.CltvExpiryDelta),
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2019-12-19 10:57:38 +03:00
|
|
|
// UnmarshalFeatures converts a list of uint32's into a valid feature vector.
|
|
|
|
// This method checks that feature bit pairs aren't assigned toegether, and
|
|
|
|
// validates transitive depdencies.
|
2019-12-20 03:25:59 +03:00
|
|
|
func UnmarshalFeatures(
|
|
|
|
rpcFeatures []lnrpc.FeatureBit) (*lnwire.FeatureVector, error) {
|
|
|
|
|
|
|
|
// If no destination features are specified we'll return nil to signal
|
|
|
|
// that the router should try to use the graph as a fallback.
|
|
|
|
if rpcFeatures == nil {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
2019-12-19 10:57:38 +03:00
|
|
|
raw := lnwire.NewRawFeatureVector()
|
|
|
|
for _, bit := range rpcFeatures {
|
|
|
|
err := raw.SafeSet(lnwire.FeatureBit(bit))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return lnwire.NewFeatureVector(raw, lnwire.Features), nil
|
|
|
|
}
|
|
|
|
|
2019-04-18 10:34:28 +03:00
|
|
|
// ValidatePayReqExpiry checks if the passed payment request has expired. In
|
|
|
|
// the case it has expired, an error will be returned.
|
|
|
|
func ValidatePayReqExpiry(payReq *zpay32.Invoice) error {
|
|
|
|
expiry := payReq.Expiry()
|
|
|
|
validUntil := payReq.Timestamp.Add(expiry)
|
|
|
|
if time.Now().After(validUntil) {
|
|
|
|
return fmt.Errorf("invoice expired. Valid until %v", validUntil)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
2019-10-11 22:46:10 +03:00
|
|
|
|
|
|
|
// ValidateCLTVLimit returns a valid CLTV limit given a value and a maximum. If
|
|
|
|
// the value exceeds the maximum, then an error is returned. If the value is 0,
|
|
|
|
// then the maximum is used.
|
|
|
|
func ValidateCLTVLimit(val, max uint32) (uint32, error) {
|
|
|
|
switch {
|
|
|
|
case val == 0:
|
|
|
|
return max, nil
|
|
|
|
case val > max:
|
|
|
|
return 0, fmt.Errorf("total time lock of %v exceeds max "+
|
|
|
|
"allowed %v", val, max)
|
|
|
|
default:
|
|
|
|
return val, nil
|
|
|
|
}
|
|
|
|
}
|
2019-11-05 02:11:23 +03:00
|
|
|
|
|
|
|
// UnmarshalMPP accepts the mpp_total_amt_msat and mpp_payment_addr fields from
|
|
|
|
// an RPC request and converts into an record.MPP object. An error is returned
|
|
|
|
// if the payment address is not 0 or 32 bytes. If the total amount and payment
|
|
|
|
// address are zero-value, the return value will be nil signaling there is no
|
|
|
|
// MPP record to attach to this hop. Otherwise, a non-nil reocrd will be
|
|
|
|
// contained combining the provided values.
|
|
|
|
func UnmarshalMPP(reqMPP *lnrpc.MPPRecord) (*record.MPP, error) {
|
|
|
|
// If no MPP record was submitted, assume the user wants to send a
|
|
|
|
// regular payment.
|
|
|
|
if reqMPP == nil {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
reqTotal := reqMPP.TotalAmtMsat
|
|
|
|
reqAddr := reqMPP.PaymentAddr
|
|
|
|
|
|
|
|
switch {
|
|
|
|
|
|
|
|
// No MPP fields were provided.
|
|
|
|
case reqTotal == 0 && len(reqAddr) == 0:
|
|
|
|
return nil, fmt.Errorf("missing total_msat and payment_addr")
|
|
|
|
|
|
|
|
// Total is present, but payment address is missing.
|
|
|
|
case reqTotal > 0 && len(reqAddr) == 0:
|
|
|
|
return nil, fmt.Errorf("missing payment_addr")
|
|
|
|
|
|
|
|
// Payment address is present, but total is missing.
|
|
|
|
case reqTotal == 0 && len(reqAddr) > 0:
|
|
|
|
return nil, fmt.Errorf("missing total_msat")
|
|
|
|
}
|
|
|
|
|
|
|
|
addr, err := lntypes.MakeHash(reqAddr)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("unable to parse "+
|
|
|
|
"payment_addr: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
total := lnwire.MilliSatoshi(reqTotal)
|
|
|
|
|
|
|
|
return record.NewMPP(total, addr), nil
|
|
|
|
}
|
2019-11-20 07:41:44 +03:00
|
|
|
|
|
|
|
// MarshalHTLCAttempt constructs an RPC HTLCAttempt from the db representation.
|
|
|
|
func (r *RouterBackend) MarshalHTLCAttempt(
|
|
|
|
htlc channeldb.HTLCAttempt) (*lnrpc.HTLCAttempt, error) {
|
|
|
|
|
|
|
|
var (
|
|
|
|
status lnrpc.HTLCAttempt_HTLCStatus
|
|
|
|
resolveTime int64
|
|
|
|
)
|
|
|
|
|
|
|
|
switch {
|
|
|
|
case htlc.Settle != nil:
|
|
|
|
status = lnrpc.HTLCAttempt_SUCCEEDED
|
|
|
|
resolveTime = MarshalTimeNano(htlc.Settle.SettleTime)
|
|
|
|
|
|
|
|
case htlc.Failure != nil:
|
|
|
|
status = lnrpc.HTLCAttempt_FAILED
|
|
|
|
resolveTime = MarshalTimeNano(htlc.Failure.FailTime)
|
|
|
|
|
|
|
|
default:
|
|
|
|
status = lnrpc.HTLCAttempt_IN_FLIGHT
|
|
|
|
}
|
|
|
|
|
|
|
|
route, err := r.MarshallRoute(&htlc.Route)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &lnrpc.HTLCAttempt{
|
|
|
|
Status: status,
|
|
|
|
Route: route,
|
|
|
|
AttemptTimeNs: MarshalTimeNano(htlc.AttemptTime),
|
|
|
|
ResolveTimeNs: resolveTime,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// MarshalTimeNano converts a time.Time into its nanosecond representation. If
|
|
|
|
// the time is zero, this method simply returns 0, since calling UnixNano() on a
|
|
|
|
// zero-valued time is undefined.
|
|
|
|
func MarshalTimeNano(t time.Time) int64 {
|
|
|
|
if t.IsZero() {
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
return t.UnixNano()
|
|
|
|
}
|