Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions bitcoind/bitcoind.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"log"
"os"
"strconv"
"time"

gbitcoind "github.com/toorop/go-bitcoind"
)
Expand Down Expand Up @@ -68,3 +69,27 @@ func GetTransaction(txid string) (*gbitcoind.RawTransaction, error) {

return &rawtx, nil
}

func GetDateForHeight(height int32) (*time.Time, error) {
bitcoindPort, err := strconv.Atoi(os.Getenv("BITCOIND_PORT"))
if err != nil {
return nil, fmt.Errorf("no valid port for bitcoind: %v", os.Getenv("BITCOIND_PORT"))
}
bc, err := gbitcoind.New(os.Getenv("BITCOIND_HOST"), bitcoindPort, os.Getenv("BITCOIND_USER"), os.Getenv("BITCOIND_PASSWORD"), false)
if err != nil {
return nil, fmt.Errorf("cannot create a bitcoind client: %w", err)
}

hash, err := bc.GetBlockHash(uint64(height))
if err != nil {
return nil, fmt.Errorf("error getting block hash: %w", err)
}

header, err := bc.GetBlockheader(hash)
if err != nil {
return nil, fmt.Errorf("error getting block header: %w", err)
}

t := time.Unix(header.Time, 0)
return &t, nil
}
34 changes: 34 additions & 0 deletions db.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,40 @@ func insertSubswapPayment(paymentHash, paymentRequest string, lockheight, confir
return nil
}

func getSwapsWithoutPreimage() ([]*swapper.SwapWithoutPreimage, error) {
rows, err := pgxPool.Query(context.Background(),
`SELECT payment_hash
FROM swap_payments
WHERE redeem_confirmed = false
AND payment_preimage is null
ORDER BY confirmation_height
`)
if err != nil {
return nil, fmt.Errorf("failed to query swap_payments: %w", err)
}
defer rows.Close()

var result []*swapper.SwapWithoutPreimage
for rows.Next() {
var payment_hash string
var confirmation_height int32
err = rows.Scan(
&payment_hash,
&confirmation_height,
)
if err != nil {
return nil, fmt.Errorf("rows.Scan() error: %w", err)
}

result = append(result, &swapper.SwapWithoutPreimage{
PaymentHash: payment_hash,
ConfirmationHeight: confirmation_height,
})
}

return result, nil
}

func getInProgressRedeems(blockheight int32) ([]*swapper.InProgressRedeem, error) {
ignoreBefore := blockheight - (288 * 14)
rows, err := pgxPool.Query(context.Background(),
Expand Down
13 changes: 8 additions & 5 deletions server.go
Original file line number Diff line number Diff line change
Expand Up @@ -435,9 +435,11 @@ func main() {
go registerPastBoltzReverseSwapTxNotifications()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
redeemer := swapper.NewRedeemer(ssClient, ssRouterClient, subswapClient,
updateSubswapTxid, updateSubswapPreimage, getInProgressRedeems,
setSubswapConfirmed)
swapFeeService := swapper.NewFeeService()
swapFeeService.Start(ctx)
redeemer := swapper.NewRedeemer(network, ssClient, ssRouterClient, subswapClient,
swapFeeService, updateSubswapTxid, updateSubswapPreimage,
getInProgressRedeems, getSwapsWithoutPreimage, setSubswapConfirmed)
redeemer.Start(ctx)

lsp.InitLSP()
Expand Down Expand Up @@ -516,8 +518,9 @@ func main() {
supportServer := support.NewServer(sendPaymentFailureNotification, breezStatus, lspList)
breez.RegisterSupportServer(s, supportServer)

swapperServer = swapper.NewServer(network, redisPool, client, ssClient, subswapClient, redeemer, ssWalletKitClient, ssRouterClient,
insertSubswapPayment, updateSubswapPreimage, hasFilteredAddress)
swapperServer = swapper.NewServer(network, redisPool, client, ssClient, subswapClient, redeemer,
swapFeeService, ssWalletKitClient, ssRouterClient, insertSubswapPayment,
updateSubswapPreimage, hasFilteredAddress)
breez.RegisterSwapperServer(s, swapperServer)

lspServer := &lsp.Server{
Expand Down
141 changes: 141 additions & 0 deletions swapper/fee.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package swapper

import (
"context"
"encoding/json"
"fmt"
"log"
"math"
"net/http"
"strconv"
"sync"
"time"
)

type FeeService struct {
feesLastUpdated time.Time
currentFees *whatthefeeBody
mtx sync.RWMutex
}

type whatthefeeBody struct {
Index []int32 `json:"index"`
Columns []string `json:"columns"`
Data [][]int32 `json:"data"`
}

func NewFeeService() *FeeService {
return &FeeService{}
}

func (f *FeeService) Start(ctx context.Context) {
go f.watchFeeRate(ctx)
}

func (f *FeeService) GetFeeRate(blocks int32, locktime int32) (float64, error) {
f.mtx.RLock()
defer f.mtx.RUnlock()

if f.currentFees == nil {
return 0, fmt.Errorf("still no fees")
}

if f.feesLastUpdated.Before(time.Now().Add(-time.Hour)) {
return 0, fmt.Errorf("fees are stale")
}

if len(f.currentFees.Index) < 1 {
return 0, fmt.Errorf("empty row index")
}

// get the block between 0 and SwapLockTime
b := math.Min(math.Max(0, float64(blocks)), float64(locktime))

// certainty is linear between 0.5 and 1 based on the amount of blocks left
certainty := 0.5 + (((float64(locktime) - b) / float64(locktime)) / 2)

// Get the row closest to the amount of blocks left
rowIndex := 0
prevRow := f.currentFees.Index[rowIndex]
for i := 1; i < len(f.currentFees.Index); i++ {
current := f.currentFees.Index[i]
if math.Abs(float64(current)-b) < math.Abs(float64(prevRow)-b) {
rowIndex = i
prevRow = current
}
}

if len(f.currentFees.Columns) < 1 {
return 0, fmt.Errorf("empty column index")
}

// Get the column closest to the certainty
columnIndex := 0
prevColumn, err := strconv.ParseFloat(f.currentFees.Columns[columnIndex], 64)
if err != nil {
return 0, fmt.Errorf("invalid column content '%s'", f.currentFees.Columns[columnIndex])
}
for i := 1; i < len(f.currentFees.Columns); i++ {
current, err := strconv.ParseFloat(f.currentFees.Columns[i], 64)
if err != nil {
return 0, fmt.Errorf("invalid column content '%s'", f.currentFees.Columns[i])
}
if math.Abs(current-certainty) < math.Abs(prevColumn-certainty) {
columnIndex = i
prevColumn = current
}
}

if rowIndex >= len(f.currentFees.Data) {
return 0, fmt.Errorf("could not find fee rate column in whatthefee.io response")
}
row := f.currentFees.Data[rowIndex]
if columnIndex >= len(row) {
return 0, fmt.Errorf("could not find fee rate column in whatthefee.io response")
}

rate := row[columnIndex]
satPerVByte := math.Exp(float64(rate) / 100)
return satPerVByte, nil
}

func (f *FeeService) watchFeeRate(ctx context.Context) {
for {
now := time.Now()
fees, err := f.getFees()
if err != nil {
log.Printf("failed to get current chain fee rates: %v", err)
} else {
f.mtx.Lock()
f.currentFees = fees
f.feesLastUpdated = now
f.mtx.Unlock()
}

select {
case <-time.After(time.Minute * 5):
case <-ctx.Done():
return
}
}
}

func (r *FeeService) getFees() (*whatthefeeBody, error) {
now := time.Now().Unix()
cacheBust := (now / 300) * 300
resp, err := http.Get(
fmt.Sprintf("https://whatthefee.io/data.json?c=%d", cacheBust),
)
if err != nil {
return nil, fmt.Errorf("failed to call whatthefee.io: %v", err)
}
defer resp.Body.Close()

var body whatthefeeBody
err = json.NewDecoder(resp.Body).Decode(&body)
if err != nil {
return nil, fmt.Errorf("failed to decode whatthefee.io response: %w", err)
}

return &body, nil
}
Loading