365 lines
13 KiB
Go
365 lines
13 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"git.gammaspectra.live/P2Pool/consensus/v3/monero"
|
|
"git.gammaspectra.live/P2Pool/consensus/v3/monero/address"
|
|
"git.gammaspectra.live/P2Pool/consensus/v3/p2pool/sidechain"
|
|
"git.gammaspectra.live/P2Pool/consensus/v3/types"
|
|
"git.gammaspectra.live/P2Pool/consensus/v3/utils"
|
|
"git.gammaspectra.live/P2Pool/observer-cmd-utils/index"
|
|
cmdutils "git.gammaspectra.live/P2Pool/observer-cmd-utils/utils"
|
|
hbot "github.com/whyrusleeping/hellabot"
|
|
"regexp"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type command struct {
|
|
Help string
|
|
Description string
|
|
Match *regexp.Regexp
|
|
Handle func(db *DB, entries []*channelEntry, bot *hbot.Bot, message *hbot.Message, replyTo string, matches ...string) bool
|
|
}
|
|
|
|
var guestUserRegex = regexp.MustCompile("^Guest[0-9]+_*$")
|
|
|
|
func isNickAllowed(nick string) error {
|
|
if guestUserRegex.MatchString(nick) {
|
|
return errors.New("guest user is not allowed")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func filterEntriesForChannel(bot *hbot.Bot, message *hbot.Message, entries []*channelEntry, exactMatches ...string) (result []*channelEntry) {
|
|
var actualEntry *channelEntry
|
|
var exactMatchesResult []*channelEntry
|
|
for _, e := range entries {
|
|
if e.Channel == message.To {
|
|
actualEntry = e
|
|
}
|
|
if e.ApiEndpoint != "" && e.Name != "" && slices.ContainsFunc(exactMatches, func(s string) bool {
|
|
return s == strings.ToLower(e.Name)
|
|
}) {
|
|
exactMatchesResult = append(exactMatchesResult, e)
|
|
}
|
|
if e.ApiEndpoint != "" && (message.To == bot.Nick || e.Channel == message.To) {
|
|
result = append(result, e)
|
|
}
|
|
}
|
|
if len(exactMatchesResult) > 0 {
|
|
return exactMatchesResult
|
|
}
|
|
if len(result) == 0 && actualEntry != nil && actualEntry.ApiEndpoint == "" {
|
|
for _, e := range entries {
|
|
if e.ApiEndpoint != "" {
|
|
result = append(result, e)
|
|
}
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
var commands = []command{
|
|
{
|
|
Help: ".last [pool name]",
|
|
Description: "Displays the last time each pool found a Monero block, and other metrics.",
|
|
Match: regexp.MustCompile("(?i)^\\.(last|block|lastblock|pool)[ \t]*([a-z ]+)?[ \t]*"),
|
|
Handle: func(db *DB, entries []*channelEntry, bot *hbot.Bot, message *hbot.Message, replyTo string, matches ...string) bool {
|
|
|
|
for _, e := range filterEntriesForChannel(bot, message, entries, strings.TrimSpace(strings.ToLower(matches[2]))) {
|
|
func(e *channelEntry) {
|
|
e.ChainLock.RLock()
|
|
defer e.ChainLock.RUnlock()
|
|
|
|
var lastFound, previous *index.FoundBlock
|
|
if len(e.PreviousBlocks) > 0 {
|
|
lastFound = e.PreviousBlocks[len(e.PreviousBlocks)-1]
|
|
previous = e.PreviousBlocks.GetPrevious(lastFound)
|
|
}
|
|
poolInfo := getPoolInfo(e.ApiEndpoint)
|
|
|
|
effort := float64(0)
|
|
if previous != nil {
|
|
effort = float64(lastFound.CumulativeDifficulty.Sub(previous.CumulativeDifficulty).Mul64(100).Lo) / float64(lastFound.MainBlock.Difficulty)
|
|
}
|
|
|
|
currentEffort := toFloat64(poolInfo.SideChain.Effort.Current)
|
|
|
|
bot.Msg(replyTo, fmt.Sprintf(
|
|
"Pool %s, last block found at height %s%d%s %s, %s UTC :: %s :: Effort %s%.02f%%%s :: %s%d miner outputs%s paid for %s%s%s XMR%s :: Current Effort %s%.02f%%%s :: Pool height %d :: Pool Hashrate %sH/s :: Global hashrate %sH/s",
|
|
e.Name,
|
|
FormatColorRed, lastFound.MainBlock.Height, FormatReset,
|
|
TimeElapsed(lastFound.MainBlock.Timestamp),
|
|
time.Unix(int64(lastFound.MainBlock.Timestamp), 0).UTC().Format(time.DateTime),
|
|
GetShareLink(e.ApiEndpoint, lastFound.SideHeight, lastFound.MainBlock.Id),
|
|
EffortColor(effort), effort, FormatReset,
|
|
FormatColorOrange, lastFound.WindowOutputs, FormatReset,
|
|
FormatColorOrange, FormatBold, utils.XMRUnits(lastFound.MainBlock.Reward), FormatReset,
|
|
EffortColor(currentEffort), currentEffort, FormatReset,
|
|
e.Tip.SideHeight,
|
|
utils.SiUnits(float64(types.DifficultyFrom64(e.Tip.Difficulty).Div64(e.Consensus.TargetBlockTime).Lo), 2),
|
|
utils.SiUnits(float64(poolInfo.MainChain.Difficulty.Div64(monero.BlockTime).Lo), 2),
|
|
))
|
|
}(e)
|
|
}
|
|
return true
|
|
},
|
|
},
|
|
{
|
|
Help: ".status [pool name]",
|
|
Description: "Displays your pool and share status across all subscriptions to your nick",
|
|
Match: regexp.MustCompile("(?i)^\\.(status|shares)[ \t]*([a-z ]+)?[ \t]*"),
|
|
Handle: func(db *DB, entries []*channelEntry, bot *hbot.Bot, message *hbot.Message, replyTo string, matches ...string) bool {
|
|
subs := db.GetByNick(message.Name)
|
|
if len(subs) == 0 {
|
|
bot.Msg(replyTo, "No known subscriptions to your nick.")
|
|
return true
|
|
}
|
|
type result struct {
|
|
Name string
|
|
Endpoint string
|
|
ShareCount int
|
|
UncleCount int
|
|
YourWeight types.Difficulty
|
|
TotalWeight types.Difficulty
|
|
Tip *index.SideBlock
|
|
Address *address.Address
|
|
MinerId uint64
|
|
SharesPosition *cmdutils.PositionChart
|
|
UnclesPosition *cmdutils.PositionChart
|
|
Consensus *sidechain.Consensus
|
|
}
|
|
|
|
var hasResults bool
|
|
var results []*result
|
|
|
|
for _, e := range filterEntriesForChannel(bot, message, entries, strings.TrimSpace(strings.ToLower(matches[2]))) {
|
|
func(e *channelEntry) {
|
|
e.ChainLock.RLock()
|
|
defer e.ChainLock.RUnlock()
|
|
|
|
var tr []*result
|
|
for _, sub := range subs {
|
|
var r result
|
|
r.Name = e.Name
|
|
r.Tip = e.Tip
|
|
r.Endpoint = e.ApiEndpoint
|
|
r.Address = sub.Address
|
|
r.Consensus = e.Consensus
|
|
|
|
r.SharesPosition = cmdutils.NewPositionChart(30, uint64(e.Tip.WindowDepth))
|
|
r.UnclesPosition = cmdutils.NewPositionChart(30, uint64(e.Tip.WindowDepth))
|
|
tr = append(tr, &r)
|
|
}
|
|
|
|
e.IterateWindow(func(b *index.SideBlock, weight types.Difficulty) {
|
|
for _, r := range tr {
|
|
r.TotalWeight = r.TotalWeight.Add(weight)
|
|
if b.MinerAddress.Compare(r.Address) == 0 {
|
|
r.MinerId = b.Miner
|
|
r.YourWeight = r.YourWeight.Add(weight)
|
|
if b.IsUncle() {
|
|
r.UnclesPosition.Add(int(e.Tip.SideHeight-b.SideHeight), 1)
|
|
r.UncleCount++
|
|
} else {
|
|
r.SharesPosition.Add(int(e.Tip.SideHeight-b.SideHeight), 1)
|
|
r.ShareCount++
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
for _, r := range tr {
|
|
if r.ShareCount > 0 || r.UncleCount > 0 {
|
|
results = append(results, r)
|
|
hasResults = true
|
|
}
|
|
}
|
|
|
|
}(e)
|
|
}
|
|
|
|
if !hasResults {
|
|
bot.Msg(replyTo, "You do not currently have any shares within the PPLNS window across the tracked pools.")
|
|
} else {
|
|
for _, r := range results {
|
|
ratio := float64(r.YourWeight.Lo) / float64(r.TotalWeight.Lo)
|
|
bot.Msg(replyTo, fmt.Sprintf(
|
|
"Pool %s, your shares %d (+%d uncles) ~%.03f%% %sH/s :: Miner Address %s%s%s :: Miner Statistics %s/m/%s :: Shares/Uncles position %s %s",
|
|
r.Name,
|
|
r.ShareCount,
|
|
r.UncleCount,
|
|
ratio*100,
|
|
utils.SiUnits(ratio*float64(types.DifficultyFrom64(r.Tip.Difficulty).Div64(r.Consensus.TargetBlockTime).Lo), 2),
|
|
FormatItalic, cmdutils.Shorten(string(r.Address.ToBase58()), 10), FormatReset,
|
|
r.Endpoint,
|
|
cmdutils.EncodeBinaryNumber(r.MinerId),
|
|
r.SharesPosition.String(),
|
|
r.UnclesPosition.String(),
|
|
))
|
|
}
|
|
}
|
|
return true
|
|
|
|
},
|
|
},
|
|
{
|
|
Help: ".payout [pool name]",
|
|
Description: "Displays your last payout details across all pools",
|
|
Match: regexp.MustCompile("(?i)^\\.(payouts?|payments?|last-payments?)[ \t]*([a-z ]+)?[ \t]*"),
|
|
Handle: func(db *DB, entries []*channelEntry, bot *hbot.Bot, message *hbot.Message, replyTo string, matches ...string) bool {
|
|
subs := db.GetByNick(message.Name)
|
|
if len(subs) == 0 {
|
|
bot.Msg(replyTo, "No known subscriptions to your nick.")
|
|
return true
|
|
}
|
|
|
|
type result struct {
|
|
Name string
|
|
Endpoint string
|
|
Payouts []*index.Payout
|
|
Address *address.Address
|
|
Consensus *sidechain.Consensus
|
|
}
|
|
|
|
var results []*result
|
|
|
|
for _, e := range filterEntriesForChannel(bot, message, entries, strings.TrimSpace(strings.ToLower(matches[2]))) {
|
|
func(e *channelEntry) {
|
|
for _, sub := range subs {
|
|
payouts := getSliceFromAPI[*index.Payout](e.ApiEndpoint, fmt.Sprintf("/api/payouts/%s", sub.Address.ToBase58()))
|
|
if len(payouts) > 0 {
|
|
results = append(results, &result{
|
|
Name: e.Name,
|
|
Endpoint: e.ApiEndpoint,
|
|
Payouts: payouts,
|
|
Address: sub.Address,
|
|
Consensus: e.Consensus,
|
|
})
|
|
}
|
|
}
|
|
}(e)
|
|
}
|
|
|
|
if len(results) == 0 {
|
|
bot.Msg(replyTo, "You do not currently have any previous payouts across the tracked pools.")
|
|
} else {
|
|
for _, r := range results {
|
|
bot.Msg(replyTo, fmt.Sprintf(
|
|
"Pool %s, last payout for address %s%s%s was %s%s%s XMR%s on block %s%d%s %s, %s UTC :: %s :: Verify payout %s",
|
|
r.Name,
|
|
FormatItalic, cmdutils.Shorten(string(r.Address.ToBase58()), 10), FormatReset,
|
|
FormatColorOrange, FormatBold, utils.XMRUnits(r.Payouts[0].Reward), FormatReset,
|
|
FormatColorRed, r.Payouts[0].MainHeight, FormatReset,
|
|
TimeElapsed(r.Payouts[0].Timestamp),
|
|
time.Unix(int64(r.Payouts[0].Timestamp), 0).UTC().Format(time.DateTime),
|
|
GetShareLink(r.Endpoint, r.Payouts[0].SideHeight, r.Payouts[0].MainId),
|
|
GetPayoutLink(r.Endpoint, r.Payouts[0].SideHeight, r.Payouts[0].Index, r.Consensus),
|
|
))
|
|
}
|
|
|
|
}
|
|
return true
|
|
|
|
},
|
|
},
|
|
|
|
{
|
|
Help: ".subscribe MONERO_ADDRESS",
|
|
Description: "Subscribes your nick, while online, to shares and payouts specified by MONERO_ADDRESS. You can have multiple subscriptions.",
|
|
Match: regexp.MustCompile("(?i)^\\.(sub|subscribe)[ \t]+(4[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+)[ \t]*"),
|
|
Handle: func(db *DB, entries []*channelEntry, bot *hbot.Bot, message *hbot.Message, replyTo string, matches ...string) bool {
|
|
if err := isNickAllowed(message.Name); err != nil {
|
|
bot.Msg(replyTo, fmt.Sprintf("Cannot subscribe: %s", err))
|
|
return true
|
|
}
|
|
|
|
addr := address.FromBase58(matches[2])
|
|
if addr == nil {
|
|
bot.Msg(replyTo, "Cannot subscribe: Invalid Monero address")
|
|
return true
|
|
}
|
|
|
|
if err := db.Store(&Subscription{
|
|
Address: addr,
|
|
Nick: message.Name,
|
|
}); err == nil {
|
|
bot.Msg(replyTo, fmt.Sprintf("Subscribed your nick to shares found by %s%s%s while you are online. You can private message this bot for any commands instead of using public channels.", FormatItalic, cmdutils.Shorten(string(addr.ToBase58()), 10), FormatReset))
|
|
for _, e := range entries {
|
|
if e.ApiEndpoint == "" {
|
|
continue
|
|
}
|
|
func() {
|
|
e.ChainLock.RLock()
|
|
defer e.ChainLock.RUnlock()
|
|
|
|
var yourWeight types.Difficulty
|
|
var totalWeight types.Difficulty
|
|
|
|
e.IterateWindow(func(b *index.SideBlock, weight types.Difficulty) {
|
|
totalWeight = totalWeight.Add(weight)
|
|
if b.MinerAddress.Compare(addr) == 0 {
|
|
yourWeight = yourWeight.Add(weight)
|
|
}
|
|
})
|
|
|
|
shareRatio := float64(yourWeight.Lo) / float64(totalWeight.Lo)
|
|
if shareRatio > notificationPoolShare { //warn about spammy notifications
|
|
bot.Msg(replyTo, fmt.Sprintf("You have more than %.01f%% of the %s pool total share weight (%s) with %.03f%%. Share notifications will not be sent above this threshold. Consider using the /api/events interface directly.", notificationPoolShare*100, e.Name, e.ApiEndpoint, shareRatio*100))
|
|
}
|
|
}()
|
|
}
|
|
return true
|
|
} else {
|
|
bot.Msg(replyTo, fmt.Sprintf("Cannot subscribe: %s", err))
|
|
return true
|
|
}
|
|
},
|
|
},
|
|
{
|
|
Help: ".unsubscribe MONERO_ADDRESS",
|
|
Description: "Unsubscribes your nick from shares and payouts specified by MONERO_ADDRESS.",
|
|
Match: regexp.MustCompile("(?i)^\\.(unsub|unsubscribe)[ \t]+(4[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+)[ \t]*"),
|
|
Handle: func(db *DB, entries []*channelEntry, bot *hbot.Bot, message *hbot.Message, replyTo string, matches ...string) bool {
|
|
addr := address.FromBase58(matches[2])
|
|
if addr == nil {
|
|
bot.Msg(replyTo, "Cannot unsubscribe: Invalid Monero address")
|
|
return true
|
|
}
|
|
|
|
sub := &Subscription{
|
|
Address: addr,
|
|
Nick: message.Name,
|
|
}
|
|
|
|
if err := db.Unsubscribe(sub); err != nil {
|
|
bot.Msg(replyTo, fmt.Sprintf("Cannot unsubscribe: %s", err))
|
|
return true
|
|
} else {
|
|
bot.Msg(replyTo, fmt.Sprintf("Unsubscribed your nick from shares found by %s%s%s", FormatItalic, cmdutils.Shorten(string(addr.ToBase58()), 10), FormatReset))
|
|
return true
|
|
}
|
|
},
|
|
},
|
|
{
|
|
Help: ".unsubscribe",
|
|
Description: "Unsubscribes from all subscriptions tied to your nick",
|
|
//fallback unsubscribe for everything
|
|
Match: regexp.MustCompile("(?i)^[.!]?(unsub|unsubscribe)[ \t]*"),
|
|
Handle: func(db *DB, entries []*channelEntry, bot *hbot.Bot, message *hbot.Message, replyTo string, matches ...string) bool {
|
|
removed := 0
|
|
|
|
for _, sub := range db.GetByNick(message.Name) {
|
|
db.Unsubscribe(sub)
|
|
removed++
|
|
}
|
|
|
|
bot.Msg(replyTo, fmt.Sprintf("Unsubscribed your nick from %d subscriptions.", removed))
|
|
return true
|
|
},
|
|
},
|
|
}
|