276 lines
7.2 KiB
Go
276 lines
7.2 KiB
Go
package paymentsdb
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcd/btcec/v2"
|
|
"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/sqldb/sqlc"
|
|
"github.com/lightningnetwork/lnd/tlv"
|
|
)
|
|
|
|
// dbPaymentToCreationInfo converts database payment data to the
|
|
// PaymentCreationInfo struct.
|
|
func dbPaymentToCreationInfo(paymentIdentifier []byte, amountMsat int64,
|
|
createdAt time.Time, intentPayload []byte,
|
|
firstHopCustomRecords lnwire.CustomRecords) *PaymentCreationInfo {
|
|
|
|
// This is the payment hash for non-AMP payments and the SetID for AMP
|
|
// payments.
|
|
var identifier lntypes.Hash
|
|
copy(identifier[:], paymentIdentifier)
|
|
|
|
return &PaymentCreationInfo{
|
|
PaymentIdentifier: identifier,
|
|
Value: lnwire.MilliSatoshi(amountMsat),
|
|
// The creation time is stored in the database as UTC but here
|
|
// we convert it to local time.
|
|
CreationTime: createdAt.Local(),
|
|
PaymentRequest: intentPayload,
|
|
FirstHopCustomRecords: firstHopCustomRecords,
|
|
}
|
|
}
|
|
|
|
// dbAttemptToHTLCAttempt converts a database HTLC attempt to an HTLCAttempt.
|
|
func dbAttemptToHTLCAttempt(dbAttempt sqlc.FetchHtlcAttemptsForPaymentsRow,
|
|
hops []sqlc.FetchHopsForAttemptsRow,
|
|
hopCustomRecords map[int64][]sqlc.PaymentHopCustomRecord,
|
|
routeCustomRecords []sqlc.PaymentAttemptFirstHopCustomRecord) (
|
|
*HTLCAttempt, error) {
|
|
|
|
// Convert route-level first hop custom records to CustomRecords map.
|
|
var firstHopWireCustomRecords lnwire.CustomRecords
|
|
if len(routeCustomRecords) > 0 {
|
|
firstHopWireCustomRecords = make(lnwire.CustomRecords)
|
|
for _, record := range routeCustomRecords {
|
|
firstHopWireCustomRecords[uint64(record.Key)] =
|
|
record.Value
|
|
}
|
|
}
|
|
|
|
// Build the route from the database data.
|
|
route, err := dbDataToRoute(
|
|
hops, hopCustomRecords, dbAttempt.FirstHopAmountMsat,
|
|
dbAttempt.RouteTotalTimeLock, dbAttempt.RouteTotalAmount,
|
|
dbAttempt.RouteSourceKey, firstHopWireCustomRecords,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to convert to route: %w",
|
|
err)
|
|
}
|
|
|
|
hash, err := lntypes.MakeHash(dbAttempt.PaymentHash)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse payment "+
|
|
"hash: %w", err)
|
|
}
|
|
|
|
// Create the attempt info.
|
|
var sessionKey [32]byte
|
|
copy(sessionKey[:], dbAttempt.SessionKey)
|
|
|
|
info := HTLCAttemptInfo{
|
|
AttemptID: uint64(dbAttempt.AttemptIndex),
|
|
sessionKey: sessionKey,
|
|
Route: *route,
|
|
AttemptTime: dbAttempt.AttemptTime,
|
|
Hash: &hash,
|
|
}
|
|
|
|
attempt := &HTLCAttempt{
|
|
HTLCAttemptInfo: info,
|
|
}
|
|
|
|
// If there's no resolution type, the attempt is still in-flight.
|
|
// Return early without processing settlement or failure info.
|
|
if !dbAttempt.ResolutionType.Valid {
|
|
return attempt, nil
|
|
}
|
|
|
|
// Add settlement info if present.
|
|
if HTLCAttemptResolutionType(dbAttempt.ResolutionType.Int32) ==
|
|
HTLCAttemptResolutionSettled {
|
|
|
|
var preimage lntypes.Preimage
|
|
copy(preimage[:], dbAttempt.SettlePreimage)
|
|
|
|
attempt.Settle = &HTLCSettleInfo{
|
|
Preimage: preimage,
|
|
SettleTime: dbAttempt.ResolutionTime.Time,
|
|
}
|
|
}
|
|
|
|
// Add failure info if present.
|
|
if HTLCAttemptResolutionType(dbAttempt.ResolutionType.Int32) ==
|
|
HTLCAttemptResolutionFailed {
|
|
|
|
failure := &HTLCFailInfo{
|
|
FailTime: dbAttempt.ResolutionTime.Time,
|
|
}
|
|
|
|
if dbAttempt.HtlcFailReason.Valid {
|
|
failure.Reason = HTLCFailReason(
|
|
dbAttempt.HtlcFailReason.Int32,
|
|
)
|
|
}
|
|
|
|
if dbAttempt.FailureSourceIndex.Valid {
|
|
failure.FailureSourceIndex = uint32(
|
|
dbAttempt.FailureSourceIndex.Int32,
|
|
)
|
|
}
|
|
|
|
// Decode the failure message if present.
|
|
if len(dbAttempt.FailureMsg) > 0 {
|
|
msg, err := lnwire.DecodeFailureMessage(
|
|
bytes.NewReader(dbAttempt.FailureMsg), 0,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decode "+
|
|
"failure message: %w", err)
|
|
}
|
|
failure.Message = msg
|
|
}
|
|
|
|
attempt.Failure = failure
|
|
}
|
|
|
|
return attempt, nil
|
|
}
|
|
|
|
// dbDataToRoute converts database route data to a route.Route.
|
|
func dbDataToRoute(hops []sqlc.FetchHopsForAttemptsRow,
|
|
hopCustomRecords map[int64][]sqlc.PaymentHopCustomRecord,
|
|
firstHopAmountMsat int64, totalTimeLock int32, totalAmount int64,
|
|
sourceKey []byte, firstHopWireCustomRecords lnwire.CustomRecords) (
|
|
*route.Route, error) {
|
|
|
|
if len(hops) == 0 {
|
|
return nil, fmt.Errorf("no hops provided")
|
|
}
|
|
|
|
// Hops are already sorted by hop_index from the SQL query.
|
|
routeHops := make([]*route.Hop, len(hops))
|
|
|
|
for i, hop := range hops {
|
|
pubKey, err := route.NewVertexFromBytes(hop.PubKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse pub key: %w",
|
|
err)
|
|
}
|
|
|
|
var channelID uint64
|
|
if hop.Scid != "" {
|
|
// The SCID is stored as a string representation
|
|
// of the uint64.
|
|
var err error
|
|
channelID, err = strconv.ParseUint(hop.Scid, 10, 64)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse "+
|
|
"scid: %w", err)
|
|
}
|
|
}
|
|
|
|
routeHop := &route.Hop{
|
|
PubKeyBytes: pubKey,
|
|
ChannelID: channelID,
|
|
OutgoingTimeLock: uint32(hop.OutgoingTimeLock),
|
|
AmtToForward: lnwire.MilliSatoshi(hop.AmtToForward),
|
|
}
|
|
|
|
// Add MPP record if present.
|
|
if len(hop.MppPaymentAddr) > 0 {
|
|
var paymentAddr [32]byte
|
|
copy(paymentAddr[:], hop.MppPaymentAddr)
|
|
routeHop.MPP = record.NewMPP(
|
|
lnwire.MilliSatoshi(hop.MppTotalMsat.Int64),
|
|
paymentAddr,
|
|
)
|
|
}
|
|
|
|
// Add AMP record if present.
|
|
if len(hop.AmpRootShare) > 0 {
|
|
var rootShare [32]byte
|
|
copy(rootShare[:], hop.AmpRootShare)
|
|
var setID [32]byte
|
|
copy(setID[:], hop.AmpSetID)
|
|
|
|
routeHop.AMP = record.NewAMP(
|
|
rootShare, setID,
|
|
uint32(hop.AmpChildIndex.Int32),
|
|
)
|
|
}
|
|
|
|
// Add blinding point if present (only for introduction node
|
|
// in blinded route).
|
|
if len(hop.BlindingPoint) > 0 {
|
|
pubKey, err := btcec.ParsePubKey(hop.BlindingPoint)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse "+
|
|
"blinding point: %w", err)
|
|
}
|
|
routeHop.BlindingPoint = pubKey
|
|
}
|
|
|
|
// Add encrypted data if present (for all blinded hops).
|
|
if len(hop.EncryptedData) > 0 {
|
|
routeHop.EncryptedData = hop.EncryptedData
|
|
}
|
|
|
|
// Add total amount if present (only for final hop in blinded
|
|
// route).
|
|
if hop.BlindedPathTotalAmt.Valid {
|
|
routeHop.TotalAmtMsat = lnwire.MilliSatoshi(
|
|
hop.BlindedPathTotalAmt.Int64,
|
|
)
|
|
}
|
|
|
|
// Add hop-level custom records.
|
|
if records, ok := hopCustomRecords[hop.ID]; ok {
|
|
routeHop.CustomRecords = make(
|
|
record.CustomSet,
|
|
)
|
|
for _, rec := range records {
|
|
routeHop.CustomRecords[uint64(rec.Key)] =
|
|
rec.Value
|
|
}
|
|
}
|
|
|
|
// Add metadata if present.
|
|
if len(hop.MetaData) > 0 {
|
|
routeHop.Metadata = hop.MetaData
|
|
}
|
|
|
|
routeHops[i] = routeHop
|
|
}
|
|
|
|
// Parse the source node public key.
|
|
var sourceNode route.Vertex
|
|
copy(sourceNode[:], sourceKey)
|
|
|
|
route := &route.Route{
|
|
TotalTimeLock: uint32(totalTimeLock),
|
|
TotalAmount: lnwire.MilliSatoshi(totalAmount),
|
|
SourcePubKey: sourceNode,
|
|
Hops: routeHops,
|
|
FirstHopWireCustomRecords: firstHopWireCustomRecords,
|
|
}
|
|
|
|
// Set the first hop amount if it is set.
|
|
if firstHopAmountMsat != 0 {
|
|
route.FirstHopAmount = tlv.NewRecordT[tlv.TlvType0](
|
|
tlv.NewBigSizeT(lnwire.MilliSatoshi(
|
|
firstHopAmountMsat,
|
|
)),
|
|
)
|
|
}
|
|
|
|
return route, nil
|
|
}
|