Files
p9/cmd/gui/main.go
Loki Verloren 0e2bba237a initial commit
2021-05-03 10:43:10 +02:00

723 lines
19 KiB
Go

package gui
import (
"crypto/rand"
"fmt"
"net"
"os"
"runtime"
"strings"
"sync"
"time"
"github.com/niubaoshu/gotiny"
"github.com/tyler-smith/go-bip39"
"github.com/p9c/p9/pkg/log"
"github.com/p9c/p9/pkg/opts/meta"
"github.com/p9c/p9/pkg/opts/text"
"github.com/p9c/p9/pkg/chainrpc/p2padvt"
"github.com/p9c/p9/pkg/pipe"
"github.com/p9c/p9/pkg/transport"
"github.com/p9c/p9/pod/state"
uberatomic "go.uber.org/atomic"
"github.com/p9c/p9/pkg/gel/gio/op/paint"
"github.com/p9c/p9/pkg/qu"
"github.com/p9c/p9/pkg/interrupt"
"github.com/p9c/p9/pkg/gel"
"github.com/p9c/p9/pkg/btcjson"
l "github.com/p9c/p9/pkg/gel/gio/layout"
"github.com/p9c/p9/cmd/gui/cfg"
"github.com/p9c/p9/pkg/apputil"
"github.com/p9c/p9/pkg/rpcclient"
"github.com/p9c/p9/pkg/util/rununit"
)
// Main is the entrypoint for the wallet GUI
func Main(cx *state.State) (e error) {
size := uberatomic.NewInt32(0)
noWallet := true
wg := &WalletGUI{
cx: cx,
invalidate: qu.Ts(16),
quit: cx.KillAll,
Size: size,
noWallet: &noWallet,
otherNodes: make(map[uint64]*nodeSpec),
certs: cx.Config.ReadCAFile(),
}
return wg.Run()
}
type BoolMap map[string]*gel.Bool
type ListMap map[string]*gel.List
type CheckableMap map[string]*gel.Checkable
type ClickableMap map[string]*gel.Clickable
type Inputs map[string]*gel.Input
type Passwords map[string]*gel.Password
type IncDecMap map[string]*gel.IncDec
type WalletGUI struct {
wg sync.WaitGroup
cx *state.State
quit qu.C
State *State
noWallet *bool
node, wallet, miner *rununit.RunUnit
walletToLock time.Time
walletLockTime int
ChainMutex, WalletMutex sync.Mutex
ChainClient, WalletClient *rpcclient.Client
WalletWatcher qu.C
*gel.Window
Size *uberatomic.Int32
MainApp *gel.App
invalidate qu.C
unlockPage *gel.App
loadingPage *gel.App
config *cfg.Config
configs cfg.GroupsMap
unlockPassword *gel.Password
sidebarButtons []*gel.Clickable
buttonBarButtons []*gel.Clickable
statusBarButtons []*gel.Clickable
receiveAddressbookClickables []*gel.Clickable
sendAddressbookClickables []*gel.Clickable
quitClickable *gel.Clickable
bools BoolMap
lists ListMap
checkables CheckableMap
clickables ClickableMap
inputs Inputs
passwords Passwords
incdecs IncDecMap
console *Console
RecentTxsWidget, TxHistoryWidget l.Widget
recentTxsClickables, txHistoryClickables []*gel.Clickable
txHistoryList []btcjson.ListTransactionsResult
openTxID, prevOpenTxID *uberatomic.String
originTxDetail string
txMx sync.Mutex
stateLoaded *uberatomic.Bool
currentReceiveQRCode *paint.ImageOp
currentReceiveAddress string
currentReceiveQR l.Widget
currentReceiveRegenClickable *gel.Clickable
currentReceiveCopyClickable *gel.Clickable
currentReceiveRegenerate *uberatomic.Bool
// currentReceiveGetNew *uberatomic.Bool
sendClickable *gel.Clickable
ready *uberatomic.Bool
mainDirection l.Direction
preRendering bool
// ReceiveAddressbook l.Widget
// SendAddressbook l.Widget
ReceivePage *ReceivePage
SendPage *SendPage
// toasts *toast.Toasts
// dialog *dialog.Dialog
createSeed []byte
createWords, showWords, createMatch string
createVerifying bool
restoring bool
lastUpdated uberatomic.Int64
multiConn *transport.Channel
otherNodes map[uint64]*nodeSpec
uuid uint64
peerCount *uberatomic.Int32
certs []byte
}
type nodeSpec struct {
time.Time
addr string
}
func (wg *WalletGUI) Run() (e error) {
wg.openTxID = uberatomic.NewString("")
var mc *transport.Channel
quit := qu.T()
// I.Ln(wg.cx.Config.MulticastPass.V(), string(wg.cx.Config.MulticastPass.
// Bytes()))
if mc, e = transport.NewBroadcastChannel(
"controller",
wg,
wg.cx.Config.MulticastPass.Bytes(),
transport.DefaultPort,
16384,
handlersMulticast,
quit,
); E.Chk(e) {
return
}
wg.multiConn = mc
wg.peerCount = uberatomic.NewInt32(0)
wg.prevOpenTxID = uberatomic.NewString("")
wg.stateLoaded = uberatomic.NewBool(false)
wg.currentReceiveRegenerate = uberatomic.NewBool(true)
wg.ready = uberatomic.NewBool(false)
wg.Window = gel.NewWindowP9(wg.quit)
wg.Dark = wg.cx.Config.DarkTheme
wg.Colors.SetDarkTheme(wg.Dark.True())
*wg.noWallet = true
wg.GetButtons()
wg.lists = wg.GetLists()
wg.clickables = wg.GetClickables()
wg.checkables = wg.GetCheckables()
before := func() { D.Ln("running before") }
after := func() { D.Ln("running after") }
I.Ln(os.Args[1:])
options := []string{os.Args[0]}
// options = append(options, wg.cx.Config.FoundArgs...)
// options = append(options, "pipelog")
wg.node = wg.GetRunUnit(
"NODE", before, after,
append(options, "node")...,
// "node",
)
wg.wallet = wg.GetRunUnit(
"WLLT", before, after,
append(options, "wallet")...,
// "wallet",
)
wg.miner = wg.GetRunUnit(
"MINE", before, after,
append(options, "kopach")...,
// "wallet",
)
// I.S(wg.node, wg.wallet, wg.miner)
wg.bools = wg.GetBools()
wg.inputs = wg.GetInputs()
wg.passwords = wg.GetPasswords()
// wg.toasts = toast.New(wg.th)
// wg.dialog = dialog.New(wg.th)
wg.console = wg.ConsolePage()
wg.quitClickable = wg.Clickable()
wg.incdecs = wg.GetIncDecs()
wg.Size = wg.Window.Width
wg.currentReceiveCopyClickable = wg.WidgetPool.GetClickable()
wg.currentReceiveRegenClickable = wg.WidgetPool.GetClickable()
wg.currentReceiveQR = func(gtx l.Context) l.Dimensions {
return l.Dimensions{}
}
wg.ReceivePage = wg.GetReceivePage()
wg.SendPage = wg.GetSendPage()
wg.MainApp = wg.GetAppWidget()
wg.State = GetNewState(wg.cx.ActiveNet, wg.MainApp.ActivePageGetAtomic())
wg.unlockPage = wg.getWalletUnlockAppWidget()
wg.loadingPage = wg.getLoadingPage()
if !apputil.FileExists(wg.cx.Config.WalletFile.V()) {
I.Ln("wallet file does not exist", wg.cx.Config.WalletFile.V())
} else {
*wg.noWallet = false
// if !*wg.cx.Config.NodeOff {
// // wg.startNode()
// wg.node.Start()
// }
if wg.cx.Config.Generate.True() && wg.cx.Config.GenThreads.V() != 0 {
// wg.startMiner()
wg.miner.Start()
}
wg.unlockPassword.Focus()
}
interrupt.AddHandler(
func() {
D.Ln("quitting wallet gui")
// consume.Kill(wg.Node)
// consume.Kill(wg.Miner)
// wg.gracefulShutdown()
wg.quit.Q()
},
)
go func() {
ticker := time.NewTicker(time.Second)
out:
for {
select {
case <-ticker.C:
go func() {
if e = wg.Advertise(); E.Chk(e) {
}
if wg.node.Running() {
if wg.ChainClient != nil {
if !wg.ChainClient.Disconnected() {
var pi []btcjson.GetPeerInfoResult
if pi, e = wg.ChainClient.GetPeerInfo(); E.Chk(e) {
return
}
wg.peerCount.Store(int32(len(pi)))
wg.Invalidate()
}
}
}
}()
case <-wg.invalidate.Wait():
T.Ln("invalidating render queue")
wg.Window.Window.Invalidate()
// TODO: make a more appropriate trigger for this - ie, when state actually changes.
// if wg.wallet.Running() && wg.stateLoaded.Load() {
// filename := filepath.Join(wg.cx.DataDir, "state.json")
// if e := wg.State.Save(filename, wg.cx.Config.WalletPass); E.Chk(e) {
// }
// }
case <-wg.cx.KillAll.Wait():
break out
case <-wg.quit.Wait():
break out
}
}
}()
if e := wg.Window.
Size(56, 32).
Title("ParallelCoin Wallet").
Open().
Run(
func(gtx l.Context) l.Dimensions {
return wg.Fill(
"DocBg", l.Center, 0, 0, func(gtx l.Context) l.Dimensions {
return gel.If(
*wg.noWallet,
wg.CreateWalletPage,
func(gtx l.Context) l.Dimensions {
switch {
case wg.stateLoaded.Load():
return wg.MainApp.Fn()(gtx)
default:
return wg.unlockPage.Fn()(gtx)
}
},
)(gtx)
},
).Fn(gtx)
},
wg.quit.Q,
wg.quit,
); E.Chk(e) {
}
wg.gracefulShutdown()
wg.quit.Q()
return
}
func (wg *WalletGUI) GetButtons() {
wg.sidebarButtons = make([]*gel.Clickable, 12)
// wg.walletLocked.Store(true)
for i := range wg.sidebarButtons {
wg.sidebarButtons[i] = wg.Clickable()
}
wg.buttonBarButtons = make([]*gel.Clickable, 5)
for i := range wg.buttonBarButtons {
wg.buttonBarButtons[i] = wg.Clickable()
}
wg.statusBarButtons = make([]*gel.Clickable, 8)
for i := range wg.statusBarButtons {
wg.statusBarButtons[i] = wg.Clickable()
}
}
func (wg *WalletGUI) ShuffleSeed() {
wg.createSeed = make([]byte, 32)
_, _ = rand.Read(wg.createSeed)
var e error
var wk string
if wk, e = bip39.NewMnemonic(wg.createSeed); E.Chk(e) {
panic(e)
}
wg.createWords = wk
// wg.createMatch = wk
wks := strings.Split(wk, " ")
var out string
for i := 0; i < 24; i += 4 {
out += strings.Join(wks[i:i+4], " ")
if i+4 < 24 {
out += "\n"
}
}
wg.showWords = out
}
func (wg *WalletGUI) GetInputs() Inputs {
wg.ShuffleSeed()
return Inputs{
"receiveAmount": wg.Input("", "Amount", "DocText", "PanelBg", "DocBg", func(amt string) {}, func(string) {}),
"receiveMessage": wg.Input(
"",
"Title",
"DocText",
"PanelBg",
"DocBg",
func(pass string) {},
func(string) {},
),
"sendAddress": wg.Input(
"",
"Parallelcoin Address",
"DocText",
"PanelBg",
"DocBg",
func(amt string) {},
func(string) {},
),
"sendAmount": wg.Input("", "Amount", "DocText", "PanelBg", "DocBg", func(amt string) {}, func(string) {}),
"sendMessage": wg.Input(
"",
"Title",
"DocText",
"PanelBg",
"DocBg",
func(pass string) {},
func(string) {},
),
"console": wg.Input(
"",
"enter rpc command",
"DocText",
"Transparent",
"PanelBg",
func(pass string) {},
func(string) {},
),
"walletWords": wg.Input(
/*wg.createWords*/ "", "wallet word seed", "DocText", "DocBg", "PanelBg", func(string) {},
func(seedWords string) {
wg.createMatch = seedWords
wg.Invalidate()
},
),
"walletRestore": wg.Input(
/*wg.createWords*/ "", "enter seed to restore", "DocText", "DocBg", "PanelBg", func(string) {},
func(seedWords string) {
var e error
wg.createMatch = seedWords
if wg.createSeed, e = bip39.EntropyFromMnemonic(seedWords); E.Chk(e) {
return
}
wg.createWords = seedWords
wg.Invalidate()
},
),
// "walletSeed": wg.Input(
// seedString, "wallet seed", "DocText", "DocBg", "PanelBg", func(seedHex string) {
// var e error
// if wg.createSeed, e = hex.DecodeString(seedHex); E.Chk(e) {
// return
// }
// var wk string
// if wk, e = bip39.NewMnemonic(wg.createSeed); E.Chk(e) {
// panic(e)
// }
// wg.createWords=wk
// wks := strings.Split(wk, " ")
// var out string
// for i := 0; i < 24; i += 4 {
// out += strings.Join(wks[i:i+4], " ") + "\n"
// }
// wg.showWords = out
// }, nil,
// ),
}
}
// GetPasswords returns the passwords used in the wallet GUI
func (wg *WalletGUI) GetPasswords() (passwords Passwords) {
passwords = Passwords{
"passEditor": wg.Password(
"password (minimum 8 characters length)",
text.New(meta.Data{}, ""),
"DocText",
"DocBg",
"PanelBg",
func(pass string) {},
),
"confirmPassEditor": wg.Password(
"confirm",
text.New(meta.Data{}, ""),
"DocText",
"DocBg",
"PanelBg",
func(pass string) {},
),
"publicPassEditor": wg.Password(
"public password (optional)",
wg.cx.Config.WalletPass,
"Primary",
"DocText",
"PanelBg",
func(pass string) {},
),
}
return
}
func (wg *WalletGUI) GetIncDecs() IncDecMap {
return IncDecMap{
"generatethreads": wg.IncDec().
NDigits(2).
Min(0).
Max(runtime.NumCPU()).
SetCurrent(wg.cx.Config.GenThreads.V()).
ChangeHook(
func(n int) {
D.Ln("threads value now", n)
go func() {
D.Ln("setting thread count")
if wg.miner.Running() && n != 0 {
wg.miner.Stop()
wg.miner.Start()
}
if n == 0 {
wg.miner.Stop()
}
wg.cx.Config.GenThreads.Set(n)
_ = wg.cx.Config.WriteToFile(wg.cx.Config.ConfigFile.V())
// if wg.miner.Running() {
// D.Ln("restarting miner")
// wg.miner.Stop()
// wg.miner.Start()
// }
}()
},
),
"idleTimeout": wg.IncDec().
Scale(4).
Min(60).
Max(3600).
NDigits(4).
Amount(60).
SetCurrent(300).
ChangeHook(
func(n int) {
D.Ln("idle timeout", time.Duration(n)*time.Second)
},
),
}
}
func (wg *WalletGUI) GetRunUnit(
name string, before, after func(), args ...string,
) *rununit.RunUnit {
I.Ln("getting rununit for", name, args)
// we have to copy the args otherwise further mutations affect this one
argsCopy := make([]string, len(args))
copy(argsCopy, args)
return rununit.New(name, before, after, pipe.SimpleLog(name),
pipe.FilterNone, wg.quit, argsCopy...)
}
func (wg *WalletGUI) GetLists() (o ListMap) {
return ListMap{
"createWallet": wg.List(),
"overview": wg.List(),
"balances": wg.List(),
"recent": wg.List(),
"send": wg.List(),
"sendMedium": wg.List(),
"sendAddresses": wg.List(),
"receive": wg.List(),
"receiveMedium": wg.List(),
"receiveAddresses": wg.List(),
"transactions": wg.List(),
"settings": wg.List(),
"received": wg.List(),
"history": wg.List(),
"txdetail": wg.List(),
}
}
func (wg *WalletGUI) GetClickables() ClickableMap {
return ClickableMap{
"balanceConfirmed": wg.Clickable(),
"balanceUnconfirmed": wg.Clickable(),
"balanceTotal": wg.Clickable(),
"createWallet": wg.Clickable(),
"createVerify": wg.Clickable(),
"createShuffle": wg.Clickable(),
"createRestore": wg.Clickable(),
"genesis": wg.Clickable(),
"autofill": wg.Clickable(),
"quit": wg.Clickable(),
"sendSend": wg.Clickable(),
"sendSave": wg.Clickable(),
"sendFromRequest": wg.Clickable(),
"receiveCreateNewAddress": wg.Clickable(),
"receiveClear": wg.Clickable(),
"receiveShow": wg.Clickable(),
"receiveRemove": wg.Clickable(),
"transactions10": wg.Clickable(),
"transactions30": wg.Clickable(),
"transactions50": wg.Clickable(),
"txPageForward": wg.Clickable(),
"txPageBack": wg.Clickable(),
"theme": wg.Clickable(),
}
}
func (wg *WalletGUI) GetCheckables() CheckableMap {
return CheckableMap{}
}
func (wg *WalletGUI) GetBools() BoolMap {
return BoolMap{
"runstate": wg.Bool(wg.node.Running()),
"encryption": wg.Bool(false),
"seed": wg.Bool(false),
"testnet": wg.Bool(false),
"lan": wg.Bool(false),
"solo": wg.Bool(false),
"ihaveread": wg.Bool(false),
"showGenerate": wg.Bool(true),
"showSent": wg.Bool(true),
"showReceived": wg.Bool(true),
"showImmature": wg.Bool(true),
}
}
var shuttingDown = false
func (wg *WalletGUI) gracefulShutdown() {
if shuttingDown {
D.Ln(log.Caller("already called gracefulShutdown", 1))
return
} else {
shuttingDown = true
}
D.Ln("\nquitting wallet gui\n")
if wg.miner.Running() {
D.Ln("stopping miner")
wg.miner.Stop()
wg.miner.Shutdown()
}
if wg.wallet.Running() {
D.Ln("stopping wallet")
wg.wallet.Stop()
wg.wallet.Shutdown()
wg.unlockPassword.Wipe()
// wg.walletLocked.Store(true)
}
if wg.node.Running() {
D.Ln("stopping node")
wg.node.Stop()
wg.node.Shutdown()
}
// wg.ChainMutex.Lock()
if wg.ChainClient != nil {
D.Ln("stopping chain client")
wg.ChainClient.Shutdown()
wg.ChainClient = nil
}
// wg.ChainMutex.Unlock()
// wg.WalletMutex.Lock()
if wg.WalletClient != nil {
D.Ln("stopping wallet client")
wg.WalletClient.Shutdown()
wg.WalletClient = nil
}
// wg.WalletMutex.Unlock()
// interrupt.Request()
// time.Sleep(time.Second)
wg.quit.Q()
}
var handlersMulticast = transport.Handlers{
// string(sol.Magic): processSolMsg,
string(p2padvt.Magic): processAdvtMsg,
// string(hashrate.Magic): processHashrateMsg,
}
func processAdvtMsg(
ctx interface{}, src net.Addr, dst string, b []byte,
) (e error) {
wg := ctx.(*WalletGUI)
if wg.cx.Config.Discovery.False() {
return
}
if wg.ChainClient == nil {
T.Ln("no chain client to process advertisment")
return
}
var j p2padvt.Advertisment
gotiny.Unmarshal(b, &j)
// I.S(j)
var peerUUID uint64
peerUUID = j.UUID
// I.Ln("peerUUID of advertisment", peerUUID, wg.otherNodes)
if int(peerUUID) == wg.cx.Config.UUID.V() {
D.Ln("ignoring own advertisment message")
return
}
if _, ok := wg.otherNodes[peerUUID]; !ok {
var pi []btcjson.GetPeerInfoResult
if pi, e = wg.ChainClient.GetPeerInfo(); E.Chk(e) {
}
for i := range pi {
for k := range j.IPs {
jpa := net.JoinHostPort(k, fmt.Sprint(j.P2P))
if jpa == pi[i].Addr {
I.Ln("not connecting to node already connected outbound")
return
}
if jpa == pi[i].AddrLocal {
I.Ln("not connecting to node already connected inbound")
return
}
}
}
// if we haven't already added it to the permanent peer list, we can add it now
I.Ln("connecting to lan peer with same PSK", j.IPs, peerUUID)
wg.otherNodes[peerUUID] = &nodeSpec{}
wg.otherNodes[peerUUID].Time = time.Now()
for i := range j.IPs {
addy := net.JoinHostPort(i, fmt.Sprint(j.P2P))
for j := range pi {
if addy == pi[j].Addr || addy == pi[j].AddrLocal {
// not connecting to peer we already have connected to
return
}
}
}
// try all IPs
for addr := range j.IPs {
peerIP := net.JoinHostPort(addr, fmt.Sprint(j.P2P))
if e = wg.ChainClient.AddNode(peerIP, "add"); E.Chk(e) {
continue
}
D.Ln("connected to peer via address", peerIP)
wg.otherNodes[peerUUID].addr = peerIP
break
}
I.Ln(peerUUID, "added", "otherNodes", wg.otherNodes)
} else {
// update last seen time for peerUUID for garbage collection of stale disconnected
// nodes
D.Ln("other node seen again", peerUUID, wg.otherNodes[peerUUID].addr)
wg.otherNodes[peerUUID].Time = time.Now()
}
// I.S(wg.otherNodes)
// If we lose connection for more than 9 seconds we delete and if the node
// reappears it can be reconnected
for i := range wg.otherNodes {
D.Ln(i, wg.otherNodes[i])
tn := time.Now()
if tn.Sub(wg.otherNodes[i].Time) > time.Second*6 {
// also remove from connection manager
if e = wg.ChainClient.AddNode(wg.otherNodes[i].addr, "remove"); E.Chk(e) {
}
D.Ln("deleting", tn, wg.otherNodes[i], i)
delete(wg.otherNodes, i)
}
}
// on := int32(len(wg.otherNodes))
// wg.otherNodeCount.Store(on)
return
}