main.go raw
1 package gui
2
3 import (
4 "crypto/rand"
5 "fmt"
6 "net"
7 "os"
8 "runtime"
9 "strings"
10 "sync"
11 "time"
12
13 "github.com/niubaoshu/gotiny"
14 "github.com/tyler-smith/go-bip39"
15
16 "github.com/p9c/p9/pkg/log"
17 "github.com/p9c/p9/pkg/opts/meta"
18 "github.com/p9c/p9/pkg/opts/text"
19 "github.com/p9c/p9/pkg/chainrpc/p2padvt"
20 "github.com/p9c/p9/pkg/pipe"
21 "github.com/p9c/p9/pkg/transport"
22 "github.com/p9c/p9/pod/state"
23
24 uberatomic "go.uber.org/atomic"
25
26 "github.com/p9c/p9/pkg/gel/gio/op/paint"
27
28 "github.com/p9c/p9/pkg/qu"
29
30 "github.com/p9c/p9/pkg/interrupt"
31
32 "github.com/p9c/p9/pkg/gel"
33 "github.com/p9c/p9/pkg/btcjson"
34
35 l "github.com/p9c/p9/pkg/gel/gio/layout"
36
37 "github.com/p9c/p9/cmd/gui/cfg"
38 "github.com/p9c/p9/pkg/apputil"
39 "github.com/p9c/p9/pkg/rpcclient"
40 "github.com/p9c/p9/pkg/util/rununit"
41 )
42
43 // Main is the entrypoint for the wallet GUI
44 func Main(cx *state.State) (e error) {
45 size := uberatomic.NewInt32(0)
46 noWallet := true
47 wg := &WalletGUI{
48 cx: cx,
49 invalidate: qu.Ts(16),
50 quit: cx.KillAll,
51 Size: size,
52 noWallet: &noWallet,
53 otherNodes: make(map[uint64]*nodeSpec),
54 certs: cx.Config.ReadCAFile(),
55
56 }
57 return wg.Run()
58 }
59
60 type BoolMap map[string]*gel.Bool
61 type ListMap map[string]*gel.List
62 type CheckableMap map[string]*gel.Checkable
63 type ClickableMap map[string]*gel.Clickable
64 type Inputs map[string]*gel.Input
65 type Passwords map[string]*gel.Password
66 type IncDecMap map[string]*gel.IncDec
67
68 type WalletGUI struct {
69 wg sync.WaitGroup
70 cx *state.State
71 quit qu.C
72 State *State
73 noWallet *bool
74 node, wallet, miner *rununit.RunUnit
75 walletToLock time.Time
76 walletLockTime int
77 ChainMutex, WalletMutex sync.Mutex
78 ChainClient, WalletClient *rpcclient.Client
79 WalletWatcher qu.C
80 *gel.Window
81 Size *uberatomic.Int32
82 MainApp *gel.App
83 invalidate qu.C
84 unlockPage *gel.App
85 loadingPage *gel.App
86 config *cfg.Config
87 configs cfg.GroupsMap
88 unlockPassword *gel.Password
89 sidebarButtons []*gel.Clickable
90 buttonBarButtons []*gel.Clickable
91 statusBarButtons []*gel.Clickable
92 receiveAddressbookClickables []*gel.Clickable
93 sendAddressbookClickables []*gel.Clickable
94 quitClickable *gel.Clickable
95 bools BoolMap
96 lists ListMap
97 checkables CheckableMap
98 clickables ClickableMap
99 inputs Inputs
100 passwords Passwords
101 incdecs IncDecMap
102 console *Console
103 RecentTxsWidget, TxHistoryWidget l.Widget
104 recentTxsClickables, txHistoryClickables []*gel.Clickable
105 txHistoryList []btcjson.ListTransactionsResult
106 openTxID, prevOpenTxID *uberatomic.String
107 originTxDetail string
108 txMx sync.Mutex
109 stateLoaded *uberatomic.Bool
110 currentReceiveQRCode *paint.ImageOp
111 currentReceiveAddress string
112 currentReceiveQR l.Widget
113 currentReceiveRegenClickable *gel.Clickable
114 currentReceiveCopyClickable *gel.Clickable
115 currentReceiveRegenerate *uberatomic.Bool
116 // currentReceiveGetNew *uberatomic.Bool
117 sendClickable *gel.Clickable
118 ready *uberatomic.Bool
119 mainDirection l.Direction
120 preRendering bool
121 // ReceiveAddressbook l.Widget
122 // SendAddressbook l.Widget
123 ReceivePage *ReceivePage
124 SendPage *SendPage
125 // toasts *toast.Toasts
126 // dialog *dialog.Dialog
127 createSeed []byte
128 createWords, showWords, createMatch string
129 createVerifying bool
130 restoring bool
131 lastUpdated uberatomic.Int64
132 multiConn *transport.Channel
133 otherNodes map[uint64]*nodeSpec
134 uuid uint64
135 peerCount *uberatomic.Int32
136 certs []byte
137 }
138
139 type nodeSpec struct {
140 time.Time
141 addr string
142 }
143
144 func (wg *WalletGUI) Run() (e error) {
145 wg.openTxID = uberatomic.NewString("")
146 var mc *transport.Channel
147 quit := qu.T()
148 // I.Ln(wg.cx.Config.MulticastPass.V(), string(wg.cx.Config.MulticastPass.
149 // Bytes()))
150 if mc, e = transport.NewBroadcastChannel(
151 "controller",
152 wg,
153 wg.cx.Config.MulticastPass.Bytes(),
154 transport.DefaultPort,
155 16384,
156 handlersMulticast,
157 quit,
158 ); E.Chk(e) {
159 return
160 }
161 wg.multiConn = mc
162 wg.peerCount = uberatomic.NewInt32(0)
163 wg.prevOpenTxID = uberatomic.NewString("")
164 wg.stateLoaded = uberatomic.NewBool(false)
165 wg.currentReceiveRegenerate = uberatomic.NewBool(true)
166 wg.ready = uberatomic.NewBool(false)
167 wg.Window = gel.NewWindowP9(wg.quit)
168 wg.Dark = wg.cx.Config.DarkTheme
169 wg.Colors.SetDarkTheme(wg.Dark.True())
170 *wg.noWallet = true
171 wg.GetButtons()
172 wg.lists = wg.GetLists()
173 wg.clickables = wg.GetClickables()
174 wg.checkables = wg.GetCheckables()
175 before := func() { D.Ln("running before") }
176 after := func() { D.Ln("running after") }
177 I.Ln(os.Args[1:])
178 options := []string{os.Args[0]}
179 // options = append(options, wg.cx.Config.FoundArgs...)
180 // options = append(options, "pipelog")
181 wg.node = wg.GetRunUnit(
182 "NODE", before, after,
183 append(options, "node")...,
184 // "node",
185 )
186 wg.wallet = wg.GetRunUnit(
187 "WLLT", before, after,
188 append(options, "wallet")...,
189 // "wallet",
190 )
191 wg.miner = wg.GetRunUnit(
192 "MINE", before, after,
193 append(options, "kopach")...,
194 // "wallet",
195 )
196 // I.S(wg.node, wg.wallet, wg.miner)
197 wg.bools = wg.GetBools()
198 wg.inputs = wg.GetInputs()
199 wg.passwords = wg.GetPasswords()
200 // wg.toasts = toast.New(wg.th)
201 // wg.dialog = dialog.New(wg.th)
202 wg.console = wg.ConsolePage()
203 wg.quitClickable = wg.Clickable()
204 wg.incdecs = wg.GetIncDecs()
205 wg.Size = wg.Window.Width
206 wg.currentReceiveCopyClickable = wg.WidgetPool.GetClickable()
207 wg.currentReceiveRegenClickable = wg.WidgetPool.GetClickable()
208 wg.currentReceiveQR = func(gtx l.Context) l.Dimensions {
209 return l.Dimensions{}
210 }
211 wg.ReceivePage = wg.GetReceivePage()
212 wg.SendPage = wg.GetSendPage()
213 wg.MainApp = wg.GetAppWidget()
214 wg.State = GetNewState(wg.cx.ActiveNet, wg.MainApp.ActivePageGetAtomic())
215 wg.unlockPage = wg.getWalletUnlockAppWidget()
216 wg.loadingPage = wg.getLoadingPage()
217 if !apputil.FileExists(wg.cx.Config.WalletFile.V()) {
218 I.Ln("wallet file does not exist", wg.cx.Config.WalletFile.V())
219 } else {
220 *wg.noWallet = false
221 // if !*wg.cx.Config.NodeOff {
222 // // wg.startNode()
223 // wg.node.Start()
224 // }
225 if wg.cx.Config.Generate.True() && wg.cx.Config.GenThreads.V() != 0 {
226 // wg.startMiner()
227 wg.miner.Start()
228 }
229 wg.unlockPassword.Focus()
230 }
231 interrupt.AddHandler(
232 func() {
233 D.Ln("quitting wallet gui")
234 // consume.Kill(wg.Node)
235 // consume.Kill(wg.Miner)
236 // wg.gracefulShutdown()
237 wg.quit.Q()
238 },
239 )
240 go func() {
241 ticker := time.NewTicker(time.Second)
242 out:
243 for {
244 select {
245 case <-ticker.C:
246 go func() {
247 if e = wg.Advertise(); E.Chk(e) {
248 }
249 if wg.node.Running() {
250 if wg.ChainClient != nil {
251 if !wg.ChainClient.Disconnected() {
252 var pi []btcjson.GetPeerInfoResult
253 if pi, e = wg.ChainClient.GetPeerInfo(); E.Chk(e) {
254 return
255 }
256 wg.peerCount.Store(int32(len(pi)))
257 wg.Invalidate()
258 }
259 }
260 }
261 }()
262 case <-wg.invalidate.Wait():
263 T.Ln("invalidating render queue")
264 wg.Window.Window.Invalidate()
265 // TODO: make a more appropriate trigger for this - ie, when state actually changes.
266 // if wg.wallet.Running() && wg.stateLoaded.Load() {
267 // filename := filepath.Join(wg.cx.DataDir, "state.json")
268 // if e := wg.State.Save(filename, wg.cx.Config.WalletPass); E.Chk(e) {
269 // }
270 // }
271 case <-wg.cx.KillAll.Wait():
272 break out
273 case <-wg.quit.Wait():
274 break out
275 }
276 }
277 }()
278 if e := wg.Window.
279 Size(56, 32).
280 Title("ParallelCoin Wallet").
281 Open().
282 Run(
283 func(gtx l.Context) l.Dimensions {
284 return wg.Fill(
285 "DocBg", l.Center, 0, 0, func(gtx l.Context) l.Dimensions {
286 return gel.If(
287 *wg.noWallet,
288 wg.CreateWalletPage,
289 func(gtx l.Context) l.Dimensions {
290 switch {
291 case wg.stateLoaded.Load():
292 return wg.MainApp.Fn()(gtx)
293 default:
294 return wg.unlockPage.Fn()(gtx)
295 }
296 },
297 )(gtx)
298 },
299 ).Fn(gtx)
300 },
301 wg.quit.Q,
302 wg.quit,
303 ); E.Chk(e) {
304 }
305 wg.gracefulShutdown()
306 wg.quit.Q()
307 return
308 }
309
310 func (wg *WalletGUI) GetButtons() {
311 wg.sidebarButtons = make([]*gel.Clickable, 12)
312 // wg.walletLocked.Store(true)
313 for i := range wg.sidebarButtons {
314 wg.sidebarButtons[i] = wg.Clickable()
315 }
316 wg.buttonBarButtons = make([]*gel.Clickable, 5)
317 for i := range wg.buttonBarButtons {
318 wg.buttonBarButtons[i] = wg.Clickable()
319 }
320 wg.statusBarButtons = make([]*gel.Clickable, 8)
321 for i := range wg.statusBarButtons {
322 wg.statusBarButtons[i] = wg.Clickable()
323 }
324 }
325
326 func (wg *WalletGUI) ShuffleSeed() {
327 wg.createSeed = make([]byte, 32)
328 _, _ = rand.Read(wg.createSeed)
329 var e error
330 var wk string
331 if wk, e = bip39.NewMnemonic(wg.createSeed); E.Chk(e) {
332 panic(e)
333 }
334 wg.createWords = wk
335 // wg.createMatch = wk
336 wks := strings.Split(wk, " ")
337 var out string
338 for i := 0; i < 24; i += 4 {
339 out += strings.Join(wks[i:i+4], " ")
340 if i+4 < 24 {
341 out += "\n"
342 }
343 }
344 wg.showWords = out
345 }
346
347 func (wg *WalletGUI) GetInputs() Inputs {
348 wg.ShuffleSeed()
349 return Inputs{
350 "receiveAmount": wg.Input("", "Amount", "DocText", "PanelBg", "DocBg", func(amt string) {}, func(string) {}),
351 "receiveMessage": wg.Input(
352 "",
353 "Title",
354 "DocText",
355 "PanelBg",
356 "DocBg",
357 func(pass string) {},
358 func(string) {},
359 ),
360
361 "sendAddress": wg.Input(
362 "",
363 "Parallelcoin Address",
364 "DocText",
365 "PanelBg",
366 "DocBg",
367 func(amt string) {},
368 func(string) {},
369 ),
370 "sendAmount": wg.Input("", "Amount", "DocText", "PanelBg", "DocBg", func(amt string) {}, func(string) {}),
371 "sendMessage": wg.Input(
372 "",
373 "Title",
374 "DocText",
375 "PanelBg",
376 "DocBg",
377 func(pass string) {},
378 func(string) {},
379 ),
380
381 "console": wg.Input(
382 "",
383 "enter rpc command",
384 "DocText",
385 "Transparent",
386 "PanelBg",
387 func(pass string) {},
388 func(string) {},
389 ),
390 "walletWords": wg.Input(
391 /*wg.createWords*/ "", "wallet word seed", "DocText", "DocBg", "PanelBg", func(string) {},
392 func(seedWords string) {
393 wg.createMatch = seedWords
394 wg.Invalidate()
395 },
396 ),
397 "walletRestore": wg.Input(
398 /*wg.createWords*/ "", "enter seed to restore", "DocText", "DocBg", "PanelBg", func(string) {},
399 func(seedWords string) {
400 var e error
401 wg.createMatch = seedWords
402 if wg.createSeed, e = bip39.EntropyFromMnemonic(seedWords); E.Chk(e) {
403 return
404 }
405 wg.createWords = seedWords
406 wg.Invalidate()
407 },
408 ),
409 // "walletSeed": wg.Input(
410 // seedString, "wallet seed", "DocText", "DocBg", "PanelBg", func(seedHex string) {
411 // var e error
412 // if wg.createSeed, e = hex.DecodeString(seedHex); E.Chk(e) {
413 // return
414 // }
415 // var wk string
416 // if wk, e = bip39.NewMnemonic(wg.createSeed); E.Chk(e) {
417 // panic(e)
418 // }
419 // wg.createWords=wk
420 // wks := strings.Split(wk, " ")
421 // var out string
422 // for i := 0; i < 24; i += 4 {
423 // out += strings.Join(wks[i:i+4], " ") + "\n"
424 // }
425 // wg.showWords = out
426 // }, nil,
427 // ),
428 }
429 }
430
431 // GetPasswords returns the passwords used in the wallet GUI
432 func (wg *WalletGUI) GetPasswords() (passwords Passwords) {
433 passwords = Passwords{
434 "passEditor": wg.Password(
435 "password (minimum 8 characters length)",
436 text.New(meta.Data{}, ""),
437 "DocText",
438 "DocBg",
439 "PanelBg",
440 func(pass string) {},
441 ),
442 "confirmPassEditor": wg.Password(
443 "confirm",
444 text.New(meta.Data{}, ""),
445 "DocText",
446 "DocBg",
447 "PanelBg",
448 func(pass string) {},
449 ),
450 "publicPassEditor": wg.Password(
451 "public password (optional)",
452 wg.cx.Config.WalletPass,
453 "Primary",
454 "DocText",
455 "PanelBg",
456 func(pass string) {},
457 ),
458 }
459 return
460 }
461
462 func (wg *WalletGUI) GetIncDecs() IncDecMap {
463 return IncDecMap{
464 "generatethreads": wg.IncDec().
465 NDigits(2).
466 Min(0).
467 Max(runtime.NumCPU()).
468 SetCurrent(wg.cx.Config.GenThreads.V()).
469 ChangeHook(
470 func(n int) {
471 D.Ln("threads value now", n)
472 go func() {
473 D.Ln("setting thread count")
474 if wg.miner.Running() && n != 0 {
475 wg.miner.Stop()
476 wg.miner.Start()
477 }
478 if n == 0 {
479 wg.miner.Stop()
480 }
481 wg.cx.Config.GenThreads.Set(n)
482 _ = wg.cx.Config.WriteToFile(wg.cx.Config.ConfigFile.V())
483 // if wg.miner.Running() {
484 // D.Ln("restarting miner")
485 // wg.miner.Stop()
486 // wg.miner.Start()
487 // }
488 }()
489 },
490 ),
491 "idleTimeout": wg.IncDec().
492 Scale(4).
493 Min(60).
494 Max(3600).
495 NDigits(4).
496 Amount(60).
497 SetCurrent(300).
498 ChangeHook(
499 func(n int) {
500 D.Ln("idle timeout", time.Duration(n)*time.Second)
501 },
502 ),
503 }
504 }
505
506 func (wg *WalletGUI) GetRunUnit(
507 name string, before, after func(), args ...string,
508 ) *rununit.RunUnit {
509 I.Ln("getting rununit for", name, args)
510 // we have to copy the args otherwise further mutations affect this one
511 argsCopy := make([]string, len(args))
512 copy(argsCopy, args)
513 return rununit.New(name, before, after, pipe.SimpleLog(name),
514 pipe.FilterNone, wg.quit, argsCopy...)
515 }
516
517 func (wg *WalletGUI) GetLists() (o ListMap) {
518 return ListMap{
519 "createWallet": wg.List(),
520 "overview": wg.List(),
521 "balances": wg.List(),
522 "recent": wg.List(),
523 "send": wg.List(),
524 "sendMedium": wg.List(),
525 "sendAddresses": wg.List(),
526 "receive": wg.List(),
527 "receiveMedium": wg.List(),
528 "receiveAddresses": wg.List(),
529 "transactions": wg.List(),
530 "settings": wg.List(),
531 "received": wg.List(),
532 "history": wg.List(),
533 "txdetail": wg.List(),
534 }
535 }
536
537 func (wg *WalletGUI) GetClickables() ClickableMap {
538 return ClickableMap{
539 "balanceConfirmed": wg.Clickable(),
540 "balanceUnconfirmed": wg.Clickable(),
541 "balanceTotal": wg.Clickable(),
542 "createWallet": wg.Clickable(),
543 "createVerify": wg.Clickable(),
544 "createShuffle": wg.Clickable(),
545 "createRestore": wg.Clickable(),
546 "genesis": wg.Clickable(),
547 "autofill": wg.Clickable(),
548 "quit": wg.Clickable(),
549 "sendSend": wg.Clickable(),
550 "sendSave": wg.Clickable(),
551 "sendFromRequest": wg.Clickable(),
552 "receiveCreateNewAddress": wg.Clickable(),
553 "receiveClear": wg.Clickable(),
554 "receiveShow": wg.Clickable(),
555 "receiveRemove": wg.Clickable(),
556 "transactions10": wg.Clickable(),
557 "transactions30": wg.Clickable(),
558 "transactions50": wg.Clickable(),
559 "txPageForward": wg.Clickable(),
560 "txPageBack": wg.Clickable(),
561 "theme": wg.Clickable(),
562 }
563 }
564
565 func (wg *WalletGUI) GetCheckables() CheckableMap {
566 return CheckableMap{}
567 }
568
569 func (wg *WalletGUI) GetBools() BoolMap {
570 return BoolMap{
571 "runstate": wg.Bool(wg.node.Running()),
572 "encryption": wg.Bool(false),
573 "seed": wg.Bool(false),
574 "testnet": wg.Bool(false),
575 "lan": wg.Bool(false),
576 "solo": wg.Bool(false),
577 "ihaveread": wg.Bool(false),
578 "showGenerate": wg.Bool(true),
579 "showSent": wg.Bool(true),
580 "showReceived": wg.Bool(true),
581 "showImmature": wg.Bool(true),
582 }
583 }
584
585 var shuttingDown = false
586
587 func (wg *WalletGUI) gracefulShutdown() {
588 if shuttingDown {
589 D.Ln(log.Caller("already called gracefulShutdown", 1))
590 return
591 } else {
592 shuttingDown = true
593 }
594 D.Ln("\nquitting wallet gui\n")
595 if wg.miner.Running() {
596 D.Ln("stopping miner")
597 wg.miner.Stop()
598 wg.miner.Shutdown()
599 }
600 if wg.wallet.Running() {
601 D.Ln("stopping wallet")
602 wg.wallet.Stop()
603 wg.wallet.Shutdown()
604 wg.unlockPassword.Wipe()
605 // wg.walletLocked.Store(true)
606 }
607 if wg.node.Running() {
608 D.Ln("stopping node")
609 wg.node.Stop()
610 wg.node.Shutdown()
611 }
612 // wg.ChainMutex.Lock()
613 if wg.ChainClient != nil {
614 D.Ln("stopping chain client")
615 wg.ChainClient.Shutdown()
616 wg.ChainClient = nil
617 }
618 // wg.ChainMutex.Unlock()
619 // wg.WalletMutex.Lock()
620 if wg.WalletClient != nil {
621 D.Ln("stopping wallet client")
622 wg.WalletClient.Shutdown()
623 wg.WalletClient = nil
624 }
625 // wg.WalletMutex.Unlock()
626 // interrupt.Request()
627 // time.Sleep(time.Second)
628 wg.quit.Q()
629 }
630
631 var handlersMulticast = transport.Handlers{
632 // string(sol.Magic): processSolMsg,
633 string(p2padvt.Magic): processAdvtMsg,
634 // string(hashrate.Magic): processHashrateMsg,
635 }
636
637 func processAdvtMsg(
638 ctx interface{}, src net.Addr, dst string, b []byte,
639 ) (e error) {
640 wg := ctx.(*WalletGUI)
641 if wg.cx.Config.Discovery.False() {
642 return
643 }
644 if wg.ChainClient == nil {
645 T.Ln("no chain client to process advertisment")
646 return
647 }
648 var j p2padvt.Advertisment
649 gotiny.Unmarshal(b, &j)
650 // I.S(j)
651 var peerUUID uint64
652 peerUUID = j.UUID
653 // I.Ln("peerUUID of advertisment", peerUUID, wg.otherNodes)
654 if int(peerUUID) == wg.cx.Config.UUID.V() {
655 D.Ln("ignoring own advertisment message")
656 return
657 }
658 if _, ok := wg.otherNodes[peerUUID]; !ok {
659 var pi []btcjson.GetPeerInfoResult
660 if pi, e = wg.ChainClient.GetPeerInfo(); E.Chk(e) {
661 }
662 for i := range pi {
663 for k := range j.IPs {
664 jpa := net.JoinHostPort(k, fmt.Sprint(j.P2P))
665 if jpa == pi[i].Addr {
666 I.Ln("not connecting to node already connected outbound")
667 return
668 }
669 if jpa == pi[i].AddrLocal {
670 I.Ln("not connecting to node already connected inbound")
671 return
672 }
673 }
674 }
675 // if we haven't already added it to the permanent peer list, we can add it now
676 I.Ln("connecting to lan peer with same PSK", j.IPs, peerUUID)
677 wg.otherNodes[peerUUID] = &nodeSpec{}
678 wg.otherNodes[peerUUID].Time = time.Now()
679 for i := range j.IPs {
680 addy := net.JoinHostPort(i, fmt.Sprint(j.P2P))
681 for j := range pi {
682 if addy == pi[j].Addr || addy == pi[j].AddrLocal {
683 // not connecting to peer we already have connected to
684 return
685 }
686 }
687 }
688 // try all IPs
689 for addr := range j.IPs {
690 peerIP := net.JoinHostPort(addr, fmt.Sprint(j.P2P))
691 if e = wg.ChainClient.AddNode(peerIP, "add"); E.Chk(e) {
692 continue
693 }
694 D.Ln("connected to peer via address", peerIP)
695 wg.otherNodes[peerUUID].addr = peerIP
696 break
697 }
698 I.Ln(peerUUID, "added", "otherNodes", wg.otherNodes)
699 } else {
700 // update last seen time for peerUUID for garbage collection of stale disconnected
701 // nodes
702 D.Ln("other node seen again", peerUUID, wg.otherNodes[peerUUID].addr)
703 wg.otherNodes[peerUUID].Time = time.Now()
704 }
705 // I.S(wg.otherNodes)
706 // If we lose connection for more than 9 seconds we delete and if the node
707 // reappears it can be reconnected
708 for i := range wg.otherNodes {
709 D.Ln(i, wg.otherNodes[i])
710 tn := time.Now()
711 if tn.Sub(wg.otherNodes[i].Time) > time.Second*6 {
712 // also remove from connection manager
713 if e = wg.ChainClient.AddNode(wg.otherNodes[i].addr, "remove"); E.Chk(e) {
714 }
715 D.Ln("deleting", tn, wg.otherNodes[i], i)
716 delete(wg.otherNodes, i)
717 }
718 }
719 // on := int32(len(wg.otherNodes))
720 // wg.otherNodeCount.Store(on)
721 return
722 }
723