state.go raw

   1  package gui
   2  
   3  import (
   4  	"crypto/cipher"
   5  	"encoding/json"
   6  	"io/ioutil"
   7  	"time"
   8  
   9  	"github.com/p9c/p9/pkg/amt"
  10  	"github.com/p9c/p9/pkg/btcaddr"
  11  	"github.com/p9c/p9/pkg/chaincfg"
  12  
  13  	uberatomic "go.uber.org/atomic"
  14  
  15  	l "github.com/p9c/p9/pkg/gel/gio/layout"
  16  
  17  	"github.com/p9c/p9/pkg/btcjson"
  18  	"github.com/p9c/p9/pkg/chainhash"
  19  	"github.com/p9c/p9/pkg/gcm"
  20  	"github.com/p9c/p9/pkg/transport"
  21  	"github.com/p9c/p9/pkg/util/atom"
  22  )
  23  
  24  const ZeroAddress = "1111111111111111111114oLvT2"
  25  
  26  // CategoryFilter marks which transactions to omit from the filtered transaction list
  27  type CategoryFilter struct {
  28  	Send     bool
  29  	Generate bool
  30  	Immature bool
  31  	Receive  bool
  32  	Unknown  bool
  33  }
  34  
  35  func (c *CategoryFilter) Filter(s string) (include bool) {
  36  	include = true
  37  	if c.Send && s == "send" {
  38  		include = false
  39  	}
  40  	if c.Generate && s == "generate" {
  41  		include = false
  42  	}
  43  	if c.Immature && s == "immature" {
  44  		include = false
  45  	}
  46  	if c.Receive && s == "receive" {
  47  		include = false
  48  	}
  49  	if c.Unknown && s == "unknown" {
  50  		include = false
  51  	}
  52  	return
  53  }
  54  
  55  type AddressEntry struct {
  56  	Address  string     `json:"address"`
  57  	Message  string     `json:"message,omitempty"`
  58  	Label    string     `json:"label,omitempty"`
  59  	Amount   amt.Amount `json:"amount"`
  60  	Created  time.Time  `json:"created"`
  61  	Modified time.Time  `json:"modified"`
  62  	TxID     string     `json:txid,omitempty'`
  63  }
  64  
  65  type State struct {
  66  	lastUpdated             *atom.Time
  67  	bestBlockHeight         *atom.Int32
  68  	bestBlockHash           *atom.Hash
  69  	balance                 *atom.Float64
  70  	balanceUnconfirmed      *atom.Float64
  71  	goroutines              []l.Widget
  72  	allTxs                  *atom.ListTransactionsResult
  73  	filteredTxs             *atom.ListTransactionsResult
  74  	filter                  CategoryFilter
  75  	filterChanged           *atom.Bool
  76  	currentReceivingAddress *atom.Address
  77  	isAddress               *atom.Bool
  78  	activePage              *uberatomic.String
  79  	sendAddresses           []AddressEntry
  80  	receiveAddresses        []AddressEntry
  81  }
  82  
  83  func GetNewState(params *chaincfg.Params, activePage *uberatomic.String) *State {
  84  	fc := &atom.Bool{
  85  		Bool: uberatomic.NewBool(false),
  86  	}
  87  	return &State{
  88  		lastUpdated:     atom.NewTime(time.Now()),
  89  		bestBlockHeight: &atom.Int32{Int32: uberatomic.NewInt32(0)},
  90  		bestBlockHash:   atom.NewHash(chainhash.Hash{}),
  91  		balance:         &atom.Float64{Float64: uberatomic.NewFloat64(0)},
  92  		balanceUnconfirmed: &atom.Float64{
  93  			Float64: uberatomic.NewFloat64(0),
  94  		},
  95  		goroutines: nil,
  96  		allTxs: atom.NewListTransactionsResult(
  97  			[]btcjson.ListTransactionsResult{},
  98  		),
  99  		filteredTxs: atom.NewListTransactionsResult(
 100  			[]btcjson.ListTransactionsResult{},
 101  		),
 102  		filter:        CategoryFilter{},
 103  		filterChanged: fc,
 104  		currentReceivingAddress: atom.NewAddress(
 105  			&btcaddr.PubKeyHash{},
 106  			params,
 107  		),
 108  		isAddress:  &atom.Bool{Bool: uberatomic.NewBool(false)},
 109  		activePage: activePage,
 110  	}
 111  }
 112  
 113  func (s *State) BumpLastUpdated() {
 114  	s.lastUpdated.Store(time.Now())
 115  }
 116  
 117  func (s *State) SetReceivingAddress(addr btcaddr.Address) {
 118  	s.currentReceivingAddress.Store(addr)
 119  }
 120  
 121  func (s *State) IsReceivingAddress() bool {
 122  	addr := s.currentReceivingAddress.String.Load()
 123  	if addr == ZeroAddress || addr == "" {
 124  		s.isAddress.Store(false)
 125  	} else {
 126  		s.isAddress.Store(true)
 127  	}
 128  	return s.isAddress.Load()
 129  }
 130  
 131  // Save the state to the specified file
 132  func (s *State) Save(filename string, pass []byte, debug bool) (e error) {
 133  	D.Ln("saving state...")
 134  	marshalled := s.Marshal()
 135  	var j []byte
 136  	if j, e = json.MarshalIndent(marshalled, "", "  "); E.Chk(e) {
 137  		return
 138  	}
 139  	// D.Ln(string(j))
 140  	var ciph cipher.AEAD
 141  	if ciph, e = gcm.GetCipher(pass); E.Chk(e) {
 142  		return
 143  	}
 144  	var nonce []byte
 145  	if nonce, e = transport.GetNonce(ciph); E.Chk(e) {
 146  		return
 147  	}
 148  	crypted := append(nonce, ciph.Seal(nil, nonce, j, nil)...)
 149  	var b []byte
 150  	_ = b
 151  	if b, e = ciph.Open(nil, nonce, crypted[len(nonce):], nil); E.Chk(e) {
 152  		// since it was just created it should not fail to decrypt
 153  		panic(e)
 154  		// interrupt.Request()
 155  		return
 156  	}
 157  	if e = ioutil.WriteFile(filename, crypted, 0600); E.Chk(e) {
 158  	}
 159  	if debug {
 160  		if e = ioutil.WriteFile(filename+".clear", j, 0600); E.Chk(e) {
 161  		}
 162  	}
 163  	return
 164  }
 165  
 166  // Load in the configuration from the specified file and decrypt using the given password
 167  func (s *State) Load(filename string, pass []byte) (e error) {
 168  	D.Ln("loading state...")
 169  	var data []byte
 170  	var ciph cipher.AEAD
 171  	if data, e = ioutil.ReadFile(filename); E.Chk(e) {
 172  		return
 173  	}
 174  	D.Ln("cipher:", string(pass))
 175  	if ciph, e = gcm.GetCipher(pass); E.Chk(e) {
 176  		return
 177  	}
 178  	ns := ciph.NonceSize()
 179  	D.Ln("nonce size:", ns)
 180  	nonce := data[:ns]
 181  	data = data[ns:]
 182  	var b []byte
 183  	if b, e = ciph.Open(nil, nonce, data, nil); E.Chk(e) {
 184  		// interrupt.Request()
 185  		return
 186  	}
 187  	// yay, right password, now unmarshal
 188  	ss := &Marshalled{}
 189  	if e = json.Unmarshal(b, ss); E.Chk(e) {
 190  		return
 191  	}
 192  	// D.Ln(string(b))
 193  	ss.Unmarshal(s)
 194  	return
 195  }
 196  
 197  type Marshalled struct {
 198  	LastUpdated        time.Time
 199  	BestBlockHeight    int32
 200  	BestBlockHash      chainhash.Hash
 201  	Balance            float64
 202  	BalanceUnconfirmed float64
 203  	AllTxs             []btcjson.ListTransactionsResult
 204  	Filter             CategoryFilter
 205  	ReceivingAddress   string
 206  	ActivePage         string
 207  	ReceiveAddressBook []AddressEntry
 208  	SendAddressBook    []AddressEntry
 209  }
 210  
 211  func (s *State) Marshal() (out *Marshalled) {
 212  	out = &Marshalled{
 213  		LastUpdated:        s.lastUpdated.Load(),
 214  		BestBlockHeight:    s.bestBlockHeight.Load(),
 215  		BestBlockHash:      s.bestBlockHash.Load(),
 216  		Balance:            s.balance.Load(),
 217  		BalanceUnconfirmed: s.balanceUnconfirmed.Load(),
 218  		AllTxs:             s.allTxs.Load(),
 219  		Filter:             s.filter,
 220  		ReceivingAddress:   s.currentReceivingAddress.Load().EncodeAddress(),
 221  		ActivePage:         s.activePage.Load(),
 222  		ReceiveAddressBook: s.receiveAddresses,
 223  		SendAddressBook:    s.sendAddresses,
 224  	}
 225  	return
 226  }
 227  
 228  func (m *Marshalled) Unmarshal(s *State) {
 229  	s.lastUpdated.Store(m.LastUpdated)
 230  	s.bestBlockHeight.Store(m.BestBlockHeight)
 231  	s.bestBlockHash.Store(m.BestBlockHash)
 232  	s.balance.Store(m.Balance)
 233  	s.balanceUnconfirmed.Store(m.BalanceUnconfirmed)
 234  	if len(s.allTxs.Load()) < len(m.AllTxs) {
 235  		s.allTxs.Store(m.AllTxs)
 236  	}
 237  	s.receiveAddresses = m.ReceiveAddressBook
 238  	s.sendAddresses = m.SendAddressBook
 239  	s.filter = m.Filter
 240  
 241  	if m.ReceivingAddress != "1111111111111111111114oLvT2" {
 242  		var e error
 243  		var ra btcaddr.Address
 244  		if ra, e = btcaddr.Decode(m.ReceivingAddress, s.currentReceivingAddress.ForNet); E.Chk(e) {
 245  		}
 246  		s.currentReceivingAddress.Store(ra)
 247  	}
 248  	s.SetActivePage(m.ActivePage)
 249  	return
 250  }
 251  
 252  func (s *State) Goroutines() []l.Widget {
 253  	return s.goroutines
 254  }
 255  
 256  func (s *State) SetGoroutines(gr []l.Widget) {
 257  	s.goroutines = gr
 258  }
 259  
 260  func (s *State) SetAllTxs(atxs []btcjson.ListTransactionsResult) {
 261  	s.allTxs.Store(atxs)
 262  	// generate filtered state
 263  	filteredTxs := make([]btcjson.ListTransactionsResult, 0, len(s.allTxs.Load()))
 264  	for i := range atxs {
 265  		if s.filter.Filter(atxs[i].Category) {
 266  			filteredTxs = append(filteredTxs, atxs[i])
 267  		}
 268  	}
 269  	s.filteredTxs.Store(filteredTxs)
 270  }
 271  
 272  func (s *State) LastUpdated() time.Time {
 273  	return s.lastUpdated.Load()
 274  }
 275  
 276  func (s *State) BestBlockHeight() int32 {
 277  	return s.bestBlockHeight.Load()
 278  }
 279  
 280  func (s *State) BestBlockHash() *chainhash.Hash {
 281  	o := s.bestBlockHash.Load()
 282  	return &o
 283  }
 284  
 285  func (s *State) Balance() float64 {
 286  	return s.balance.Load()
 287  }
 288  
 289  func (s *State) BalanceUnconfirmed() float64 {
 290  	return s.balanceUnconfirmed.Load()
 291  }
 292  
 293  func (s *State) ActivePage() string {
 294  	return s.activePage.Load()
 295  }
 296  
 297  func (s *State) SetActivePage(page string) {
 298  	s.activePage.Store(page)
 299  }
 300  
 301  func (s *State) SetBestBlockHeight(height int32) {
 302  	s.BumpLastUpdated()
 303  	s.bestBlockHeight.Store(height)
 304  }
 305  
 306  func (s *State) SetBestBlockHash(h *chainhash.Hash) {
 307  	s.BumpLastUpdated()
 308  	s.bestBlockHash.Store(*h)
 309  }
 310  
 311  func (s *State) SetBalance(total float64) {
 312  	s.BumpLastUpdated()
 313  	s.balance.Store(total)
 314  }
 315  
 316  func (s *State) SetBalanceUnconfirmed(unconfirmed float64) {
 317  	s.BumpLastUpdated()
 318  	s.balanceUnconfirmed.Store(unconfirmed)
 319  }
 320