2019-05-23 20:05:26 +02:00
|
|
|
package channeldb
|
2018-08-12 15:18:35 +02:00
|
|
|
|
|
|
|
import (
|
2019-05-23 20:05:28 +02:00
|
|
|
"bytes"
|
|
|
|
"encoding/binary"
|
2018-08-12 15:18:35 +02:00
|
|
|
"errors"
|
2019-05-23 20:05:29 +02:00
|
|
|
"fmt"
|
2020-06-10 12:34:27 +02:00
|
|
|
"io"
|
2021-06-25 18:21:59 +02:00
|
|
|
"sync"
|
2018-08-12 15:18:35 +02:00
|
|
|
|
2021-04-26 19:08:11 +02:00
|
|
|
"github.com/lightningnetwork/lnd/kvdb"
|
2019-05-23 20:05:26 +02:00
|
|
|
"github.com/lightningnetwork/lnd/lntypes"
|
2018-08-12 15:18:35 +02:00
|
|
|
)
|
|
|
|
|
2021-06-25 18:21:59 +02:00
|
|
|
const (
|
|
|
|
// paymentSeqBlockSize is the block size used when we batch allocate
|
|
|
|
// payment sequences for future payments.
|
|
|
|
paymentSeqBlockSize = 1000
|
|
|
|
)
|
|
|
|
|
2018-08-12 15:18:35 +02:00
|
|
|
var (
|
2018-08-10 23:00:50 +02:00
|
|
|
// ErrAlreadyPaid signals we have already paid this payment hash.
|
|
|
|
ErrAlreadyPaid = errors.New("invoice is already paid")
|
2018-08-12 15:18:35 +02:00
|
|
|
|
2018-08-10 23:00:50 +02:00
|
|
|
// ErrPaymentInFlight signals that payment for this payment hash is
|
|
|
|
// already "in flight" on the network.
|
2018-08-12 15:18:35 +02:00
|
|
|
ErrPaymentInFlight = errors.New("payment is in transition")
|
|
|
|
|
2020-04-01 00:13:25 +02:00
|
|
|
// ErrPaymentNotInitiated is returned if the payment wasn't initiated.
|
2018-08-12 15:18:35 +02:00
|
|
|
ErrPaymentNotInitiated = errors.New("payment isn't initiated")
|
|
|
|
|
2019-05-23 20:05:31 +02:00
|
|
|
// ErrPaymentAlreadySucceeded is returned in the event we attempt to
|
|
|
|
// change the status of a payment already succeeded.
|
|
|
|
ErrPaymentAlreadySucceeded = errors.New("payment is already succeeded")
|
2018-08-10 23:00:50 +02:00
|
|
|
|
2020-04-01 00:13:25 +02:00
|
|
|
// ErrPaymentAlreadyFailed is returned in the event we attempt to alter
|
|
|
|
// a failed payment.
|
2019-05-23 20:05:28 +02:00
|
|
|
ErrPaymentAlreadyFailed = errors.New("payment has already failed")
|
|
|
|
|
2018-08-10 23:00:50 +02:00
|
|
|
// ErrUnknownPaymentStatus is returned when we do not recognize the
|
|
|
|
// existing state of a payment.
|
|
|
|
ErrUnknownPaymentStatus = errors.New("unknown payment status")
|
2019-04-30 13:24:37 +02:00
|
|
|
|
2020-04-01 00:13:25 +02:00
|
|
|
// ErrPaymentTerminal is returned if we attempt to alter a payment that
|
|
|
|
// already has reached a terminal condition.
|
2022-06-08 15:44:21 +02:00
|
|
|
ErrPaymentTerminal = errors.New("payment has reached terminal " +
|
|
|
|
"condition")
|
2020-04-01 00:13:25 +02:00
|
|
|
|
|
|
|
// ErrAttemptAlreadySettled is returned if we try to alter an already
|
|
|
|
// settled HTLC attempt.
|
|
|
|
ErrAttemptAlreadySettled = errors.New("attempt already settled")
|
|
|
|
|
|
|
|
// ErrAttemptAlreadyFailed is returned if we try to alter an already
|
|
|
|
// failed HTLC attempt.
|
|
|
|
ErrAttemptAlreadyFailed = errors.New("attempt already failed")
|
|
|
|
|
2020-04-01 00:13:27 +02:00
|
|
|
// ErrValueMismatch is returned if we try to register a non-MPP attempt
|
|
|
|
// with an amount that doesn't match the payment amount.
|
|
|
|
ErrValueMismatch = errors.New("attempted value doesn't match payment" +
|
|
|
|
"amount")
|
|
|
|
|
|
|
|
// ErrValueExceedsAmt is returned if we try to register an attempt that
|
|
|
|
// would take the total sent amount above the payment amount.
|
|
|
|
ErrValueExceedsAmt = errors.New("attempted value exceeds payment" +
|
|
|
|
"amount")
|
|
|
|
|
|
|
|
// ErrNonMPPayment is returned if we try to register an MPP attempt for
|
2022-01-13 17:29:43 +01:00
|
|
|
// a payment that already has a non-MPP attempt registered.
|
2020-04-01 00:13:27 +02:00
|
|
|
ErrNonMPPayment = errors.New("payment has non-MPP attempts")
|
|
|
|
|
|
|
|
// ErrMPPayment is returned if we try to register a non-MPP attempt for
|
2022-01-13 17:29:43 +01:00
|
|
|
// a payment that already has an MPP attempt registered.
|
2020-04-01 00:13:27 +02:00
|
|
|
ErrMPPayment = errors.New("payment has MPP attempts")
|
|
|
|
|
|
|
|
// ErrMPPPaymentAddrMismatch is returned if we try to register an MPP
|
|
|
|
// shard where the payment address doesn't match existing shards.
|
|
|
|
ErrMPPPaymentAddrMismatch = errors.New("payment address mismatch")
|
|
|
|
|
|
|
|
// ErrMPPTotalAmountMismatch is returned if we try to register an MPP
|
|
|
|
// shard where the total amount doesn't match existing shards.
|
2022-06-08 15:44:21 +02:00
|
|
|
ErrMPPTotalAmountMismatch = errors.New("mp payment total amount " +
|
|
|
|
"mismatch")
|
2020-04-01 00:13:27 +02:00
|
|
|
|
2019-04-30 13:24:37 +02:00
|
|
|
// errNoAttemptInfo is returned when no attempt info is stored yet.
|
|
|
|
errNoAttemptInfo = errors.New("unable to find attempt info for " +
|
|
|
|
"inflight payment")
|
2020-06-10 12:34:27 +02:00
|
|
|
|
|
|
|
// errNoSequenceNrIndex is returned when an attempt to lookup a payment
|
|
|
|
// index is made for a sequence number that is not indexed.
|
|
|
|
errNoSequenceNrIndex = errors.New("payment sequence number index " +
|
|
|
|
"does not exist")
|
2018-08-12 15:18:35 +02:00
|
|
|
)
|
|
|
|
|
2019-05-29 08:57:04 +02:00
|
|
|
// PaymentControl implements persistence for payments and payment attempts.
|
|
|
|
type PaymentControl struct {
|
2021-06-25 18:21:59 +02:00
|
|
|
paymentSeqMx sync.Mutex
|
|
|
|
currPaymentSeq uint64
|
|
|
|
storedPaymentSeq uint64
|
|
|
|
db *DB
|
2018-08-12 15:18:35 +02:00
|
|
|
}
|
|
|
|
|
2019-05-29 08:57:04 +02:00
|
|
|
// NewPaymentControl creates a new instance of the PaymentControl.
|
|
|
|
func NewPaymentControl(db *DB) *PaymentControl {
|
|
|
|
return &PaymentControl{
|
2019-05-23 20:05:28 +02:00
|
|
|
db: db,
|
2018-08-12 15:18:35 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-05-23 20:05:28 +02:00
|
|
|
// InitPayment checks or records the given PaymentCreationInfo with the DB,
|
2020-04-01 00:13:25 +02:00
|
|
|
// making sure it does not already exist as an in-flight payment. When this
|
2022-01-13 17:29:43 +01:00
|
|
|
// method returns successfully, the payment is guaranteed to be in the InFlight
|
2019-05-23 20:05:28 +02:00
|
|
|
// state.
|
2019-05-29 08:57:04 +02:00
|
|
|
func (p *PaymentControl) InitPayment(paymentHash lntypes.Hash,
|
2019-05-23 20:05:28 +02:00
|
|
|
info *PaymentCreationInfo) error {
|
|
|
|
|
2021-06-25 18:21:59 +02:00
|
|
|
// Obtain a new sequence number for this payment. This is used
|
|
|
|
// to sort the payments in order of creation, and also acts as
|
|
|
|
// a unique identifier for each payment.
|
|
|
|
sequenceNum, err := p.nextPaymentSequence()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2019-05-23 20:05:28 +02:00
|
|
|
var b bytes.Buffer
|
|
|
|
if err := serializePaymentCreationInfo(&b, info); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
infoBytes := b.Bytes()
|
|
|
|
|
2019-05-23 20:05:31 +02:00
|
|
|
var updateErr error
|
2021-06-25 18:21:59 +02:00
|
|
|
err = kvdb.Batch(p.db.Backend, func(tx kvdb.RwTx) error {
|
2019-05-23 20:05:31 +02:00
|
|
|
// Reset the update error, to avoid carrying over an error
|
|
|
|
// from a previous execution of the batched db transaction.
|
|
|
|
updateErr = nil
|
|
|
|
|
2021-06-16 22:20:13 +02:00
|
|
|
prefetchPayment(tx, paymentHash)
|
2019-05-23 20:05:31 +02:00
|
|
|
bucket, err := createPaymentBucket(tx, paymentHash)
|
2018-08-21 06:14:52 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2019-05-23 20:05:26 +02:00
|
|
|
// Get the existing status of this payment, if any.
|
2020-02-20 10:13:23 +01:00
|
|
|
paymentStatus, err := fetchPaymentStatus(bucket)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2019-05-23 20:05:26 +02:00
|
|
|
|
2018-08-21 06:14:52 +02:00
|
|
|
switch paymentStatus {
|
|
|
|
|
2019-05-23 20:05:28 +02:00
|
|
|
// We allow retrying failed payments.
|
|
|
|
case StatusFailed:
|
|
|
|
|
2019-05-23 20:05:28 +02:00
|
|
|
// This is a new payment that is being initialized for the
|
|
|
|
// first time.
|
2019-05-23 20:05:31 +02:00
|
|
|
case StatusUnknown:
|
2018-08-21 06:14:52 +02:00
|
|
|
|
2019-05-23 20:05:28 +02:00
|
|
|
// We already have an InFlight payment on the network. We will
|
2019-05-23 20:05:28 +02:00
|
|
|
// disallow any new payments.
|
2019-05-23 20:05:26 +02:00
|
|
|
case StatusInFlight:
|
2019-05-23 20:05:31 +02:00
|
|
|
updateErr = ErrPaymentInFlight
|
2019-05-23 20:05:28 +02:00
|
|
|
return nil
|
2018-08-21 06:14:52 +02:00
|
|
|
|
2019-05-23 20:05:31 +02:00
|
|
|
// We've already succeeded a payment to this payment hash,
|
2019-05-23 20:05:28 +02:00
|
|
|
// forbid the switch from sending another.
|
2019-05-23 20:05:31 +02:00
|
|
|
case StatusSucceeded:
|
2019-05-23 20:05:31 +02:00
|
|
|
updateErr = ErrAlreadyPaid
|
2019-05-23 20:05:28 +02:00
|
|
|
return nil
|
2018-08-21 06:14:52 +02:00
|
|
|
|
|
|
|
default:
|
2019-05-23 20:05:31 +02:00
|
|
|
updateErr = ErrUnknownPaymentStatus
|
2019-05-23 20:05:28 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-06-10 12:34:27 +02:00
|
|
|
// Before we set our new sequence number, we check whether this
|
|
|
|
// payment has a previously set sequence number and remove its
|
|
|
|
// index entry if it exists. This happens in the case where we
|
|
|
|
// have a previously attempted payment which was left in a state
|
|
|
|
// where we can retry.
|
|
|
|
seqBytes := bucket.Get(paymentSequenceKey)
|
|
|
|
if seqBytes != nil {
|
|
|
|
indexBucket := tx.ReadWriteBucket(paymentsIndexBucket)
|
|
|
|
if err := indexBucket.Delete(seqBytes); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Once we have obtained a sequence number, we add an entry
|
|
|
|
// to our index bucket which will map the sequence number to
|
2021-03-31 12:23:08 +02:00
|
|
|
// our payment identifier.
|
|
|
|
err = createPaymentIndexEntry(
|
|
|
|
tx, sequenceNum, info.PaymentIdentifier,
|
|
|
|
)
|
2020-06-10 12:34:27 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2019-05-23 20:05:28 +02:00
|
|
|
err = bucket.Put(paymentSequenceKey, sequenceNum)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add the payment info to the bucket, which contains the
|
|
|
|
// static information for this payment
|
|
|
|
err = bucket.Put(paymentCreationInfoKey, infoBytes)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-02-20 18:08:01 +01:00
|
|
|
// We'll delete any lingering HTLCs to start with, in case we
|
|
|
|
// are initializing a payment that was attempted earlier, but
|
|
|
|
// left in a state where we could retry.
|
2019-12-13 03:22:19 +01:00
|
|
|
err = bucket.DeleteNestedBucket(paymentHtlcsBucket)
|
|
|
|
if err != nil && err != kvdb.ErrBucketNotFound {
|
2019-05-23 20:05:28 +02:00
|
|
|
return err
|
2018-08-21 06:14:52 +02:00
|
|
|
}
|
|
|
|
|
2019-05-23 20:05:30 +02:00
|
|
|
// Also delete any lingering failure info now that we are
|
|
|
|
// re-attempting.
|
|
|
|
return bucket.Delete(paymentFailInfoKey)
|
2018-08-21 06:14:52 +02:00
|
|
|
})
|
2018-08-12 15:18:35 +02:00
|
|
|
if err != nil {
|
2019-09-09 11:41:43 +02:00
|
|
|
return err
|
2018-08-12 15:18:35 +02:00
|
|
|
}
|
|
|
|
|
2019-05-23 20:05:31 +02:00
|
|
|
return updateErr
|
2018-08-12 15:18:35 +02:00
|
|
|
}
|
|
|
|
|
2022-06-12 06:18:07 +02:00
|
|
|
// DeleteFailedAttempts deletes all failed htlcs for a payment if configured
|
|
|
|
// by the PaymentControl db.
|
|
|
|
func (p *PaymentControl) DeleteFailedAttempts(hash lntypes.Hash) error {
|
|
|
|
if !p.db.keepFailedPaymentAttempts {
|
|
|
|
const failedHtlcsOnly = true
|
|
|
|
err := p.db.DeletePayment(hash, failedHtlcsOnly)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-06-10 12:34:27 +02:00
|
|
|
// paymentIndexTypeHash is a payment index type which indicates that we have
|
|
|
|
// created an index of payment sequence number to payment hash.
|
|
|
|
type paymentIndexType uint8
|
|
|
|
|
|
|
|
// paymentIndexTypeHash is a payment index type which indicates that we have
|
|
|
|
// created an index of payment sequence number to payment hash.
|
|
|
|
const paymentIndexTypeHash paymentIndexType = 0
|
|
|
|
|
|
|
|
// createPaymentIndexEntry creates a payment hash typed index for a payment. The
|
|
|
|
// index produced contains a payment index type (which can be used in future to
|
2021-03-31 12:23:08 +02:00
|
|
|
// signal different payment index types) and the payment identifier.
|
2020-06-10 12:34:27 +02:00
|
|
|
func createPaymentIndexEntry(tx kvdb.RwTx, sequenceNumber []byte,
|
2021-03-31 12:23:08 +02:00
|
|
|
id lntypes.Hash) error {
|
2020-06-10 12:34:27 +02:00
|
|
|
|
|
|
|
var b bytes.Buffer
|
2021-03-31 12:23:08 +02:00
|
|
|
if err := WriteElements(&b, paymentIndexTypeHash, id[:]); err != nil {
|
2020-06-10 12:34:27 +02:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
indexes := tx.ReadWriteBucket(paymentsIndexBucket)
|
|
|
|
return indexes.Put(sequenceNumber, b.Bytes())
|
|
|
|
}
|
|
|
|
|
|
|
|
// deserializePaymentIndex deserializes a payment index entry. This function
|
|
|
|
// currently only supports deserialization of payment hash indexes, and will
|
|
|
|
// fail for other types.
|
|
|
|
func deserializePaymentIndex(r io.Reader) (lntypes.Hash, error) {
|
|
|
|
var (
|
|
|
|
indexType paymentIndexType
|
|
|
|
paymentHash []byte
|
|
|
|
)
|
|
|
|
|
|
|
|
if err := ReadElements(r, &indexType, &paymentHash); err != nil {
|
|
|
|
return lntypes.Hash{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// While we only have on payment index type, we do not need to use our
|
|
|
|
// index type to deserialize the index. However, we sanity check that
|
|
|
|
// this type is as expected, since we had to read it out anyway.
|
|
|
|
if indexType != paymentIndexTypeHash {
|
|
|
|
return lntypes.Hash{}, fmt.Errorf("unknown payment index "+
|
|
|
|
"type: %v", indexType)
|
|
|
|
}
|
|
|
|
|
|
|
|
hash, err := lntypes.MakeHash(paymentHash)
|
|
|
|
if err != nil {
|
|
|
|
return lntypes.Hash{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return hash, nil
|
|
|
|
}
|
|
|
|
|
2020-02-07 10:31:27 +01:00
|
|
|
// RegisterAttempt atomically records the provided HTLCAttemptInfo to the
|
2019-05-23 20:05:28 +02:00
|
|
|
// DB.
|
2019-05-29 08:57:04 +02:00
|
|
|
func (p *PaymentControl) RegisterAttempt(paymentHash lntypes.Hash,
|
2020-04-06 09:26:52 +02:00
|
|
|
attempt *HTLCAttemptInfo) (*MPPayment, error) {
|
2019-05-23 20:05:28 +02:00
|
|
|
|
|
|
|
// Serialize the information before opening the db transaction.
|
|
|
|
var a bytes.Buffer
|
2020-02-20 18:08:01 +01:00
|
|
|
err := serializeHTLCAttemptInfo(&a, attempt)
|
|
|
|
if err != nil {
|
2020-04-06 09:26:52 +02:00
|
|
|
return nil, err
|
2019-05-23 20:05:28 +02:00
|
|
|
}
|
2020-02-20 18:08:01 +01:00
|
|
|
htlcInfoBytes := a.Bytes()
|
2019-05-23 20:05:28 +02:00
|
|
|
|
2020-02-20 18:08:01 +01:00
|
|
|
htlcIDBytes := make([]byte, 8)
|
|
|
|
binary.BigEndian.PutUint64(htlcIDBytes, attempt.AttemptID)
|
2019-05-23 20:05:28 +02:00
|
|
|
|
2020-04-06 09:26:52 +02:00
|
|
|
var payment *MPPayment
|
|
|
|
err = kvdb.Batch(p.db.Backend, func(tx kvdb.RwTx) error {
|
2021-06-16 22:20:13 +02:00
|
|
|
prefetchPayment(tx, paymentHash)
|
2019-12-13 03:22:19 +01:00
|
|
|
bucket, err := fetchPaymentBucketUpdate(tx, paymentHash)
|
2020-02-20 18:08:01 +01:00
|
|
|
if err != nil {
|
2019-05-23 20:05:28 +02:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-04-06 09:26:52 +02:00
|
|
|
p, err := fetchPayment(bucket)
|
2020-04-01 00:13:25 +02:00
|
|
|
if err != nil {
|
2020-02-20 18:08:01 +01:00
|
|
|
return err
|
2019-05-23 20:05:28 +02:00
|
|
|
}
|
|
|
|
|
2020-04-01 00:13:27 +02:00
|
|
|
// We cannot register a new attempt if the payment already has
|
2021-04-23 08:39:45 +02:00
|
|
|
// reached a terminal condition. We check this before
|
|
|
|
// ensureInFlight because it is a more general check.
|
2020-04-06 09:26:52 +02:00
|
|
|
settle, fail := p.TerminalInfo()
|
2020-04-01 00:13:25 +02:00
|
|
|
if settle != nil || fail != nil {
|
|
|
|
return ErrPaymentTerminal
|
|
|
|
}
|
|
|
|
|
2021-04-23 08:39:45 +02:00
|
|
|
// Ensure the payment is in-flight.
|
|
|
|
if err := ensureInFlight(p); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-04-01 00:13:27 +02:00
|
|
|
// Make sure any existing shards match the new one with regards
|
|
|
|
// to MPP options.
|
|
|
|
mpp := attempt.Route.FinalHop().MPP
|
2020-04-06 09:26:52 +02:00
|
|
|
for _, h := range p.InFlightHTLCs() {
|
2020-04-01 00:13:27 +02:00
|
|
|
hMpp := h.Route.FinalHop().MPP
|
|
|
|
|
|
|
|
switch {
|
|
|
|
// We tried to register a non-MPP attempt for a MPP
|
|
|
|
// payment.
|
|
|
|
case mpp == nil && hMpp != nil:
|
|
|
|
return ErrMPPayment
|
|
|
|
|
|
|
|
// We tried to register a MPP shard for a non-MPP
|
|
|
|
// payment.
|
|
|
|
case mpp != nil && hMpp == nil:
|
|
|
|
return ErrNonMPPayment
|
|
|
|
|
|
|
|
// Non-MPP payment, nothing more to validate.
|
|
|
|
case mpp == nil:
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check that MPP options match.
|
|
|
|
if mpp.PaymentAddr() != hMpp.PaymentAddr() {
|
|
|
|
return ErrMPPPaymentAddrMismatch
|
|
|
|
}
|
|
|
|
|
|
|
|
if mpp.TotalMsat() != hMpp.TotalMsat() {
|
|
|
|
return ErrMPPTotalAmountMismatch
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If this is a non-MPP attempt, it must match the total amount
|
|
|
|
// exactly.
|
|
|
|
amt := attempt.Route.ReceiverAmt()
|
2020-04-06 09:26:52 +02:00
|
|
|
if mpp == nil && amt != p.Info.Value {
|
2020-04-01 00:13:27 +02:00
|
|
|
return ErrValueMismatch
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure we aren't sending more than the total payment amount.
|
2020-04-06 09:26:52 +02:00
|
|
|
sentAmt, _ := p.SentAmt()
|
|
|
|
if sentAmt+amt > p.Info.Value {
|
2020-04-01 00:13:27 +02:00
|
|
|
return ErrValueExceedsAmt
|
|
|
|
}
|
|
|
|
|
2020-02-20 18:08:01 +01:00
|
|
|
htlcsBucket, err := bucket.CreateBucketIfNotExists(
|
|
|
|
paymentHtlcsBucket,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-08-16 18:59:48 +02:00
|
|
|
err = htlcsBucket.Put(
|
|
|
|
htlcBucketKey(htlcAttemptInfoKey, htlcIDBytes),
|
|
|
|
htlcInfoBytes,
|
|
|
|
)
|
2020-04-06 09:26:52 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Retrieve attempt info for the notification.
|
|
|
|
payment, err = fetchPayment(bucket)
|
|
|
|
return err
|
2019-05-23 20:05:28 +02:00
|
|
|
})
|
2020-04-06 09:26:52 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return payment, err
|
2020-02-20 18:08:01 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// SettleAttempt marks the given attempt settled with the preimage. If this is
|
|
|
|
// a multi shard payment, this might implicitly mean that the full payment
|
|
|
|
// succeeded.
|
|
|
|
//
|
|
|
|
// After invoking this method, InitPayment should always return an error to
|
|
|
|
// prevent us from making duplicate payments to the same payment hash. The
|
|
|
|
// provided preimage is atomically saved to the DB for record keeping.
|
|
|
|
func (p *PaymentControl) SettleAttempt(hash lntypes.Hash,
|
|
|
|
attemptID uint64, settleInfo *HTLCSettleInfo) (*MPPayment, error) {
|
|
|
|
|
|
|
|
var b bytes.Buffer
|
|
|
|
if err := serializeHTLCSettleInfo(&b, settleInfo); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
settleBytes := b.Bytes()
|
|
|
|
|
|
|
|
return p.updateHtlcKey(hash, attemptID, htlcSettleInfoKey, settleBytes)
|
|
|
|
}
|
|
|
|
|
|
|
|
// FailAttempt marks the given payment attempt failed.
|
|
|
|
func (p *PaymentControl) FailAttempt(hash lntypes.Hash,
|
2020-04-06 09:26:52 +02:00
|
|
|
attemptID uint64, failInfo *HTLCFailInfo) (*MPPayment, error) {
|
2020-02-20 18:08:01 +01:00
|
|
|
|
|
|
|
var b bytes.Buffer
|
|
|
|
if err := serializeHTLCFailInfo(&b, failInfo); err != nil {
|
2020-04-06 09:26:52 +02:00
|
|
|
return nil, err
|
2019-05-23 20:05:28 +02:00
|
|
|
}
|
2020-02-20 18:08:01 +01:00
|
|
|
failBytes := b.Bytes()
|
2019-05-23 20:05:28 +02:00
|
|
|
|
2020-04-06 09:26:52 +02:00
|
|
|
return p.updateHtlcKey(hash, attemptID, htlcFailInfoKey, failBytes)
|
2019-05-23 20:05:28 +02:00
|
|
|
}
|
|
|
|
|
2020-02-20 18:08:01 +01:00
|
|
|
// updateHtlcKey updates a database key for the specified htlc.
|
|
|
|
func (p *PaymentControl) updateHtlcKey(paymentHash lntypes.Hash,
|
|
|
|
attemptID uint64, key, value []byte) (*MPPayment, error) {
|
2019-05-23 20:05:28 +02:00
|
|
|
|
2021-08-16 18:59:48 +02:00
|
|
|
aid := make([]byte, 8)
|
|
|
|
binary.BigEndian.PutUint64(aid, attemptID)
|
2018-08-21 06:14:52 +02:00
|
|
|
|
2020-02-20 18:08:01 +01:00
|
|
|
var payment *MPPayment
|
2019-12-13 03:22:19 +01:00
|
|
|
err := kvdb.Batch(p.db.Backend, func(tx kvdb.RwTx) error {
|
|
|
|
payment = nil
|
|
|
|
|
2021-06-16 22:20:13 +02:00
|
|
|
prefetchPayment(tx, paymentHash)
|
2019-12-13 03:22:19 +01:00
|
|
|
bucket, err := fetchPaymentBucketUpdate(tx, paymentHash)
|
2020-02-20 18:08:01 +01:00
|
|
|
if err != nil {
|
2019-05-23 20:05:28 +02:00
|
|
|
return err
|
|
|
|
}
|
2018-08-21 06:14:52 +02:00
|
|
|
|
2020-04-01 00:13:25 +02:00
|
|
|
p, err := fetchPayment(bucket)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// We can only update keys of in-flight payments. We allow
|
|
|
|
// updating keys even if the payment has reached a terminal
|
|
|
|
// condition, since the HTLC outcomes must still be updated.
|
|
|
|
if err := ensureInFlight(p); err != nil {
|
2020-02-20 18:08:01 +01:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2019-12-13 03:22:19 +01:00
|
|
|
htlcsBucket := bucket.NestedReadWriteBucket(paymentHtlcsBucket)
|
2020-02-20 18:08:01 +01:00
|
|
|
if htlcsBucket == nil {
|
|
|
|
return fmt.Errorf("htlcs bucket not found")
|
2018-08-21 06:14:52 +02:00
|
|
|
}
|
|
|
|
|
2021-08-16 18:59:48 +02:00
|
|
|
if htlcsBucket.Get(htlcBucketKey(htlcAttemptInfoKey, aid)) == nil {
|
2020-02-20 18:08:01 +01:00
|
|
|
return fmt.Errorf("HTLC with ID %v not registered",
|
|
|
|
attemptID)
|
|
|
|
}
|
|
|
|
|
2020-04-01 00:13:25 +02:00
|
|
|
// Make sure the shard is not already failed or settled.
|
2021-08-16 18:59:48 +02:00
|
|
|
if htlcsBucket.Get(htlcBucketKey(htlcFailInfoKey, aid)) != nil {
|
2020-04-01 00:13:25 +02:00
|
|
|
return ErrAttemptAlreadyFailed
|
|
|
|
}
|
|
|
|
|
2021-08-16 18:59:48 +02:00
|
|
|
if htlcsBucket.Get(htlcBucketKey(htlcSettleInfoKey, aid)) != nil {
|
2020-04-01 00:13:25 +02:00
|
|
|
return ErrAttemptAlreadySettled
|
|
|
|
}
|
|
|
|
|
2020-02-20 18:08:01 +01:00
|
|
|
// Add or update the key for this htlc.
|
2021-08-16 18:59:48 +02:00
|
|
|
err = htlcsBucket.Put(htlcBucketKey(key, aid), value)
|
2019-04-30 13:24:37 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Retrieve attempt info for the notification.
|
2019-11-08 12:39:51 +01:00
|
|
|
payment, err = fetchPayment(bucket)
|
|
|
|
return err
|
2018-08-21 06:14:52 +02:00
|
|
|
})
|
2018-08-12 15:18:35 +02:00
|
|
|
if err != nil {
|
2019-04-30 13:24:37 +02:00
|
|
|
return nil, err
|
2018-08-12 15:18:35 +02:00
|
|
|
}
|
|
|
|
|
2020-02-20 18:08:01 +01:00
|
|
|
return payment, err
|
2018-08-12 15:18:35 +02:00
|
|
|
}
|
|
|
|
|
2019-05-23 20:05:30 +02:00
|
|
|
// Fail transitions a payment into the Failed state, and records the reason the
|
|
|
|
// payment failed. After invoking this method, InitPayment should return nil on
|
|
|
|
// its next call for this payment hash, allowing the switch to make a
|
|
|
|
// subsequent payment.
|
2019-05-29 08:57:04 +02:00
|
|
|
func (p *PaymentControl) Fail(paymentHash lntypes.Hash,
|
2019-11-08 12:39:51 +01:00
|
|
|
reason FailureReason) (*MPPayment, error) {
|
2019-05-23 20:05:30 +02:00
|
|
|
|
2019-06-04 17:18:41 +02:00
|
|
|
var (
|
|
|
|
updateErr error
|
2019-11-08 12:39:51 +01:00
|
|
|
payment *MPPayment
|
2019-06-04 17:18:41 +02:00
|
|
|
)
|
2019-12-13 03:22:19 +01:00
|
|
|
err := kvdb.Batch(p.db.Backend, func(tx kvdb.RwTx) error {
|
2018-08-21 06:14:52 +02:00
|
|
|
// Reset the update error, to avoid carrying over an error
|
|
|
|
// from a previous execution of the batched db transaction.
|
|
|
|
updateErr = nil
|
2019-11-08 12:39:51 +01:00
|
|
|
payment = nil
|
2018-08-21 06:14:52 +02:00
|
|
|
|
2021-06-16 22:20:13 +02:00
|
|
|
prefetchPayment(tx, paymentHash)
|
2019-12-13 03:22:19 +01:00
|
|
|
bucket, err := fetchPaymentBucketUpdate(tx, paymentHash)
|
2019-05-23 20:05:31 +02:00
|
|
|
if err == ErrPaymentNotInitiated {
|
|
|
|
updateErr = ErrPaymentNotInitiated
|
|
|
|
return nil
|
|
|
|
} else if err != nil {
|
2019-05-23 20:05:28 +02:00
|
|
|
return err
|
|
|
|
}
|
2018-08-21 06:14:52 +02:00
|
|
|
|
2021-07-07 03:41:38 +02:00
|
|
|
// We mark the payment as failed as long as it is known. This
|
2020-04-01 00:13:25 +02:00
|
|
|
// lets the last attempt to fail with a terminal write its
|
|
|
|
// failure to the PaymentControl without synchronizing with
|
|
|
|
// other attempts.
|
|
|
|
paymentStatus, err := fetchPaymentStatus(bucket)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if paymentStatus == StatusUnknown {
|
|
|
|
updateErr = ErrPaymentNotInitiated
|
2019-05-23 20:05:28 +02:00
|
|
|
return nil
|
2018-08-21 06:14:52 +02:00
|
|
|
}
|
|
|
|
|
2019-05-23 20:05:30 +02:00
|
|
|
// Put the failure reason in the bucket for record keeping.
|
|
|
|
v := []byte{byte(reason)}
|
2019-06-04 17:18:41 +02:00
|
|
|
err = bucket.Put(paymentFailInfoKey, v)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Retrieve attempt info for the notification, if available.
|
2019-11-08 12:39:51 +01:00
|
|
|
payment, err = fetchPayment(bucket)
|
2020-02-20 18:08:01 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
2018-08-21 06:14:52 +02:00
|
|
|
})
|
2018-08-12 15:18:35 +02:00
|
|
|
if err != nil {
|
2019-06-04 17:18:41 +02:00
|
|
|
return nil, err
|
2018-08-12 15:18:35 +02:00
|
|
|
}
|
|
|
|
|
2019-11-08 12:39:51 +01:00
|
|
|
return payment, updateErr
|
2018-08-12 15:18:35 +02:00
|
|
|
}
|
2019-05-23 20:05:26 +02:00
|
|
|
|
2019-04-30 13:24:37 +02:00
|
|
|
// FetchPayment returns information about a payment from the database.
|
|
|
|
func (p *PaymentControl) FetchPayment(paymentHash lntypes.Hash) (
|
2019-11-08 12:39:51 +01:00
|
|
|
*MPPayment, error) {
|
2019-04-30 13:24:37 +02:00
|
|
|
|
2019-11-08 12:39:51 +01:00
|
|
|
var payment *MPPayment
|
2020-05-07 00:45:50 +02:00
|
|
|
err := kvdb.View(p.db, func(tx kvdb.RTx) error {
|
2021-06-16 22:20:13 +02:00
|
|
|
prefetchPayment(tx, paymentHash)
|
2019-04-30 13:24:37 +02:00
|
|
|
bucket, err := fetchPaymentBucket(tx, paymentHash)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
payment, err = fetchPayment(bucket)
|
|
|
|
|
|
|
|
return err
|
2020-10-20 16:18:40 +02:00
|
|
|
}, func() {
|
|
|
|
payment = nil
|
2019-04-30 13:24:37 +02:00
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return payment, nil
|
|
|
|
}
|
|
|
|
|
2021-06-16 22:20:13 +02:00
|
|
|
// prefetchPayment attempts to prefetch as much of the payment as possible to
|
|
|
|
// reduce DB roundtrips.
|
|
|
|
func prefetchPayment(tx kvdb.RTx, paymentHash lntypes.Hash) {
|
|
|
|
rb := kvdb.RootBucket(tx)
|
|
|
|
kvdb.Prefetch(
|
|
|
|
rb,
|
|
|
|
[]string{
|
|
|
|
// Prefetch all keys in the payment's bucket.
|
|
|
|
string(paymentsRootBucket),
|
|
|
|
string(paymentHash[:]),
|
|
|
|
},
|
|
|
|
[]string{
|
|
|
|
// Prefetch all keys in the payment's htlc bucket.
|
|
|
|
string(paymentsRootBucket),
|
|
|
|
string(paymentHash[:]),
|
|
|
|
string(paymentHtlcsBucket),
|
|
|
|
},
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2019-05-23 20:05:31 +02:00
|
|
|
// createPaymentBucket creates or fetches the sub-bucket assigned to this
|
2019-05-23 20:05:26 +02:00
|
|
|
// payment hash.
|
2019-12-13 03:22:19 +01:00
|
|
|
func createPaymentBucket(tx kvdb.RwTx, paymentHash lntypes.Hash) (
|
|
|
|
kvdb.RwBucket, error) {
|
2019-05-23 20:05:26 +02:00
|
|
|
|
2019-12-13 03:22:19 +01:00
|
|
|
payments, err := tx.CreateTopLevelBucket(paymentsRootBucket)
|
2019-05-23 20:05:26 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return payments.CreateBucketIfNotExists(paymentHash[:])
|
|
|
|
}
|
|
|
|
|
2019-05-23 20:05:31 +02:00
|
|
|
// fetchPaymentBucket fetches the sub-bucket assigned to this payment hash. If
|
|
|
|
// the bucket does not exist, it returns ErrPaymentNotInitiated.
|
2020-05-07 00:45:50 +02:00
|
|
|
func fetchPaymentBucket(tx kvdb.RTx, paymentHash lntypes.Hash) (
|
2020-05-07 00:48:00 +02:00
|
|
|
kvdb.RBucket, error) {
|
2019-05-23 20:05:31 +02:00
|
|
|
|
2019-12-13 03:22:19 +01:00
|
|
|
payments := tx.ReadBucket(paymentsRootBucket)
|
2019-05-23 20:05:31 +02:00
|
|
|
if payments == nil {
|
|
|
|
return nil, ErrPaymentNotInitiated
|
|
|
|
}
|
|
|
|
|
2019-12-13 03:22:19 +01:00
|
|
|
bucket := payments.NestedReadBucket(paymentHash[:])
|
|
|
|
if bucket == nil {
|
|
|
|
return nil, ErrPaymentNotInitiated
|
|
|
|
}
|
|
|
|
|
|
|
|
return bucket, nil
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// fetchPaymentBucketUpdate is identical to fetchPaymentBucket, but it returns a
|
|
|
|
// bucket that can be written to.
|
|
|
|
func fetchPaymentBucketUpdate(tx kvdb.RwTx, paymentHash lntypes.Hash) (
|
|
|
|
kvdb.RwBucket, error) {
|
|
|
|
|
|
|
|
payments := tx.ReadWriteBucket(paymentsRootBucket)
|
|
|
|
if payments == nil {
|
|
|
|
return nil, ErrPaymentNotInitiated
|
|
|
|
}
|
|
|
|
|
|
|
|
bucket := payments.NestedReadWriteBucket(paymentHash[:])
|
2019-05-23 20:05:31 +02:00
|
|
|
if bucket == nil {
|
|
|
|
return nil, ErrPaymentNotInitiated
|
|
|
|
}
|
|
|
|
|
|
|
|
return bucket, nil
|
|
|
|
}
|
|
|
|
|
2019-05-23 20:05:28 +02:00
|
|
|
// nextPaymentSequence returns the next sequence number to store for a new
|
|
|
|
// payment.
|
2021-06-25 18:21:59 +02:00
|
|
|
func (p *PaymentControl) nextPaymentSequence() ([]byte, error) {
|
|
|
|
p.paymentSeqMx.Lock()
|
|
|
|
defer p.paymentSeqMx.Unlock()
|
|
|
|
|
|
|
|
// Set a new upper bound in the DB every 1000 payments to avoid
|
|
|
|
// conflicts on the sequence when using etcd.
|
|
|
|
if p.currPaymentSeq == p.storedPaymentSeq {
|
|
|
|
var currPaymentSeq, newUpperBound uint64
|
|
|
|
if err := kvdb.Update(p.db.Backend, func(tx kvdb.RwTx) error {
|
|
|
|
paymentsBucket, err := tx.CreateTopLevelBucket(
|
|
|
|
paymentsRootBucket,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2019-05-23 20:05:28 +02:00
|
|
|
|
2021-06-25 18:21:59 +02:00
|
|
|
currPaymentSeq = paymentsBucket.Sequence()
|
|
|
|
newUpperBound = currPaymentSeq + paymentSeqBlockSize
|
|
|
|
return paymentsBucket.SetSequence(newUpperBound)
|
|
|
|
}, func() {}); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// We lazy initialize the cached currPaymentSeq here using the
|
|
|
|
// first nextPaymentSequence() call. This if statement will auto
|
|
|
|
// initialize our stored currPaymentSeq, since by default both
|
|
|
|
// this variable and storedPaymentSeq are zero which in turn
|
|
|
|
// will have us fetch the current values from the DB.
|
|
|
|
if p.currPaymentSeq == 0 {
|
|
|
|
p.currPaymentSeq = currPaymentSeq
|
|
|
|
}
|
|
|
|
|
|
|
|
p.storedPaymentSeq = newUpperBound
|
2019-05-23 20:05:28 +02:00
|
|
|
}
|
|
|
|
|
2021-06-25 18:21:59 +02:00
|
|
|
p.currPaymentSeq++
|
2019-05-23 20:05:28 +02:00
|
|
|
b := make([]byte, 8)
|
2021-06-25 18:21:59 +02:00
|
|
|
binary.BigEndian.PutUint64(b, p.currPaymentSeq)
|
|
|
|
|
2019-05-23 20:05:28 +02:00
|
|
|
return b, nil
|
|
|
|
}
|
|
|
|
|
2019-05-23 20:05:31 +02:00
|
|
|
// fetchPaymentStatus fetches the payment status of the payment. If the payment
|
|
|
|
// isn't found, it will default to "StatusUnknown".
|
2020-05-07 00:48:00 +02:00
|
|
|
func fetchPaymentStatus(bucket kvdb.RBucket) (PaymentStatus, error) {
|
2020-04-01 00:13:25 +02:00
|
|
|
// Creation info should be set for all payments, regardless of state.
|
|
|
|
// If not, it is unknown.
|
|
|
|
if bucket.Get(paymentCreationInfoKey) == nil {
|
|
|
|
return StatusUnknown, nil
|
2019-05-23 20:05:31 +02:00
|
|
|
}
|
2019-05-23 20:05:26 +02:00
|
|
|
|
2020-04-01 00:13:25 +02:00
|
|
|
payment, err := fetchPayment(bucket)
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
2019-05-23 20:05:26 +02:00
|
|
|
}
|
|
|
|
|
2020-04-01 00:13:25 +02:00
|
|
|
return payment.Status, nil
|
2019-05-23 20:05:26 +02:00
|
|
|
}
|
2019-05-23 20:05:28 +02:00
|
|
|
|
|
|
|
// ensureInFlight checks whether the payment found in the given bucket has
|
|
|
|
// status InFlight, and returns an error otherwise. This should be used to
|
|
|
|
// ensure we only mark in-flight payments as succeeded or failed.
|
2020-04-01 00:13:25 +02:00
|
|
|
func ensureInFlight(payment *MPPayment) error {
|
|
|
|
paymentStatus := payment.Status
|
2019-05-23 20:05:28 +02:00
|
|
|
|
|
|
|
switch {
|
|
|
|
|
2020-04-01 00:13:25 +02:00
|
|
|
// The payment was indeed InFlight.
|
2019-05-23 20:05:28 +02:00
|
|
|
case paymentStatus == StatusInFlight:
|
|
|
|
return nil
|
|
|
|
|
|
|
|
// Our records show the payment as unknown, meaning it never
|
|
|
|
// should have left the switch.
|
2019-05-23 20:05:31 +02:00
|
|
|
case paymentStatus == StatusUnknown:
|
2019-05-23 20:05:28 +02:00
|
|
|
return ErrPaymentNotInitiated
|
|
|
|
|
|
|
|
// The payment succeeded previously.
|
2019-05-23 20:05:31 +02:00
|
|
|
case paymentStatus == StatusSucceeded:
|
|
|
|
return ErrPaymentAlreadySucceeded
|
2019-05-23 20:05:28 +02:00
|
|
|
|
|
|
|
// The payment was already failed.
|
|
|
|
case paymentStatus == StatusFailed:
|
|
|
|
return ErrPaymentAlreadyFailed
|
|
|
|
|
|
|
|
default:
|
|
|
|
return ErrUnknownPaymentStatus
|
|
|
|
}
|
|
|
|
}
|
2019-05-23 20:05:29 +02:00
|
|
|
|
|
|
|
// FetchInFlightPayments returns all payments with status InFlight.
|
2021-03-30 12:10:30 +02:00
|
|
|
func (p *PaymentControl) FetchInFlightPayments() ([]*MPPayment, error) {
|
|
|
|
var inFlights []*MPPayment
|
2020-05-07 00:45:50 +02:00
|
|
|
err := kvdb.View(p.db, func(tx kvdb.RTx) error {
|
2019-12-13 03:22:19 +01:00
|
|
|
payments := tx.ReadBucket(paymentsRootBucket)
|
2019-05-23 20:05:29 +02:00
|
|
|
if payments == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return payments.ForEach(func(k, _ []byte) error {
|
2019-12-13 03:22:19 +01:00
|
|
|
bucket := payments.NestedReadBucket(k)
|
2019-05-23 20:05:29 +02:00
|
|
|
if bucket == nil {
|
|
|
|
return fmt.Errorf("non bucket element")
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the status is not InFlight, we can return early.
|
2020-02-20 10:13:23 +01:00
|
|
|
paymentStatus, err := fetchPaymentStatus(bucket)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2019-05-23 20:05:29 +02:00
|
|
|
if paymentStatus != StatusInFlight {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-03-30 12:10:30 +02:00
|
|
|
p, err := fetchPayment(bucket)
|
2019-05-23 20:05:29 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-03-30 12:10:30 +02:00
|
|
|
inFlights = append(inFlights, p)
|
2019-05-23 20:05:29 +02:00
|
|
|
return nil
|
|
|
|
})
|
2020-10-20 16:18:40 +02:00
|
|
|
}, func() {
|
|
|
|
inFlights = nil
|
2019-05-23 20:05:29 +02:00
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return inFlights, nil
|
|
|
|
}
|