diff --git a/CHANGELOG.md b/CHANGELOG.md index df7d1fe..48a5e52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Versions +## 1.7.8 + +- Respect spendable and receivable limits for max swap calculation +- Show peg-in ETA as time and not remaining duration +- Wait for broadcasted CaimJoin tx to appear in local mempool +- Fix channel stats not resetting after a successful swap +- Fix invoiced stats not calculating without circular rebalancing +- AutoFee: confirm 'Update All' button click + ## 1.7.7 - Fix Auto Fee not working for channels with no outbound forwards diff --git a/cmd/psweb/handlers.go b/cmd/psweb/handlers.go index d14831a..f2bc5d8 100644 --- a/cmd/psweb/handlers.go +++ b/cmd/psweb/handlers.go @@ -434,29 +434,43 @@ func peerHandler(w http.ResponseWriter, r *http.Request) { // this is what peerswap will use bitcoinFeeRate := ln.EstimateFee() - // should match peerswap estimation - swapFeeReserveBTC := uint64(math.Ceil(bitcoinFeeRate * 350)) - // arbitrary haircut to avoid 'no matching outgoing channel available' - maxLiquidSwapIn := min(int64(satAmount)-SWAP_LBTC_RESERVE, int64(maxRemoteBalance)-10000) + // should match peerswap estimations + swapFeeReserveBTC := int64(math.Ceil(bitcoinFeeRate * OPENING_TX_SIZE_BTC)) + swapFeeReserveLBTC := int64(math.Ceil(feeRate * OPENING_TX_SIZE_BTC)) + if hasDiscountedvSize { + swapFeeReserveLBTC = int64(math.Ceil(feeRate * OPENING_TX_SIZE_LBTC_DISCOUNTED)) + } + + selectedChannel := peer.Channels[maxRemoteBalanceIndex].ChannelId + + spendable, receivable, err := ln.FetchChannelLimits(cl) + + if err != nil { + log.Printf("error fetching channel reserves: %v", err) + redirectWithError(w, r, "/config?", err) + return + } + + // haircut to avoid 'no matching outgoing channel available' + maxLiquidSwapIn := min(int64(satAmount)-SWAP_LBTC_RESERVE, int64(receivable[selectedChannel])) if maxLiquidSwapIn < 100_000 { maxLiquidSwapIn = 0 } peerLiquidBalance := "" maxLiquidSwapOut := uint64(0) - selectedChannel := peer.Channels[maxRemoteBalanceIndex].ChannelId channelCapacity := peer.Channels[maxRemoteBalanceIndex].RemoteBalance + peer.Channels[maxRemoteBalanceIndex].LocalBalance if ptr := ln.LiquidBalances[peer.NodeId]; ptr != nil { if ptr.Amount < 100_000 { peerLiquidBalance = "<100k" } else { - peerLiquidBalance = formatWithThousandSeparators(ptr.Amount) + peerLiquidBalance = "≥" + formatWithThousandSeparators(ptr.Amount) } - maxLiquidSwapOut = uint64(max(0, min(int64(maxLocalBalance)-SWAP_OUT_CHANNEL_RESERVE, int64(ptr.Amount)))) + maxLiquidSwapOut = uint64(max(0, min(int64(spendable[selectedChannel]), int64(ptr.Amount))-swapFeeReserveLBTC)) } else { - maxLiquidSwapOut = uint64(max(0, int64(maxLocalBalance)-SWAP_OUT_CHANNEL_RESERVE)) + maxLiquidSwapOut = uint64(max(0, int64(spendable[selectedChannel])-swapFeeReserveLBTC)) } if maxLiquidSwapOut >= 100_000 { @@ -472,11 +486,11 @@ func peerHandler(w http.ResponseWriter, r *http.Request) { if ptr.Amount < 100_000 { peerBitcoinBalance = "<100k" } else { - peerBitcoinBalance = formatWithThousandSeparators(ptr.Amount) + peerBitcoinBalance = "≥" + formatWithThousandSeparators(ptr.Amount) } - maxBitcoinSwapOut = uint64(max(0, min(int64(maxLocalBalance)-SWAP_OUT_CHANNEL_RESERVE, int64(ptr.Amount)-int64(swapFeeReserveBTC)))) + maxBitcoinSwapOut = uint64(max(0, min(int64(spendable[selectedChannel]), int64(ptr.Amount))-swapFeeReserveBTC)) } else { - maxBitcoinSwapOut = uint64(max(0, int64(maxLocalBalance)-SWAP_OUT_CHANNEL_RESERVE)) + maxBitcoinSwapOut = uint64(max(0, int64(spendable[selectedChannel])-swapFeeReserveBTC)) } if maxBitcoinSwapOut >= 100_000 { @@ -486,8 +500,8 @@ func peerHandler(w http.ResponseWriter, r *http.Request) { maxBitcoinSwapOut = 0 } - // arbitrary haircuts to avoid 'no matching outgoing channel available' - maxBitcoinSwapIn := min(btcBalance-int64(swapFeeReserveBTC), int64(maxRemoteBalance)-10000) + // haircuts to avoid 'no matching outgoing channel available' + maxBitcoinSwapIn := min(btcBalance-swapFeeReserveBTC, int64(receivable[selectedChannel])) if maxBitcoinSwapIn < 100_000 { maxBitcoinSwapIn = 0 } @@ -548,55 +562,58 @@ func peerHandler(w http.ResponseWriter, r *http.Request) { } type Page struct { - Authenticated bool - ErrorMessage string - PopUpMessage string - MempoolFeeRate float64 - BtcFeeRate float64 - ColorScheme string - Peer *peerswaprpc.PeerSwapPeer - PeerAlias string - NodeUrl string - Allowed bool - Suspicious bool - LBTC bool - BTC bool - LiquidBalance uint64 - BitcoinBalance uint64 - ActiveSwaps string - DirectionIn bool - Stats []*ln.ForwardingStats - ChannelInfo []*ln.ChanneInfo - PeerSwapPeer bool - MyAlias string - SenderOutFee int64 - SenderOutFeePPM int64 - SenderInFee int64 - ReceiverInFee int64 - ReceiverOutFee int64 - SenderInFeePPM int64 - ReceiverInFeePPM int64 - ReceiverOutFeePPM int64 - KeysendSats uint64 - OutputsBTC *[]ln.UTXO - OutputsLBTC *[]liquid.UTXO - HasInboundFees bool - PeerBitcoinBalance string // "" means no data - MaxBitcoinSwapOut uint64 - RecommendBitcoinSwapOut uint64 - MaxBitcoinSwapIn int64 - RecommendBitcoinSwapIn int64 - PeerLiquidBalance string // "" means no data - MaxLiquidSwapOut uint64 - RecommendLiquidSwapOut uint64 - MaxLiquidSwapIn int64 - RecommendLiquidSwapIn int64 - SelectedChannel uint64 - HasDiscountedvSize bool - RedColor string - IsOnline bool - AnchorReserve uint64 - LiquidReserve uint64 + Authenticated bool + ErrorMessage string + PopUpMessage string + MempoolFeeRate float64 + BtcFeeRate float64 + ColorScheme string + Peer *peerswaprpc.PeerSwapPeer + PeerAlias string + NodeUrl string + Allowed bool + Suspicious bool + LBTC bool + BTC bool + LiquidBalance uint64 + BitcoinBalance uint64 + ActiveSwaps string + DirectionIn bool + Stats []*ln.ForwardingStats + ChannelInfo []*ln.ChanneInfo + PeerSwapPeer bool + MyAlias string + SenderOutFee int64 + SenderOutFeePPM int64 + SenderInFee int64 + ReceiverInFee int64 + ReceiverOutFee int64 + SenderInFeePPM int64 + ReceiverInFeePPM int64 + ReceiverOutFeePPM int64 + KeysendSats uint64 + OutputsBTC *[]ln.UTXO + OutputsLBTC *[]liquid.UTXO + HasInboundFees bool + PeerBitcoinBalance string // "" means no data + MaxBitcoinSwapOut uint64 + RecommendBitcoinSwapOut uint64 + MaxBitcoinSwapIn int64 + RecommendBitcoinSwapIn int64 + PeerLiquidBalance string // "" means no data + MaxLiquidSwapOut uint64 + RecommendLiquidSwapOut uint64 + MaxLiquidSwapIn int64 + RecommendLiquidSwapIn int64 + SelectedChannel uint64 + HasDiscountedvSize bool + RedColor string + IsOnline bool + AnchorReserve uint64 + LiquidReserve uint64 + OPENING_TX_SIZE_BTC int64 + OPENING_TX_SIZE_LBTC int64 + OPENING_TX_SIZE_LBTC_DISCOUNTED int64 } redColor := "red" @@ -605,55 +622,58 @@ func peerHandler(w http.ResponseWriter, r *http.Request) { } data := Page{ - Authenticated: config.Config.SecureConnection && config.Config.Password != "", - ErrorMessage: errorMessage, - PopUpMessage: popupMessage, - BtcFeeRate: bitcoinFeeRate, - MempoolFeeRate: feeRate, - ColorScheme: config.Config.ColorScheme, - Peer: peer, - PeerAlias: getNodeAlias(peer.NodeId), - NodeUrl: config.Config.NodeApi, - Allowed: stringIsInSlice(peer.NodeId, allowlistedPeers), - Suspicious: stringIsInSlice(peer.NodeId, suspiciousPeers), - BTC: stringIsInSlice("btc", peer.SupportedAssets), - LBTC: stringIsInSlice("lbtc", peer.SupportedAssets), - LiquidBalance: satAmount, - BitcoinBalance: uint64(btcBalance), - ActiveSwaps: convertSwapsToHTMLTable(activeSwaps, "", "", ""), - DirectionIn: directionIn, - Stats: stats, - ChannelInfo: channelInfo, - PeerSwapPeer: psPeer, - MyAlias: ln.MyNodeAlias, - SenderOutFee: senderOutFee, - SenderOutFeePPM: senderOutFeePPM, - SenderInFee: senderInFee, - ReceiverInFee: receiverInFee, - ReceiverOutFee: receiverOutFee, - SenderInFeePPM: senderInFeePPM, - ReceiverInFeePPM: receiverInFeePPM, - ReceiverOutFeePPM: receiverOutFeePPM, - KeysendSats: keysendSats, - OutputsBTC: &utxosBTC, - OutputsLBTC: &utxosLBTC, - HasInboundFees: ln.HasInboundFees(), - PeerBitcoinBalance: peerBitcoinBalance, - MaxBitcoinSwapOut: maxBitcoinSwapOut, - RecommendBitcoinSwapOut: recommendBitcoinSwapOut, - MaxBitcoinSwapIn: maxBitcoinSwapIn, - RecommendBitcoinSwapIn: recommendBitcoinSwapIn, - PeerLiquidBalance: peerLiquidBalance, - MaxLiquidSwapOut: maxLiquidSwapOut, - RecommendLiquidSwapOut: recommendLiquidSwapOut, - MaxLiquidSwapIn: maxLiquidSwapIn, - RecommendLiquidSwapIn: recommendLiquidSwapIn, - SelectedChannel: selectedChannel, - HasDiscountedvSize: hasDiscountedvSize, - RedColor: redColor, - IsOnline: isOnline, - AnchorReserve: ANCHOR_RESERVE, - LiquidReserve: SWAP_LBTC_RESERVE, + Authenticated: config.Config.SecureConnection && config.Config.Password != "", + ErrorMessage: errorMessage, + PopUpMessage: popupMessage, + BtcFeeRate: bitcoinFeeRate, + MempoolFeeRate: feeRate, + ColorScheme: config.Config.ColorScheme, + Peer: peer, + PeerAlias: getNodeAlias(peer.NodeId), + NodeUrl: config.Config.NodeApi, + Allowed: stringIsInSlice(peer.NodeId, allowlistedPeers), + Suspicious: stringIsInSlice(peer.NodeId, suspiciousPeers), + BTC: stringIsInSlice("btc", peer.SupportedAssets), + LBTC: stringIsInSlice("lbtc", peer.SupportedAssets), + LiquidBalance: satAmount, + BitcoinBalance: uint64(btcBalance), + ActiveSwaps: convertSwapsToHTMLTable(activeSwaps, "", "", ""), + DirectionIn: directionIn, + Stats: stats, + ChannelInfo: channelInfo, + PeerSwapPeer: psPeer, + MyAlias: ln.MyNodeAlias, + SenderOutFee: senderOutFee, + SenderOutFeePPM: senderOutFeePPM, + SenderInFee: senderInFee, + ReceiverInFee: receiverInFee, + ReceiverOutFee: receiverOutFee, + SenderInFeePPM: senderInFeePPM, + ReceiverInFeePPM: receiverInFeePPM, + ReceiverOutFeePPM: receiverOutFeePPM, + KeysendSats: keysendSats, + OutputsBTC: &utxosBTC, + OutputsLBTC: &utxosLBTC, + HasInboundFees: ln.HasInboundFees(), + PeerBitcoinBalance: peerBitcoinBalance, + MaxBitcoinSwapOut: maxBitcoinSwapOut, + RecommendBitcoinSwapOut: recommendBitcoinSwapOut, + MaxBitcoinSwapIn: maxBitcoinSwapIn, + RecommendBitcoinSwapIn: recommendBitcoinSwapIn, + PeerLiquidBalance: peerLiquidBalance, + MaxLiquidSwapOut: maxLiquidSwapOut, + RecommendLiquidSwapOut: recommendLiquidSwapOut, + MaxLiquidSwapIn: maxLiquidSwapIn, + RecommendLiquidSwapIn: recommendLiquidSwapIn, + SelectedChannel: selectedChannel, + HasDiscountedvSize: hasDiscountedvSize, + RedColor: redColor, + IsOnline: isOnline, + AnchorReserve: ANCHOR_RESERVE, + LiquidReserve: SWAP_LBTC_RESERVE, + OPENING_TX_SIZE_BTC: OPENING_TX_SIZE_BTC, + OPENING_TX_SIZE_LBTC: OPENING_TX_SIZE_LBTC, + OPENING_TX_SIZE_LBTC_DISCOUNTED: OPENING_TX_SIZE_LBTC_DISCOUNTED, } // executing template named "peer" @@ -705,7 +725,7 @@ func bitcoinHandler(w http.ResponseWriter, r *http.Request) { Confirmations int32 TargetConfirmations int32 Progress int32 - Duration string + ETA string FeeRate float64 LiquidFeeRate float64 MempoolFeeRate float64 @@ -722,7 +742,7 @@ func bitcoinHandler(w http.ResponseWriter, r *http.Request) { IsClaimJoin bool ClaimJoinStatus string HasClaimJoinPending bool - ClaimJoinETA int + ClaimJoinHours int ClaimJointTimeLimit string } @@ -761,7 +781,7 @@ func bitcoinHandler(w http.ResponseWriter, r *http.Request) { duration := time.Duration(10*(int32(peginBlocks)-confs)) * time.Minute maxConfs := int32(peginBlocks) - cjETA := 34 + cjHours := 34 cjTimeLimit := "" currentBlockHeight := int32(ln.GetBlockHeight()) @@ -770,15 +790,15 @@ func bitcoinHandler(w http.ResponseWriter, r *http.Request) { maxConfs = target - currentBlockHeight + confs duration = time.Duration(10*(target-currentBlockHeight)) * time.Minute } else if ln.ClaimJoinHandler != "" { - cjETA = int((int32(ln.JoinBlockHeight) - currentBlockHeight + int32(peginBlocks)) / 6) + cjHours = int((int32(ln.JoinBlockHeight) - currentBlockHeight + int32(peginBlocks)) / 6) cjTimeLimit = time.Now().Add(time.Duration(10*(ln.JoinBlockHeight-uint32(currentBlockHeight))) * time.Minute).Format("3:04 PM") } progress := confs * 100 / int32(maxConfs) - formattedDuration := time.Time{}.Add(duration).Format("15h 04m") + eta := time.Now().Add(duration).Format("3:04 PM") if duration < 0 { - formattedDuration = "Past due" + eta = "Past due" } data := Page{ @@ -797,7 +817,7 @@ func bitcoinHandler(w http.ResponseWriter, r *http.Request) { Confirmations: confs, TargetConfirmations: maxConfs, Progress: progress, - Duration: formattedDuration, + ETA: eta, FeeRate: config.Config.PeginFeeRate, MempoolFeeRate: mempoolFeeRate, LiquidFeeRate: liquid.EstimateFee(), @@ -814,7 +834,7 @@ func bitcoinHandler(w http.ResponseWriter, r *http.Request) { ClaimJoinStatus: ln.ClaimStatus, HasClaimJoinPending: ln.ClaimJoinHandler != "", ClaimJointTimeLimit: cjTimeLimit, - ClaimJoinETA: cjETA, + ClaimJoinHours: cjHours, } // executing template named "bitcoin" @@ -1285,6 +1305,8 @@ func afHandler(w http.ResponseWriter, r *http.Request) { for i, f := range *forwardsLog { (*forwardsLog)[i].AliasIn = getNodeAlias(peerNodeId[f.ChanIdIn]) (*forwardsLog)[i].AliasOut = getNodeAlias(peerNodeId[f.ChanIdOut]) + (*forwardsLog)[i].Inbound = (*forwardsLog)[i].AliasIn == peerName + (*forwardsLog)[i].Outbound = (*forwardsLog)[i].AliasOut == peerName (*forwardsLog)[i].TimeAgo = timePassedAgo(time.Unix(int64(f.TS), 0)) (*forwardsLog)[i].TimeUTC = time.Unix(int64(f.TS), 0).UTC().Format(time.RFC1123) } @@ -1880,11 +1902,12 @@ func submitHandler(w http.ResponseWriter, r *http.Request) { config.Config.PeginTxId = txid config.Config.PeginFeeRate = 0 + ln.ClaimStatus = "Awaiting funding tx to confirm" log.Println("External Funding TxId:", txid) duration := time.Duration(10*(int32(peginBlocks)-tx.Confirmations)) * time.Minute - formattedDuration := time.Time{}.Add(duration).Format("15h 04m") - telegramSendMessage("⏰ Started peg in " + formatWithThousandSeparators(uint64(config.Config.PeginAmount)) + " sats. Time left: " + formattedDuration + ". TxId: `" + txid + "`") + eta := time.Now().Add(duration).Format("3:04 PM") + telegramSendMessage("⏰ Started peg in " + formatWithThousandSeparators(uint64(config.Config.PeginAmount)) + " sats. ETA: " + eta + ". TxId: `" + txid + "`") } config.Save() diff --git a/cmd/psweb/ln/claimjoin.go b/cmd/psweb/ln/claimjoin.go index 345829a..73fded8 100644 --- a/cmd/psweb/ln/claimjoin.go +++ b/cmd/psweb/ln/claimjoin.go @@ -484,19 +484,37 @@ func Broadcast(fromNodeId string, message *Message) bool { if ClaimJoinHandler == message.Sender { txId := string(message.Payload) if MyRole == "joiner" && txId != "" && config.Config.PeginClaimScript != "done" && len(ClaimParties) == 1 { - // verify that my address was funded var decoded liquid.Transaction - _, err := liquid.GetRawTransaction(txId, &decoded) - if err != nil { - ClaimStatus = "Error decoding posted transaction" - return false + var err error + + // wait 60 seconds for the posted tx to appear in our local mempool + timeout := time.After(60 * time.Second) + ticker := time.NewTicker(5 * time.Second) // Check every 5 seconds + + for { + select { + case <-timeout: + ClaimStatus = "Transaction did not appear in mempool within 60 seconds" + log.Println("Transaction did not appear in mempool within 60 seconds:", err) + ticker.Stop() + return false + case <-ticker.C: + _, err = liquid.GetRawTransaction(txId, &decoded) + if err == nil { + // Transaction found, proceed + ticker.Stop() + goto FoundTransaction + } + } } + FoundTransaction: addressInfo, err := liquid.GetAddressInfo(ClaimParties[0].Address) if err != nil { return false } + // verify that my address was funded ok := false for _, output := range decoded.Vout { if output.ScriptPubKey.Address == addressInfo.Unconfidential { diff --git a/cmd/psweb/ln/cln.go b/cmd/psweb/ln/cln.go index b67d86e..c7d8930 100644 --- a/cmd/psweb/ln/cln.go +++ b/cmd/psweb/ln/cln.go @@ -1580,3 +1580,41 @@ func SendCustomMessage(peerId string, message *Message) error { return nil } + +type ListPeerChannelsResponse struct { + Channels []PeerChannel `json:"channels"` +} + +type PeerChannel struct { + PeerId string `json:"peer_id"` + PeerConnected bool `json:"peer_connected"` + State string `json:"state"` + ShortChannelId string `json:"short_channel_id,omitempty"` + TotalMsat glightning.Amount `json:"total_msat,omitempty"` + ToUsMsat glightning.Amount `json:"to_us_msat,omitempty"` + ReceivableMsat glightning.Amount `json:"receivable_msat,omitempty"` + SpendableMsat glightning.Amount `json:"spendable_msat,omitempty"` + TheirReserveMsat glightning.Amount `json:"their_reserve_msat,omitempty"` + OurReserveMsat glightning.Amount `json:"our_reserve_msat,omitempty"` +} + +// spendable, receivable, mapped by channelId +func FetchChannelLimits(client *glightning.Lightning) (spendable map[uint64]uint64, receivable map[uint64]uint64, err error) { + var response ListPeerChannelsResponse + err = client.Request(&ListPeerChannelsRequest{}, &response) + if err != nil { + return + } + + spendable = make(map[uint64]uint64) + receivable = make(map[uint64]uint64) + + // Iterate over channels to map channel ids + for _, ch := range response.Channels { + id := ConvertClnToLndChannelId(ch.ShortChannelId) + spendable[id] = ch.SpendableMsat.MSat() / 1000 + receivable[id] = ch.ReceivableMsat.MSat() / 1000 + } + + return +} diff --git a/cmd/psweb/ln/common.go b/cmd/psweb/ln/common.go index d437d2f..819b446 100644 --- a/cmd/psweb/ln/common.go +++ b/cmd/psweb/ln/common.go @@ -212,6 +212,8 @@ type DataPoint struct { ChanIdOut uint64 AliasIn string AliasOut string + Inbound bool + Outbound bool TimeAgo string TimeUTC string } diff --git a/cmd/psweb/ln/lnd.go b/cmd/psweb/ln/lnd.go index a1e4721..1d523dd 100644 --- a/cmd/psweb/ln/lnd.go +++ b/cmd/psweb/ln/lnd.go @@ -1599,23 +1599,23 @@ func GetChannelStats(channelId uint64, timeStamp uint64) *ChannelStats { e := inv[i] if uint64(e.AcceptTime) > timeStamp { // check if it is related to a circular rebalancing + found := false htcls, ok := rebalanceInHtlcs.Read(channelId) if ok { - found := false for _, r := range htcls { if e.AmtMsat == uint64(r.Route.TotalAmtMsat-r.Route.TotalFeesMsat) { found = true break } } - if found { - // remove invoice to avoid double counting - inv = append(inv[:i], inv[i+1:]...) - invoiceHtlcs.Write(channelId, inv) - i-- - } else { - invoicedMsat += e.AmtMsat - } + } + if found { + // remove invoice to avoid double counting + inv = append(inv[:i], inv[i+1:]...) + invoiceHtlcs.Write(channelId, inv) + i-- + } else { + invoicedMsat += e.AmtMsat } } } @@ -2340,3 +2340,27 @@ try_to_connect: return false } + +// spendable, receivable, mapped by channelId +func FetchChannelLimits(client lnrpc.LightningClient) (spendable map[uint64]uint64, receivable map[uint64]uint64, err error) { + res, err := client.ListChannels(context.Background(), &lnrpc.ListChannelsRequest{ + ActiveOnly: false, + InactiveOnly: false, + PublicOnly: false, + PrivateOnly: false, + }) + + if err != nil { + return + } + + spendable = make(map[uint64]uint64) + receivable = make(map[uint64]uint64) + + for _, ch := range res.Channels { + spendable[ch.ChanId] = min(uint64(ch.GetLocalBalance()-int64(ch.GetLocalConstraints().GetChanReserveSat())), ch.GetLocalConstraints().GetMaxPendingAmtMsat()/1000) + receivable[ch.ChanId] = min(uint64(ch.GetRemoteBalance()-int64(ch.GetRemoteConstraints().GetChanReserveSat())), ch.GetRemoteConstraints().GetMaxPendingAmtMsat()/1000) + } + + return +} diff --git a/cmd/psweb/main.go b/cmd/psweb/main.go index ab2dcf4..5e4f903 100644 --- a/cmd/psweb/main.go +++ b/cmd/psweb/main.go @@ -34,15 +34,17 @@ import ( const ( // App VERSION tag - VERSION = "v1.7.7" - // Swap Out reserve to deduct from channel local balance - SWAP_OUT_CHANNEL_RESERVE = 10_000 - // https://github.com/ElementsProject/peerswap/pull/304#issuecomment-2303931071 - SWAP_LBTC_RESERVE = 1_200 + VERSION = "v1.7.8" // Unusable BTC balance ANCHOR_RESERVE = 25_000 // assume creatediscountct=1 for mainnet in elements.conf ELEMENTS_DISCOUNTED_VSIZE_VERSION = 230203 + // opening tx sizes for fee estimates + OPENING_TX_SIZE_BTC = 350 + OPENING_TX_SIZE_LBTC = 3000 + OPENING_TX_SIZE_LBTC_DISCOUNTED = 750 + // https://github.com/ElementsProject/peerswap/pull/304#issuecomment-2303931071 + SWAP_LBTC_RESERVE = 1_200 ) type SwapParams struct { @@ -495,7 +497,7 @@ func convertPeersToHTMLTable( for _, swap := range swaps { ts, ok := swapTimestamps.Read(swap.LndChanId) - if simplifySwapState(swap.State) == "success" && ok && ts < swap.CreatedAt { + if simplifySwapState(swap.State) == "success" && (!ok || ts < swap.CreatedAt) { swapTimestamps.Write(swap.LndChanId, swap.CreatedAt) } } @@ -707,7 +709,7 @@ func convertPeersToHTMLTable( bal := "<100k" if btcBalance >= 100_000 { flooredBalance = toMil(btcBalance) - bal = formatWithThousandSeparators(btcBalance) + bal = "≥" + formatWithThousandSeparators(btcBalance) } peerTable += "" + flooredBalance + "" } @@ -723,7 +725,7 @@ func convertPeersToHTMLTable( bal := "<100k" if lbtcBalance >= 100_000 { flooredBalance = toMil(lbtcBalance) - bal = formatWithThousandSeparators(lbtcBalance) + bal = "≥" + formatWithThousandSeparators(lbtcBalance) } peerTable += "" + flooredBalance + "" } @@ -1808,12 +1810,14 @@ func advertiseBalances() { cutOff := time.Now().AddDate(0, 0, -1).Unix() - 120 + _, receivable, err := ln.FetchChannelLimits(cl) + for _, peer := range res3.GetPeers() { // find the largest remote balance maxBalance := uint64(0) if ln.AdvertiseBitcoinBalance || ln.AdvertiseLiquidBalance { for _, ch := range peer.Channels { - maxBalance = max(maxBalance, ch.RemoteBalance-SWAP_OUT_CHANNEL_RESERVE) + maxBalance = max(maxBalance, receivable[ch.ChannelId]) } } diff --git a/cmd/psweb/telegram.go b/cmd/psweb/telegram.go index cc911a6..4849386 100644 --- a/cmd/psweb/telegram.go +++ b/cmd/psweb/telegram.go @@ -101,23 +101,23 @@ func telegramStart() { if ln.ClaimBlockHeight == 0 { duration = time.Duration(10*(int32(peginBlocks)-confs)) * time.Minute } - formattedDuration := time.Time{}.Add(duration).Format("15h 04m") + eta := time.Now().Add(duration).Format("3:04 PM") if duration < 0 { - formattedDuration = "Past due" + eta = "Past due" } t = "🧬 " + ln.ClaimStatus if ln.MyRole == "none" && ln.ClaimJoinHandler != "" { - t += ". Time left to apply: " + formattedDuration + t += ". Time limit to apply: " + eta } else if confs > 0 { - t += ". Claim ETA: " + formattedDuration + t += ". ETA: " + eta } } else { // solo peg-in duration := time.Duration(10*(int32(peginBlocks)-confs)) * time.Minute - formattedDuration := time.Time{}.Add(duration).Format("15h 04m") + eta := time.Now().Add(duration).Format("3:04 PM") t = "⏰ Amount: " + formatWithThousandSeparators(uint64(config.Config.PeginAmount)) + " sats, Confs: " + strconv.Itoa(int(confs)) if config.Config.PeginClaimScript != "" { - t += "/102, Time left: " + formattedDuration + t += "/102, ETA: " + eta } t += ". TxId: `" + config.Config.PeginTxId + "`" } diff --git a/cmd/psweb/templates/af.gohtml b/cmd/psweb/templates/af.gohtml index d50fb0f..8290f29 100644 --- a/cmd/psweb/templates/af.gohtml +++ b/cmd/psweb/templates/af.gohtml @@ -75,7 +75,7 @@ style="color:{{.RedColor}}" {{end}}>{{.LocalPct}}% local, Current fee rate: {{fs .FeeRate}}{{if .HasInboundFees}}, Inbound rate: {{.InboundRate}}{{end}}

{{end}} -
+ @@ -275,7 +275,7 @@ - + @@ -285,7 +285,13 @@ - + {{end}} @@ -413,7 +419,16 @@ document.getElementById(formId).submit(); } // warning message if Max HTLC % > 0 - function confirmSubmit() { + function confirmSubmit(event) { + // Detect the clicked button + const clickedButton = event.submitter; // Modern browsers support this + if (clickedButton.name == 'update_all') { + var confirmed = confirm("This will update highlighted values for all custom rules. Are you sure?"); + if (!confirmed) { + // user cancels, prevent form submission + return false; + } + } if (Number(document.getElementById("maxHtlcPct").value)>0) { var confirmed = confirm("Setting Max HTLC size to a % of Local Balance will only take effect when Local Balance exceeds 50% of Capacity. Still, reducing Max HTLC can prevent receiving certain large swaps and inbound payments. A better way to avoid failed HTLCs is to bump channel's fee rate upon every fail. Please confirm if you still want to set Max HTLC % > 0."); if (!confirmed) { diff --git a/cmd/psweb/templates/bitcoin.gohtml b/cmd/psweb/templates/bitcoin.gohtml index 737eb76..e7cbeca 100644 --- a/cmd/psweb/templates/bitcoin.gohtml +++ b/cmd/psweb/templates/bitcoin.gohtml @@ -138,7 +138,7 @@ Amount is capped at remote channel balance to mimic brute force discovery" for=" {{if .IsExternal}}

1. Please fund this mainchain Bitcoin address externally:

- +

@@ -176,8 +176,8 @@ Amount is capped at remote channel balance to mimic brute force discovery" for="
@@ -213,7 +212,12 @@ Amount is capped at remote channel balance to mimic brute force discovery" for=" TxId: {{if .IsClaimJoin}} @@ -736,10 +740,10 @@ Amount is capped at remote channel balance to mimic brute force discovery" for=" let hours = "17 hours"; {{if .CanClaimJoin}} if (document.getElementById("claimJoin").checked) { - hours = "17-{{.ClaimJoinETA}} hours"; + hours = "17-{{.ClaimJoinHours}} hours"; } {{end}} - text += "\nClaim ETA: " + hours; + text += "\nDuration: " + hours; } if ({{.BitcoinBalance}} - peginAmount < 25000) { @@ -768,7 +772,7 @@ Amount is capped at remote channel balance to mimic brute force discovery" for=" {{else}} - +

diff --git a/cmd/psweb/templates/liquid.gohtml b/cmd/psweb/templates/liquid.gohtml index 0f17a6e..6e2c110 100644 --- a/cmd/psweb/templates/liquid.gohtml +++ b/cmd/psweb/templates/liquid.gohtml @@ -188,7 +188,7 @@ Amount is capped at remote channel balance to mimic brute force discovery" for="
Time In OutAmtAmount PPM
{{.TimeAgo}} {{.AliasIn}} {{.AliasOut}}{{m .Amount}}{{m .Amount}} {{fmt .PPM}}
{{if eq .Confirmations -1}} - Transaction not found in local mempool! - {{if .IsPegin}}

Provide another TxId or cancel the peg-in. + Transaction not found in mempool! Refresh this page to search again. + {{if .IsPegin}}

If you fee bumped an external funding, TxId may have changed.

@@ -188,7 +188,6 @@ Amount is capped at remote channel balance to mimic brute force discovery" for="
-
{{end}} @@ -203,7 +202,7 @@ Amount is capped at remote channel balance to mimic brute force discovery" for=" ETA:
- {{.Duration}} + {{.ETA}} {{end}} {{end}} - {{.PeginTxId}} + {{.PeginTxId}} + + 📄📄 +
-

TxId: {{.TxId}}

+

TxId: {{.TxId}}


@@ -232,7 +232,7 @@ Amount is capped at remote channel balance to mimic brute force discovery" for=" {{else}} - +

diff --git a/cmd/psweb/templates/peer.gohtml b/cmd/psweb/templates/peer.gohtml index 2464489..bc9446a 100644 --- a/cmd/psweb/templates/peer.gohtml +++ b/cmd/psweb/templates/peer.gohtml @@ -351,18 +351,18 @@ if (asset == "btc") { claimPadding = 200; // BTC padding hardcoded here: // https://github.com/ElementsProject/peerswap/blob/89d79f7bf2ea0d5bec61933a2f8438947250db70/onchain/bitcoin.go#L224 - vbyteSize = 350; // onchain.EstimatedOpeningTxSize - title = "Assumed opening transaction size 350 vB"; + vbyteSize = {{.OPENING_TX_SIZE_BTC}}; // onchain.EstimatedOpeningTxSize + title = "Assumed opening transaction size {{.OPENING_TX_SIZE_BTC}} vB"; } else { {{if .HasDiscountedvSize}} claimSize = 330; - vbyteSize = 750; // prepaid - title = "Assumed opening transaction size: 750 discounded vB"; + vbyteSize = {{.OPENING_TX_SIZE_LBTC_DISCOUNTED}}; // prepaid + title = "Assumed opening transaction size: {{.OPENING_TX_SIZE_LBTC_DISCOUNTED}} discounted vB"; discount =" discount"; {{else}} claimSize = 1350; - vbyteSize = 3000; // prepaid - title = "Assumed opening transaction size: 3000 vB"; + vbyteSize = {{.OPENING_TX_SIZE_LBTC}}; // prepaid + title = "Assumed opening transaction size: {{.OPENING_TX_SIZE_LBTC}} vB"; {{end}} } diff --git a/cmd/psweb/templates/reusable.gohtml b/cmd/psweb/templates/reusable.gohtml index ad03b0f..700aba9 100644 --- a/cmd/psweb/templates/reusable.gohtml +++ b/cmd/psweb/templates/reusable.gohtml @@ -72,19 +72,39 @@ }); } - function copyToClipboard(inputName) { - // Get the text to copy - var copyText = document.getElementById(inputName); + function copyToClipboard(elementId) { + var element = document.getElementById(elementId); - // Select the text - copyText.select(); - copyText.setSelectionRange(0, 99999); // For mobile devices + if (!element) { + console.error("Element not found:", elementId); + return; + } + + var textToCopy = ""; + + // If the element is an tag, get its href + if (element.tagName === "A") { + textToCopy = element.href; + } + // If it's an input or textarea, get its value + else if (element.tagName === "INPUT" || element.tagName === "TEXTAREA") { + textToCopy = element.value; + } + // Otherwise, get the inner text + else { + textToCopy = element.innerText; + } - // Copy the selected text + // Copy the text to clipboard + var tempTextArea = document.createElement("textarea"); + tempTextArea.value = textToCopy; + document.body.appendChild(tempTextArea); + tempTextArea.select(); document.execCommand("copy"); + document.body.removeChild(tempTextArea); // Alert the user that the text has been copied - displayTemporaryMessage("Copied the address to clipboard"); + displayTemporaryMessage("Copied to clipboard"); } function displayTemporaryMessage(msg) {