github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/stellar/wallet_state.go (about)

     1  package stellar
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"strconv"
     8  	"strings"
     9  	"sync"
    10  	"time"
    11  
    12  	"github.com/golang/groupcache/singleflight"
    13  	"github.com/keybase/client/go/libkb"
    14  	"github.com/keybase/client/go/protocol/stellar1"
    15  	"github.com/keybase/client/go/stellar/remote"
    16  )
    17  
    18  // ErrAccountNotFound is returned when the account is not in
    19  // WalletState's accounts map.
    20  var ErrAccountNotFound = errors.New("account not found for user")
    21  
    22  // ErrRefreshQueueFull is returned when the refresh queue
    23  // is clogged up.
    24  var ErrRefreshQueueFull = errors.New("refresh queue is full")
    25  
    26  // WalletState holds all the current data for all the accounts
    27  // for the user.  It is also a remote.Remoter and should be used
    28  // in place of it so network calls can be avoided.
    29  type WalletState struct {
    30  	libkb.Contextified
    31  	remote.Remoter
    32  	accounts       map[stellar1.AccountID]*AccountState
    33  	rates          map[string]rateEntry
    34  	refreshGroup   *singleflight.Group
    35  	refreshReqs    chan stellar1.AccountID
    36  	refreshCount   int
    37  	backgroundStop chan struct{}
    38  	backgroundDone chan struct{}
    39  	rateGroup      *singleflight.Group
    40  	shutdownOnce   sync.Once
    41  	sync.Mutex
    42  	seqnoMu       sync.Mutex
    43  	seqnoLockHeld bool
    44  	options       *Options
    45  	bkgCancelFn   context.CancelFunc
    46  }
    47  
    48  var _ remote.Remoter = (*WalletState)(nil)
    49  
    50  // NewWalletState creates a wallet state with a remoter that will be
    51  // used for any network calls.
    52  func NewWalletState(g *libkb.GlobalContext, r remote.Remoter) *WalletState {
    53  	ws := &WalletState{
    54  		Contextified:   libkb.NewContextified(g),
    55  		Remoter:        r,
    56  		accounts:       make(map[stellar1.AccountID]*AccountState),
    57  		rates:          make(map[string]rateEntry),
    58  		refreshGroup:   &singleflight.Group{},
    59  		refreshReqs:    make(chan stellar1.AccountID, 100),
    60  		backgroundDone: make(chan struct{}),
    61  		backgroundStop: make(chan struct{}),
    62  		rateGroup:      &singleflight.Group{},
    63  		options:        NewOptions(),
    64  	}
    65  
    66  	g.PushShutdownHook(ws.Shutdown)
    67  
    68  	ctx, cancelFn := context.WithCancel(context.TODO())
    69  	ws.bkgCancelFn = cancelFn
    70  	go ws.backgroundRefresh(ctx)
    71  
    72  	return ws
    73  }
    74  
    75  // Shutdown terminates any background operations and cleans up.
    76  func (w *WalletState) Shutdown(mctx libkb.MetaContext) error {
    77  	w.shutdownOnce.Do(func() {
    78  		mctx.Debug("WalletState shutting down")
    79  		w.Lock()
    80  		w.resetWithLock(mctx)
    81  		close(w.backgroundStop)
    82  		w.bkgCancelFn()
    83  		mctx.Debug("waiting for background refresh requests to finish")
    84  		select {
    85  		case <-w.backgroundDone:
    86  		case <-time.After(5 * time.Second):
    87  			mctx.Debug("timed out waiting for background refresh requests to finish")
    88  		}
    89  		w.Unlock()
    90  		mctx.Debug("WalletState shut down complete")
    91  	})
    92  	return nil
    93  }
    94  
    95  // SeqnoLock acquires a lock on seqno operations.  NewSeqnoProvider calls it.
    96  // After all operations with a seqno provider are done (i.e. fully submitted
    97  // to stellard), then the lock should be released with SeqnoUnlock.
    98  func (w *WalletState) SeqnoLock() {
    99  	w.seqnoMu.Lock()
   100  	w.Lock()
   101  	w.seqnoLockHeld = true
   102  	w.Unlock()
   103  }
   104  
   105  // SeqnoUnlock releases the lock on seqno operations.
   106  func (w *WalletState) SeqnoUnlock() {
   107  	w.Lock()
   108  	w.seqnoMu.Unlock()
   109  	w.seqnoLockHeld = false
   110  	w.Unlock()
   111  }
   112  
   113  // BaseFee returns stellard's current suggestion for the base operation fee.
   114  func (w *WalletState) BaseFee(mctx libkb.MetaContext) uint64 {
   115  	return w.options.BaseFee(mctx, w)
   116  }
   117  
   118  // AccountName returns the name for an account.
   119  func (w *WalletState) AccountName(accountID stellar1.AccountID) (string, error) {
   120  	a, ok := w.accountState(accountID)
   121  	if !ok {
   122  		return "", ErrAccountNotFound
   123  	}
   124  
   125  	a.RLock()
   126  	defer a.RUnlock()
   127  
   128  	return a.name, nil
   129  }
   130  
   131  // IsPrimary returns true if an account is the primary account for the user.
   132  func (w *WalletState) IsPrimary(accountID stellar1.AccountID) (bool, error) {
   133  	a, ok := w.accountState(accountID)
   134  	if !ok {
   135  		return false, ErrAccountNotFound
   136  	}
   137  
   138  	a.RLock()
   139  	defer a.RUnlock()
   140  
   141  	return a.isPrimary, nil
   142  }
   143  
   144  // AccountMode returns the mode of the account (USER or MOBILE).
   145  // MOBILE accounts can only get access to the secret key from a mobile device.
   146  func (w *WalletState) AccountMode(accountID stellar1.AccountID) (stellar1.AccountMode, error) {
   147  	a, ok := w.accountState(accountID)
   148  	if !ok {
   149  		return stellar1.AccountMode_NONE, ErrAccountNotFound
   150  	}
   151  
   152  	a.RLock()
   153  	defer a.RUnlock()
   154  
   155  	return a.accountMode, nil
   156  }
   157  
   158  // accountState returns the AccountState object for an accountID.
   159  // If it doesn't exist in `accounts`, it will return nil, false.
   160  func (w *WalletState) accountState(accountID stellar1.AccountID) (*AccountState, bool) {
   161  	w.Lock()
   162  	defer w.Unlock()
   163  
   164  	a, ok := w.accounts[accountID]
   165  	return a, ok
   166  }
   167  
   168  // accountStateBuild returns the AccountState object for an accountID.
   169  // If it doesn't exist in `accounts`, it will make an empty one and
   170  // add it to `accounts` before returning it.
   171  func (w *WalletState) accountStateBuild(accountID stellar1.AccountID) (account *AccountState, built bool) {
   172  	w.Lock()
   173  	defer w.Unlock()
   174  
   175  	a, ok := w.accounts[accountID]
   176  	if ok {
   177  		return a, false
   178  	}
   179  
   180  	a = newAccountState(accountID, w.Remoter, w.refreshReqs)
   181  	w.accounts[accountID] = a
   182  
   183  	return a, true
   184  }
   185  
   186  // accountStateRefresh returns the AccountState object for an accountID.
   187  // If it doesn't exist in `accounts`, it will make an empty one, add
   188  // it to `accounts`, and refresh the data in it before returning.
   189  func (w *WalletState) accountStateRefresh(ctx context.Context, accountID stellar1.AccountID, reason string) (*AccountState, error) {
   190  	w.Lock()
   191  	defer w.Unlock()
   192  
   193  	a, ok := w.accounts[accountID]
   194  	if ok {
   195  		return a, nil
   196  	}
   197  
   198  	reason = "accountStateRefresh: " + reason
   199  	a = newAccountState(accountID, w.Remoter, w.refreshReqs)
   200  	mctx := libkb.NewMetaContext(ctx, w.G())
   201  	if err := a.Refresh(mctx, w.G().NotifyRouter, reason); err != nil {
   202  		mctx.Debug("error refreshing account %s: %s", accountID, err)
   203  		return nil, err
   204  	}
   205  	w.accounts[accountID] = a
   206  
   207  	return a, nil
   208  }
   209  
   210  // Primed returns true if the WalletState has been refreshed.
   211  func (w *WalletState) Primed() bool {
   212  	w.Lock()
   213  	defer w.Unlock()
   214  	return w.refreshCount > 0
   215  }
   216  
   217  // UpdateAccountEntries gets the bundle from the server and updates the individual
   218  // account entries with the server's bundle information.
   219  func (w *WalletState) UpdateAccountEntries(mctx libkb.MetaContext, reason string) (err error) {
   220  	defer mctx.Trace(fmt.Sprintf("WalletState.UpdateAccountEntries [%s]", reason), &err)()
   221  
   222  	bundle, err := remote.FetchSecretlessBundle(mctx)
   223  	if err != nil {
   224  		return err
   225  	}
   226  
   227  	return w.UpdateAccountEntriesWithBundle(mctx, reason, bundle)
   228  }
   229  
   230  // UpdateAccountEntriesWithBundle updates the individual account entries with the
   231  // bundle information.
   232  func (w *WalletState) UpdateAccountEntriesWithBundle(mctx libkb.MetaContext, reason string, bundle *stellar1.Bundle) (err error) {
   233  	defer mctx.Trace(fmt.Sprintf("WalletState.UpdateAccountEntriesWithBundle [%s]", reason), &err)()
   234  
   235  	if bundle == nil {
   236  		return errors.New("nil bundle")
   237  	}
   238  
   239  	active := make(map[stellar1.AccountID]bool)
   240  	for _, account := range bundle.Accounts {
   241  		a, _ := w.accountStateBuild(account.AccountID)
   242  		a.updateEntry(account)
   243  		active[account.AccountID] = true
   244  	}
   245  
   246  	// clean out any unusued accounts
   247  	w.Lock()
   248  	for accountID := range w.accounts {
   249  		if active[accountID] {
   250  			continue
   251  		}
   252  		delete(w.accounts, accountID)
   253  	}
   254  	w.Unlock()
   255  
   256  	return nil
   257  }
   258  
   259  // RefreshAll refreshes all the accounts.
   260  func (w *WalletState) RefreshAll(mctx libkb.MetaContext, reason string) error {
   261  	_, err := w.refreshGroup.Do("RefreshAll", func() (interface{}, error) {
   262  		doErr := w.refreshAll(mctx, reason)
   263  		return nil, doErr
   264  	})
   265  	return err
   266  }
   267  
   268  func (w *WalletState) refreshAll(mctx libkb.MetaContext, reason string) (err error) {
   269  	defer mctx.Trace(fmt.Sprintf("WalletState.RefreshAll [%s]", reason), &err)()
   270  
   271  	// get all details in one call
   272  	all, err := w.AllDetailsPlusPayments(mctx)
   273  	if err != nil {
   274  		return err
   275  	}
   276  
   277  	// make a map out of results for easier lookup
   278  	details := make(map[stellar1.AccountID]stellar1.DetailsPlusPayments)
   279  	for _, entry := range all {
   280  		details[entry.Details.AccountID] = entry
   281  	}
   282  
   283  	// we need to get this to get the account names and primary status
   284  	bundle, err := remote.FetchSecretlessBundle(mctx)
   285  	if err != nil {
   286  		return err
   287  	}
   288  
   289  	var lastErr error
   290  	for _, account := range bundle.Accounts {
   291  		a, _ := w.accountStateBuild(account.AccountID)
   292  		a.updateEntry(account)
   293  
   294  		var dp *stellar1.DetailsPlusPayments
   295  		d, ok := details[account.AccountID]
   296  		if ok {
   297  			dp = &d
   298  		}
   299  
   300  		if err := a.RefreshWithDetails(mctx, w.G().NotifyRouter, reason, dp); err != nil {
   301  			mctx.Debug("error refreshing account %s: %s", account.AccountID, err)
   302  			lastErr = err
   303  		}
   304  	}
   305  	if lastErr != nil {
   306  		mctx.Debug("RefreshAll last error: %s", lastErr)
   307  		return lastErr
   308  	}
   309  
   310  	w.Lock()
   311  	w.refreshCount++
   312  	w.Unlock()
   313  
   314  	return nil
   315  }
   316  
   317  // Refresh gets all the data from the server for an account.
   318  func (w *WalletState) Refresh(mctx libkb.MetaContext, accountID stellar1.AccountID, reason string) error {
   319  	a, ok := w.accountState(accountID)
   320  	if !ok {
   321  		return ErrAccountNotFound
   322  	}
   323  	return a.Refresh(mctx, w.G().NotifyRouter, reason)
   324  }
   325  
   326  // RefreshAsync makes a request to refresh an account in the background.
   327  // It clears the refresh time to ensure that a refresh happens/
   328  func (w *WalletState) RefreshAsync(mctx libkb.MetaContext, accountID stellar1.AccountID, reason string) error {
   329  	a, ok := w.accountState(accountID)
   330  	if !ok {
   331  		return ErrAccountNotFound
   332  	}
   333  
   334  	// if someone calls this, they need a refresh to happen, so make
   335  	// sure that the next refresh for this accountID isn't skipped.
   336  	a.Lock()
   337  	a.rtime = time.Time{}
   338  	a.Unlock()
   339  
   340  	select {
   341  	case w.refreshReqs <- accountID:
   342  	case <-time.After(200 * time.Millisecond):
   343  		// don't wait for full channel
   344  		mctx.Debug("refreshReqs channel clogged trying to enqueue %s for %q", accountID, reason)
   345  		return ErrRefreshQueueFull
   346  	}
   347  
   348  	return nil
   349  }
   350  
   351  // ForceSeqnoRefresh refreshes the seqno for an account.
   352  func (w *WalletState) ForceSeqnoRefresh(mctx libkb.MetaContext, accountID stellar1.AccountID) error {
   353  	a, ok := w.accountState(accountID)
   354  	if !ok {
   355  		return ErrAccountNotFound
   356  	}
   357  	return a.ForceSeqnoRefresh(mctx)
   358  }
   359  
   360  // backgroundRefresh gets any refresh requests and will refresh
   361  // the account state if sufficient time has passed since the
   362  // last refresh.
   363  func (w *WalletState) backgroundRefresh(ctx context.Context) {
   364  	mctx := libkb.NewMetaContext(ctx, w.G()).WithLogTag("WABR")
   365  	var done bool
   366  	for !done {
   367  		select {
   368  		case accountID := <-w.refreshReqs:
   369  			a, ok := w.accountState(accountID)
   370  			if !ok {
   371  				continue
   372  			}
   373  			a.RLock()
   374  			rt := a.rtime
   375  			a.RUnlock()
   376  
   377  			if time.Since(rt) < 120*time.Second {
   378  				mctx.Debug("WalletState.backgroundRefresh skipping for %s due to recent refresh", accountID)
   379  				continue
   380  			}
   381  
   382  			if err := a.Refresh(mctx, w.G().NotifyRouter, "background"); err != nil {
   383  				mctx.Debug("WalletState.backgroundRefresh error for %s: %s", accountID, err)
   384  			}
   385  		case <-w.backgroundStop:
   386  			mctx.Debug("WalletState.backgroundRefresh: stop channel closed, stopping the loop")
   387  			done = true
   388  		}
   389  	}
   390  	close(w.backgroundDone)
   391  }
   392  
   393  // AccountSeqno is an override of remoter's AccountSeqno that uses
   394  // the stored value.
   395  func (w *WalletState) AccountSeqno(ctx context.Context, accountID stellar1.AccountID) (uint64, error) {
   396  	a, err := w.accountStateRefresh(ctx, accountID, "AccountSeqno")
   397  	if err != nil {
   398  		return 0, err
   399  	}
   400  
   401  	return a.AccountSeqno(ctx)
   402  }
   403  
   404  // AccountSeqnoAndBump gets the current seqno for an account and increments
   405  // the stored value.
   406  func (w *WalletState) AccountSeqnoAndBump(ctx context.Context, accountID stellar1.AccountID) (uint64, error) {
   407  	w.Lock()
   408  	hasSeqnoLock := w.seqnoLockHeld
   409  	w.Unlock()
   410  	if !hasSeqnoLock {
   411  		return 0, errors.New("you must hold SeqnoLock() before AccountSeqnoAndBump")
   412  	}
   413  	a, err := w.accountStateRefresh(ctx, accountID, "AccountSeqnoAndBump")
   414  	if err != nil {
   415  		return 0, err
   416  	}
   417  	return a.AccountSeqnoAndBump(ctx)
   418  }
   419  
   420  // Balances is an override of remoter's Balances that uses stored data.
   421  func (w *WalletState) Balances(ctx context.Context, accountID stellar1.AccountID) ([]stellar1.Balance, error) {
   422  	a, ok := w.accountState(accountID)
   423  	if !ok {
   424  		// Balances is used frequently to get balances for other users,
   425  		// so if accountID isn't in WalletState, just use the remote
   426  		// to get the balances.
   427  		w.G().Log.CDebugf(ctx, "WalletState:Balances using remoter for %s", accountID)
   428  		return w.Remoter.Balances(ctx, accountID)
   429  	}
   430  
   431  	w.G().Log.CDebugf(ctx, "WalletState:Balances using account state for %s", accountID)
   432  	return a.Balances(ctx)
   433  }
   434  
   435  // Details is an override of remoter's Details that uses stored data.
   436  func (w *WalletState) Details(ctx context.Context, accountID stellar1.AccountID) (stellar1.AccountDetails, error) {
   437  	a, err := w.accountStateRefresh(ctx, accountID, "Details")
   438  	if err != nil {
   439  		return stellar1.AccountDetails{}, err
   440  	}
   441  	details, err := a.Details(ctx)
   442  	if err == nil && details.AccountID != accountID {
   443  		w.G().Log.CDebugf(ctx, "WalletState:Details account id mismatch.  returning %+v for account id %q", details, accountID)
   444  	}
   445  	return details, err
   446  }
   447  
   448  // PendingPayments is an override of remoter's PendingPayments that uses stored data.
   449  func (w *WalletState) PendingPayments(ctx context.Context, accountID stellar1.AccountID, limit int) ([]stellar1.PaymentSummary, error) {
   450  	a, err := w.accountStateRefresh(ctx, accountID, "PendingPayments")
   451  	if err != nil {
   452  		return nil, err
   453  	}
   454  	payments, err := a.PendingPayments(ctx, limit)
   455  	if err == nil {
   456  		w.G().Log.CDebugf(ctx, "WalletState pending payments for %s: %d", accountID, len(payments))
   457  	} else {
   458  		w.G().Log.CDebugf(ctx, "WalletState pending payments error for %s: %s", accountID, err)
   459  
   460  	}
   461  	return payments, err
   462  }
   463  
   464  // RecentPayments is an override of remoter's RecentPayments that uses stored data.
   465  func (w *WalletState) RecentPayments(ctx context.Context, arg remote.RecentPaymentsArg) (stellar1.PaymentsPage, error) {
   466  	useAccountState := true
   467  	switch {
   468  	case arg.Limit != 0 && arg.Limit != 50:
   469  		useAccountState = false
   470  	case arg.Cursor != nil:
   471  		useAccountState = false
   472  	case !arg.SkipPending:
   473  		useAccountState = false
   474  	}
   475  
   476  	if !useAccountState {
   477  		w.G().Log.CDebugf(ctx, "WalletState:RecentPayments using remote due to parameters")
   478  		return w.Remoter.RecentPayments(ctx, arg)
   479  	}
   480  
   481  	a, err := w.accountStateRefresh(ctx, arg.AccountID, "RecentPayments")
   482  	if err != nil {
   483  		return stellar1.PaymentsPage{}, err
   484  	}
   485  
   486  	return a.RecentPayments(ctx)
   487  }
   488  
   489  // AddPendingTx adds information about a tx that was submitted to the network.
   490  // This allows WalletState to keep track of anything pending when managing
   491  // the account seqno.
   492  func (w *WalletState) AddPendingTx(ctx context.Context, accountID stellar1.AccountID, txID stellar1.TransactionID, seqno uint64) error {
   493  	a, ok := w.accountState(accountID)
   494  	if !ok {
   495  		return fmt.Errorf("AddPendingTx: account id %q not in wallet state", accountID)
   496  	}
   497  
   498  	w.G().Log.CDebugf(ctx, "WalletState: account %s adding pending tx %s/%d", accountID, txID, seqno)
   499  
   500  	return a.AddPendingTx(ctx, txID, seqno)
   501  }
   502  
   503  // RemovePendingTx removes a pending tx from WalletState.  It doesn't matter
   504  // if it succeeded or failed, just that it is done.
   505  func (w *WalletState) RemovePendingTx(ctx context.Context, accountID stellar1.AccountID, txID stellar1.TransactionID) error {
   506  	a, ok := w.accountState(accountID)
   507  	if !ok {
   508  		return fmt.Errorf("RemovePendingTx: account id %q not in wallet state", accountID)
   509  	}
   510  
   511  	w.G().Log.CDebugf(ctx, "WalletState: account %s removing pending tx %s", accountID, txID)
   512  
   513  	return a.RemovePendingTx(ctx, txID)
   514  }
   515  
   516  // SubmitPayment is an override of remoter's SubmitPayment.
   517  func (w *WalletState) SubmitPayment(ctx context.Context, post stellar1.PaymentDirectPost) (stellar1.PaymentResult, error) {
   518  	w.Lock()
   519  	hasSeqnoLock := w.seqnoLockHeld
   520  	w.Unlock()
   521  	if !hasSeqnoLock {
   522  		return stellar1.PaymentResult{}, errors.New("you must hold SeqnoLock() before SubmitPayment")
   523  	}
   524  	return w.Remoter.SubmitPayment(ctx, post)
   525  }
   526  
   527  // SubmitRelayPayment is an override of remoter's SubmitRelayPayment.
   528  func (w *WalletState) SubmitRelayPayment(ctx context.Context, post stellar1.PaymentRelayPost) (stellar1.PaymentResult, error) {
   529  	w.Lock()
   530  	hasSeqnoLock := w.seqnoLockHeld
   531  	w.Unlock()
   532  	if !hasSeqnoLock {
   533  		return stellar1.PaymentResult{}, errors.New("you must hold SeqnoLock() before SubmitRelayPayment")
   534  	}
   535  	return w.Remoter.SubmitRelayPayment(ctx, post)
   536  }
   537  
   538  // SubmitRelayClaim is an override of remoter's SubmitRelayClaim.
   539  func (w *WalletState) SubmitRelayClaim(ctx context.Context, post stellar1.RelayClaimPost) (stellar1.RelayClaimResult, error) {
   540  	w.Lock()
   541  	hasSeqnoLock := w.seqnoLockHeld
   542  	w.Unlock()
   543  	if !hasSeqnoLock {
   544  		return stellar1.RelayClaimResult{}, errors.New("you must hold SeqnoLock() before SubmitRelayClaim")
   545  	}
   546  	result, err := w.Remoter.SubmitRelayClaim(ctx, post)
   547  	if err == nil {
   548  		mctx := libkb.NewMetaContext(ctx, w.G())
   549  		if rerr := w.RefreshAll(mctx, "SubmitRelayClaim"); rerr != nil {
   550  			mctx.Debug("RefreshAll after SubmitRelayClaim error: %s", rerr)
   551  		}
   552  	}
   553  	return result, err
   554  
   555  }
   556  
   557  // MarkAsRead is an override of remoter's MarkAsRead.
   558  func (w *WalletState) MarkAsRead(ctx context.Context, accountID stellar1.AccountID, mostRecentID stellar1.TransactionID) error {
   559  	err := w.Remoter.MarkAsRead(ctx, accountID, mostRecentID)
   560  	if err == nil {
   561  		mctx := libkb.NewMetaContext(ctx, w.G())
   562  		if rerr := w.RefreshAsync(mctx, accountID, "MarkAsRead"); rerr != nil {
   563  			mctx.Debug("Refresh after MarkAsRead error: %s", err)
   564  		}
   565  	}
   566  	return err
   567  }
   568  
   569  type rateEntry struct {
   570  	currency string
   571  	rate     stellar1.OutsideExchangeRate
   572  	ctime    time.Time
   573  }
   574  
   575  // ExchangeRate is an overrider of remoter's ExchangeRate.
   576  func (w *WalletState) ExchangeRate(ctx context.Context, currency string) (stellar1.OutsideExchangeRate, error) {
   577  	w.Lock()
   578  	existing, ok := w.rates[currency]
   579  	w.Unlock()
   580  	age := time.Since(existing.ctime)
   581  	if ok && age < 1*time.Minute {
   582  		w.G().Log.CDebugf(ctx, "using cached value for ExchangeRate(%s) => %+v (%s old)", currency, existing.rate, age)
   583  		return existing.rate, nil
   584  	}
   585  	if ok {
   586  		w.G().Log.CDebugf(ctx, "skipping cache for ExchangeRate(%s) because too old (%s)", currency, age)
   587  	}
   588  	w.G().Log.CDebugf(ctx, "ExchangeRate(%s) using remote", currency)
   589  
   590  	rateRes, err := w.rateGroup.Do(currency, func() (interface{}, error) {
   591  		return w.Remoter.ExchangeRate(ctx, currency)
   592  	})
   593  	rate, ok := rateRes.(stellar1.OutsideExchangeRate)
   594  	if !ok {
   595  		return stellar1.OutsideExchangeRate{}, errors.New("invalid cast")
   596  	}
   597  
   598  	if err == nil {
   599  		w.Lock()
   600  		w.rates[currency] = rateEntry{
   601  			currency: currency,
   602  			rate:     rate,
   603  			ctime:    time.Now(),
   604  		}
   605  		w.Unlock()
   606  		w.G().Log.CDebugf(ctx, "ExchangeRate(%s) => %+v, setting cache", currency, rate)
   607  	}
   608  
   609  	return rate, err
   610  }
   611  
   612  // DumpToLog outputs a summary of WalletState to the debug log.
   613  func (w *WalletState) DumpToLog(mctx libkb.MetaContext) {
   614  	mctx.Debug(w.String())
   615  }
   616  
   617  // String returns a string representation of WalletState suitable for debug
   618  // logging.
   619  func (w *WalletState) String() string {
   620  	w.Lock()
   621  	defer w.Unlock()
   622  	var pieces []string
   623  	for _, acctState := range w.accounts {
   624  		pieces = append(pieces, acctState.String())
   625  	}
   626  
   627  	return fmt.Sprintf("WalletState (# accts: %d): %s", len(w.accounts), strings.Join(pieces, ", "))
   628  }
   629  
   630  // Reset clears all the data in the WalletState.
   631  func (w *WalletState) Reset(mctx libkb.MetaContext) {
   632  	w.Lock()
   633  	defer w.Unlock()
   634  	w.resetWithLock(mctx)
   635  }
   636  
   637  // resetWithLock can only be called after w.Lock().
   638  func (w *WalletState) resetWithLock(mctx libkb.MetaContext) {
   639  	for _, a := range w.accounts {
   640  		a.Reset(mctx)
   641  	}
   642  
   643  	w.accounts = make(map[stellar1.AccountID]*AccountState)
   644  }
   645  
   646  type txPending struct {
   647  	seqno uint64
   648  	ctime time.Time
   649  }
   650  
   651  type inuseSeqno struct {
   652  	ctime time.Time
   653  }
   654  
   655  // AccountState holds the current data for a stellar account.
   656  type AccountState struct {
   657  	// these are only set when AccountState created, they never change
   658  	accountID    stellar1.AccountID
   659  	remoter      remote.Remoter
   660  	refreshGroup *singleflight.Group
   661  	refreshReqs  chan stellar1.AccountID
   662  
   663  	sync.RWMutex // protects everything that follows
   664  	seqno        uint64
   665  	isPrimary    bool
   666  	name         string
   667  	accountMode  stellar1.AccountMode
   668  	balances     []stellar1.Balance
   669  	details      *stellar1.AccountDetails
   670  	pending      []stellar1.PaymentSummary
   671  	recent       *stellar1.PaymentsPage
   672  	rtime        time.Time // time of last refresh
   673  	done         bool
   674  	pendingTxs   map[stellar1.TransactionID]txPending
   675  	inuseSeqnos  map[uint64]inuseSeqno
   676  }
   677  
   678  func newAccountState(accountID stellar1.AccountID, r remote.Remoter, reqsCh chan stellar1.AccountID) *AccountState {
   679  	return &AccountState{
   680  		accountID:    accountID,
   681  		remoter:      r,
   682  		refreshGroup: &singleflight.Group{},
   683  		refreshReqs:  reqsCh,
   684  		pendingTxs:   make(map[stellar1.TransactionID]txPending),
   685  		inuseSeqnos:  make(map[uint64]inuseSeqno),
   686  	}
   687  }
   688  
   689  // Refresh updates all the data for this account from the server.
   690  func (a *AccountState) Refresh(mctx libkb.MetaContext, router *libkb.NotifyRouter, reason string) error {
   691  	_, err := a.refreshGroup.Do("Refresh", func() (interface{}, error) {
   692  		doErr := a.refresh(mctx, router, reason)
   693  		return nil, doErr
   694  	})
   695  	return err
   696  }
   697  
   698  // RefreshWithDetails updates all the data for this account with the provided details data.
   699  func (a *AccountState) RefreshWithDetails(mctx libkb.MetaContext, router *libkb.NotifyRouter, reason string, details *stellar1.DetailsPlusPayments) error {
   700  	_, err := a.refreshGroup.Do("Refresh", func() (interface{}, error) {
   701  		var doErr error
   702  		if details != nil {
   703  			doErr = a.refreshWithDetails(mctx, router, reason, details)
   704  		} else {
   705  			mctx.Debug("RefreshWithDetails called with nil details, using network refresh")
   706  			doErr = a.refresh(mctx, router, reason)
   707  		}
   708  		return nil, doErr
   709  	})
   710  	return err
   711  }
   712  
   713  func (a *AccountState) refresh(mctx libkb.MetaContext, router *libkb.NotifyRouter, reason string) (err error) {
   714  	defer mctx.Trace(fmt.Sprintf("WalletState.Refresh(%s) [%s]", a.accountID, reason), &err)()
   715  
   716  	dpp, err := a.remoter.DetailsPlusPayments(mctx.Ctx(), a.accountID)
   717  	if err != nil {
   718  		mctx.Debug("refresh DetailsPlusPayments error: %s", err)
   719  		return err
   720  	}
   721  
   722  	return a.refreshWithDetails(mctx, router, reason, &dpp)
   723  }
   724  
   725  func (a *AccountState) refreshWithDetails(mctx libkb.MetaContext, router *libkb.NotifyRouter, reason string, dpp *stellar1.DetailsPlusPayments) (err error) {
   726  	var seqno uint64
   727  	if dpp.Details.Seqno != "" {
   728  		seqno, err = strconv.ParseUint(dpp.Details.Seqno, 10, 64)
   729  		if err != nil {
   730  			return err
   731  		}
   732  	}
   733  
   734  	a.Lock()
   735  	if seqno > a.seqno {
   736  		a.seqno = seqno
   737  	}
   738  
   739  	if a.accountID != dpp.Details.AccountID {
   740  		mctx.Debug("refreshWithDetails dpp.Details.AccountID (%s) != a.accountID (%s)", dpp.Details.AccountID, a.accountID)
   741  		return fmt.Errorf("refreshWithDetails [%s], account ID in parameter does not match(%s != %s)", reason, dpp.Details.AccountID, a.accountID)
   742  	}
   743  
   744  	a.balances = dpp.Details.Balances
   745  
   746  	notifyDetails := detailsChanged(a.details, &dpp.Details)
   747  	a.details = &dpp.Details
   748  
   749  	notifyPending := pendingChanged(a.pending, dpp.PendingPayments)
   750  	a.pending = dpp.PendingPayments
   751  
   752  	notifyRecent := recentChanged(a.recent, &dpp.RecentPayments)
   753  	a.recent = &dpp.RecentPayments
   754  
   755  	// get these while locked
   756  	isPrimary := a.isPrimary
   757  	name := a.name
   758  	accountMode := a.accountMode
   759  
   760  	a.rtime = time.Now()
   761  
   762  	a.Unlock()
   763  
   764  	if notifyDetails && router != nil {
   765  		accountLocal, err := AccountDetailsToWalletAccountLocal(mctx, a.accountID, dpp.Details, isPrimary, name, accountMode)
   766  		if err == nil {
   767  			router.HandleWalletAccountDetailsUpdate(mctx.Ctx(), a.accountID, accountLocal)
   768  		} else {
   769  			mctx.Debug("AccountDetailsToWalletAccountLocal error: %s", err)
   770  		}
   771  	}
   772  	if notifyDetails {
   773  		err = getGlobal(mctx.G()).UpdateUnreadCount(mctx.Ctx(), a.accountID, dpp.Details.UnreadPayments)
   774  		if err != nil {
   775  			mctx.Debug("UpdateUnreadCount error: %s", err)
   776  		}
   777  	}
   778  
   779  	if notifyPending && router != nil {
   780  		local, err := RemotePendingToLocal(mctx, a.remoter, a.accountID, dpp.PendingPayments)
   781  		if err == nil {
   782  			router.HandleWalletPendingPaymentsUpdate(mctx.Ctx(), a.accountID, local)
   783  		} else {
   784  			mctx.Debug("RemotePendingToLocal error: %s", err)
   785  		}
   786  	}
   787  
   788  	if notifyRecent && router != nil {
   789  		localPage, err := RemoteRecentPaymentsToPage(mctx, a.remoter, a.accountID, dpp.RecentPayments)
   790  		if err == nil {
   791  			router.HandleWalletRecentPaymentsUpdate(mctx.Ctx(), a.accountID, localPage)
   792  		} else {
   793  			mctx.Debug("RemoteRecentPaymentsToPage error: %s", err)
   794  		}
   795  	}
   796  
   797  	return nil
   798  }
   799  
   800  // ForceSeqnoRefresh refreshes the seqno for an account.
   801  func (a *AccountState) ForceSeqnoRefresh(mctx libkb.MetaContext) error {
   802  	seqno, err := a.remoter.AccountSeqno(mctx.Ctx(), a.accountID)
   803  	if err != nil {
   804  		return err
   805  	}
   806  
   807  	a.Lock()
   808  	defer a.Unlock()
   809  
   810  	if seqno == a.seqno {
   811  		mctx.Debug("ForceSeqnoRefresh did not update AccountState for %s (existing: %d, remote: %d)", a.accountID, a.seqno, seqno)
   812  		return nil
   813  	}
   814  
   815  	if seqno > a.seqno {
   816  		// if network is greater than cached, then update
   817  		mctx.Debug("ForceSeqnoRefresh updated seqno for %s: %d => %d", a.accountID, a.seqno, seqno)
   818  		a.seqno = seqno
   819  		return nil
   820  	}
   821  
   822  	// delete any stale pending tx (in case missed notification somehow)
   823  	for k, v := range a.pendingTxs {
   824  		age := time.Since(v.ctime)
   825  		if age > 30*time.Second {
   826  			mctx.Debug("ForceSeqnoRefresh removing pending tx %s due to old age (%s)", k, age)
   827  			delete(a.pendingTxs, k)
   828  		}
   829  	}
   830  
   831  	// delete any stale inuse seqnos (in case missed notification somehow)
   832  	for k, v := range a.inuseSeqnos {
   833  		if seqno > k {
   834  			mctx.Debug("ForceSeqnoRefresh removing inuse seqno %d due to network seqno > to it (%s)", k, seqno)
   835  			delete(a.inuseSeqnos, k)
   836  		}
   837  		age := time.Since(v.ctime)
   838  		if age > 30*time.Second {
   839  			mctx.Debug("ForceSeqnoRefresh removing inuse seqno %d due to old age (%s)", k, age)
   840  			delete(a.inuseSeqnos, k)
   841  		}
   842  	}
   843  
   844  	if len(a.pendingTxs) == 0 && len(a.inuseSeqnos) == 0 {
   845  		// if no pending tx or inuse seqnos, then network should be correct
   846  		mctx.Debug("ForceSeqnoRefresh corrected seqno for %s: %d => %d", a.accountID, a.seqno, seqno)
   847  		a.seqno = seqno
   848  		return nil
   849  	}
   850  
   851  	mctx.Debug("ForceSeqnoRefresh did not update AccountState for %s due to pending tx/seqnos (existing: %d, remote: %d, pending txs: %d, inuse seqnos: %d)", a.accountID, a.seqno, seqno, len(a.pendingTxs), len(a.inuseSeqnos))
   852  
   853  	return nil
   854  }
   855  
   856  // SeqnoDebug outputs some information about the seqno state.
   857  func (a *AccountState) SeqnoDebug(mctx libkb.MetaContext) {
   858  	mctx.Debug("SEQNO debug for %s: pending txs %d, inuse seqnos: %d", a.accountID, len(a.pendingTxs), len(a.inuseSeqnos))
   859  	mctx.Debug("SEQNO debug for %s: inuse seqnos: %+v", a.accountID, a.inuseSeqnos)
   860  }
   861  
   862  // AccountSeqno returns the seqno that has already been fetched for
   863  // this account.
   864  func (a *AccountState) AccountSeqno(ctx context.Context) (uint64, error) {
   865  	a.RLock()
   866  	defer a.RUnlock()
   867  	return a.seqno, nil
   868  }
   869  
   870  // AccountSeqnoAndBump returns the seqno that has already been fetched for
   871  // this account.  It bumps the seqno up by one.
   872  func (a *AccountState) AccountSeqnoAndBump(ctx context.Context) (uint64, error) {
   873  	a.Lock()
   874  	defer a.Unlock()
   875  	result := a.seqno
   876  
   877  	a.seqno++
   878  
   879  	// need to keep track that we are going to use this seqno
   880  	// in a tx.  This record keeping avoids a race where
   881  	// multiple seqno providers rushing to use seqnos before
   882  	// AddPendingTx is called.
   883  	//
   884  	// The "in use" seqno is result+1 since the transaction builders
   885  	// add 1 to result when they make the transaction.
   886  	a.inuseSeqnos[a.seqno] = inuseSeqno{ctime: time.Now()}
   887  
   888  	return result, nil
   889  }
   890  
   891  // AddPendingTx adds information about a tx that was submitted to the network.
   892  // This allows AccountState to keep track of anything pending when managing
   893  // the account seqno.
   894  func (a *AccountState) AddPendingTx(ctx context.Context, txID stellar1.TransactionID, seqno uint64) error {
   895  	a.Lock()
   896  	defer a.Unlock()
   897  
   898  	// remove the inuse seqno since the pendingTx will track it now
   899  	delete(a.inuseSeqnos, seqno)
   900  
   901  	a.pendingTxs[txID] = txPending{seqno: seqno, ctime: time.Now()}
   902  
   903  	return nil
   904  }
   905  
   906  // RemovePendingTx removes a pending tx from WalletState.  It doesn't matter
   907  // if it succeeded or failed, just that it is done.
   908  func (a *AccountState) RemovePendingTx(ctx context.Context, txID stellar1.TransactionID) error {
   909  	a.Lock()
   910  	defer a.Unlock()
   911  
   912  	delete(a.pendingTxs, txID)
   913  
   914  	return nil
   915  }
   916  
   917  // Balances returns the balances that have already been fetched for
   918  // this account.
   919  func (a *AccountState) Balances(ctx context.Context) ([]stellar1.Balance, error) {
   920  	a.Lock()
   921  	defer a.Unlock()
   922  	a.enqueueRefreshReq()
   923  	return a.balances, nil
   924  }
   925  
   926  // Details returns the account details that have already been fetched for this account.
   927  func (a *AccountState) Details(ctx context.Context) (stellar1.AccountDetails, error) {
   928  	a.Lock()
   929  	defer a.Unlock()
   930  	a.enqueueRefreshReq()
   931  	if a.details == nil {
   932  		return stellar1.AccountDetails{AccountID: a.accountID}, nil
   933  	}
   934  	return *a.details, nil
   935  }
   936  
   937  // PendingPayments returns the pending payments that have already been fetched for
   938  // this account.
   939  func (a *AccountState) PendingPayments(ctx context.Context, limit int) ([]stellar1.PaymentSummary, error) {
   940  	a.Lock()
   941  	defer a.Unlock()
   942  	a.enqueueRefreshReq()
   943  	if limit > 0 && limit < len(a.pending) {
   944  		return a.pending[:limit], nil
   945  	}
   946  	return a.pending, nil
   947  }
   948  
   949  // RecentPayments returns the recent payments that have already been fetched for
   950  // this account.
   951  func (a *AccountState) RecentPayments(ctx context.Context) (stellar1.PaymentsPage, error) {
   952  	a.Lock()
   953  	defer a.Unlock()
   954  	a.enqueueRefreshReq()
   955  	if a.recent == nil {
   956  		return stellar1.PaymentsPage{}, nil
   957  	}
   958  	return *a.recent, nil
   959  }
   960  
   961  // Reset sets the refreshReqs channel to nil so nothing will be put on it.
   962  func (a *AccountState) Reset(mctx libkb.MetaContext) {
   963  	a.Lock()
   964  	defer a.Unlock()
   965  
   966  	a.refreshReqs = nil
   967  	a.done = true
   968  }
   969  
   970  // String returns a small string representation of AccountState suitable for
   971  // debug logging.
   972  func (a *AccountState) String() string {
   973  	a.RLock()
   974  	defer a.RUnlock()
   975  	if a.recent != nil {
   976  		return fmt.Sprintf("%s (seqno: %d, balances: %d, pending: %d, payments: %d)", a.accountID, a.seqno, len(a.balances), len(a.pending), len(a.recent.Payments))
   977  	}
   978  	return fmt.Sprintf("%s (seqno: %d, balances: %d, pending: %d, payments: nil)", a.accountID, a.seqno, len(a.balances), len(a.pending))
   979  }
   980  
   981  func (a *AccountState) updateEntry(entry stellar1.BundleEntry) {
   982  	a.Lock()
   983  	defer a.Unlock()
   984  
   985  	a.isPrimary = entry.IsPrimary
   986  	a.name = entry.Name
   987  	a.accountMode = entry.Mode
   988  }
   989  
   990  // enqueueRefreshReq adds an account ID to the refresh request queue.
   991  // It doesn't attempt to add if a.done.  Should be called after Lock().
   992  func (a *AccountState) enqueueRefreshReq() {
   993  	if a.done {
   994  		return
   995  	}
   996  	select {
   997  	case a.refreshReqs <- a.accountID:
   998  	case <-time.After(5 * time.Second):
   999  		// channel full or nil after shutdown, just ignore
  1000  	}
  1001  }
  1002  
  1003  func detailsChanged(a, b *stellar1.AccountDetails) bool {
  1004  	if a == nil && b == nil {
  1005  		return false
  1006  	}
  1007  	if a == nil && b != nil {
  1008  		return true
  1009  	}
  1010  	if a.Seqno != b.Seqno {
  1011  		return true
  1012  	}
  1013  	if a.UnreadPayments != b.UnreadPayments {
  1014  		return true
  1015  	}
  1016  	if a.Available != b.Available {
  1017  		return true
  1018  	}
  1019  	if b.ReadTransactionID != nil && (a.ReadTransactionID == nil || *a.ReadTransactionID != *b.ReadTransactionID) {
  1020  		return true
  1021  	}
  1022  	if a.SubentryCount != b.SubentryCount {
  1023  		return true
  1024  	}
  1025  	if a.DisplayCurrency != b.DisplayCurrency {
  1026  		return true
  1027  	}
  1028  	if len(a.Balances) != len(b.Balances) {
  1029  		return true
  1030  	}
  1031  	for i := 0; i < len(a.Balances); i++ {
  1032  		if a.Balances[i] != b.Balances[i] {
  1033  			return true
  1034  		}
  1035  	}
  1036  	if len(a.Reserves) != len(b.Reserves) {
  1037  		return true
  1038  	}
  1039  	if a.InflationDestination != b.InflationDestination {
  1040  		return true
  1041  	}
  1042  	for i := 0; i < len(a.Reserves); i++ {
  1043  		if a.Reserves[i] != b.Reserves[i] {
  1044  			return true
  1045  		}
  1046  	}
  1047  	return false
  1048  }
  1049  
  1050  func pendingChanged(a, b []stellar1.PaymentSummary) bool {
  1051  	if len(a) != len(b) {
  1052  		return true
  1053  	}
  1054  	if len(a) == 0 {
  1055  		return false
  1056  	}
  1057  
  1058  	for i := 0; i < len(a); i++ {
  1059  		atxid, err := a[i].TransactionID()
  1060  		if err != nil {
  1061  			return true
  1062  		}
  1063  		btxid, err := b[i].TransactionID()
  1064  		if err != nil {
  1065  			return true
  1066  		}
  1067  		if atxid != btxid {
  1068  			return true
  1069  		}
  1070  
  1071  		astatus, err := a[i].TransactionStatus()
  1072  		if err != nil {
  1073  			return true
  1074  		}
  1075  		bstatus, err := b[i].TransactionStatus()
  1076  		if err != nil {
  1077  			return true
  1078  		}
  1079  
  1080  		if astatus != bstatus {
  1081  			return true
  1082  		}
  1083  	}
  1084  
  1085  	return false
  1086  }
  1087  
  1088  func recentChanged(a, b *stellar1.PaymentsPage) bool {
  1089  	if a == nil && b == nil {
  1090  		return false
  1091  	}
  1092  	if a == nil && b != nil {
  1093  		return true
  1094  	}
  1095  	if len(a.Payments) != len(b.Payments) {
  1096  		return true
  1097  	}
  1098  	if a.Cursor != nil && b.Cursor != nil {
  1099  		if *a.Cursor != *b.Cursor {
  1100  			return true
  1101  		}
  1102  	}
  1103  	if len(a.Payments) == 0 {
  1104  		return false
  1105  	}
  1106  	existing, err := a.Payments[0].TransactionID()
  1107  	if err == nil {
  1108  		next, err := b.Payments[0].TransactionID()
  1109  		if err == nil {
  1110  			if existing != next {
  1111  				return true
  1112  			}
  1113  		}
  1114  	}
  1115  	return false
  1116  }