You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1315 lines
38 KiB
1315 lines
38 KiB
package storage |
|
|
|
import ( |
|
"fmt" |
|
"math" |
|
"math/big" |
|
"strconv" |
|
"strings" |
|
"time" |
|
|
|
"gopkg.in/redis.v3" |
|
|
|
"github.com/yuriy0803/open-etc-pool-friends/util" |
|
) |
|
|
|
type Config struct { |
|
Endpoint string `json:"endpoint"` |
|
Password string `json:"password"` |
|
Database int64 `json:"database"` |
|
PoolSize int `json:"poolSize"` |
|
} |
|
|
|
type RedisClient struct { |
|
client *redis.Client |
|
prefix string |
|
pplns int64 |
|
} |
|
|
|
type PoolCharts struct { |
|
Timestamp int64 `json:"x"` |
|
TimeFormat string `json:"timeFormat"` |
|
PoolHash int64 `json:"y"` |
|
} |
|
|
|
type MinerCharts struct { |
|
Timestamp int64 `json:"x"` |
|
TimeFormat string `json:"timeFormat"` |
|
MinerHash int64 `json:"minerHash"` |
|
MinerLargeHash int64 `json:"minerLargeHash"` |
|
WorkerOnline string `json:"workerOnline"` |
|
} |
|
|
|
type PaymentCharts struct { |
|
Timestamp int64 `json:"x"` |
|
TimeFormat string `json:"timeFormat"` |
|
Amount int64 `json:"amount"` |
|
} |
|
|
|
type SumRewardData struct { |
|
Interval int64 `json:"inverval"` |
|
Reward int64 `json:"reward"` |
|
Name string `json:"name"` |
|
Offset int64 `json:"offset"` |
|
} |
|
|
|
type RewardData struct { |
|
Height int64 `json:"blockheight"` |
|
Timestamp int64 `json:"timestamp"` |
|
BlockHash string `json:"blockhash"` |
|
Reward int64 `json:"reward"` |
|
Percent float64 `json:"percent"` |
|
Immature bool `json:"immature"` |
|
} |
|
|
|
type BlockData struct { |
|
Login string `json:"login"` |
|
Height int64 `json:"height"` |
|
Timestamp int64 `json:"timestamp"` |
|
Difficulty int64 `json:"difficulty"` |
|
TotalShares int64 `json:"shares"` |
|
Uncle bool `json:"uncle"` |
|
UncleHeight int64 `json:"uncleHeight"` |
|
Orphan bool `json:"orphan"` |
|
Hash string `json:"hash"` |
|
Nonce string `json:"-"` |
|
PowHash string `json:"-"` |
|
MixDigest string `json:"-"` |
|
Reward *big.Int `json:"-"` |
|
ExtraReward *big.Int `json:"-"` |
|
ImmatureReward string `json:"-"` |
|
RewardString string `json:"reward"` |
|
RoundHeight int64 `json:"-"` |
|
candidateKey string |
|
immatureKey string |
|
} |
|
|
|
func (b *BlockData) RewardInShannon() int64 { |
|
reward := new(big.Int).Div(b.Reward, util.Shannon) |
|
return reward.Int64() |
|
} |
|
|
|
func (b *BlockData) serializeHash() string { |
|
if len(b.Hash) > 0 { |
|
return b.Hash |
|
} else { |
|
return "0x0" |
|
} |
|
} |
|
|
|
func (b *BlockData) RoundKey() string { |
|
return join(b.RoundHeight, b.Hash) |
|
} |
|
|
|
func (b *BlockData) key() string { |
|
return join(b.UncleHeight, b.Orphan, b.Nonce, b.serializeHash(), b.Timestamp, b.Difficulty, b.TotalShares, b.Reward, b.Login) |
|
} |
|
|
|
type Miner struct { |
|
LastBeat int64 `json:"lastBeat"` |
|
HR int64 `json:"hr"` |
|
Offline bool `json:"offline"` |
|
startedAt int64 |
|
} |
|
|
|
type Worker struct { |
|
Miner |
|
TotalHR int64 `json:"hr2"` |
|
} |
|
|
|
func NewRedisClient(cfg *Config, prefix string, pplns int64) *RedisClient { |
|
client := redis.NewClient(&redis.Options{ |
|
Addr: cfg.Endpoint, |
|
Password: cfg.Password, |
|
DB: cfg.Database, |
|
PoolSize: cfg.PoolSize, |
|
}) |
|
return &RedisClient{client: client, prefix: prefix, pplns: pplns} |
|
} |
|
|
|
func (r *RedisClient) Client() *redis.Client { |
|
return r.client |
|
} |
|
|
|
func (r *RedisClient) Check() (string, error) { |
|
return r.client.Ping().Result() |
|
} |
|
|
|
func (r *RedisClient) BgSave() (string, error) { |
|
return r.client.BgSave().Result() |
|
} |
|
|
|
// Always returns list of addresses. If Redis fails it will return empty list. |
|
func (r *RedisClient) GetBlacklist() ([]string, error) { |
|
cmd := r.client.SMembers(r.formatKey("blacklist")) |
|
if cmd.Err() != nil { |
|
return []string{}, cmd.Err() |
|
} |
|
return cmd.Val(), nil |
|
} |
|
|
|
// Always returns list of IPs. If Redis fails it will return empty list. |
|
func (r *RedisClient) GetWhitelist() ([]string, error) { |
|
cmd := r.client.SMembers(r.formatKey("whitelist")) |
|
if cmd.Err() != nil { |
|
return []string{}, cmd.Err() |
|
} |
|
return cmd.Val(), nil |
|
} |
|
|
|
func (r *RedisClient) WritePoolCharts(time1 int64, time2 string, poolHash string) error { |
|
s := join(time1, time2, poolHash) |
|
cmd := r.client.ZAdd(r.formatKey("charts", "pool"), redis.Z{Score: float64(time1), Member: s}) |
|
return cmd.Err() |
|
} |
|
|
|
func (r *RedisClient) WriteMinerCharts(time1 int64, time2, k string, hash, largeHash, workerOnline int64) error { |
|
s := join(time1, time2, hash, largeHash, workerOnline) |
|
cmd := r.client.ZAdd(r.formatKey("charts", "miner", k), redis.Z{Score: float64(time1), Member: s}) |
|
return cmd.Err() |
|
} |
|
|
|
func (r *RedisClient) GetPoolCharts(poolHashLen int64) (stats []*PoolCharts, err error) { |
|
|
|
tx := r.client.Multi() |
|
defer tx.Close() |
|
|
|
now := util.MakeTimestamp() / 1000 |
|
|
|
cmds, err := tx.Exec(func() error { |
|
tx.ZRemRangeByScore(r.formatKey("charts", "pool"), "-inf", fmt.Sprint("(", now-172800)) |
|
tx.ZRevRangeWithScores(r.formatKey("charts", "pool"), 0, poolHashLen) |
|
return nil |
|
}) |
|
|
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
stats = convertPoolChartsResults(cmds[1].(*redis.ZSliceCmd)) |
|
return stats, nil |
|
} |
|
|
|
func convertPoolChartsResults(raw *redis.ZSliceCmd) []*PoolCharts { |
|
var result []*PoolCharts |
|
for _, v := range raw.Val() { |
|
// "Timestamp:TimeFormat:Hash" |
|
pc := PoolCharts{} |
|
pc.Timestamp = int64(v.Score) |
|
str := v.Member.(string) |
|
pc.TimeFormat = str[strings.Index(str, ":")+1 : strings.LastIndex(str, ":")] |
|
pc.PoolHash, _ = strconv.ParseInt(str[strings.LastIndex(str, ":")+1:], 10, 64) |
|
result = append(result, &pc) |
|
} |
|
var reverse []*PoolCharts |
|
for i := len(result) - 1; i >= 0; i-- { |
|
reverse = append(reverse, result[i]) |
|
} |
|
return reverse |
|
} |
|
|
|
func convertMinerChartsResults(raw *redis.ZSliceCmd) []*MinerCharts { |
|
var result []*MinerCharts |
|
for _, v := range raw.Val() { |
|
// "Timestamp:TimeFormat:Hash:largeHash:workerOnline" |
|
mc := MinerCharts{} |
|
mc.Timestamp = int64(v.Score) |
|
str := v.Member.(string) |
|
mc.TimeFormat = strings.Split(str, ":")[1] |
|
mc.MinerHash, _ = strconv.ParseInt(strings.Split(str, ":")[2], 10, 64) |
|
mc.MinerLargeHash, _ = strconv.ParseInt(strings.Split(str, ":")[3], 10, 64) |
|
mc.WorkerOnline = strings.Split(str, ":")[4] |
|
result = append(result, &mc) |
|
} |
|
var reverse []*MinerCharts |
|
for i := len(result) - 1; i >= 0; i-- { |
|
reverse = append(reverse, result[i]) |
|
} |
|
return reverse |
|
} |
|
|
|
func (r *RedisClient) GetAllMinerAccount() (account []string, err error) { |
|
var c int64 |
|
for { |
|
now := util.MakeTimestamp() / 1000 |
|
c, keys, err := r.client.Scan(c, r.formatKey("miners", "*"), now).Result() |
|
|
|
if err != nil { |
|
return account, err |
|
} |
|
for _, key := range keys { |
|
m := strings.Split(key, ":") |
|
//if ( len(m) >= 2 && strings.Index(strings.ToLower(m[2]), "0x") == 0) { |
|
if len(m) >= 2 { |
|
account = append(account, m[2]) |
|
} |
|
} |
|
if c == 0 { |
|
break |
|
} |
|
} |
|
return account, nil |
|
} |
|
|
|
func (r *RedisClient) GetMinerCharts(hashNum int64, login string) (stats []*MinerCharts, err error) { |
|
|
|
tx := r.client.Multi() |
|
defer tx.Close() |
|
now := util.MakeTimestamp() / 1000 |
|
cmds, err := tx.Exec(func() error { |
|
tx.ZRemRangeByScore(r.formatKey("charts", "miner", login), "-inf", fmt.Sprint("(", now-172800)) |
|
tx.ZRevRangeWithScores(r.formatKey("charts", "miner", login), 0, hashNum) |
|
return nil |
|
}) |
|
if err != nil { |
|
return nil, err |
|
} |
|
stats = convertMinerChartsResults(cmds[1].(*redis.ZSliceCmd)) |
|
return stats, nil |
|
} |
|
|
|
func (r *RedisClient) GetPaymentCharts(login string) (stats []*PaymentCharts, err error) { |
|
|
|
tx := r.client.Multi() |
|
defer tx.Close() |
|
cmds, err := tx.Exec(func() error { |
|
tx.ZRevRangeWithScores(r.formatKey("payments", login), 0, 360) |
|
return nil |
|
}) |
|
if err != nil { |
|
return nil, err |
|
} |
|
stats = convertPaymentChartsResults(cmds[0].(*redis.ZSliceCmd)) |
|
//fmt.Println(stats) |
|
return stats, nil |
|
} |
|
|
|
func (r *RedisClient) WriteNodeState(id string, height uint64, diff *big.Int) error { |
|
tx := r.client.Multi() |
|
defer tx.Close() |
|
|
|
now := util.MakeTimestamp() / 1000 |
|
|
|
_, err := tx.Exec(func() error { |
|
tx.HSet(r.formatKey("nodes"), join(id, "name"), id) |
|
tx.HSet(r.formatKey("nodes"), join(id, "height"), strconv.FormatUint(height, 10)) |
|
tx.HSet(r.formatKey("nodes"), join(id, "difficulty"), diff.String()) |
|
tx.HSet(r.formatKey("nodes"), join(id, "lastBeat"), strconv.FormatInt(now, 10)) |
|
return nil |
|
}) |
|
return err |
|
} |
|
|
|
func (r *RedisClient) GetNodeStates() ([]map[string]interface{}, error) { |
|
cmd := r.client.HGetAllMap(r.formatKey("nodes")) |
|
if cmd.Err() != nil { |
|
return nil, cmd.Err() |
|
} |
|
m := make(map[string]map[string]interface{}) |
|
for key, value := range cmd.Val() { |
|
parts := strings.Split(key, ":") |
|
if val, ok := m[parts[0]]; ok { |
|
val[parts[1]] = value |
|
} else { |
|
node := make(map[string]interface{}) |
|
node[parts[1]] = value |
|
m[parts[0]] = node |
|
} |
|
} |
|
v := make([]map[string]interface{}, len(m), len(m)) |
|
i := 0 |
|
for _, value := range m { |
|
v[i] = value |
|
i++ |
|
} |
|
return v, nil |
|
} |
|
|
|
func (r *RedisClient) checkPoWExist(height uint64, params []string) (bool, error) { |
|
// Sweep PoW backlog for previous blocks, we have 3 templates back in RAM |
|
r.client.ZRemRangeByScore(r.formatKey("pow"), "-inf", fmt.Sprint("(", height-8)) |
|
val, err := r.client.ZAdd(r.formatKey("pow"), redis.Z{Score: float64(height), Member: strings.Join(params, ":")}).Result() |
|
return val == 0, err |
|
} |
|
|
|
func (r *RedisClient) WriteShare(login, id string, params []string, diff int64, height uint64, window time.Duration) (bool, error) { |
|
exist, err := r.checkPoWExist(height, params) |
|
if err != nil { |
|
return false, err |
|
} |
|
// Duplicate share, (nonce, powHash, mixDigest) pair exist |
|
if exist { |
|
return true, nil |
|
} |
|
tx := r.client.Multi() |
|
defer tx.Close() |
|
|
|
ms := util.MakeTimestamp() |
|
ts := ms / 1000 |
|
|
|
_, err = tx.Exec(func() error { |
|
r.writeShare(tx, ms, ts, login, id, diff, window) |
|
tx.HIncrBy(r.formatKey("stats"), "roundShares", diff) |
|
return nil |
|
}) |
|
return false, err |
|
} |
|
|
|
func (r *RedisClient) WriteBlock(login, id string, params []string, diff, roundDiff int64, height uint64, window time.Duration) (bool, error) { |
|
exist, err := r.checkPoWExist(height, params) |
|
if err != nil { |
|
return false, err |
|
} |
|
// Duplicate share, (nonce, powHash, mixDigest) pair exist |
|
if exist { |
|
return true, nil |
|
} |
|
tx := r.client.Multi() |
|
defer tx.Close() |
|
|
|
ms := util.MakeTimestamp() |
|
ts := ms / 1000 |
|
|
|
cmds, err := tx.Exec(func() error { |
|
r.writeShare(tx, ms, ts, login, id, diff, window) |
|
tx.HSet(r.formatKey("stats"), "lastBlockFound", strconv.FormatInt(ts, 10)) |
|
tx.HDel(r.formatKey("stats"), "roundShares") |
|
tx.ZIncrBy(r.formatKey("finders"), 1, login) |
|
tx.HIncrBy(r.formatKey("miners", login), "blocksFound", 1) |
|
tx.HGetAllMap(r.formatKey("shares", "roundCurrent")) |
|
tx.Del(r.formatKey("shares", "roundCurrent")) |
|
tx.LRange(r.formatKey("lastshares"), 0, r.pplns) |
|
return nil |
|
}) |
|
if err != nil { |
|
return false, err |
|
} else { |
|
|
|
shares := cmds[len(cmds)-1].(*redis.StringSliceCmd).Val() |
|
|
|
tx2 := r.client.Multi() |
|
defer tx2.Close() |
|
|
|
totalshares := make(map[string]int64) |
|
for _, val := range shares { |
|
totalshares[val] += 1 |
|
} |
|
|
|
_, err := tx2.Exec(func() error { |
|
for k, v := range totalshares { |
|
tx2.HIncrBy(r.formatRound(int64(height), params[0]), k, v) |
|
} |
|
return nil |
|
}) |
|
if err != nil { |
|
return false, err |
|
} |
|
|
|
sharesMap, _ := cmds[len(cmds)-3].(*redis.StringStringMapCmd).Result() |
|
totalShares := int64(0) |
|
for _, v := range sharesMap { |
|
n, _ := strconv.ParseInt(v, 10, 64) |
|
totalShares += n |
|
} |
|
hashHex := strings.Join(params, ":") |
|
s := join(hashHex, ts, roundDiff, totalShares, login) |
|
cmd := r.client.ZAdd(r.formatKey("blocks", "candidates"), redis.Z{Score: float64(height), Member: s}) |
|
return false, cmd.Err() |
|
} |
|
} |
|
|
|
func (r *RedisClient) writeShare(tx *redis.Multi, ms, ts int64, login, id string, diff int64, expire time.Duration) { |
|
times := int(diff / 1000000000) |
|
for i := 0; i < times; i++ { |
|
tx.LPush(r.formatKey("lastshares"), login) |
|
} |
|
tx.LTrim(r.formatKey("lastshares"), 0, r.pplns) |
|
tx.HIncrBy(r.formatKey("shares", "roundCurrent"), login, diff) |
|
tx.ZAdd(r.formatKey("hashrate"), redis.Z{Score: float64(ts), Member: join(diff, login, id, ms)}) |
|
tx.ZAdd(r.formatKey("hashrate", login), redis.Z{Score: float64(ts), Member: join(diff, id, ms)}) |
|
tx.Expire(r.formatKey("hashrate", login), expire) // Will delete hashrates for miners that gone |
|
tx.HSet(r.formatKey("miners", login), "lastShare", strconv.FormatInt(ts, 10)) |
|
} |
|
|
|
func (r *RedisClient) formatKey(args ...interface{}) string { |
|
return join(r.prefix, join(args...)) |
|
} |
|
|
|
func (r *RedisClient) formatRound(height int64, nonce string) string { |
|
return r.formatKey("shares", "round"+strconv.FormatInt(height, 10), nonce) |
|
} |
|
|
|
func join(args ...interface{}) string { |
|
s := make([]string, len(args)) |
|
for i, v := range args { |
|
switch v.(type) { |
|
case string: |
|
s[i] = v.(string) |
|
case int64: |
|
s[i] = strconv.FormatInt(v.(int64), 10) |
|
case uint64: |
|
s[i] = strconv.FormatUint(v.(uint64), 10) |
|
case float64: |
|
s[i] = strconv.FormatFloat(v.(float64), 'f', 0, 64) |
|
case bool: |
|
if v.(bool) { |
|
s[i] = "1" |
|
} else { |
|
s[i] = "0" |
|
} |
|
case *big.Rat: |
|
x := v.(*big.Rat) |
|
if x != nil { |
|
s[i] = x.FloatString(9) |
|
} else { |
|
s[i] = "0" |
|
} |
|
case *big.Int: |
|
n := v.(*big.Int) |
|
if n != nil { |
|
s[i] = n.String() |
|
} else { |
|
s[i] = "0" |
|
} |
|
default: |
|
panic("Invalid type specified for conversion") |
|
} |
|
} |
|
return strings.Join(s, ":") |
|
} |
|
|
|
func (r *RedisClient) GetCandidates(maxHeight int64) ([]*BlockData, error) { |
|
option := redis.ZRangeByScore{Min: "0", Max: strconv.FormatInt(maxHeight, 10)} |
|
cmd := r.client.ZRangeByScoreWithScores(r.formatKey("blocks", "candidates"), option) |
|
if cmd.Err() != nil { |
|
return nil, cmd.Err() |
|
} |
|
return convertCandidateResults(cmd), nil |
|
} |
|
|
|
func (r *RedisClient) GetImmatureBlocks(maxHeight int64) ([]*BlockData, error) { |
|
option := redis.ZRangeByScore{Min: "0", Max: strconv.FormatInt(maxHeight, 10)} |
|
cmd := r.client.ZRangeByScoreWithScores(r.formatKey("blocks", "immature"), option) |
|
if cmd.Err() != nil { |
|
return nil, cmd.Err() |
|
} |
|
return convertBlockResults(cmd), nil |
|
} |
|
|
|
func (r *RedisClient) GetRewards(login string) ([]*RewardData, error) { |
|
option := redis.ZRangeByScore{Min: "0", Max: strconv.FormatInt(10, 10)} |
|
cmd := r.client.ZRangeByScoreWithScores(r.formatKey("rewards", login), option) |
|
if cmd.Err() != nil { |
|
return nil, cmd.Err() |
|
} |
|
return convertRewardResults(cmd), nil |
|
} |
|
|
|
func (r *RedisClient) GetRoundShares(height int64, nonce string) (map[string]int64, error) { |
|
result := make(map[string]int64) |
|
cmd := r.client.HGetAllMap(r.formatRound(height, nonce)) |
|
if cmd.Err() != nil { |
|
return nil, cmd.Err() |
|
} |
|
sharesMap, _ := cmd.Result() |
|
for login, v := range sharesMap { |
|
n, _ := strconv.ParseInt(v, 10, 64) |
|
result[login] = n |
|
} |
|
return result, nil |
|
} |
|
|
|
func (r *RedisClient) GetPayees() ([]string, error) { |
|
payees := make(map[string]struct{}) |
|
var result []string |
|
var c int64 |
|
|
|
for { |
|
var keys []string |
|
var err error |
|
c, keys, err = r.client.Scan(c, r.formatKey("miners", "*"), 100).Result() |
|
if err != nil { |
|
return nil, err |
|
} |
|
for _, row := range keys { |
|
login := strings.Split(row, ":")[2] |
|
payees[login] = struct{}{} |
|
} |
|
if c == 0 { |
|
break |
|
} |
|
} |
|
for login, _ := range payees { |
|
result = append(result, login) |
|
} |
|
return result, nil |
|
} |
|
|
|
func (r *RedisClient) GetTotalShares() (int64, error) { |
|
cmd := r.client.LLen(r.formatKey("lastshares")) |
|
if cmd.Err() == redis.Nil { |
|
return 0, nil |
|
} else if cmd.Err() != nil { |
|
return 0, cmd.Err() |
|
} |
|
return cmd.Val(), nil |
|
} |
|
|
|
func (r *RedisClient) GetBalance(login string) (int64, error) { |
|
cmd := r.client.HGet(r.formatKey("miners", login), "balance") |
|
if cmd.Err() == redis.Nil { |
|
return 0, nil |
|
} else if cmd.Err() != nil { |
|
return 0, cmd.Err() |
|
} |
|
return cmd.Int64() |
|
} |
|
|
|
func (r *RedisClient) LockPayouts(login string, amount int64) error { |
|
key := r.formatKey("payments", "lock") |
|
result := r.client.SetNX(key, join(login, amount), 0).Val() |
|
if !result { |
|
return fmt.Errorf("Unable to acquire lock '%s'", key) |
|
} |
|
return nil |
|
} |
|
|
|
func (r *RedisClient) UnlockPayouts() error { |
|
key := r.formatKey("payments", "lock") |
|
_, err := r.client.Del(key).Result() |
|
return err |
|
} |
|
|
|
func (r *RedisClient) IsPayoutsLocked() (bool, error) { |
|
_, err := r.client.Get(r.formatKey("payments", "lock")).Result() |
|
if err == redis.Nil { |
|
return false, nil |
|
} else if err != nil { |
|
return false, err |
|
} else { |
|
return true, nil |
|
} |
|
} |
|
|
|
type PendingPayment struct { |
|
Timestamp int64 `json:"timestamp"` |
|
Amount int64 `json:"amount"` |
|
Address string `json:"login"` |
|
} |
|
|
|
func (r *RedisClient) GetPendingPayments() []*PendingPayment { |
|
raw := r.client.ZRevRangeWithScores(r.formatKey("payments", "pending"), 0, -1) |
|
var result []*PendingPayment |
|
for _, v := range raw.Val() { |
|
// timestamp -> "address:amount" |
|
payment := PendingPayment{} |
|
payment.Timestamp = int64(v.Score) |
|
fields := strings.Split(v.Member.(string), ":") |
|
payment.Address = fields[0] |
|
payment.Amount, _ = strconv.ParseInt(fields[1], 10, 64) |
|
result = append(result, &payment) |
|
} |
|
return result |
|
} |
|
|
|
// Deduct miner's balance for payment |
|
func (r *RedisClient) UpdateBalance(login string, amount int64) error { |
|
tx := r.client.Multi() |
|
defer tx.Close() |
|
|
|
ts := util.MakeTimestamp() / 1000 |
|
|
|
_, err := tx.Exec(func() error { |
|
tx.HIncrBy(r.formatKey("miners", login), "balance", (amount * -1)) |
|
tx.HIncrBy(r.formatKey("miners", login), "pending", amount) |
|
tx.HIncrBy(r.formatKey("finances"), "balance", (amount * -1)) |
|
tx.HIncrBy(r.formatKey("finances"), "pending", amount) |
|
tx.ZAdd(r.formatKey("payments", "pending"), redis.Z{Score: float64(ts), Member: join(login, amount)}) |
|
return nil |
|
}) |
|
return err |
|
} |
|
|
|
func (r *RedisClient) RollbackBalance(login string, amount int64) error { |
|
tx := r.client.Multi() |
|
defer tx.Close() |
|
|
|
_, err := tx.Exec(func() error { |
|
tx.HIncrBy(r.formatKey("miners", login), "balance", amount) |
|
tx.HIncrBy(r.formatKey("miners", login), "pending", (amount * -1)) |
|
tx.HIncrBy(r.formatKey("finances"), "balance", amount) |
|
tx.HIncrBy(r.formatKey("finances"), "pending", (amount * -1)) |
|
tx.ZRem(r.formatKey("payments", "pending"), join(login, amount)) |
|
return nil |
|
}) |
|
return err |
|
} |
|
|
|
func (r *RedisClient) WritePayment(login, txHash string, amount int64) error { |
|
tx := r.client.Multi() |
|
defer tx.Close() |
|
|
|
ts := util.MakeTimestamp() / 1000 |
|
|
|
_, err := tx.Exec(func() error { |
|
tx.HIncrBy(r.formatKey("miners", login), "pending", (amount * -1)) |
|
tx.HIncrBy(r.formatKey("miners", login), "paid", amount) |
|
tx.HIncrBy(r.formatKey("finances"), "pending", (amount * -1)) |
|
tx.HIncrBy(r.formatKey("finances"), "paid", amount) |
|
tx.ZAdd(r.formatKey("payments", "all"), redis.Z{Score: float64(ts), Member: join(txHash, login, amount)}) |
|
tx.ZRemRangeByRank(r.formatKey("payments", "all"), 0, -10000) |
|
tx.ZAdd(r.formatKey("payments", login), redis.Z{Score: float64(ts), Member: join(txHash, amount)}) |
|
tx.ZRemRangeByRank(r.formatKey("payments", login), 0, -100) |
|
tx.ZRem(r.formatKey("payments", "pending"), join(login, amount)) |
|
tx.Del(r.formatKey("payments", "lock")) |
|
tx.HIncrBy(r.formatKey("paymentsTotal"), "all", 1) |
|
tx.HIncrBy(r.formatKey("paymentsTotal"), login, 1) |
|
return nil |
|
}) |
|
return err |
|
} |
|
|
|
func (r *RedisClient) WriteReward(login string, amount int64, percent *big.Rat, immature bool, block *BlockData) error { |
|
if amount <= 0 { |
|
return nil |
|
} |
|
tx := r.client.Multi() |
|
defer tx.Close() |
|
|
|
addStr := join(amount, percent, immature, block.Hash, block.Height, block.Timestamp) |
|
remStr := join(amount, percent, !immature, block.Hash, block.Height, block.Timestamp) |
|
remscore := block.Timestamp - 3600*24*40 // Store the last 40 Days |
|
|
|
_, err := tx.Exec(func() error { |
|
tx.ZAdd(r.formatKey("rewards", login), redis.Z{Score: float64(block.Timestamp), Member: addStr}) |
|
tx.ZRem(r.formatKey("rewards", login), remStr) |
|
tx.ZRemRangeByScore(r.formatKey("rewards", login), "-inf", "("+strconv.FormatInt(remscore, 10)) |
|
|
|
return nil |
|
}) |
|
return err |
|
} |
|
|
|
func (r *RedisClient) WriteImmatureBlock(block *BlockData, roundRewards map[string]int64) error { |
|
tx := r.client.Multi() |
|
defer tx.Close() |
|
|
|
_, err := tx.Exec(func() error { |
|
r.writeImmatureBlock(tx, block) |
|
total := int64(0) |
|
for login, amount := range roundRewards { |
|
total += amount |
|
tx.HIncrBy(r.formatKey("miners", login), "immature", amount) |
|
tx.HSetNX(r.formatKey("credits", "immature", block.Height, block.Hash), login, strconv.FormatInt(amount, 10)) |
|
} |
|
tx.HIncrBy(r.formatKey("finances"), "immature", total) |
|
return nil |
|
}) |
|
return err |
|
} |
|
|
|
func (r *RedisClient) WriteMaturedBlock(block *BlockData, roundRewards map[string]int64) error { |
|
creditKey := r.formatKey("credits", "immature", block.RoundHeight, block.Hash) |
|
tx, err := r.client.Watch(creditKey) |
|
// Must decrement immatures using existing log entry |
|
immatureCredits := tx.HGetAllMap(creditKey) |
|
if err != nil { |
|
return err |
|
} |
|
defer tx.Close() |
|
|
|
ts := util.MakeTimestamp() / 1000 |
|
value := join(block.Hash, ts, block.Reward) |
|
|
|
_, err = tx.Exec(func() error { |
|
r.writeMaturedBlock(tx, block) |
|
tx.ZAdd(r.formatKey("credits", "all"), redis.Z{Score: float64(block.Height), Member: value}) |
|
|
|
// Decrement immature balances |
|
totalImmature := int64(0) |
|
for login, amountString := range immatureCredits.Val() { |
|
amount, _ := strconv.ParseInt(amountString, 10, 64) |
|
totalImmature += amount |
|
tx.HIncrBy(r.formatKey("miners", login), "immature", (amount * -1)) |
|
} |
|
|
|
// Increment balances |
|
total := int64(0) |
|
for login, amount := range roundRewards { |
|
total += amount |
|
// NOTICE: Maybe expire round reward entry in 604800 (a week)? |
|
tx.HIncrBy(r.formatKey("miners", login), "balance", amount) |
|
tx.HSetNX(r.formatKey("credits", block.Height, block.Hash), login, strconv.FormatInt(amount, 10)) |
|
} |
|
tx.Del(creditKey) |
|
tx.HIncrBy(r.formatKey("finances"), "balance", total) |
|
tx.HIncrBy(r.formatKey("finances"), "immature", (totalImmature * -1)) |
|
tx.HSet(r.formatKey("finances"), "lastCreditHeight", strconv.FormatInt(block.Height, 10)) |
|
tx.HSet(r.formatKey("finances"), "lastCreditHash", block.Hash) |
|
tx.HIncrBy(r.formatKey("finances"), "totalMined", block.RewardInShannon()) |
|
tx.Expire(r.formatKey("credits", block.Height, block.Hash), 604800*time.Second) |
|
return nil |
|
}) |
|
return err |
|
} |
|
|
|
func (r *RedisClient) WriteOrphan(block *BlockData) error { |
|
creditKey := r.formatKey("credits", "immature", block.RoundHeight, block.Hash) |
|
tx, err := r.client.Watch(creditKey) |
|
// Must decrement immatures using existing log entry |
|
immatureCredits := tx.HGetAllMap(creditKey) |
|
if err != nil { |
|
return err |
|
} |
|
defer tx.Close() |
|
|
|
_, err = tx.Exec(func() error { |
|
r.writeMaturedBlock(tx, block) |
|
|
|
// Decrement immature balances |
|
totalImmature := int64(0) |
|
for login, amountString := range immatureCredits.Val() { |
|
amount, _ := strconv.ParseInt(amountString, 10, 64) |
|
totalImmature += amount |
|
tx.HIncrBy(r.formatKey("miners", login), "immature", (amount * -1)) |
|
} |
|
tx.Del(creditKey) |
|
tx.HIncrBy(r.formatKey("finances"), "immature", (totalImmature * -1)) |
|
return nil |
|
}) |
|
return err |
|
} |
|
|
|
func (r *RedisClient) WritePendingOrphans(blocks []*BlockData) error { |
|
tx := r.client.Multi() |
|
defer tx.Close() |
|
|
|
_, err := tx.Exec(func() error { |
|
for _, block := range blocks { |
|
r.writeImmatureBlock(tx, block) |
|
} |
|
return nil |
|
}) |
|
return err |
|
} |
|
|
|
func (r *RedisClient) writeImmatureBlock(tx *redis.Multi, block *BlockData) { |
|
// Redis 2.8.x returns "ERR source and destination objects are the same" |
|
if block.Height != block.RoundHeight { |
|
tx.Rename(r.formatRound(block.RoundHeight, block.Nonce), r.formatRound(block.Height, block.Nonce)) |
|
} |
|
tx.ZRem(r.formatKey("blocks", "candidates"), block.candidateKey) |
|
tx.ZAdd(r.formatKey("blocks", "immature"), redis.Z{Score: float64(block.Height), Member: block.key()}) |
|
} |
|
|
|
func (r *RedisClient) writeMaturedBlock(tx *redis.Multi, block *BlockData) { |
|
tx.Del(r.formatRound(block.RoundHeight, block.Nonce)) |
|
tx.ZRem(r.formatKey("blocks", "immature"), block.immatureKey) |
|
tx.ZAdd(r.formatKey("blocks", "matured"), redis.Z{Score: float64(block.Height), Member: block.key()}) |
|
} |
|
|
|
func (r *RedisClient) IsMinerExists(login string) (bool, error) { |
|
return r.client.Exists(r.formatKey("miners", login)).Result() |
|
} |
|
|
|
func (r *RedisClient) GetMinerStats(login string, maxPayments int64) (map[string]interface{}, error) { |
|
stats := make(map[string]interface{}) |
|
|
|
tx := r.client.Multi() |
|
defer tx.Close() |
|
|
|
cmds, err := tx.Exec(func() error { |
|
tx.HGetAllMap(r.formatKey("miners", login)) |
|
tx.ZRevRangeWithScores(r.formatKey("payments", login), 0, maxPayments-1) |
|
tx.HGet(r.formatKey("paymentsTotal"), login) |
|
tx.HGet(r.formatKey("shares", "currentShares"), login) |
|
tx.LRange(r.formatKey("lastshares"), 0, r.pplns) |
|
tx.ZRevRangeWithScores(r.formatKey("rewards", login), 0, 39) |
|
tx.ZRevRangeWithScores(r.formatKey("rewards", login), 0, -1) |
|
return nil |
|
}) |
|
|
|
if err != nil && err != redis.Nil { |
|
return nil, err |
|
} else { |
|
result, _ := cmds[0].(*redis.StringStringMapCmd).Result() |
|
stats["stats"] = convertStringMap(result) |
|
payments := convertPaymentsResults(cmds[1].(*redis.ZSliceCmd)) |
|
stats["payments"] = payments |
|
stats["paymentsTotal"], _ = cmds[2].(*redis.StringCmd).Int64() |
|
shares := cmds[4].(*redis.StringSliceCmd).Val() |
|
csh := 0 |
|
for _, val := range shares { |
|
if val == login { |
|
csh++ |
|
} |
|
} |
|
stats["roundShares"] = csh |
|
} |
|
|
|
return stats, nil |
|
} |
|
|
|
// Try to convert all numeric strings to int64 |
|
func convertStringMap(m map[string]string) map[string]interface{} { |
|
result := make(map[string]interface{}) |
|
var err error |
|
for k, v := range m { |
|
result[k], err = strconv.ParseInt(v, 10, 64) |
|
if err != nil { |
|
result[k] = v |
|
} |
|
} |
|
return result |
|
} |
|
|
|
// WARNING: Must run it periodically to flush out of window hashrate entries |
|
func (r *RedisClient) FlushStaleStats(window, largeWindow time.Duration) (int64, error) { |
|
now := util.MakeTimestamp() / 1000 |
|
max := fmt.Sprint("(", now-int64(window/time.Second)) |
|
total, err := r.client.ZRemRangeByScore(r.formatKey("hashrate"), "-inf", max).Result() |
|
if err != nil { |
|
return total, err |
|
} |
|
|
|
var c int64 |
|
miners := make(map[string]struct{}) |
|
max = fmt.Sprint("(", now-int64(largeWindow/time.Second)) |
|
|
|
for { |
|
var keys []string |
|
var err error |
|
c, keys, err = r.client.Scan(c, r.formatKey("hashrate", "*"), 100).Result() |
|
if err != nil { |
|
return total, err |
|
} |
|
for _, row := range keys { |
|
login := strings.Split(row, ":")[2] |
|
if _, ok := miners[login]; !ok { |
|
n, err := r.client.ZRemRangeByScore(r.formatKey("hashrate", login), "-inf", max).Result() |
|
if err != nil { |
|
return total, err |
|
} |
|
miners[login] = struct{}{} |
|
total += n |
|
} |
|
} |
|
if c == 0 { |
|
break |
|
} |
|
} |
|
return total, nil |
|
} |
|
|
|
func (r *RedisClient) CollectStats(smallWindow time.Duration, maxBlocks, maxPayments int64) (map[string]interface{}, error) { |
|
window := int64(smallWindow / time.Second) |
|
stats := make(map[string]interface{}) |
|
|
|
tx := r.client.Multi() |
|
defer tx.Close() |
|
|
|
now := util.MakeTimestamp() / 1000 |
|
|
|
cmds, err := tx.Exec(func() error { |
|
tx.ZRemRangeByScore(r.formatKey("hashrate"), "-inf", fmt.Sprint("(", now-window)) |
|
tx.ZRangeWithScores(r.formatKey("hashrate"), 0, -1) |
|
tx.HGetAllMap(r.formatKey("stats")) |
|
tx.ZRevRangeWithScores(r.formatKey("blocks", "candidates"), 0, -1) |
|
tx.ZRevRangeWithScores(r.formatKey("blocks", "immature"), 0, -1) |
|
tx.ZRevRangeWithScores(r.formatKey("blocks", "matured"), 0, maxBlocks-1) |
|
tx.ZCard(r.formatKey("blocks", "candidates")) |
|
tx.ZCard(r.formatKey("blocks", "immature")) |
|
tx.ZCard(r.formatKey("blocks", "matured")) |
|
tx.HGet(r.formatKey("paymentsTotal"), "all") |
|
tx.ZRevRangeWithScores(r.formatKey("payments", "all"), 0, maxPayments-1) |
|
tx.LLen(r.formatKey("lastshares")) |
|
tx.ZRevRangeWithScores(r.formatKey("finders"), 0, -1) |
|
return nil |
|
}) |
|
|
|
if (err != nil) && (err != redis.Nil) { |
|
return nil, err |
|
} |
|
|
|
result, _ := cmds[2].(*redis.StringStringMapCmd).Result() |
|
result["nShares"] = strconv.FormatInt(cmds[11].(*redis.IntCmd).Val(), 10) |
|
stats["stats"] = convertStringMap(result) |
|
candidates := convertCandidateResults(cmds[3].(*redis.ZSliceCmd)) |
|
stats["candidates"] = candidates |
|
stats["candidatesTotal"] = cmds[6].(*redis.IntCmd).Val() |
|
|
|
immature := convertBlockResults(cmds[4].(*redis.ZSliceCmd)) |
|
stats["immature"] = immature |
|
stats["immatureTotal"] = cmds[7].(*redis.IntCmd).Val() |
|
|
|
matured := convertBlockResults(cmds[5].(*redis.ZSliceCmd)) |
|
stats["matured"] = matured |
|
stats["maturedTotal"] = cmds[8].(*redis.IntCmd).Val() |
|
|
|
payments := convertPaymentsResults(cmds[10].(*redis.ZSliceCmd)) |
|
stats["payments"] = payments |
|
stats["paymentsTotal"], _ = cmds[9].(*redis.StringCmd).Int64() |
|
|
|
finders := convertFindersResults(cmds[12].(*redis.ZSliceCmd)) |
|
stats["finders"] = finders |
|
|
|
totalHashrate, miners := convertMinersStats(window, cmds[1].(*redis.ZSliceCmd)) |
|
stats["miners"] = miners |
|
stats["minersTotal"] = len(miners) |
|
stats["hashrate"] = totalHashrate |
|
return stats, nil |
|
} |
|
|
|
func (r *RedisClient) CollectWorkersStats(sWindow, lWindow time.Duration, login string) (map[string]interface{}, error) { |
|
smallWindow := int64(sWindow / time.Second) |
|
largeWindow := int64(lWindow / time.Second) |
|
stats := make(map[string]interface{}) |
|
|
|
tx := r.client.Multi() |
|
defer tx.Close() |
|
|
|
now := util.MakeTimestamp() / 1000 |
|
|
|
cmds, err := tx.Exec(func() error { |
|
tx.ZRemRangeByScore(r.formatKey("hashrate", login), "-inf", fmt.Sprint("(", now-largeWindow)) |
|
tx.ZRangeWithScores(r.formatKey("hashrate", login), 0, -1) |
|
tx.ZRevRangeWithScores(r.formatKey("rewards", login), 0, 39) |
|
tx.ZRevRangeWithScores(r.formatKey("rewards", login), 0, -1) |
|
return nil |
|
}) |
|
|
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
totalHashrate := int64(0) |
|
currentHashrate := int64(0) |
|
online := int64(0) |
|
offline := int64(0) |
|
workers := convertWorkersStats(smallWindow, cmds[1].(*redis.ZSliceCmd)) |
|
|
|
for id, worker := range workers { |
|
timeOnline := now - worker.startedAt |
|
if timeOnline < 600 { |
|
timeOnline = 600 |
|
} |
|
|
|
boundary := timeOnline |
|
if timeOnline >= smallWindow { |
|
boundary = smallWindow |
|
} |
|
worker.HR = worker.HR / boundary |
|
|
|
boundary = timeOnline |
|
if timeOnline >= largeWindow { |
|
boundary = largeWindow |
|
} |
|
worker.TotalHR = worker.TotalHR / boundary |
|
|
|
if worker.LastBeat < (now - smallWindow/2) { |
|
worker.Offline = true |
|
offline++ |
|
} else { |
|
online++ |
|
} |
|
|
|
currentHashrate += worker.HR |
|
totalHashrate += worker.TotalHR |
|
workers[id] = worker |
|
} |
|
stats["workers"] = workers |
|
stats["workersTotal"] = len(workers) |
|
stats["workersOnline"] = online |
|
stats["workersOffline"] = offline |
|
stats["hashrate"] = totalHashrate |
|
stats["currentHashrate"] = currentHashrate |
|
|
|
stats["rewards"] = convertRewardResults(cmds[2].(*redis.ZSliceCmd)) // last 40 |
|
rewards := convertRewardResults(cmds[3].(*redis.ZSliceCmd)) // all |
|
|
|
var dorew []*SumRewardData |
|
dorew = append(dorew, &SumRewardData{Name: "Last 60 minutes", Interval: 3600, Offset: 0}) |
|
dorew = append(dorew, &SumRewardData{Name: "Last 12 hours", Interval: 3600 * 12, Offset: 0}) |
|
dorew = append(dorew, &SumRewardData{Name: "Last 24 hours", Interval: 3600 * 24, Offset: 0}) |
|
dorew = append(dorew, &SumRewardData{Name: "Last 7 days", Interval: 3600 * 24 * 7, Offset: 0}) |
|
dorew = append(dorew, &SumRewardData{Name: "Last 30 days", Interval: 3600 * 24 * 30, Offset: 0}) |
|
|
|
for _, reward := range rewards { |
|
|
|
for _, dore := range dorew { |
|
dore.Reward += 0 |
|
if reward.Timestamp > now-dore.Interval { |
|
dore.Reward += reward.Reward |
|
} |
|
} |
|
} |
|
stats["sumrewards"] = dorew |
|
stats["24hreward"] = dorew[2].Reward |
|
return stats, nil |
|
} |
|
|
|
func (r *RedisClient) CollectLuckStats(windows []int) (map[string]interface{}, error) { |
|
stats := make(map[string]interface{}) |
|
|
|
tx := r.client.Multi() |
|
defer tx.Close() |
|
|
|
max := int64(windows[len(windows)-1]) |
|
|
|
cmds, err := tx.Exec(func() error { |
|
tx.ZRevRangeWithScores(r.formatKey("blocks", "immature"), 0, -1) |
|
tx.ZRevRangeWithScores(r.formatKey("blocks", "matured"), 0, max-1) |
|
return nil |
|
}) |
|
if err != nil { |
|
return stats, err |
|
} |
|
blocks := convertBlockResults(cmds[0].(*redis.ZSliceCmd), cmds[1].(*redis.ZSliceCmd)) |
|
|
|
calcLuck := func(max int) (int, float64, float64, float64) { |
|
var total int |
|
var sharesDiff, uncles, orphans float64 |
|
for i, block := range blocks { |
|
if i > (max - 1) { |
|
break |
|
} |
|
if block.Uncle { |
|
uncles++ |
|
} |
|
if block.Orphan { |
|
orphans++ |
|
} |
|
sharesDiff += float64(block.TotalShares) / float64(block.Difficulty) |
|
total++ |
|
} |
|
if total > 0 { |
|
sharesDiff /= float64(total) |
|
uncles /= float64(total) |
|
orphans /= float64(total) |
|
} |
|
return total, sharesDiff, uncles, orphans |
|
} |
|
for _, max := range windows { |
|
total, sharesDiff, uncleRate, orphanRate := calcLuck(max) |
|
row := map[string]float64{ |
|
"luck": sharesDiff, "uncleRate": uncleRate, "orphanRate": orphanRate, |
|
} |
|
stats[strconv.Itoa(total)] = row |
|
if total < max { |
|
break |
|
} |
|
} |
|
return stats, nil |
|
} |
|
|
|
func convertCandidateResults(raw *redis.ZSliceCmd) []*BlockData { |
|
var result []*BlockData |
|
for _, v := range raw.Val() { |
|
// "nonce:powHash:mixDigest:timestamp:diff:totalShares" |
|
block := BlockData{} |
|
block.Height = int64(v.Score) |
|
block.RoundHeight = block.Height |
|
fields := strings.Split(v.Member.(string), ":") |
|
block.Nonce = fields[0] |
|
block.PowHash = fields[1] |
|
block.MixDigest = fields[2] |
|
block.Timestamp, _ = strconv.ParseInt(fields[3], 10, 64) |
|
block.Difficulty, _ = strconv.ParseInt(fields[4], 10, 64) |
|
block.TotalShares, _ = strconv.ParseInt(fields[5], 10, 64) |
|
block.Login = fields[6] |
|
block.candidateKey = v.Member.(string) |
|
result = append(result, &block) |
|
} |
|
return result |
|
} |
|
|
|
func convertRewardResults(rows ...*redis.ZSliceCmd) []*RewardData { |
|
var result []*RewardData |
|
for _, row := range rows { |
|
for _, v := range row.Val() { |
|
// "amount:percent:immature:block.Hash:block.height" |
|
reward := RewardData{} |
|
reward.Timestamp = int64(v.Score) |
|
fields := strings.Split(v.Member.(string), ":") |
|
//block.UncleHeight, _ = strconv.ParseInt(fields[0], 10, 64) |
|
reward.BlockHash = fields[3] |
|
reward.Reward, _ = strconv.ParseInt(fields[0], 10, 64) |
|
reward.Percent, _ = strconv.ParseFloat(fields[1], 64) |
|
reward.Immature, _ = strconv.ParseBool(fields[2]) |
|
reward.Height, _ = strconv.ParseInt(fields[4], 10, 64) |
|
result = append(result, &reward) |
|
} |
|
} |
|
return result |
|
} |
|
|
|
func convertBlockResults(rows ...*redis.ZSliceCmd) []*BlockData { |
|
var result []*BlockData |
|
for _, row := range rows { |
|
for _, v := range row.Val() { |
|
// "uncleHeight:orphan:nonce:blockHash:timestamp:diff:totalShares:rewardInWei" |
|
block := BlockData{} |
|
block.Height = int64(v.Score) |
|
block.RoundHeight = block.Height |
|
fields := strings.Split(v.Member.(string), ":") |
|
block.UncleHeight, _ = strconv.ParseInt(fields[0], 10, 64) |
|
block.Uncle = block.UncleHeight > 0 |
|
block.Orphan, _ = strconv.ParseBool(fields[1]) |
|
block.Nonce = fields[2] |
|
block.Hash = fields[3] |
|
block.Timestamp, _ = strconv.ParseInt(fields[4], 10, 64) |
|
block.Difficulty, _ = strconv.ParseInt(fields[5], 10, 64) |
|
block.TotalShares, _ = strconv.ParseInt(fields[6], 10, 64) |
|
block.RewardString = fields[7] |
|
block.ImmatureReward = fields[7] |
|
block.Login = fields[8] |
|
block.immatureKey = v.Member.(string) |
|
result = append(result, &block) |
|
} |
|
} |
|
return result |
|
} |
|
|
|
// Build per login workers's total shares map {'rig-1': 12345, 'rig-2': 6789, ...} |
|
// TS => diff, id, ms |
|
func convertWorkersStats(window int64, raw *redis.ZSliceCmd) map[string]Worker { |
|
now := util.MakeTimestamp() / 1000 |
|
workers := make(map[string]Worker) |
|
|
|
for _, v := range raw.Val() { |
|
parts := strings.Split(v.Member.(string), ":") |
|
share, _ := strconv.ParseInt(parts[0], 10, 64) |
|
id := parts[1] |
|
score := int64(v.Score) |
|
worker := workers[id] |
|
|
|
// Add for large window |
|
worker.TotalHR += share |
|
|
|
// Add for small window if matches |
|
if score >= now-window { |
|
worker.HR += share |
|
} |
|
|
|
if worker.LastBeat < score { |
|
worker.LastBeat = score |
|
} |
|
if worker.startedAt > score || worker.startedAt == 0 { |
|
worker.startedAt = score |
|
} |
|
workers[id] = worker |
|
} |
|
return workers |
|
} |
|
|
|
func convertMinersStats(window int64, raw *redis.ZSliceCmd) (int64, map[string]Miner) { |
|
now := util.MakeTimestamp() / 1000 |
|
miners := make(map[string]Miner) |
|
totalHashrate := int64(0) |
|
|
|
for _, v := range raw.Val() { |
|
parts := strings.Split(v.Member.(string), ":") |
|
share, _ := strconv.ParseInt(parts[0], 10, 64) |
|
id := parts[1] |
|
score := int64(v.Score) |
|
miner := miners[id] |
|
miner.HR += share |
|
|
|
if miner.LastBeat < score { |
|
miner.LastBeat = score |
|
} |
|
if miner.startedAt > score || miner.startedAt == 0 { |
|
miner.startedAt = score |
|
} |
|
miners[id] = miner |
|
} |
|
|
|
for id, miner := range miners { |
|
timeOnline := now - miner.startedAt |
|
if timeOnline < 600 { |
|
timeOnline = 600 |
|
} |
|
|
|
boundary := timeOnline |
|
if timeOnline >= window { |
|
boundary = window |
|
} |
|
miner.HR = miner.HR / boundary |
|
|
|
if miner.LastBeat < (now - window/2) { |
|
miner.Offline = true |
|
} |
|
totalHashrate += miner.HR |
|
miners[id] = miner |
|
} |
|
return totalHashrate, miners |
|
} |
|
|
|
func convertPaymentsResults(raw *redis.ZSliceCmd) []map[string]interface{} { |
|
var result []map[string]interface{} |
|
for _, v := range raw.Val() { |
|
tx := make(map[string]interface{}) |
|
tx["timestamp"] = int64(v.Score) |
|
fields := strings.Split(v.Member.(string), ":") |
|
tx["tx"] = fields[0] |
|
// Individual or whole payments row |
|
if len(fields) < 3 { |
|
tx["amount"], _ = strconv.ParseInt(fields[1], 10, 64) |
|
} else { |
|
tx["address"] = fields[1] |
|
tx["amount"], _ = strconv.ParseInt(fields[2], 10, 64) |
|
} |
|
result = append(result, tx) |
|
} |
|
var reverse []map[string]interface{} |
|
for i := len(result) - 1; i >= 0; i-- { |
|
reverse = append(reverse, result[i]) |
|
} |
|
return result |
|
} |
|
|
|
func convertFindersResults(raw *redis.ZSliceCmd) []map[string]interface{} { |
|
var result []map[string]interface{} |
|
for _, v := range raw.Val() { |
|
miner := make(map[string]interface{}) |
|
miner["blocks"] = int64(v.Score) |
|
miner["address"] = v.Member.(string) |
|
result = append(result, miner) |
|
} |
|
return result |
|
} |
|
|
|
/* |
|
Timestamp int64 `json:"x"` |
|
TimeFormat string `json:"timeFormat"` |
|
Amount int64 `json:"amount"` |
|
*/ |
|
func convertPaymentChartsResults(raw *redis.ZSliceCmd) []*PaymentCharts { |
|
var result []*PaymentCharts |
|
for _, v := range raw.Val() { |
|
pc := PaymentCharts{} |
|
pc.Timestamp = int64(v.Score) |
|
tm := time.Unix(pc.Timestamp, 0) |
|
pc.TimeFormat = tm.Format("2006-01-02") + " 00_00" |
|
fields := strings.Split(v.Member.(string), ":") |
|
pc.Amount, _ = strconv.ParseInt(fields[1], 10, 64) |
|
//fmt.Printf("%d : %s : %d \n", pc.Timestamp, pc.TimeFormat, pc.Amount) |
|
|
|
var chkAppend bool |
|
for _, pcc := range result { |
|
if pcc.TimeFormat == pc.TimeFormat { |
|
pcc.Amount += pc.Amount |
|
chkAppend = true |
|
} |
|
} |
|
if !chkAppend { |
|
pc.Timestamp -= int64(math.Mod(float64(v.Score), float64(86400))) |
|
result = append(result, &pc) |
|
} |
|
} |
|
var reverse []*PaymentCharts |
|
for i := len(result) - 1; i >= 0; i-- { |
|
reverse = append(reverse, result[i]) |
|
} |
|
return reverse |
|
}
|
|
|