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