145
vendor/github.com/status-im/status-go/services/ext/mailservers/cache.go
generated
vendored
Normal file
145
vendor/github.com/status-im/status-go/services/ext/mailservers/cache.go
generated
vendored
Normal file
@@ -0,0 +1,145 @@
|
||||
package mailservers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/iterator"
|
||||
"github.com/syndtr/goleveldb/leveldb/util"
|
||||
|
||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
||||
|
||||
"github.com/status-im/status-go/db"
|
||||
"github.com/status-im/status-go/eth-node/types"
|
||||
)
|
||||
|
||||
// NewPeerRecord returns instance of the peer record.
|
||||
func NewPeerRecord(node *enode.Node) PeerRecord {
|
||||
return PeerRecord{node: node}
|
||||
}
|
||||
|
||||
// PeerRecord is set data associated with each peer that is stored on disk.
|
||||
// PeerRecord stored with a enode as a key in leveldb, and body marshalled as json.
|
||||
type PeerRecord struct {
|
||||
node *enode.Node
|
||||
|
||||
// last time it was used.
|
||||
LastUsed time.Time
|
||||
}
|
||||
|
||||
// Encode encodes PeerRecords to bytes.
|
||||
func (r PeerRecord) Encode() ([]byte, error) {
|
||||
return json.Marshal(r)
|
||||
}
|
||||
|
||||
// ID returns enode identity of the node.
|
||||
func (r PeerRecord) ID() enode.ID {
|
||||
return r.node.ID()
|
||||
}
|
||||
|
||||
// Node returs pointer to original object.
|
||||
// enode.Node doensn't allow modification on the object.
|
||||
func (r PeerRecord) Node() *enode.Node {
|
||||
return r.node
|
||||
}
|
||||
|
||||
// EncodeKey returns bytes that will should be used as a key in persistent storage.
|
||||
func (r PeerRecord) EncodeKey() ([]byte, error) {
|
||||
return r.Node().MarshalText()
|
||||
}
|
||||
|
||||
// NewCache returns pointer to a Cache instance.
|
||||
func NewCache(db *leveldb.DB) *Cache {
|
||||
return &Cache{db: db}
|
||||
}
|
||||
|
||||
// Cache is wrapper for operations on disk with leveldb.
|
||||
type Cache struct {
|
||||
db *leveldb.DB
|
||||
}
|
||||
|
||||
// Replace deletes old and adds new records in the persistent cache.
|
||||
func (c *Cache) Replace(nodes []*enode.Node) error {
|
||||
batch := new(leveldb.Batch)
|
||||
iter := createPeersIterator(c.db)
|
||||
defer iter.Release()
|
||||
newNodes := nodesToMap(nodes)
|
||||
for iter.Next() {
|
||||
record, err := unmarshalKeyValue(keyWithoutPrefix(iter.Key()), iter.Value())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, exist := newNodes[types.EnodeID(record.ID())]; exist {
|
||||
delete(newNodes, types.EnodeID(record.ID()))
|
||||
} else {
|
||||
batch.Delete(iter.Key())
|
||||
}
|
||||
}
|
||||
for _, n := range newNodes {
|
||||
enodeKey, err := n.MarshalText()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// we put nil as default value doesn't have any state associated with them.
|
||||
batch.Put(db.Key(db.MailserversCache, enodeKey), nil)
|
||||
}
|
||||
return c.db.Write(batch, nil)
|
||||
}
|
||||
|
||||
// LoadAll loads all records from persistent database.
|
||||
func (c *Cache) LoadAll() (rst []PeerRecord, err error) {
|
||||
iter := createPeersIterator(c.db)
|
||||
for iter.Next() {
|
||||
record, err := unmarshalKeyValue(keyWithoutPrefix(iter.Key()), iter.Value())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rst = append(rst, record)
|
||||
}
|
||||
return rst, nil
|
||||
}
|
||||
|
||||
// UpdateRecord updates single record.
|
||||
func (c *Cache) UpdateRecord(record PeerRecord) error {
|
||||
enodeKey, err := record.EncodeKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
value, err := record.Encode()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.db.Put(db.Key(db.MailserversCache, enodeKey), value, nil)
|
||||
}
|
||||
|
||||
func unmarshalKeyValue(key, value []byte) (record PeerRecord, err error) {
|
||||
enodeKey := key
|
||||
node := new(enode.Node)
|
||||
err = node.UnmarshalText(enodeKey)
|
||||
if err != nil {
|
||||
return record, err
|
||||
}
|
||||
record = PeerRecord{node: node}
|
||||
if len(value) != 0 {
|
||||
err = json.Unmarshal(value, &record)
|
||||
}
|
||||
return record, err
|
||||
}
|
||||
|
||||
func nodesToMap(nodes []*enode.Node) map[types.EnodeID]*enode.Node {
|
||||
rst := map[types.EnodeID]*enode.Node{}
|
||||
for _, n := range nodes {
|
||||
rst[types.EnodeID(n.ID())] = n
|
||||
}
|
||||
return rst
|
||||
}
|
||||
|
||||
func createPeersIterator(level *leveldb.DB) iterator.Iterator {
|
||||
return level.NewIterator(util.BytesPrefix([]byte{byte(db.MailserversCache)}), nil)
|
||||
}
|
||||
|
||||
// keyWithoutPrefix removes first byte from key.
|
||||
func keyWithoutPrefix(key []byte) []byte {
|
||||
return key[1:]
|
||||
}
|
||||
271
vendor/github.com/status-im/status-go/services/ext/mailservers/connmanager.go
generated
vendored
Normal file
271
vendor/github.com/status-im/status-go/services/ext/mailservers/connmanager.go
generated
vendored
Normal file
@@ -0,0 +1,271 @@
|
||||
package mailservers
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/event"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/p2p"
|
||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
||||
|
||||
"github.com/status-im/status-go/eth-node/types"
|
||||
)
|
||||
|
||||
const (
|
||||
peerEventsBuffer = 10 // sufficient buffer to avoid blocking a p2p feed.
|
||||
whisperEventsBuffer = 20 // sufficient buffer to avod blocking a eventSub envelopes feed.
|
||||
)
|
||||
|
||||
// PeerAdderRemover is an interface for adding or removing peers.
|
||||
type PeerAdderRemover interface {
|
||||
AddPeer(node *enode.Node)
|
||||
RemovePeer(node *enode.Node)
|
||||
}
|
||||
|
||||
// PeerEventsSubscriber interface to subscribe for p2p.PeerEvent's.
|
||||
type PeerEventsSubscriber interface {
|
||||
SubscribeEvents(chan *p2p.PeerEvent) event.Subscription
|
||||
}
|
||||
|
||||
// EnvelopeEventSubscriber interface to subscribe for types.EnvelopeEvent's.
|
||||
type EnvelopeEventSubscriber interface {
|
||||
SubscribeEnvelopeEvents(chan<- types.EnvelopeEvent) types.Subscription
|
||||
}
|
||||
|
||||
type p2pServer interface {
|
||||
PeerAdderRemover
|
||||
PeerEventsSubscriber
|
||||
}
|
||||
|
||||
// NewConnectionManager creates an instance of ConnectionManager.
|
||||
func NewConnectionManager(server p2pServer, eventSub EnvelopeEventSubscriber, target, maxFailures int, timeout time.Duration) *ConnectionManager {
|
||||
return &ConnectionManager{
|
||||
server: server,
|
||||
eventSub: eventSub,
|
||||
connectedTarget: target,
|
||||
maxFailures: maxFailures,
|
||||
notifications: make(chan []*enode.Node),
|
||||
timeoutWaitAdded: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
// ConnectionManager manages keeps target of peers connected.
|
||||
type ConnectionManager struct {
|
||||
wg sync.WaitGroup
|
||||
quit chan struct{}
|
||||
|
||||
server p2pServer
|
||||
eventSub EnvelopeEventSubscriber
|
||||
|
||||
notifications chan []*enode.Node
|
||||
connectedTarget int
|
||||
timeoutWaitAdded time.Duration
|
||||
maxFailures int
|
||||
}
|
||||
|
||||
// Notify sends a non-blocking notification about new nodes.
|
||||
func (ps *ConnectionManager) Notify(nodes []*enode.Node) {
|
||||
ps.wg.Add(1)
|
||||
go func() {
|
||||
select {
|
||||
case ps.notifications <- nodes:
|
||||
case <-ps.quit:
|
||||
}
|
||||
ps.wg.Done()
|
||||
}()
|
||||
|
||||
}
|
||||
|
||||
// Start subscribes to a p2p server and handles new peers and state updates for those peers.
|
||||
func (ps *ConnectionManager) Start() {
|
||||
ps.quit = make(chan struct{})
|
||||
ps.wg.Add(1)
|
||||
go func() {
|
||||
state := newInternalState(ps.server, ps.connectedTarget, ps.timeoutWaitAdded)
|
||||
events := make(chan *p2p.PeerEvent, peerEventsBuffer)
|
||||
sub := ps.server.SubscribeEvents(events)
|
||||
whisperEvents := make(chan types.EnvelopeEvent, whisperEventsBuffer)
|
||||
whisperSub := ps.eventSub.SubscribeEnvelopeEvents(whisperEvents)
|
||||
requests := map[types.Hash]struct{}{}
|
||||
failuresPerServer := map[types.EnodeID]int{}
|
||||
|
||||
defer sub.Unsubscribe()
|
||||
defer whisperSub.Unsubscribe()
|
||||
defer ps.wg.Done()
|
||||
for {
|
||||
select {
|
||||
case <-ps.quit:
|
||||
return
|
||||
case err := <-sub.Err():
|
||||
log.Error("retry after error subscribing to p2p events", "error", err)
|
||||
return
|
||||
case err := <-whisperSub.Err():
|
||||
log.Error("retry after error suscribing to eventSub events", "error", err)
|
||||
return
|
||||
case newNodes := <-ps.notifications:
|
||||
state.processReplacement(newNodes, events)
|
||||
case ev := <-events:
|
||||
processPeerEvent(state, ev)
|
||||
case ev := <-whisperEvents:
|
||||
// TODO treat failed requests the same way as expired
|
||||
switch ev.Event {
|
||||
case types.EventMailServerRequestSent:
|
||||
requests[ev.Hash] = struct{}{}
|
||||
case types.EventMailServerRequestCompleted:
|
||||
// reset failures count on first success
|
||||
failuresPerServer[ev.Peer] = 0
|
||||
delete(requests, ev.Hash)
|
||||
case types.EventMailServerRequestExpired:
|
||||
_, exist := requests[ev.Hash]
|
||||
if !exist {
|
||||
continue
|
||||
}
|
||||
failuresPerServer[ev.Peer]++
|
||||
log.Debug("request to a mail server expired, disconnect a peer", "address", ev.Peer)
|
||||
if failuresPerServer[ev.Peer] >= ps.maxFailures {
|
||||
state.nodeDisconnected(ev.Peer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Stop gracefully closes all background goroutines and waits until they finish.
|
||||
func (ps *ConnectionManager) Stop() {
|
||||
if ps.quit == nil {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-ps.quit:
|
||||
return
|
||||
default:
|
||||
}
|
||||
close(ps.quit)
|
||||
ps.wg.Wait()
|
||||
ps.quit = nil
|
||||
}
|
||||
|
||||
func (state *internalState) processReplacement(newNodes []*enode.Node, events <-chan *p2p.PeerEvent) {
|
||||
replacement := map[types.EnodeID]*enode.Node{}
|
||||
for _, n := range newNodes {
|
||||
replacement[types.EnodeID(n.ID())] = n
|
||||
}
|
||||
state.replaceNodes(replacement)
|
||||
if state.ReachedTarget() {
|
||||
log.Debug("already connected with required target", "target", state.target)
|
||||
return
|
||||
}
|
||||
if state.timeout != 0 {
|
||||
log.Debug("waiting defined timeout to establish connections",
|
||||
"timeout", state.timeout, "target", state.target)
|
||||
timer := time.NewTimer(state.timeout)
|
||||
waitForConnections(state, timer.C, events)
|
||||
timer.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
func newInternalState(srv PeerAdderRemover, target int, timeout time.Duration) *internalState {
|
||||
return &internalState{
|
||||
options: options{target: target, timeout: timeout},
|
||||
srv: srv,
|
||||
connected: map[types.EnodeID]struct{}{},
|
||||
currentNodes: map[types.EnodeID]*enode.Node{},
|
||||
}
|
||||
}
|
||||
|
||||
type options struct {
|
||||
target int
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
type internalState struct {
|
||||
options
|
||||
srv PeerAdderRemover
|
||||
|
||||
connected map[types.EnodeID]struct{}
|
||||
currentNodes map[types.EnodeID]*enode.Node
|
||||
}
|
||||
|
||||
func (state *internalState) ReachedTarget() bool {
|
||||
return len(state.connected) >= state.target
|
||||
}
|
||||
|
||||
func (state *internalState) replaceNodes(new map[types.EnodeID]*enode.Node) {
|
||||
for nid, n := range state.currentNodes {
|
||||
if _, exist := new[nid]; !exist {
|
||||
delete(state.connected, nid)
|
||||
state.srv.RemovePeer(n)
|
||||
}
|
||||
}
|
||||
if !state.ReachedTarget() {
|
||||
for _, n := range new {
|
||||
state.srv.AddPeer(n)
|
||||
}
|
||||
}
|
||||
state.currentNodes = new
|
||||
}
|
||||
|
||||
func (state *internalState) nodeAdded(peer types.EnodeID) {
|
||||
n, exist := state.currentNodes[peer]
|
||||
if !exist {
|
||||
return
|
||||
}
|
||||
if state.ReachedTarget() {
|
||||
state.srv.RemovePeer(n)
|
||||
} else {
|
||||
state.connected[types.EnodeID(n.ID())] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
func (state *internalState) nodeDisconnected(peer types.EnodeID) {
|
||||
n, exist := state.currentNodes[peer] // unrelated event
|
||||
if !exist {
|
||||
return
|
||||
}
|
||||
_, exist = state.connected[peer] // check if already disconnected
|
||||
if !exist {
|
||||
return
|
||||
}
|
||||
if len(state.currentNodes) == 1 { // keep node connected if we don't have another choice
|
||||
return
|
||||
}
|
||||
state.srv.RemovePeer(n) // remove peer permanently, otherwise p2p.Server will try to reconnect
|
||||
delete(state.connected, peer)
|
||||
if !state.ReachedTarget() { // try to connect with any other selected (but not connected) node
|
||||
for nid, n := range state.currentNodes {
|
||||
_, exist := state.connected[nid]
|
||||
if exist || peer == nid {
|
||||
continue
|
||||
}
|
||||
state.srv.AddPeer(n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func processPeerEvent(state *internalState, ev *p2p.PeerEvent) {
|
||||
switch ev.Type {
|
||||
case p2p.PeerEventTypeAdd:
|
||||
log.Debug("connected to a mailserver", "address", ev.Peer)
|
||||
state.nodeAdded(types.EnodeID(ev.Peer))
|
||||
case p2p.PeerEventTypeDrop:
|
||||
log.Debug("mailserver disconnected", "address", ev.Peer)
|
||||
state.nodeDisconnected(types.EnodeID(ev.Peer))
|
||||
}
|
||||
}
|
||||
|
||||
func waitForConnections(state *internalState, timeout <-chan time.Time, events <-chan *p2p.PeerEvent) {
|
||||
for {
|
||||
select {
|
||||
case ev := <-events:
|
||||
processPeerEvent(state, ev)
|
||||
if state.ReachedTarget() {
|
||||
return
|
||||
}
|
||||
case <-timeout:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
85
vendor/github.com/status-im/status-go/services/ext/mailservers/connmonitor.go
generated
vendored
Normal file
85
vendor/github.com/status-im/status-go/services/ext/mailservers/connmonitor.go
generated
vendored
Normal file
@@ -0,0 +1,85 @@
|
||||
package mailservers
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
|
||||
"github.com/status-im/status-go/eth-node/types"
|
||||
)
|
||||
|
||||
// NewLastUsedConnectionMonitor returns pointer to the instance of LastUsedConnectionMonitor.
|
||||
func NewLastUsedConnectionMonitor(ps *PeerStore, cache *Cache, eventSub EnvelopeEventSubscriber) *LastUsedConnectionMonitor {
|
||||
return &LastUsedConnectionMonitor{
|
||||
ps: ps,
|
||||
cache: cache,
|
||||
eventSub: eventSub,
|
||||
}
|
||||
}
|
||||
|
||||
// LastUsedConnectionMonitor watches relevant events and reflects it in cache.
|
||||
type LastUsedConnectionMonitor struct {
|
||||
ps *PeerStore
|
||||
cache *Cache
|
||||
|
||||
eventSub EnvelopeEventSubscriber
|
||||
|
||||
quit chan struct{}
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
// Start spins a separate goroutine to watch connections.
|
||||
func (mon *LastUsedConnectionMonitor) Start() {
|
||||
mon.quit = make(chan struct{})
|
||||
mon.wg.Add(1)
|
||||
go func() {
|
||||
events := make(chan types.EnvelopeEvent, whisperEventsBuffer)
|
||||
sub := mon.eventSub.SubscribeEnvelopeEvents(events)
|
||||
defer sub.Unsubscribe()
|
||||
defer mon.wg.Done()
|
||||
for {
|
||||
select {
|
||||
case <-mon.quit:
|
||||
return
|
||||
case err := <-sub.Err():
|
||||
log.Error("retry after error suscribing to eventSub events", "error", err)
|
||||
return
|
||||
case ev := <-events:
|
||||
node := mon.ps.Get(ev.Peer)
|
||||
if node == nil {
|
||||
continue
|
||||
}
|
||||
if ev.Event == types.EventMailServerRequestCompleted {
|
||||
err := mon.updateRecord(ev.Peer)
|
||||
if err != nil {
|
||||
log.Error("unable to update storage", "peer", ev.Peer, "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (mon *LastUsedConnectionMonitor) updateRecord(nodeID types.EnodeID) error {
|
||||
node := mon.ps.Get(nodeID)
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
return mon.cache.UpdateRecord(PeerRecord{node: node, LastUsed: time.Now()})
|
||||
}
|
||||
|
||||
// Stop closes channel to signal a quit and waits until all goroutines are stoppped.
|
||||
func (mon *LastUsedConnectionMonitor) Stop() {
|
||||
if mon.quit == nil {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-mon.quit:
|
||||
return
|
||||
default:
|
||||
}
|
||||
close(mon.quit)
|
||||
mon.wg.Wait()
|
||||
mon.quit = nil
|
||||
}
|
||||
63
vendor/github.com/status-im/status-go/services/ext/mailservers/peerstore.go
generated
vendored
Normal file
63
vendor/github.com/status-im/status-go/services/ext/mailservers/peerstore.go
generated
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
package mailservers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/ethereum/go-ethereum/p2p"
|
||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
||||
|
||||
"github.com/status-im/status-go/eth-node/types"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNoConnected returned when mail servers are not connected.
|
||||
ErrNoConnected = errors.New("no connected mail servers")
|
||||
)
|
||||
|
||||
// PeersProvider is an interface for requesting list of peers.
|
||||
type PeersProvider interface {
|
||||
Peers() []*p2p.Peer
|
||||
}
|
||||
|
||||
// NewPeerStore returns an instance of PeerStore.
|
||||
func NewPeerStore(cache *Cache) *PeerStore {
|
||||
return &PeerStore{
|
||||
nodes: map[types.EnodeID]*enode.Node{},
|
||||
cache: cache,
|
||||
}
|
||||
}
|
||||
|
||||
// PeerStore stores list of selected mail servers and keeps N of them connected.
|
||||
type PeerStore struct {
|
||||
mu sync.RWMutex
|
||||
nodes map[types.EnodeID]*enode.Node
|
||||
|
||||
cache *Cache
|
||||
}
|
||||
|
||||
// Exist confirms that peers was added to a store.
|
||||
func (ps *PeerStore) Exist(nodeID types.EnodeID) bool {
|
||||
ps.mu.RLock()
|
||||
defer ps.mu.RUnlock()
|
||||
_, exist := ps.nodes[nodeID]
|
||||
return exist
|
||||
}
|
||||
|
||||
// Get returns instance of the node with requested ID or nil if ID is not found.
|
||||
func (ps *PeerStore) Get(nodeID types.EnodeID) *enode.Node {
|
||||
ps.mu.RLock()
|
||||
defer ps.mu.RUnlock()
|
||||
return ps.nodes[nodeID]
|
||||
}
|
||||
|
||||
// Update updates peers locally.
|
||||
func (ps *PeerStore) Update(nodes []*enode.Node) error {
|
||||
ps.mu.Lock()
|
||||
ps.nodes = map[types.EnodeID]*enode.Node{}
|
||||
for _, n := range nodes {
|
||||
ps.nodes[types.EnodeID(n.ID())] = n
|
||||
}
|
||||
ps.mu.Unlock()
|
||||
return ps.cache.Replace(nodes)
|
||||
}
|
||||
54
vendor/github.com/status-im/status-go/services/ext/mailservers/utils.go
generated
vendored
Normal file
54
vendor/github.com/status-im/status-go/services/ext/mailservers/utils.go
generated
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
package mailservers
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
||||
|
||||
"github.com/status-im/status-go/eth-node/types"
|
||||
)
|
||||
|
||||
// GetFirstConnected returns first connected peer that is also added to a peer store.
|
||||
// Raises ErrNoConnected if no peers are added to a peer store.
|
||||
func GetFirstConnected(provider PeersProvider, store *PeerStore) (*enode.Node, error) {
|
||||
peers := provider.Peers()
|
||||
for _, p := range peers {
|
||||
if store.Exist(types.EnodeID(p.ID())) {
|
||||
return p.Node(), nil
|
||||
}
|
||||
}
|
||||
return nil, ErrNoConnected
|
||||
}
|
||||
|
||||
// NodesNotifee interface to be notified when new nodes are received.
|
||||
type NodesNotifee interface {
|
||||
Notify([]*enode.Node)
|
||||
}
|
||||
|
||||
// EnsureUsedRecordsAddedFirst checks if any nodes were marked as connected before app went offline.
|
||||
func EnsureUsedRecordsAddedFirst(ps *PeerStore, conn NodesNotifee) error {
|
||||
records, err := ps.cache.LoadAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(records) == 0 {
|
||||
return nil
|
||||
}
|
||||
sort.Slice(records, func(i, j int) bool {
|
||||
return records[i].LastUsed.After(records[j].LastUsed)
|
||||
})
|
||||
all := recordsToNodes(records)
|
||||
if !records[0].LastUsed.IsZero() {
|
||||
conn.Notify(all[:1])
|
||||
}
|
||||
conn.Notify(all)
|
||||
return nil
|
||||
}
|
||||
|
||||
func recordsToNodes(records []PeerRecord) []*enode.Node {
|
||||
nodes := make([]*enode.Node, len(records))
|
||||
for i := range records {
|
||||
nodes[i] = records[i].Node()
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
Reference in New Issue
Block a user