980f5e0879
* Will later include proper tx fee calculation based on size after selection
252 lines
5.8 KiB
Go
252 lines
5.8 KiB
Go
package wallet
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"errors"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
"github.com/btcsuite/btcd/btcec"
|
|
"github.com/btcsuite/btcd/btcjson"
|
|
"github.com/btcsuite/btcd/wire"
|
|
"github.com/btcsuite/btcutil"
|
|
"github.com/btcsuite/btcutil/coinset"
|
|
"github.com/btcsuite/btcwallet/waddrmgr"
|
|
btcwallet "github.com/btcsuite/btcwallet/wallet"
|
|
"github.com/btcsuite/btcwallet/walletdb"
|
|
)
|
|
|
|
var (
|
|
// Namespace bucket keys.
|
|
lightningNamespaceKey = []byte("lightning")
|
|
|
|
// Error types
|
|
ErrInsufficientFunds = errors.New("not enough available outputs to create funding transaction")
|
|
)
|
|
|
|
// FundingRequest...
|
|
type FundingReserveRequest struct {
|
|
fundingAmount btcutil.Amount
|
|
|
|
// Insuffcient funds etc..
|
|
err chan error // Buffered
|
|
|
|
resp chan *FundingReserveResponse // Buffered
|
|
}
|
|
|
|
// FundingResponse...
|
|
type FundingReserveResponse struct {
|
|
fundingInputs []*wire.TxIn
|
|
changeOutputs []*wire.TxOut
|
|
}
|
|
|
|
// FundingReserveCancelMsg...
|
|
type FundingReserveCancelMsg struct {
|
|
reserveToCancel *FundingReserveResponse
|
|
}
|
|
|
|
// FundingReserveRequest...
|
|
type FundingCompleteRequest struct {
|
|
fundingReservation *FundingReserveResponse
|
|
|
|
theirInputs []*wire.TxIn // Includes sig-script to redeem.
|
|
theirChangeOutputs []*wire.TxOut
|
|
theirKey *btcec.PublicKey
|
|
|
|
err chan error // Buffered
|
|
|
|
resp chan FundingCompleteResponse // Buffered
|
|
}
|
|
|
|
// FundingCompleteResponse....
|
|
type FundingCompleteResponse struct {
|
|
fundingTx *btcutil.Tx
|
|
}
|
|
|
|
// LightningWallet....
|
|
// responsible for internal global (from the point of view of a user/node)
|
|
// channel state. Requests to modify this state come in via messages over
|
|
// channels, same with replies.
|
|
// Embedded wallet backed by boltdb...
|
|
type LightningWallet struct {
|
|
sync.RWMutex
|
|
|
|
db *walletdb.DB
|
|
|
|
// A namespace within boltdb reserved for ln-based wallet meta-data.
|
|
// TODO(roasbeef): which possible other namespaces are relevant?
|
|
lnNamespace *walletdb.Namespace
|
|
|
|
btcwallet.Wallet
|
|
|
|
msgChan chan interface{}
|
|
|
|
//lockedInputs []*LockedPrevOut
|
|
//lockedOutputs []*LockedOutPoint
|
|
|
|
started int32
|
|
shutdown int32
|
|
|
|
quit chan struct{}
|
|
|
|
wg sync.WaitGroup
|
|
}
|
|
|
|
// Start...
|
|
func (l *LightningWallet) Start() error {
|
|
// Already started?
|
|
if atomic.AddInt32(&l.started, 1) != 1 {
|
|
return nil
|
|
}
|
|
|
|
l.wg.Add(1)
|
|
go l.requestHandler()
|
|
|
|
return nil
|
|
}
|
|
|
|
// Stop...
|
|
func (l *LightningWallet) Stop() error {
|
|
if atomic.AddInt32(&l.shutdown, 1) != 1 {
|
|
return nil
|
|
}
|
|
|
|
close(l.quit)
|
|
l.wg.Wait()
|
|
return nil
|
|
}
|
|
|
|
// requestHandler....
|
|
func (l *LightningWallet) requestHandler() {
|
|
out:
|
|
for {
|
|
select {
|
|
case m := <-l.msgChan:
|
|
switch msg := m.(type) {
|
|
case *FundingReserveRequest:
|
|
l.handleFundingReserveRequest(msg)
|
|
case *FundingReserveCancelMsg:
|
|
l.handleFundingCancelRequest(msg)
|
|
}
|
|
case <-l.quit:
|
|
// TODO: do some clean up
|
|
break out
|
|
}
|
|
}
|
|
|
|
l.wg.Done()
|
|
}
|
|
|
|
// handleFundingReserveRequest...
|
|
func (l *LightningWallet) handleFundingReserveRequest(req *FundingReserveRequest) {
|
|
fundingAmt := req.fundingAmount
|
|
|
|
// Find all unlocked unspent outputs with greater than 6 confirmations.
|
|
maxConfs := ^int32(0)
|
|
unspentOutputs, err := l.ListUnspent(6, maxConfs, nil)
|
|
if err != nil {
|
|
req.err <- err
|
|
req.resp <- nil
|
|
return
|
|
}
|
|
|
|
// Convert the outputs to coins for coin selection below.
|
|
coins, err := outputsToCoins(unspentOutputs)
|
|
if err != nil {
|
|
req.err <- err
|
|
req.resp <- nil
|
|
return
|
|
}
|
|
|
|
// Peform coin selection over our available, unlocked unspent outputs
|
|
// in order to find enough coins to meet the funding amount requirements.
|
|
//
|
|
// TODO(roasbeef): Should extend coinset with optimal coin selection
|
|
// heuristics for our use case.
|
|
// TODO(roasbeef): factor in fees..
|
|
// NOTE: this current selection assumes "priority" is still a thing.
|
|
selector := &coinset.MaxValueAgeCoinSelector{
|
|
MaxInputs: 10,
|
|
MinChangeAmount: 10000,
|
|
}
|
|
selectedCoins, err := selector.CoinSelect(fundingAmt, coins)
|
|
|
|
// Lock the selected coins. These coins are now "reserved", this
|
|
// prevents concurrent funding requests from referring to and this
|
|
// double-spending the same set of coins.
|
|
fundingInputs := make([]*wire.TxIn, len(selectedCoins.Coins()))
|
|
for i, coin := range selectedCoins.Coins() {
|
|
txout := wire.NewOutPoint(coin.Hash(), coin.Index())
|
|
l.LockOutpoint(*txout)
|
|
|
|
// Empty sig script, we'll actually sign if this reservation is
|
|
// queued up to be completed (the other side accepts).
|
|
outPoint := wire.NewOutPoint(coin.Hash(), coin.Index())
|
|
fundingInputs[i] = wire.NewTxIn(outPoint, nil)
|
|
}
|
|
|
|
// Create some possibly neccessary change outputs.
|
|
selectedTotalValue := coinset.NewCoinSet(coins).TotalValue()
|
|
changeOutputs := make([]*wire.TxOut, 0, len(selectedCoins.Coins()))
|
|
if selectedTotalValue > fundingAmt {
|
|
// Change is necessary. Query for an available change address to
|
|
// send the remainder to.
|
|
changeAmount := selectedTotalValue - fundingAmt
|
|
changeAddr, err := l.NewChangeAddress(waddrmgr.DefaultAccountNum)
|
|
if err != nil {
|
|
req.err <- err
|
|
req.resp <- nil
|
|
}
|
|
|
|
changeOutputs = append(changeOutputs,
|
|
wire.NewTxOut(int64(changeAmount), changeAddr.ScriptAddress()))
|
|
}
|
|
|
|
// Funding reservation request succesfully handled. The funding inputs
|
|
// will be marked as unavailable until the reservation is either
|
|
// completed, or cancecled.
|
|
req.err <- nil
|
|
req.resp <- &FundingReserveResponse{
|
|
fundingInputs: fundingInputs,
|
|
changeOutputs: changeOutputs,
|
|
}
|
|
}
|
|
|
|
// lnCoin...
|
|
// to adhere to the coinset.Coin interface
|
|
type lnCoin struct {
|
|
hash *wire.ShaHash
|
|
index uint32
|
|
value btcutil.Amount
|
|
pkScript []byte
|
|
numConfs int64
|
|
valueAge int64
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
}
|
|
|
|
|
|
if err != nil {
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// handleFundingReserveCancel...
|
|
func (l *LightningWallet) handleFundingCancelRequest(req *FundingReserveCancelMsg) {
|
|
prevReservation := req.reserveToCancel
|
|
|
|
for _, unusedInput := range prevReservation.fundingInputs {
|
|
l.UnlockOutpoint(unusedInput.PreviousOutPoint)
|
|
}
|
|
|
|
// TODO(roasbeef): Is it possible to mark the unused change also as
|
|
// available?
|
|
}
|