github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/stellar/global.go (about)

     1  package stellar
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"strings"
     9  	"sync"
    10  	"time"
    11  
    12  	"github.com/keybase/client/go/badges"
    13  	"github.com/keybase/client/go/gregor"
    14  	"github.com/keybase/client/go/libkb"
    15  	"github.com/keybase/client/go/protocol/chat1"
    16  	"github.com/keybase/client/go/protocol/keybase1"
    17  	"github.com/keybase/client/go/protocol/stellar1"
    18  	"github.com/keybase/client/go/slotctx"
    19  	"github.com/keybase/client/go/stellar/remote"
    20  	"github.com/keybase/stellarnet"
    21  	"github.com/stellar/go/build"
    22  	"github.com/stellar/go/clients/federation"
    23  
    24  	// nolint
    25  	"github.com/stellar/go/clients/horizon"
    26  )
    27  
    28  func ServiceInit(g *libkb.GlobalContext, walletState *WalletState, badger *badges.Badger) {
    29  	if g.Env.GetRunMode() != libkb.ProductionRunMode {
    30  		stellarnet.SetClientAndNetwork(horizon.DefaultTestNetClient, build.TestNetwork)
    31  	}
    32  	s := NewStellar(g, walletState, badger)
    33  	g.SetStellar(s)
    34  	g.AddLogoutHook(s, "stellar")
    35  	g.AddDbNukeHook(s, "stellar")
    36  	g.PushShutdownHook(s.Shutdown)
    37  }
    38  
    39  type Stellar struct {
    40  	libkb.Contextified
    41  	remoter     remote.Remoter
    42  	walletState *WalletState
    43  
    44  	serverConfLock   sync.Mutex
    45  	cachedServerConf stellar1.StellarServerDefinitions
    46  
    47  	autoClaimRunnerLock sync.Mutex
    48  	autoClaimRunner     *AutoClaimRunner // often nil
    49  
    50  	hasWalletCacheLock sync.Mutex
    51  	hasWalletCache     map[keybase1.UserVersion]bool
    52  
    53  	federationClient federation.ClientInterface
    54  
    55  	bidLock sync.Mutex
    56  	bids    []*buildPaymentEntry
    57  
    58  	bpcLock sync.Mutex
    59  	bpc     BuildPaymentCache
    60  
    61  	disclaimerLock     sync.Mutex
    62  	disclaimerAccepted *keybase1.UserVersion // A UV who has accepted the disclaimer.
    63  
    64  	accountsLock sync.Mutex
    65  	accounts     *AccountsCache
    66  
    67  	// Slot for build payments that do not use BuildPaymentID.
    68  	buildPaymentSlot *slotctx.PrioritySlot
    69  
    70  	reconnectSlot *slotctx.Slot
    71  
    72  	badger *badges.Badger
    73  }
    74  
    75  var _ libkb.Stellar = (*Stellar)(nil)
    76  
    77  func NewStellar(g *libkb.GlobalContext, walletState *WalletState, badger *badges.Badger) *Stellar {
    78  	return &Stellar{
    79  		Contextified:     libkb.NewContextified(g),
    80  		remoter:          walletState,
    81  		walletState:      walletState,
    82  		hasWalletCache:   make(map[keybase1.UserVersion]bool),
    83  		federationClient: getFederationClient(g),
    84  		buildPaymentSlot: slotctx.NewPriority(),
    85  		reconnectSlot:    slotctx.New(),
    86  		badger:           badger,
    87  	}
    88  }
    89  
    90  type AccountsCache struct {
    91  	Stored   time.Time
    92  	Revision stellar1.BundleRevision
    93  	Accounts []stellar1.BundleEntry
    94  }
    95  
    96  func (s *Stellar) CreateWalletSoft(ctx context.Context) {
    97  	CreateWalletSoft(libkb.NewMetaContext(ctx, s.G()))
    98  }
    99  
   100  func (s *Stellar) Upkeep(ctx context.Context) error {
   101  	return Upkeep(libkb.NewMetaContext(ctx, s.G()))
   102  }
   103  
   104  func (s *Stellar) OnLogout(mctx libkb.MetaContext) error {
   105  	s.Clear(mctx)
   106  	return nil
   107  }
   108  
   109  func (s *Stellar) OnDbNuke(mctx libkb.MetaContext) error {
   110  	s.Clear(mctx)
   111  	return nil
   112  }
   113  
   114  func (s *Stellar) Shutdown(mctx libkb.MetaContext) error {
   115  	s.Clear(mctx)
   116  	return nil
   117  }
   118  
   119  func (s *Stellar) Clear(mctx libkb.MetaContext) {
   120  	s.shutdownAutoClaimRunner()
   121  	s.deleteBpc()
   122  	s.deleteDisclaimer()
   123  	s.clearBids()
   124  	s.clearAccounts()
   125  }
   126  
   127  func (s *Stellar) shutdownAutoClaimRunner() {
   128  	s.autoClaimRunnerLock.Lock()
   129  	defer s.autoClaimRunnerLock.Unlock()
   130  	// Shutdown and delete the ACR.
   131  	if acr := s.autoClaimRunner; acr != nil {
   132  		acr.Shutdown(libkb.NewMetaContextBackground(s.G()))
   133  	}
   134  	s.autoClaimRunner = nil
   135  }
   136  
   137  func (s *Stellar) deleteBpc() {
   138  	s.bpcLock.Lock()
   139  	defer s.bpcLock.Unlock()
   140  	s.bpc = nil
   141  }
   142  
   143  func (s *Stellar) deleteDisclaimer() {
   144  	s.disclaimerLock.Lock()
   145  	defer s.disclaimerLock.Unlock()
   146  	s.disclaimerAccepted = nil
   147  }
   148  
   149  func (s *Stellar) clearBids() {
   150  	s.buildPaymentSlot.Stop()
   151  	s.bidLock.Lock()
   152  	defer s.bidLock.Unlock()
   153  	for _, bid := range s.bids {
   154  		bid.Slot.Stop()
   155  	}
   156  	s.bids = nil
   157  }
   158  
   159  func (s *Stellar) clearAccounts() {
   160  	s.accountsLock.Lock()
   161  	defer s.accountsLock.Unlock()
   162  	s.accounts = nil
   163  }
   164  
   165  func (s *Stellar) GetServerDefinitions(ctx context.Context) (ret stellar1.StellarServerDefinitions, err error) {
   166  	s.serverConfLock.Lock()
   167  	defer s.serverConfLock.Unlock()
   168  	if s.cachedServerConf.Revision == 0 {
   169  		// check if still 0, we might have waited for other thread
   170  		// to finish fetching.
   171  		if ret, err = remote.FetchServerConfig(ctx, s.G()); err != nil {
   172  			return ret, err
   173  		}
   174  
   175  		s.cachedServerConf = ret
   176  	}
   177  
   178  	return s.cachedServerConf, nil
   179  }
   180  
   181  func (s *Stellar) KnownCurrencyCodeInstant(ctx context.Context, code string) (known, ok bool) {
   182  	code = strings.ToUpper(code)
   183  	if code == "XLM" {
   184  		return true, true
   185  	}
   186  	s.serverConfLock.Lock()
   187  	defer s.serverConfLock.Unlock()
   188  	if s.cachedServerConf.Revision == 0 {
   189  		return false, false
   190  	}
   191  	_, known = s.cachedServerConf.Currencies[stellar1.OutsideCurrencyCode(code)]
   192  	return known, true
   193  }
   194  
   195  // `trigger` is optional, and is of the gregor message that caused the kick.
   196  func (s *Stellar) KickAutoClaimRunner(mctx libkb.MetaContext, trigger gregor.MsgID) {
   197  	// Create the ACR if one does not exist.
   198  	mctx.Debug("KickAutoClaimRunner(trigger:%v)", trigger)
   199  	s.autoClaimRunnerLock.Lock()
   200  	defer s.autoClaimRunnerLock.Unlock()
   201  	if s.autoClaimRunner == nil {
   202  		s.autoClaimRunner = NewAutoClaimRunner(s.walletState)
   203  	}
   204  	s.autoClaimRunner.Kick(mctx, trigger)
   205  }
   206  
   207  func (s *Stellar) InformHasWallet(ctx context.Context, uv keybase1.UserVersion) {
   208  	if uv.Uid.IsNil() {
   209  		s.G().Log.CErrorf(ctx, "Stellar.InformHasWallet called with nil UID")
   210  		return
   211  	}
   212  	if uv.EldestSeqno <= 0 {
   213  		// It is not possible for such a user to have a wallet.
   214  		s.G().Log.CErrorf(ctx, "Stellar.InformHasWallet called with %v EldestSeqno", uv.EldestSeqno)
   215  		return
   216  	}
   217  	s.hasWalletCacheLock.Lock()
   218  	defer s.hasWalletCacheLock.Unlock()
   219  	s.G().Log.CDebugf(ctx, "Stellar.InformHasWallet(%v)", uv)
   220  	s.hasWalletCache[uv] = true
   221  }
   222  
   223  func (s *Stellar) CachedHasWallet(ctx context.Context, uv keybase1.UserVersion) bool {
   224  	s.hasWalletCacheLock.Lock()
   225  	defer s.hasWalletCacheLock.Unlock()
   226  	has := s.hasWalletCache[uv]
   227  	s.G().Log.CDebugf(ctx, "Stellar.CachedHasWallet(%v) -> %v", uv, has)
   228  	return has
   229  }
   230  
   231  func (s *Stellar) SetFederationClientForTest(cli federation.ClientInterface) {
   232  	s.federationClient = cli
   233  }
   234  
   235  func (s *Stellar) getBuildPaymentCache() BuildPaymentCache {
   236  	s.bpcLock.Lock()
   237  	defer s.bpcLock.Unlock()
   238  	if s.bpc == nil {
   239  		s.bpc = newBuildPaymentCache(s.remoter)
   240  	}
   241  	return s.bpc
   242  }
   243  
   244  // UpdateUnreadCount will take the unread count for an account id and
   245  // update the badger.
   246  func (s *Stellar) UpdateUnreadCount(ctx context.Context, accountID stellar1.AccountID, unread int) error {
   247  	if s.badger == nil {
   248  		s.G().Log.CDebugf(ctx, "Stellar Global has no badger")
   249  		return nil
   250  	}
   251  
   252  	s.badger.SetWalletAccountUnreadCount(ctx, accountID, unread)
   253  	return nil
   254  }
   255  
   256  // SendMiniChatPayments sends multiple payments from one sender to multiple
   257  // different recipients as fast as it can.  These come from chat messages
   258  // like "+1XLM@alice +2XLM@charlie".
   259  func (s *Stellar) SendMiniChatPayments(mctx libkb.MetaContext, convID chat1.ConversationID, payments []libkb.MiniChatPayment) ([]libkb.MiniChatPaymentResult, error) {
   260  	return SendMiniChatPayments(mctx, s.walletState, convID, payments)
   261  }
   262  
   263  // SpecMiniChatPayments creates a summary of the amounts that a list of MiniChatPayments will
   264  // result in.
   265  func (s *Stellar) SpecMiniChatPayments(mctx libkb.MetaContext, payments []libkb.MiniChatPayment) (*libkb.MiniChatPaymentSummary, error) {
   266  	return SpecMiniChatPayments(mctx, s.walletState, payments)
   267  }
   268  
   269  // HandleOobm will handle any out of band gregor messages for stellar.
   270  func (s *Stellar) HandleOobm(ctx context.Context, obm gregor.OutOfBandMessage) (bool, error) {
   271  	if obm.System() == nil {
   272  		return false, errors.New("nil system in out of band message")
   273  	}
   274  
   275  	// make a new background context for the handlers
   276  	mctx := libkb.NewMetaContextBackground(s.G()).WithLogTag("WAOOBM")
   277  
   278  	// all of these handlers should be in goroutines so they don't block the
   279  	// oobm handler thread.
   280  
   281  	switch obm.System().String() {
   282  	case "internal.reconnect":
   283  		go s.handleReconnect(mctx)
   284  		// returning false, nil here so that others can handle this one too
   285  		return false, nil
   286  	case stellar1.PushPaymentStatus:
   287  		go s.handlePaymentStatus(mctx, obm)
   288  		return true, nil
   289  	case stellar1.PushPaymentNotification:
   290  		go s.handlePaymentNotification(mctx, obm)
   291  		return true, nil
   292  	case stellar1.PushRequestStatus:
   293  		go s.handleRequestStatus(mctx, obm)
   294  		return true, nil
   295  	}
   296  
   297  	return false, nil
   298  }
   299  
   300  func (s *Stellar) handleReconnect(mctx libkb.MetaContext) {
   301  	defer mctx.Trace("Stellar.handleReconnect", nil)()
   302  	mctx.Debug("stellar received reconnect msg, doing delayed wallet refresh")
   303  	mctx = mctx.WithCtx(s.reconnectSlot.Use(mctx.Ctx()))
   304  	mctx, cancel := cancelOnMobileBackground(mctx)
   305  	defer cancel()
   306  	if err := libkb.Sleep(mctx.Ctx(), libkb.RandomJitter(4*time.Second)); err != nil {
   307  		mctx.Debug("Stellar.handleReconnect canceled")
   308  		return
   309  	}
   310  	if libkb.IsMobilePlatform() {
   311  		// sleep some more on mobile
   312  		if err := libkb.Sleep(mctx.Ctx(), libkb.RandomJitter(4*time.Second)); err != nil {
   313  			mctx.Debug("Stellar.handleReconnect canceled")
   314  			return
   315  		}
   316  	}
   317  	mctx.Debug("Stellar.handleReconnect delay complete, refreshing wallet state")
   318  
   319  	if err := s.walletState.RefreshAll(mctx, "reconnect"); err != nil {
   320  		mctx.Debug("Stellar.handleReconnect RefreshAll error: %s", err)
   321  	}
   322  }
   323  
   324  func (s *Stellar) handlePaymentStatus(mctx libkb.MetaContext, obm gregor.OutOfBandMessage) {
   325  	var err error
   326  	defer mctx.Trace("Stellar.handlePaymentStatus", &err)()
   327  
   328  	var msg stellar1.PaymentStatusMsg
   329  	if err = json.Unmarshal(obm.Body().Bytes(), &msg); err != nil {
   330  		mctx.Debug("error unmarshaling obm PaymentStatusMsg: %s", err)
   331  		return
   332  	}
   333  
   334  	paymentID := stellar1.NewPaymentID(msg.TxID)
   335  	notifiedAccountID, err := s.refreshPaymentFromNotification(mctx, msg.AccountID, paymentID)
   336  	if err != nil {
   337  		mctx.Debug("refreshPaymentFromNotification error: %s", err)
   338  		return
   339  	}
   340  
   341  	s.G().NotifyRouter.HandleWalletPaymentStatusNotification(mctx.Ctx(), notifiedAccountID, paymentID)
   342  }
   343  
   344  func (s *Stellar) handlePaymentNotification(mctx libkb.MetaContext, obm gregor.OutOfBandMessage) {
   345  	var err error
   346  	defer mctx.Trace("Stellar.handlePaymentNotification", &err)()
   347  	var msg stellar1.PaymentNotificationMsg
   348  	if err = json.Unmarshal(obm.Body().Bytes(), &msg); err != nil {
   349  		mctx.Debug("error unmarshaling obm PaymentNotificationMsg: %s", err)
   350  		return
   351  	}
   352  
   353  	notifiedAccountID, err := s.refreshPaymentFromNotification(mctx, msg.AccountID, msg.PaymentID)
   354  	if err != nil {
   355  		mctx.Debug("refreshPaymentFromNotification error: %s", err)
   356  		return
   357  	}
   358  	s.G().NotifyRouter.HandleWalletPaymentNotification(mctx.Ctx(), notifiedAccountID, msg.PaymentID)
   359  }
   360  
   361  func (s *Stellar) findAccountFromPayment(mctx libkb.MetaContext, accountID stellar1.AccountID, payment *stellar1.PaymentLocal) (stellar1.AccountID, error) {
   362  	var emptyAccountID stellar1.AccountID
   363  
   364  	// double-check that the accountID from the notification matches one of the accountIDs
   365  	// in the payment
   366  	if accountID == payment.FromAccountID || (payment.ToAccountID != nil && accountID == *payment.ToAccountID) {
   367  		ok, _, err := getGlobal(mctx.G()).OwnAccountCached(mctx, accountID)
   368  		if err != nil {
   369  			return emptyAccountID, err
   370  		}
   371  		if ok {
   372  			// the user owns the accountID in the notification
   373  			return accountID, nil
   374  		}
   375  	}
   376  
   377  	// check if the user owns either from or to accountID:
   378  
   379  	ok, _, err := getGlobal(mctx.G()).OwnAccountCached(mctx, payment.FromAccountID)
   380  	if err != nil {
   381  		return emptyAccountID, err
   382  	}
   383  	if ok {
   384  		// the running user is the sender of this payment
   385  		return payment.FromAccountID, nil
   386  	}
   387  	if payment.ToAccountID == nil {
   388  		return emptyAccountID, ErrAccountNotFound
   389  	}
   390  	ok, _, err = getGlobal(mctx.G()).OwnAccountCached(mctx, *payment.ToAccountID)
   391  	if err != nil {
   392  		return emptyAccountID, err
   393  	}
   394  	if ok {
   395  		// the running user is the recipient of this payment
   396  		return *payment.ToAccountID, nil
   397  	}
   398  	return emptyAccountID, ErrAccountNotFound
   399  }
   400  
   401  func (s *Stellar) refreshPaymentFromNotification(mctx libkb.MetaContext, accountID stellar1.AccountID, paymentID stellar1.PaymentID) (notifiedAccountID stellar1.AccountID, err error) {
   402  	var emptyAccountID stellar1.AccountID
   403  
   404  	// load the payment
   405  	loader := DefaultLoader(s.G())
   406  	loader.LoadPaymentSync(mctx.Ctx(), paymentID)
   407  	payment, ok := loader.GetPaymentLocal(mctx.Ctx(), paymentID)
   408  	if !ok {
   409  		return emptyAccountID, fmt.Errorf("couldn't find the payment immediately after loading it %v", paymentID)
   410  	}
   411  
   412  	// find the accountID for the running user in the payment (could be sender, recipient, neither)
   413  	notifiedAccountID, err = s.findAccountFromPayment(mctx, accountID, payment)
   414  	if err != nil {
   415  		return emptyAccountID, err
   416  	}
   417  	// refresh the wallet state for this accountID
   418  	if err := s.walletState.Refresh(mctx, notifiedAccountID, "notification received"); err != nil {
   419  		return notifiedAccountID, err
   420  	}
   421  	return notifiedAccountID, nil
   422  }
   423  
   424  func (s *Stellar) handleRequestStatus(mctx libkb.MetaContext, obm gregor.OutOfBandMessage) {
   425  	var err error
   426  	defer mctx.Trace("Stellar.handleRequestStatus", &err)()
   427  	var msg stellar1.RequestStatusMsg
   428  	if err = json.Unmarshal(obm.Body().Bytes(), &msg); err != nil {
   429  		mctx.Debug("error unmarshaling obm RequestStatusMsg: %s", err)
   430  		return
   431  	}
   432  
   433  	mctx.G().NotifyRouter.HandleWalletRequestStatusNotification(mctx.Ctx(), msg.ReqID)
   434  	DefaultLoader(mctx.G()).UpdateRequest(mctx.Ctx(), msg.ReqID)
   435  }
   436  
   437  type hasAcceptedDisclaimerDBEntry struct {
   438  	Version  int // 1
   439  	Accepted bool
   440  }
   441  
   442  // For a UV, accepted starts out false and transitions to true. It never becomes false again.
   443  // A cached true is returned, but a false always hits the server.
   444  func (s *Stellar) hasAcceptedDisclaimer(ctx context.Context) (bool, error) {
   445  	log := func(format string, args ...interface{}) {
   446  		s.G().Log.CDebugf(ctx, "Stellar.hasAcceptedDisclaimer "+format, args...)
   447  	}
   448  	uv, err := s.G().GetMeUV(ctx)
   449  	if err != nil {
   450  		return false, err
   451  	}
   452  	s.disclaimerLock.Lock()
   453  	defer s.disclaimerLock.Unlock()
   454  	// Check memory
   455  	memAccepted := s.disclaimerAccepted != nil && s.disclaimerAccepted.Eq(uv)
   456  	log("mem -> %v", memAccepted)
   457  	if memAccepted {
   458  		return true, nil
   459  	}
   460  	// Check disk
   461  	dbKey := libkb.DbKey{
   462  		Typ: libkb.DBStellarDisclaimer,
   463  		Key: uv.String(),
   464  	}
   465  	var dbEntry hasAcceptedDisclaimerDBEntry
   466  	found, err := s.G().LocalDb.GetInto(&dbEntry, dbKey)
   467  	log("disk -> [found:%v err:(%v) v:%v accepted:%v]", found, err, dbEntry.Version, dbEntry.Accepted)
   468  	if err == nil && found && dbEntry.Version == 1 && dbEntry.Accepted {
   469  		err = s.informAcceptedDisclaimerLocked(ctx)
   470  		if err != nil {
   471  			log("store -> err:(%v)", err)
   472  		}
   473  		return true, nil
   474  	}
   475  	// Check remote
   476  	accepted, err := remote.GetAcceptedDisclaimer(ctx, s.G())
   477  	log("remote -> [err:(%v) accepted:%v]", err, accepted)
   478  	if err != nil {
   479  		return false, err
   480  	}
   481  	if accepted {
   482  		err = s.informAcceptedDisclaimerLocked(ctx)
   483  		if err != nil {
   484  			log("store -> err:(%v)", err)
   485  		}
   486  	}
   487  	return accepted, nil
   488  }
   489  
   490  func (s *Stellar) informAcceptedDisclaimer(ctx context.Context) {
   491  	s.disclaimerLock.Lock()
   492  	defer s.disclaimerLock.Unlock()
   493  	_ = s.informAcceptedDisclaimerLocked(ctx)
   494  }
   495  
   496  func (s *Stellar) informAcceptedDisclaimerLocked(ctx context.Context) (err error) {
   497  	defer s.G().CTrace(ctx, "Stellar.informAcceptedDisclaimer", &err)()
   498  	uv, err := s.G().GetMeUV(ctx)
   499  	if err != nil {
   500  		return err
   501  	}
   502  	// Store memory
   503  	s.disclaimerAccepted = &uv
   504  	// Store disk
   505  	return s.G().LocalDb.PutObj(libkb.DbKey{
   506  		Typ: libkb.DBStellarDisclaimer,
   507  		Key: uv.String(),
   508  	}, nil, hasAcceptedDisclaimerDBEntry{
   509  		Version:  1,
   510  		Accepted: true,
   511  	})
   512  }
   513  
   514  func (s *Stellar) startBuildPayment(mctx libkb.MetaContext) (bid stellar1.BuildPaymentID, err error) {
   515  	defer func() {
   516  		x := bid.String()
   517  		if err != nil {
   518  			x = fmt.Sprintf("ERR(%v)", err.Error())
   519  		}
   520  		mctx.Debug("Stellar.startBuildPayment -> %v", x)
   521  	}()
   522  	bid, err = RandomBuildPaymentID()
   523  	if err != nil {
   524  		return "", err
   525  	}
   526  	s.bidLock.Lock()
   527  	defer s.bidLock.Unlock()
   528  	s.bids = append(s.bids, newBuildPaymentEntry(bid))
   529  	const maxConcurrentBuilds = 20
   530  	if len(s.bids) > maxConcurrentBuilds {
   531  		// Too many open payment builds. Drop the oldest ones at the beginning of the list.
   532  		// Leave the newest ones at the end of the list.
   533  		for i := 0; i < len(s.bids)-maxConcurrentBuilds; i++ {
   534  			entry := s.bids[i]
   535  			entry.Slot.Shutdown()
   536  		}
   537  		s.bids = s.bids[len(s.bids)-maxConcurrentBuilds:]
   538  	}
   539  	return bid, nil
   540  }
   541  
   542  // stopBuildPayment stops a bid forever.
   543  func (s *Stellar) stopBuildPayment(mctx libkb.MetaContext, bid stellar1.BuildPaymentID) {
   544  	mctx.Debug("Stellar.stopBuildPayment(%v)", bid)
   545  	if bid.IsNil() {
   546  		s.buildPaymentSlot.Stop()
   547  		return
   548  	}
   549  	s.bidLock.Lock()
   550  	defer s.bidLock.Unlock()
   551  	for _, entry := range s.bids {
   552  		if entry.Bid.Eq(bid) {
   553  			if entry.Stopped {
   554  				mctx.Debug("payment already stopped")
   555  				return
   556  			}
   557  			entry.Slot.Shutdown()
   558  			entry.Stopped = true
   559  			mctx.Debug("payment shutdown")
   560  			return
   561  		}
   562  	}
   563  	mctx.Debug("payment not found to stop")
   564  }
   565  
   566  // acquireBuildPayment takes ownership of a payment build.
   567  // Returns a new `mctx` that the caller should switch to. Because it runs within the slot.
   568  // When err=nil the caller owns `data` and must release it with `release` when finished.
   569  // When err!=nil data is nil.
   570  // `release` can be called even when err!=nil.
   571  // `mctx` can also be used if err!=nil.
   572  // Callers should `release` soon after their context is canceled.
   573  func (s *Stellar) acquireBuildPayment(mctx1 libkb.MetaContext, bid stellar1.BuildPaymentID, sessionID int) (
   574  	mctx libkb.MetaContext, data *buildPaymentData, release func(), err error) {
   575  	mctx = mctx1
   576  	mctx.Debug("Stellar.acquireBuildPayment(%v)", bid)
   577  	release = func() {}
   578  	s.bidLock.Lock()
   579  	defer s.bidLock.Unlock()
   580  	for _, entry := range s.bids {
   581  		entry := entry
   582  		if !entry.Bid.Eq(bid) {
   583  			continue
   584  		}
   585  		if entry.Stopped {
   586  			return mctx, nil, release, fmt.Errorf("This payment might have already been sent. Check your recent payments before trying again.")
   587  		}
   588  		mctx = mctx.WithCtx(entry.Slot.Use(mctx.Ctx(), sessionID))
   589  		if err = mctx.Ctx().Err(); err != nil {
   590  			return mctx, nil, release, err
   591  		}
   592  		err = libkb.AcquireWithContextAndTimeout(mctx.Ctx(), &entry.DataLock, 5*time.Second)
   593  		if err != nil {
   594  			mctx.Debug("error while attempting to acquire data lock: %v", err)
   595  			return mctx, nil, release, err
   596  		}
   597  		release = libkb.Once(func() {
   598  			entry.DataLock.Unlock()
   599  		})
   600  		return mctx, &entry.Data, release, nil
   601  	}
   602  	return mctx, nil, release, fmt.Errorf("payment build not found")
   603  }
   604  
   605  // finalizeBuildPayment stops a bid forever and returns its data.
   606  func (s *Stellar) finalizeBuildPayment(mctx libkb.MetaContext, bid stellar1.BuildPaymentID) (res *buildPaymentData, err error) {
   607  	mctx.Debug("Stellar.finalizeBuildPayment(%v)", bid)
   608  	s.bidLock.Lock()
   609  	defer s.bidLock.Unlock()
   610  	for _, entry := range s.bids {
   611  		entry := entry
   612  		if !entry.Bid.Eq(bid) {
   613  			continue
   614  		}
   615  		if entry.Stopped {
   616  			return nil, fmt.Errorf("This payment might have already been sent. Check your recent payments before trying again.")
   617  		}
   618  		entry.Slot.Shutdown()
   619  		entry.Stopped = true
   620  		err = libkb.AcquireWithContextAndTimeout(mctx.Ctx(), &entry.DataLock, 5*time.Second)
   621  		if err != nil {
   622  			// This likely means something in the Slot is not yielding to its context or forgot to release the lock.
   623  			mctx.Debug("error while attempting to acquire data lock: %v", err)
   624  			return nil, err
   625  		}
   626  		res = &entry.Data
   627  		entry.DataLock.Unlock()
   628  		return res, nil
   629  	}
   630  	return nil, fmt.Errorf("payment build not found")
   631  }
   632  
   633  func (s *Stellar) WalletStateForTest() *WalletState {
   634  	return s.walletState
   635  }
   636  
   637  func (s *Stellar) RemovePendingTx(mctx libkb.MetaContext, accountID stellar1.AccountID, txID stellar1.TransactionID) error {
   638  	return s.walletState.RemovePendingTx(mctx.Ctx(), accountID, txID)
   639  }
   640  
   641  // BaseFee returns the server-suggested base fee per operation.
   642  func (s *Stellar) BaseFee(mctx libkb.MetaContext) uint64 {
   643  	return s.walletState.BaseFee(mctx)
   644  }
   645  
   646  func (s *Stellar) InformBundle(mctx libkb.MetaContext, rev stellar1.BundleRevision, accounts []stellar1.BundleEntry) {
   647  	go func() {
   648  		err := libkb.AcquireWithContextAndTimeout(mctx.Ctx(), &s.accountsLock, 5*time.Second)
   649  		if err != nil {
   650  			mctx.Debug("InformBundle: error acquiring lock")
   651  			return
   652  		}
   653  		defer s.accountsLock.Unlock()
   654  		if s.accounts != nil && rev < s.accounts.Revision {
   655  			return
   656  		}
   657  		s.accounts = &AccountsCache{
   658  			Stored:   mctx.G().Clock().Now(),
   659  			Revision: rev,
   660  			Accounts: accounts,
   661  		}
   662  	}()
   663  }
   664  
   665  func (s *Stellar) InformDefaultCurrencyChange(mctx libkb.MetaContext) {
   666  	go func() {
   667  		s.getBuildPaymentCache().InformDefaultCurrencyChange(mctx)
   668  	}()
   669  }
   670  
   671  func (s *Stellar) OwnAccountCached(mctx libkb.MetaContext, accountID stellar1.AccountID) (own, isPrimary bool, err error) {
   672  	own, isPrimary, _, err = s.OwnAccountPlusNameCached(mctx, accountID)
   673  	return own, isPrimary, err
   674  }
   675  
   676  func (s *Stellar) OwnAccountPlusNameCached(mctx libkb.MetaContext, accountID stellar1.AccountID) (own, isPrimary bool, accountName string, err error) {
   677  	err = libkb.AcquireWithContextAndTimeout(mctx.Ctx(), &s.accountsLock, 5*time.Second)
   678  	if err != nil {
   679  		mctx.Debug("OwnAccountPlusNameCached: error acquiring lock")
   680  		return
   681  	}
   682  	if s.accounts != nil && mctx.G().Clock().Now().Sub(s.accounts.Stored.Round(0)) < 2*time.Minute {
   683  		for _, acc := range s.accounts.Accounts {
   684  			if acc.AccountID.Eq(accountID) {
   685  				s.accountsLock.Unlock()
   686  				return true, acc.IsPrimary, acc.Name, nil
   687  			}
   688  		}
   689  		s.accountsLock.Unlock()
   690  		return false, false, "", nil
   691  	}
   692  	s.accountsLock.Unlock()
   693  	return OwnAccountPlusName(mctx, accountID)
   694  }
   695  
   696  func (s *Stellar) Refresh(mctx libkb.MetaContext, reason string) {
   697  	if err := s.walletState.RefreshAll(mctx, reason); err != nil {
   698  		mctx.Debug("Stellar.Refresh(%s) ws.RefreshAll error: %s", reason, err)
   699  	}
   700  }
   701  
   702  // getFederationClient is a helper function used during
   703  // initialization.
   704  func getFederationClient(g *libkb.GlobalContext) federation.ClientInterface {
   705  	if g.Env.GetRunMode() != libkb.ProductionRunMode {
   706  		return federation.DefaultTestNetClient
   707  	}
   708  	return federation.DefaultPublicNetClient
   709  }
   710  
   711  // getGlobal gets the libkb.Stellar off of G and asserts it into a stellar.Stellar
   712  func getGlobal(g *libkb.GlobalContext) *Stellar {
   713  	return g.GetStellar().(*Stellar)
   714  }