diff --git a/.vscode/launch.json b/.vscode/launch.json
index 06ff138..1cda95b 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -9,7 +9,7 @@
"type": "go",
"request": "launch",
"mode": "auto",
- "buildFlags": "-tags lnd",
+ "buildFlags": "-tags cln",
"program": "${workspaceFolder}/cmd/psweb/",
"showLog": false,
"envFile": "${workspaceFolder}/.env",
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f526838..6a972d2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# Versions
+## 1.4.6
+
+- Estimate swap fees before submitting
+- LND: Fix invoice subscription reconnections
+- Auto swap: account for MaxHtlc settings
+- Keysend: account for MinHtlc settings
+- Fix swap-out fees missing for some swaps
+
## 1.4.5
- Add swap costs to home page
diff --git a/cmd/psweb/liquid/elements.go b/cmd/psweb/liquid/elements.go
index 29da4a6..deb82e0 100644
--- a/cmd/psweb/liquid/elements.go
+++ b/cmd/psweb/liquid/elements.go
@@ -394,8 +394,8 @@ type MemPoolInfo struct {
UnbroadcastCount int `json:"unbroadcastcount"`
}
-// return min fee in sat/vB
-func GetMempoolMinFee() float64 {
+// return fee estimate in sat/vB
+func EstimateFee() float64 {
client := ElementsClient()
service := &Elements{client}
params := &[]string{}
diff --git a/cmd/psweb/ln/cln.go b/cmd/psweb/ln/cln.go
index 1198da8..8b7c1b5 100644
--- a/cmd/psweb/ln/cln.go
+++ b/cmd/psweb/ln/cln.go
@@ -24,8 +24,10 @@ import (
)
const (
- Implementation = "CLN"
- fileRPC = "lightning-rpc"
+ Implementation = "CLN"
+ fileRPC = "lightning-rpc"
+ SwapFeeReserveLBTC = uint64(000)
+ SwapFeeReserveBTC = uint64(2000)
)
type Forwarding struct {
@@ -371,6 +373,7 @@ type ListForwardsRequest struct {
Status string `json:"status"`
Index string `json:"index"`
Start uint64 `json:"start"`
+ Limit uint64 `json:"limit"`
}
func (r *ListForwardsRequest) Name() string {
@@ -390,28 +393,34 @@ func CacheForwards() {
Forwards []Forwarding `json:"forwards"`
}
- start := uint64(0)
- if forwardsLastIndex > 0 {
- // continue from the last index + 1
- start = forwardsLastIndex + 1
+ totalForwards := uint64(0)
+
+ for {
+ // get incremental history
+ client.Request(&ListForwardsRequest{
+ Status: "settled",
+ Index: "created",
+ Start: forwardsLastIndex,
+ Limit: 1000,
+ }, &newForwards)
+
+ n := len(newForwards.Forwards)
+ if n > 0 {
+ forwardsLastIndex = newForwards.Forwards[n-1].CreatedIndex + 1
+ for _, f := range newForwards.Forwards {
+ chIn := ConvertClnToLndChannelId(f.InChannel)
+ chOut := ConvertClnToLndChannelId(f.OutChannel)
+ forwardsIn[chIn] = append(forwardsIn[chIn], f)
+ forwardsOut[chOut] = append(forwardsOut[chOut], f)
+ }
+ totalForwards += uint64(n)
+ } else {
+ break
+ }
}
- // get incremental history
- client.Request(&ListForwardsRequest{
- Status: "settled",
- Index: "created",
- Start: start,
- }, &newForwards)
-
- n := len(newForwards.Forwards)
- if n > 0 {
- forwardsLastIndex = newForwards.Forwards[n-1].CreatedIndex
- for _, f := range newForwards.Forwards {
- chIn := ConvertClnToLndChannelId(f.InChannel)
- chOut := ConvertClnToLndChannelId(f.OutChannel)
- forwardsIn[chIn] = append(forwardsIn[chIn], f)
- forwardsOut[chOut] = append(forwardsOut[chOut], f)
- }
+ if totalForwards > 0 {
+ log.Printf("Cached %d forwards", totalForwards)
}
}
@@ -585,98 +594,12 @@ func GetChannelStats(lndChannelId uint64, timeStamp uint64) *ChannelStats {
client, clean, err := GetClient()
if err != nil {
+ log.Println("GetClient:", err)
return &result
}
defer clean()
- var harnessNetParams = &chaincfg.TestNet3Params
- if config.Config.Chain == "mainnet" {
- harnessNetParams = &chaincfg.MainNetParams
- }
-
- var res ListHtlcsResponse
-
- err = client.Request(&ListHtlcsRequest{
- ChannelId: channelId,
- }, &res)
- if err != nil {
- log.Println("ListHtlcsRequest:", err)
- }
-
- for _, htlc := range res.HTLCs {
- switch htlc.State {
- case "SENT_REMOVE_ACK_REVOCATION":
- // direction in, look for invoices
- var inv ListInvoicesResponse
- err := client.Request(&ListInvoicesRequest{
- PaymentHash: htlc.PaymentHash,
- }, &inv)
- if err != nil {
- continue
- }
- if len(inv.Invoices) != 1 {
- continue
- }
- if inv.Invoices[0].Status == "paid" {
- continue
- }
- if inv.Invoices[0].PaidAt > timeStamp {
- if len(inv.Invoices[0].Label) > 7 {
- if inv.Invoices[0].Label[:8] == "peerswap" {
- // find swap id
- parts := strings.Split(inv.Invoices[0].Label, " ")
- if parts[2] == "fee" && len(parts[4]) > 0 {
- // save rebate payment
- SwapRebates[parts[4]] = int64(htlc.AmountMsat) / 1000
- }
- } else {
- invoicedMsat += htlc.AmountMsat
- }
- }
- }
-
- case "RCVD_REMOVE_ACK_REVOCATION":
- // direction out, look for payments
- var pmt ListSendPaysResponse
- err := client.Request(&ListSendPaysRequest{
- PaymentHash: htlc.PaymentHash,
- }, &pmt)
- if err != nil {
- continue
- }
- if len(pmt.Payments) != 1 {
- continue
- }
- if pmt.Payments[0].Status != "complete" {
- continue
- }
- if pmt.Payments[0].CompletedAt > timeStamp {
- if pmt.Payments[0].Bolt11 != "" {
- // Decode the payment request
- invoice, err := zpay32.Decode(pmt.Payments[0].Bolt11, harnessNetParams)
- if err == nil {
- if invoice.Description != nil {
- if len(*invoice.Description) > 7 {
- if (*invoice.Description)[:8] == "peerswap" {
- // find swap id
- parts := strings.Split(*invoice.Description, " ")
- if parts[2] == "fee" && len(parts[4]) > 0 {
- // save rebate payment
- SwapRebates[parts[4]] = int64(htlc.AmountMsat) / 1000
- }
- // skip peerswap-related payments
- continue
- }
- }
- }
- }
- }
- paidOutMsat += htlc.AmountMsat
- fee := pmt.Payments[0].AmountSentMsat - pmt.Payments[0].AmountMsat
- costMsat += fee
- }
- }
- }
+ invoicedMsat, paidOutMsat, costMsat = fetchPaymentsStats(client, timeStamp, channelId)
timeStampF := float64(timeStamp)
@@ -734,6 +657,14 @@ func GetChannelInfo(client *glightning.Lightning, lndChannelId uint64, nodeId st
if channelMap["short_channel_id"].(string) == channelId {
info.FeeBase = uint64(channelMap["fee_base_msat"].(float64))
info.FeeRate = uint64(channelMap["fee_proportional_millionths"].(float64))
+ updates := channelMap["updates"].(map[string]interface{})
+ local := updates["local"].(map[string]interface{})
+ remote := updates["remote"].(map[string]interface{})
+ info.PeerMinHtlcMsat = uint64(remote["htlc_minimum_msat"].(float64))
+ info.PeerMaxHtlcMsat = uint64(remote["htlc_maximum_msat"].(float64))
+ info.OurMaxHtlcMsat = uint64(local["htlc_maximum_msat"].(float64))
+ info.OurMinHtlcMsat = uint64(local["htlc_minimum_msat"].(float64))
+ info.Capacity = uint64(channelMap["total_msat"].(float64) / 1000)
break
}
}
@@ -803,7 +734,7 @@ func ListPeers(client *glightning.Lightning, peerId string, excludeIds *[]string
for _, channel := range clnPeer.Channels {
peer.Channels = append(peer.Channels, &peerswaprpc.PeerSwapPeerChannel{
- ChannelId: ConvertClnToLndChannelId(channel.ChannelId),
+ ChannelId: ConvertClnToLndChannelId(channel.ShortChannelId),
LocalBalance: channel.SpendableMilliSatoshi / 1000,
RemoteBalance: channel.ReceivableMilliSatoshi / 1000,
Active: clnPeer.Connected && channel.State == "CHANNELD_NORMAL",
@@ -900,14 +831,141 @@ func GetMyAlias() string {
return myNodeAlias
}
-func CacheHtlcs() {
- // not implemented
+// scans all channels to get peerswap lightning fees cached
+func SubscribeAll() {
+ client, clean, err := GetClient()
+ if err != nil {
+ return
+ }
+ defer clean()
+
+ peers, err := ListPeers(client, "", nil)
+ if err != nil {
+ return
+ }
+
+ // last 6 months
+ timestamp := uint64(time.Now().AddDate(0, -6, 0).Unix())
+
+ for _, peer := range peers.Peers {
+ for _, channel := range peer.Channels {
+ channelId := ConvertLndToClnChannelId(channel.ChannelId)
+ fetchPaymentsStats(client, timestamp, channelId)
+ }
+ }
}
-func CacheInvoices() {
- // not implemented
+// get invoicedMsat, paidOutMsat, costMsat
+func fetchPaymentsStats(client *glightning.Lightning, timeStamp uint64, channelId string) (uint64, uint64, uint64) {
+ var harnessNetParams = &chaincfg.TestNet3Params
+ if config.Config.Chain == "mainnet" {
+ harnessNetParams = &chaincfg.MainNetParams
+ }
+
+ var (
+ res ListHtlcsResponse
+ invoicedMsat uint64
+ paidOutMsat uint64
+ costMsat uint64
+ )
+
+ err := client.Request(&ListHtlcsRequest{
+ ChannelId: channelId,
+ }, &res)
+ if err != nil {
+ log.Println("ListHtlcsRequest:", err)
+ }
+
+ for _, htlc := range res.HTLCs {
+ switch htlc.State {
+ case "SENT_REMOVE_ACK_REVOCATION":
+ // direction in, look for invoices
+ var inv ListInvoicesResponse
+ err := client.Request(&ListInvoicesRequest{
+ PaymentHash: htlc.PaymentHash,
+ }, &inv)
+ if err != nil {
+ continue
+ }
+ if len(inv.Invoices) != 1 {
+ continue
+ }
+ if inv.Invoices[0].Status == "paid" {
+ continue
+ }
+ if inv.Invoices[0].PaidAt > timeStamp {
+ if len(inv.Invoices[0].Label) > 7 {
+ if inv.Invoices[0].Label[:8] == "peerswap" {
+ // find swap id
+ parts := strings.Split(inv.Invoices[0].Label, " ")
+ if parts[2] == "fee" && len(parts[4]) > 0 {
+ // save rebate payment
+ SwapRebates[parts[4]] = int64(htlc.AmountMsat) / 1000
+ }
+ } else {
+ invoicedMsat += htlc.AmountMsat
+ }
+ }
+ }
+
+ case "RCVD_REMOVE_ACK_REVOCATION":
+ // direction out, look for payments
+ var pmt ListSendPaysResponse
+ err := client.Request(&ListSendPaysRequest{
+ PaymentHash: htlc.PaymentHash,
+ }, &pmt)
+ if err != nil {
+ continue
+ }
+ if len(pmt.Payments) != 1 {
+ continue
+ }
+ if pmt.Payments[0].Status != "complete" {
+ continue
+ }
+ if pmt.Payments[0].CompletedAt > timeStamp {
+ if pmt.Payments[0].Bolt11 != "" {
+ // Decode the payment request
+ invoice, err := zpay32.Decode(pmt.Payments[0].Bolt11, harnessNetParams)
+ if err == nil {
+ if invoice.Description != nil {
+ if len(*invoice.Description) > 7 {
+ if (*invoice.Description)[:8] == "peerswap" {
+ // find swap id
+ parts := strings.Split(*invoice.Description, " ")
+ if parts[2] == "fee" && len(parts[4]) > 0 {
+ // save rebate payment
+ SwapRebates[parts[4]] = int64(htlc.AmountMsat) / 1000
+ }
+ // skip peerswap-related payments
+ continue
+ }
+ }
+ }
+ }
+ }
+ paidOutMsat += htlc.AmountMsat
+ fee := pmt.Payments[0].AmountSentMsat - pmt.Payments[0].AmountMsat
+ costMsat += fee
+ }
+ }
+ }
+
+ return invoicedMsat, paidOutMsat, costMsat
}
-func CachePayments() {
- // not implemented
+// Estimate sat/vB fee
+func EstimateFee() float64 {
+ client, clean, err := GetClient()
+ if err != nil {
+ return 0
+ }
+ defer clean()
+
+ res, err := client.FeeRates(glightning.PerKb)
+ if err != nil {
+ return 0
+ }
+
+ return float64(res.Details.Urgent) / 1000
}
diff --git a/cmd/psweb/ln/common.go b/cmd/psweb/ln/common.go
index c3b1240..b14a6e5 100644
--- a/cmd/psweb/ln/common.go
+++ b/cmd/psweb/ln/common.go
@@ -55,11 +55,16 @@ type ChannelStats struct {
}
type ChanneInfo struct {
- LocalBalance uint64
- RemoteBalance uint64
- FeeRate uint64
- FeeBase uint64
- Active bool
+ LocalBalance uint64
+ RemoteBalance uint64
+ FeeRate uint64
+ FeeBase uint64
+ Active bool
+ OurMaxHtlcMsat uint64
+ OurMinHtlcMsat uint64
+ PeerMaxHtlcMsat uint64
+ PeerMinHtlcMsat uint64
+ Capacity uint64
}
// lighting payments from swap out initiator to receiver
diff --git a/cmd/psweb/ln/lnd.go b/cmd/psweb/ln/lnd.go
index f2637d9..c9354e7 100644
--- a/cmd/psweb/ln/lnd.go
+++ b/cmd/psweb/ln/lnd.go
@@ -39,7 +39,12 @@ import (
"gopkg.in/macaroon.v2"
)
-const Implementation = "LND"
+const (
+ Implementation = "LND"
+ // Liquid balance to reserve in auto swap-ins
+ SwapFeeReserveLBTC = uint64(1000)
+ SwapFeeReserveBTC = uint64(2000)
+)
type InflightHTLC struct {
OutgoingChannelId uint64
@@ -674,59 +679,6 @@ func GetMyAlias() string {
return myNodeAlias
}
-// cache htlc history per channel, then subscribe to updates
-func CacheHtlcs() {
- ctx := context.Background()
- conn, err := lndConnection()
- if err != nil {
- return
- }
- defer conn.Close()
-
- client := lnrpc.NewLightningClient(conn)
-
- // initial download forwards
- downloadForwards(client)
-
- // subscribe to htlc events
- routerClient := routerrpc.NewRouterClient(conn)
- for {
- if err := subscribeForwards(ctx, routerClient); err != nil {
- log.Println("Forwards subscription error:", err)
- time.Sleep(60 * time.Second)
- // incremental download from last timestamp + 1
- downloadForwards(client)
- }
- }
-}
-
-// cache payments history per channel, then subscribe to updates
-func CachePayments() {
- ctx := context.Background()
- conn, err := lndConnection()
- if err != nil {
- return
- }
- defer conn.Close()
-
- client := lnrpc.NewLightningClient(conn)
-
- // initial download payments
- downloadPayments(client)
-
- // subscribe to htlc events
- routerClient := routerrpc.NewRouterClient(conn)
- for {
- if err := subscribePayments(ctx, routerClient); err != nil {
- log.Println("Payments subscription error:", err)
- time.Sleep(60 * time.Second)
- // incremental download from last timestamp + 1
- downloadPayments(client)
- }
- }
-
-}
-
func downloadForwards(client lnrpc.LightningClient) {
// only go back 6 months
start := uint64(time.Now().AddDate(0, -6, 0).Unix())
@@ -739,6 +691,7 @@ func downloadForwards(client lnrpc.LightningClient) {
// download forwards
offset := uint32(0)
+ totalForwards := uint64(0)
for {
res, err := client.ForwardingHistory(context.Background(), &lnrpc.ForwardingHistoryRequest{
StartTime: start,
@@ -747,6 +700,7 @@ func downloadForwards(client lnrpc.LightningClient) {
NumMaxEvents: 50000,
})
if err != nil {
+ log.Println("ForwardingHistory:", err)
return
}
@@ -757,6 +711,8 @@ func downloadForwards(client lnrpc.LightningClient) {
}
n := len(res.ForwardingEvents)
+ totalForwards += uint64(n)
+
if n > 0 {
// store the last timestamp
lastforwardCreationTs = res.ForwardingEvents[n-1].TimestampNs / 1_000_000_000
@@ -768,6 +724,10 @@ func downloadForwards(client lnrpc.LightningClient) {
// next pull start from the next index
offset = res.LastOffsetIndex
}
+
+ if totalForwards > 0 {
+ log.Printf("Cached %d forwards", totalForwards)
+ }
}
func downloadPayments(client lnrpc.LightningClient) {
@@ -781,29 +741,33 @@ func downloadPayments(client lnrpc.LightningClient) {
}
offset := uint64(0)
+ totalPayments := uint64(0)
for {
res, err := client.ListPayments(context.Background(), &lnrpc.ListPaymentsRequest{
CreationDateStart: start,
IncludeIncomplete: false,
Reversed: false,
IndexOffset: offset,
- MaxPayments: 50000,
+ MaxPayments: 100, // labels can be long
})
if err != nil {
+ log.Println("ListPayments:", err)
return
}
- // only append settled ones
+ // will only append settled ones
for _, payment := range res.Payments {
appendPayment(payment)
}
n := len(res.Payments)
+ totalPayments += uint64(n)
+
if n > 0 {
// store the last timestamp
lastPaymentCreationTs = res.Payments[n-1].CreationTimeNs / 1_000_000_000
}
- if n < 50000 {
+ if n < 100 {
// all events retrieved
break
}
@@ -811,6 +775,10 @@ func downloadPayments(client lnrpc.LightningClient) {
// next pull start from the next index
offset = res.LastIndexOffset
}
+
+ if totalPayments > 0 {
+ log.Printf("Cached %d payments", totalPayments)
+ }
}
func appendPayment(payment *lnrpc.Payment) {
@@ -969,53 +937,93 @@ func removeInflightHTLC(incomingChannelId, incomingHtlcId uint64) {
}
}
-// cache all invoices from lnd
-func CacheInvoices() {
- // refresh history
- client, cleanup, err := GetClient()
- if err != nil {
- return
- }
- defer cleanup()
-
- // only go back 6 months for itinial download
- start := uint64(time.Now().AddDate(0, -6, 0).Unix())
- offset := uint64(0)
+// cache all and subscribe to lnd
+func SubscribeAll() {
+ // loop forever until connected
for {
- res, err := client.ListInvoices(context.Background(), &lnrpc.ListInvoiceRequest{
- CreationDateStart: start,
- Reversed: false,
- IndexOffset: offset,
- NumMaxInvoices: 50000,
- })
+ conn, err := lndConnection()
if err != nil {
- return
+ continue
}
+ defer conn.Close()
+
+ client := lnrpc.NewLightningClient(conn)
+ ctx := context.Background()
+
+ // only go back 6 months for itinial download
+ start := uint64(time.Now().AddDate(0, -6, 0).Unix())
+ offset := uint64(0)
+ totalInvoices := uint64(0)
+
+ for {
+ res, err := client.ListInvoices(ctx, &lnrpc.ListInvoiceRequest{
+ CreationDateStart: start,
+ Reversed: false,
+ IndexOffset: offset,
+ NumMaxInvoices: 100, // bolt11 fields can be long
+ })
+ if err != nil {
+ log.Println("ListInvoices:", err)
+ return
+ }
- for _, invoice := range res.Invoices {
- appendInvoice(invoice)
- }
+ for _, invoice := range res.Invoices {
+ appendInvoice(invoice)
+ }
- n := len(res.Invoices)
- if n > 0 {
- // global settle index for subscription
- lastInvoiceSettleIndex = res.Invoices[n-1].SettleIndex
+ n := len(res.Invoices)
+ totalInvoices += uint64(n)
+
+ if n > 0 {
+ // settle index for subscription
+ lastInvoiceSettleIndex = res.Invoices[n-1].SettleIndex
+ }
+ if n < 100 {
+ // all invoices retrieved
+ break
+ }
+
+ // next pull start from the next index
+ offset = res.LastIndexOffset
}
- if n < 50000 {
- // all invoices retrieved
- break
+
+ if totalInvoices > 0 {
+ log.Printf("Cached %d invoices", totalInvoices)
}
- // next pull start from the next index
- offset = res.LastIndexOffset
- }
+ routerClient := routerrpc.NewRouterClient(conn)
- // subscribe
- ctx := context.Background()
- for {
- if err := subscribeInvoices(ctx, client); err != nil {
- log.Println("Invoices subscription error:", err)
- time.Sleep(5 * time.Second)
+ go func() {
+ // initial or incremental download forwards
+ downloadForwards(client)
+ // subscribe to Forwards
+ for {
+ if err := subscribeForwards(ctx, routerClient); err != nil {
+ log.Println("Forwards subscription error:", err)
+ time.Sleep(60 * time.Second)
+ }
+ }
+ }()
+
+ go func() {
+ // initial or incremental download payments
+ downloadPayments(client)
+
+ // subscribe to Payments
+ for {
+ if err := subscribePayments(ctx, routerClient); err != nil {
+ log.Println("Payments subscription error:", err)
+ time.Sleep(60 * time.Second)
+ }
+ }
+ }()
+
+ // subscribe to Invoices
+ for {
+ if err := subscribeInvoices(ctx, client); err != nil {
+ log.Println("Invoices subscription error:", err)
+ time.Sleep(60 * time.Second)
+ }
}
}
}
@@ -1027,18 +1035,20 @@ func appendInvoice(invoice *lnrpc.Invoice) {
}
// only append settled htlcs
if invoice.State == lnrpc.Invoice_SETTLED {
- if len(invoice.Memo) > 7 && invoice.Memo[:8] == "peerswap" {
- // find swap id
- parts := strings.Split(invoice.Memo, " ")
- if parts[2] == "fee" && len(parts[4]) > 0 {
- // save rebate payment
- SwapRebates[parts[4]] = int64(invoice.AmtPaidMsat) / 1000
- }
- } else {
- // skip peerswap-related
- for _, htlc := range invoice.Htlcs {
- if htlc.State == lnrpc.InvoiceHTLCState_SETTLED {
- invoiceHtlcs[htlc.ChanId] = append(invoiceHtlcs[htlc.ChanId], htlc)
+ if len(invoice.Memo) > 7 {
+ if invoice.Memo[:8] == "peerswap" {
+ // find swap id
+ parts := strings.Split(invoice.Memo, " ")
+ if parts[2] == "fee" && len(parts[4]) > 0 {
+ // save rebate payment
+ SwapRebates[parts[4]] = int64(invoice.AmtPaidMsat) / 1000
+ }
+ } else {
+ // skip peerswap-related
+ for _, htlc := range invoice.Htlcs {
+ if htlc.State == lnrpc.InvoiceHTLCState_SETTLED {
+ invoiceHtlcs[htlc.ChanId] = append(invoiceHtlcs[htlc.ChanId], htlc)
+ }
}
}
}
@@ -1144,10 +1154,10 @@ func GetForwardingStats(channelId uint64) *ForwardingStats {
}
// get fees on the channel
-func GetChannelInfo(client lnrpc.LightningClient, channelId uint64, nodeId string) *ChanneInfo {
+func GetChannelInfo(client lnrpc.LightningClient, channelId uint64, peerNodeId string) *ChanneInfo {
info := new(ChanneInfo)
- res2, err := client.GetChanInfo(context.Background(), &lnrpc.ChanInfoRequest{
+ r, err := client.GetChanInfo(context.Background(), &lnrpc.ChanInfoRequest{
ChanId: channelId,
})
if err != nil {
@@ -1155,14 +1165,24 @@ func GetChannelInfo(client lnrpc.LightningClient, channelId uint64, nodeId strin
return info
}
- policy := res2.Node1Policy
- if res2.Node1Pub == nodeId {
+ policy := r.Node1Policy
+ if r.Node1Pub == peerNodeId {
// the first policy is not ours, use the second
- policy = res2.Node2Policy
+ policy = r.Node2Policy
+ info.PeerMaxHtlcMsat = r.GetNode1Policy().GetMaxHtlcMsat()
+ info.PeerMinHtlcMsat = uint64(r.GetNode1Policy().GetMinHtlc())
+ info.OurMaxHtlcMsat = r.GetNode2Policy().GetMaxHtlcMsat()
+ info.OurMinHtlcMsat = uint64(r.GetNode2Policy().GetMinHtlc())
+ } else {
+ info.PeerMaxHtlcMsat = r.GetNode2Policy().GetMaxHtlcMsat()
+ info.PeerMinHtlcMsat = uint64(r.GetNode2Policy().GetMinHtlc())
+ info.OurMaxHtlcMsat = r.GetNode1Policy().GetMaxHtlcMsat()
+ info.OurMinHtlcMsat = uint64(r.GetNode1Policy().GetMinHtlc())
}
info.FeeRate = uint64(policy.GetFeeRateMilliMsat())
info.FeeBase = uint64(policy.GetFeeBaseMsat())
+ info.Capacity = uint64(r.Capacity)
return info
}
@@ -1377,3 +1397,22 @@ func ListPeers(client lnrpc.LightningClient, peerId string, excludeIds *[]string
func CacheForwards() {
// not implemented
}
+
+// Estimate sat/vB fee
+func EstimateFee() float64 {
+ conn, err := lndConnection()
+ if err != nil {
+ return 0
+ }
+ cl := walletrpc.NewWalletKitClient(conn)
+
+ ctx := context.Background()
+ req := &walletrpc.EstimateFeeRequest{ConfTarget: 2}
+ res, err := cl.EstimateFee(ctx, req)
+
+ if err != nil {
+ return 0
+ }
+
+ return float64(res.SatPerKw / 250)
+}
diff --git a/cmd/psweb/main.go b/cmd/psweb/main.go
index 8a12325..10ce789 100644
--- a/cmd/psweb/main.go
+++ b/cmd/psweb/main.go
@@ -33,11 +33,7 @@ import (
const (
// App version tag
- version = "v1.4.5"
-
- // Liquid balance to reserve in auto swaps
- // Min is 1000, but the swap will spend it all on fee
- swapInFeeReserve = uint64(2000)
+ version = "v1.4.6"
)
type SwapParams struct {
@@ -53,9 +49,10 @@ var (
//go:embed static
staticFiles embed.FS
//go:embed templates/*.gohtml
- tplFolder embed.FS
- logFile *os.File
- latestVersion = version
+ tplFolder embed.FS
+ logFile *os.File
+ latestVersion = version
+ // Bitcoin sat/vB from mempool.space
mempoolFeeRate = float64(0)
// onchain realized transaction costs
txFee = make(map[string]int64)
@@ -161,14 +158,11 @@ func main() {
// to speed up first load of home page
go cacheAliases()
- // download and subscribe to invoices
- go ln.CacheInvoices()
-
- // download and subscribe to forwards
- go ln.CacheHtlcs()
+ // LND: download and subscribe to invoices, forwards and payments
+ go ln.SubscribeAll()
- // download and subscribe to payments
- go ln.CachePayments()
+ // CLN: cache forwarding stats
+ go ln.CacheForwards()
// fetch all chain costs
go cacheSwapCosts()
@@ -279,6 +273,9 @@ func indexHandler(w http.ResponseWriter, r *http.Request) {
}
peers = res4.GetPeers()
+ // refresh forwarding stats
+ ln.CacheForwards()
+
peerTable := convertPeersToHTMLTable(peers, allowlistedPeers, suspiciousPeers, swaps)
//check whether to display non-PS channels or swaps
@@ -325,6 +322,7 @@ func indexHandler(w http.ResponseWriter, r *http.Request) {
Filter bool
MempoolFeeRate float64
AutoSwapEnabled bool
+ PeginPending bool
}
data := Page{
@@ -341,6 +339,7 @@ func indexHandler(w http.ResponseWriter, r *http.Request) {
BitcoinBalance: uint64(btcBalance),
Filter: nodeId != "" || state != "" || role != "",
AutoSwapEnabled: config.Config.AutoSwapEnabled,
+ PeginPending: config.Config.PeginTxId != "",
}
// executing template named "homepage"
@@ -474,6 +473,13 @@ func peerHandler(w http.ResponseWriter, r *http.Request) {
var sumRemote uint64
var stats []*ln.ForwardingStats
var channelInfo []*ln.ChanneInfo
+ var keysendSats = uint64(1)
+
+ var utxosBTC []ln.UTXO
+ ln.ListUnspent(cl, &utxosBTC, 1)
+
+ var utxosLBTC []liquid.UTXO
+ liquid.ListUnspent(&utxosLBTC)
for _, ch := range peer.Channels {
stat := ln.GetForwardingStats(ch.ChannelId)
@@ -487,6 +493,10 @@ func peerHandler(w http.ResponseWriter, r *http.Request) {
sumLocal += ch.GetLocalBalance()
sumRemote += ch.GetRemoteBalance()
+
+ // should not be less than both Min HTLC setting
+ keysendSats = max(keysendSats, msatToSatUp(info.PeerMinHtlcMsat))
+ keysendSats = max(keysendSats, msatToSatUp(info.OurMinHtlcMsat))
}
//check for error errorMessage to display
@@ -526,17 +536,25 @@ func peerHandler(w http.ResponseWriter, r *http.Request) {
SenderInFeePPM int64
ReceiverInFeePPM int64
ReceiverOutFeePPM int64
+ KeysendSats uint64
+ OutputsBTC *[]ln.UTXO
+ OutputsLBTC *[]liquid.UTXO
+ ReserveLBTC uint64
+ ReserveBTC uint64
}
- feeRate := liquid.GetMempoolMinFee()
+ feeRate := liquid.EstimateFee()
if !psPeer {
feeRate = mempoolFeeRate
}
+ // better be conservative
+ bitcoinFeeRate := max(ln.EstimateFee(), mempoolFeeRate)
+
data := Page{
ErrorMessage: errorMessage,
PopUpMessage: "",
- BtcFeeRate: mempoolFeeRate,
+ BtcFeeRate: bitcoinFeeRate,
MempoolFeeRate: feeRate,
ColorScheme: config.Config.ColorScheme,
Peer: peer,
@@ -561,6 +579,11 @@ func peerHandler(w http.ResponseWriter, r *http.Request) {
SenderInFeePPM: senderInFeePPM,
ReceiverInFeePPM: receiverInFeePPM,
ReceiverOutFeePPM: receiverOutFeePPM,
+ KeysendSats: keysendSats,
+ OutputsBTC: &utxosBTC,
+ OutputsLBTC: &utxosLBTC,
+ ReserveLBTC: ln.SwapFeeReserveLBTC,
+ ReserveBTC: ln.SwapFeeReserveBTC,
}
// executing template named "peer"
@@ -872,7 +895,7 @@ func liquidHandler(w http.ResponseWriter, r *http.Request) {
data := Page{
ErrorMessage: errorMessage,
PopUpMessage: popupMessage,
- MempoolFeeRate: liquid.GetMempoolMinFee(),
+ MempoolFeeRate: liquid.EstimateFee(),
ColorScheme: config.Config.ColorScheme,
LiquidAddress: addr,
LiquidBalance: satAmount,
@@ -1090,9 +1113,9 @@ func submitHandler(w http.ResponseWriter, r *http.Request) {
var id string
switch r.FormValue("direction") {
case "swapIn":
- id, err = ps.SwapIn(client, swapAmount, channelId, r.FormValue("asset"), r.FormValue("force") == "on")
+ id, err = ps.SwapIn(client, swapAmount, channelId, r.FormValue("asset"), false)
case "swapOut":
- id, err = ps.SwapOut(client, swapAmount, channelId, r.FormValue("asset"), r.FormValue("force") == "on")
+ id, err = ps.SwapOut(client, swapAmount, channelId, r.FormValue("asset"), false)
}
if err != nil {
@@ -1474,11 +1497,6 @@ func onTimer() {
// Check if pegin can be claimed
go checkPegin()
- // cache forwards history for CLN
- go func() {
- ln.CacheForwards()
- }()
-
// check for updates
t := internet.GetLatestTag()
if t != "" {
@@ -1647,7 +1665,7 @@ func bitcoinHandler(w http.ResponseWriter, r *http.Request) {
Duration: formattedDuration,
FeeRate: config.Config.PeginFeeRate,
MempoolFeeRate: mempoolFeeRate,
- LiquidFeeRate: liquid.GetMempoolMinFee(),
+ LiquidFeeRate: liquid.EstimateFee(),
SuggestedFeeRate: fee,
MinBumpFeeRate: config.Config.PeginFeeRate + 1,
CanBump: canBump,
@@ -2020,6 +2038,14 @@ func convertPeersToHTMLTable(peers []*peerswaprpc.PeerSwapPeer, allowlistedPeers
flowText = "\nNo flows"
}
+ if stats.FeeSat > 0 {
+ flowText += "\nRevenue: +" + formatWithThousandSeparators(stats.FeeSat)
+ }
+
+ if stats.PaidCost > 0 {
+ flowText += "\nCosts: -" + formatWithThousandSeparators(stats.PaidCost)
+ }
+
tooltip += flowText
currentProgress := fmt.Sprintf("%d%% 100%%, %d%% 100%%, %d%% 100%%, 100%% 100%%", bluePct, redPct, greenPct)
@@ -2498,7 +2524,8 @@ func cacheAliases() {
// The goal is to spend maximum available liquid
// To rebalance a channel with high enough historic fee PPM
func findSwapInCandidate(candidate *SwapParams) error {
- minAmount := config.Config.AutoSwapThresholdAmount - swapInFeeReserve
+ // extra 1000 to avoid no-change tx spending all on fees
+ minAmount := config.Config.AutoSwapThresholdAmount - ln.SwapFeeReserveLBTC - 1000
minPPM := config.Config.AutoSwapThresholdPPM
client, cleanup, err := ps.GetClient(config.Config.RpcHost)
@@ -2528,20 +2555,36 @@ func findSwapInCandidate(candidate *SwapParams) error {
}
}
+ cl, clean, err := ln.GetClient()
+ if err != nil {
+ return err
+ }
+ defer clean()
+
for _, peer := range peers {
- // ignore peer if Lighting swaps disabled
+ // ignore peer with Liquid swaps disabled
if !peer.SwapsAllowed || !stringIsInSlice("lbtc", peer.SupportedAssets) {
continue
}
for _, channel := range peer.Channels {
- // the potential swap amount to bring balance to target
- swapAmount := (channel.LocalBalance + channel.RemoteBalance) * config.Config.AutoSwapTargetPct / 100
- if swapAmount > channel.LocalBalance {
- swapAmount -= channel.LocalBalance
- } else {
- swapAmount = 0
+ chanInfo := ln.GetChannelInfo(cl, channel.ChannelId, peer.NodeId)
+ // find the potential swap amount to bring balance to target
+ targetBalance := chanInfo.Capacity * config.Config.AutoSwapTargetPct / 100
+
+ // limit target to Remote - reserve
+ reserve := chanInfo.Capacity / 100
+ targetBalance = min(targetBalance, chanInfo.Capacity-reserve)
+
+ if targetBalance < channel.LocalBalance {
+ continue
}
+ swapAmount := targetBalance - channel.LocalBalance
+
+ // limit to max HTLC setting
+ swapAmount = min(swapAmount, chanInfo.PeerMaxHtlcMsat/1000)
+ swapAmount = min(swapAmount, chanInfo.OurMaxHtlcMsat/1000)
+
// only consider active channels with enough remote balance
if channel.Active && swapAmount >= minAmount {
// use timestamp of the last swap or 6 months horizon
@@ -2558,7 +2601,7 @@ func findSwapInCandidate(candidate *SwapParams) error {
}
// aim to maximize accumulated PPM
- // if ppm ties, choose the candidate with larger RemoteBalance
+ // if ppm ties, choose the candidate with larger potential swap amount
if ppm > minPPM || ppm == minPPM && swapAmount > candidate.Amount {
// set the candidate's PPM as the new target to beat
minPPM = ppm
@@ -2619,10 +2662,8 @@ func executeAutoSwap() {
return
}
- // swap in cannot be larger than this
- if amount > satAmount-swapInFeeReserve {
- amount = satAmount - swapInFeeReserve
- }
+ // extra 1000 reserve to avoid no-change tx spending all on fees
+ amount = min(amount, satAmount-ln.SwapFeeReserveLBTC-1000)
// execute swap
id, err := ps.SwapIn(client, amount, candidate.ChannelId, "lbtc", false)
diff --git a/cmd/psweb/templates/bitcoin.gohtml b/cmd/psweb/templates/bitcoin.gohtml
index 7d061a8..68c54ad 100644
--- a/cmd/psweb/templates/bitcoin.gohtml
+++ b/cmd/psweb/templates/bitcoin.gohtml
@@ -58,7 +58,7 @@
-
+
@@ -159,7 +159,7 @@
-
+
diff --git a/cmd/psweb/templates/homepage.gohtml b/cmd/psweb/templates/homepage.gohtml
index 1fd5c2c..7556d22 100644
--- a/cmd/psweb/templates/homepage.gohtml
+++ b/cmd/psweb/templates/homepage.gohtml
@@ -14,7 +14,11 @@
🌊 Liquid: {{fmt .LiquidBalance}}
- ✔️{{else}}disabled">❌{{end}}
+ {{if .PeginPending}}
+ ⏰
+ {{else}}
+ ✔️{{else}}disabled">❌{{end}}
+ {{end}}
{{if .AutoSwapEnabled}}
🤖
{{end}}
diff --git a/cmd/psweb/templates/peer.gohtml b/cmd/psweb/templates/peer.gohtml
index ff3b78c..2aa92a5 100644
--- a/cmd/psweb/templates/peer.gohtml
+++ b/cmd/psweb/templates/peer.gohtml
@@ -61,7 +61,7 @@
-
-
+
-
-
-
-
-
+
+
+ {{range .OutputsBTC}}
+
+ {{.Address}} |
+ {{.AmountSat}} |
+
+ {{end}}
+
+
+ {{range .OutputsLBTC}}
+
+ {{.Amount}} |
+
+ {{end}}
+
{{end}}
@@ -160,7 +331,7 @@ Sincerely,
diff --git a/cmd/psweb/utils.go b/cmd/psweb/utils.go
index 3d0c005..abef052 100644
--- a/cmd/psweb/utils.go
+++ b/cmd/psweb/utils.go
@@ -147,3 +147,13 @@ func formatSigned(num int64) string {
}
return formatWithThousandSeparators(uint64(num))
}
+
+// msatToSatUp converts millisatoshis to satoshis, rounding up.
+func msatToSatUp(msat uint64) uint64 {
+ // Divide msat by 1000 and round up if there's any remainder.
+ sat := msat / 1000
+ if msat%1000 != 0 {
+ sat++
+ }
+ return sat
+}