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

     1  package stellar
     2  
     3  import (
     4  	"fmt"
     5  	"sync"
     6  	"time"
     7  
     8  	"golang.org/x/net/context"
     9  
    10  	"github.com/keybase/client/go/chat/utils"
    11  	"github.com/keybase/client/go/libkb"
    12  	"github.com/keybase/client/go/protocol/chat1"
    13  	"github.com/keybase/client/go/protocol/stellar1"
    14  )
    15  
    16  type chatMsg struct {
    17  	convID chat1.ConversationID
    18  	msgID  chat1.MessageID
    19  	sender libkb.NormalizedUsername
    20  }
    21  
    22  type PaymentStatusUpdate struct {
    23  	AccountID stellar1.AccountID
    24  	TxID      stellar1.TransactionID
    25  	Status    stellar1.PaymentStatus
    26  }
    27  
    28  const (
    29  	maxPayments = 1000
    30  	maxRequests = 1000
    31  )
    32  
    33  type Loader struct {
    34  	libkb.Contextified
    35  
    36  	payments  map[stellar1.PaymentID]*stellar1.PaymentLocal
    37  	pmessages map[stellar1.PaymentID]chatMsg
    38  	pqueue    chan stellar1.PaymentID
    39  	plist     []stellar1.PaymentID
    40  
    41  	requests  map[stellar1.KeybaseRequestID]*stellar1.RequestDetailsLocal
    42  	rmessages map[stellar1.KeybaseRequestID]chatMsg
    43  	rqueue    chan stellar1.KeybaseRequestID
    44  	rlist     []stellar1.KeybaseRequestID
    45  
    46  	listeners map[string]chan PaymentStatusUpdate
    47  
    48  	shutdownOnce sync.Once
    49  	done         bool
    50  
    51  	sync.Mutex
    52  }
    53  
    54  var defaultLoader *Loader
    55  var defaultLock sync.Mutex
    56  
    57  func NewLoader(g *libkb.GlobalContext) *Loader {
    58  	p := &Loader{
    59  		Contextified: libkb.NewContextified(g),
    60  		payments:     make(map[stellar1.PaymentID]*stellar1.PaymentLocal),
    61  		pmessages:    make(map[stellar1.PaymentID]chatMsg),
    62  		pqueue:       make(chan stellar1.PaymentID, 100),
    63  		requests:     make(map[stellar1.KeybaseRequestID]*stellar1.RequestDetailsLocal),
    64  		rmessages:    make(map[stellar1.KeybaseRequestID]chatMsg),
    65  		rqueue:       make(chan stellar1.KeybaseRequestID, 100),
    66  		listeners:    make(map[string]chan PaymentStatusUpdate),
    67  	}
    68  
    69  	go p.runPayments()
    70  	go p.runRequests()
    71  
    72  	return p
    73  }
    74  
    75  func DefaultLoader(g *libkb.GlobalContext) *Loader {
    76  	defaultLock.Lock()
    77  	defer defaultLock.Unlock()
    78  
    79  	if defaultLoader == nil {
    80  		defaultLoader = NewLoader(g)
    81  		g.PushShutdownHook(func(mctx libkb.MetaContext) error {
    82  			defaultLock.Lock()
    83  			err := defaultLoader.Shutdown()
    84  			defaultLoader = nil
    85  			defaultLock.Unlock()
    86  			return err
    87  		})
    88  	}
    89  
    90  	return defaultLoader
    91  }
    92  
    93  func (p *Loader) GetPaymentLocal(ctx context.Context, paymentID stellar1.PaymentID) (*stellar1.PaymentLocal, bool) {
    94  	p.Lock()
    95  	defer p.Unlock()
    96  	return p.getPaymentLocalLocked(ctx, paymentID)
    97  }
    98  
    99  func (p *Loader) getPaymentLocalLocked(ctx context.Context, paymentID stellar1.PaymentID) (*stellar1.PaymentLocal, bool) {
   100  	pmt, ok := p.payments[paymentID]
   101  	return pmt, ok
   102  }
   103  
   104  func (p *Loader) LoadPayment(ctx context.Context, convID chat1.ConversationID, msgID chat1.MessageID, senderUsername string, paymentID stellar1.PaymentID) *chat1.UIPaymentInfo {
   105  	defer p.G().CTrace(ctx, fmt.Sprintf("Loader.LoadPayment(cid=%s,mid=%s,pid=%s)", convID, msgID, paymentID), nil)()
   106  
   107  	p.Lock()
   108  	defer p.Unlock()
   109  
   110  	m := libkb.NewMetaContext(ctx, p.G())
   111  
   112  	if p.done {
   113  		m.Debug("loader shutdown, not loading payment %s", paymentID)
   114  		return nil
   115  	}
   116  
   117  	if len(paymentID) == 0 {
   118  		m.Debug("LoadPayment called with empty paymentID for %s/%s", convID, msgID)
   119  		return nil
   120  	}
   121  
   122  	msg, ok := p.pmessages[paymentID]
   123  	// store the msg info if necessary
   124  	if !ok {
   125  		msg = chatMsg{
   126  			convID: convID,
   127  			msgID:  msgID,
   128  			sender: libkb.NewNormalizedUsername(senderUsername),
   129  		}
   130  		p.pmessages[paymentID] = msg
   131  	} else if !msg.convID.Eq(convID) || msg.msgID != msgID {
   132  		m.Warning("existing payment message info does not match load info: (%v, %v) != (%v, %v)", msg.convID, msg.msgID, convID, msgID)
   133  	}
   134  
   135  	payment, ok := p.getPaymentLocalLocked(ctx, paymentID)
   136  	if ok {
   137  		info := p.uiPaymentInfo(m, payment, msg)
   138  		p.G().NotifyRouter.HandleChatPaymentInfo(m.Ctx(), p.G().ActiveDevice.UID(), convID, msgID, *info)
   139  		if info.Status != stellar1.PaymentStatus_COMPLETED {
   140  			// to be safe, schedule a reload of the payment in case it has
   141  			// changed since stored
   142  			p.enqueuePayment(paymentID)
   143  		}
   144  		return info
   145  	}
   146  
   147  	// not found, need to load payment in background
   148  	p.enqueuePayment(paymentID)
   149  
   150  	return nil
   151  }
   152  
   153  func (p *Loader) LoadRequest(ctx context.Context, convID chat1.ConversationID, msgID chat1.MessageID, senderUsername string, requestID stellar1.KeybaseRequestID) *chat1.UIRequestInfo {
   154  	defer p.G().CTrace(ctx, fmt.Sprintf("Loader.LoadRequest(cid=%s,mid=%s,rid=%s)", convID, msgID, requestID), nil)()
   155  
   156  	p.Lock()
   157  	defer p.Unlock()
   158  
   159  	m := libkb.NewMetaContext(ctx, p.G())
   160  
   161  	if p.done {
   162  		m.Debug("loader shutdown, not loading request %s", requestID)
   163  		return nil
   164  	}
   165  
   166  	msg, ok := p.rmessages[requestID]
   167  	// store the msg info if necessary
   168  	if !ok {
   169  		msg = chatMsg{
   170  			convID: convID,
   171  			msgID:  msgID,
   172  			sender: libkb.NewNormalizedUsername(senderUsername),
   173  		}
   174  		p.rmessages[requestID] = msg
   175  	} else if !msg.convID.Eq(convID) || msg.msgID != msgID {
   176  		m.Warning("existing request message info does not match load info: (%v, %v) != (%v, %v)", msg.convID, msg.msgID, convID, msgID)
   177  	}
   178  
   179  	request, ok := p.requests[requestID]
   180  	var info *chat1.UIRequestInfo
   181  	if ok {
   182  		info = p.uiRequestInfo(m, request, msg)
   183  	}
   184  
   185  	// always load request in background (even if found) to make sure stored value is up-to-date.
   186  	p.enqueueRequest(requestID)
   187  
   188  	return info
   189  }
   190  
   191  // UpdatePayment schedules a load of paymentID. Gregor status notification handlers
   192  // should call this to update the payment data.
   193  func (p *Loader) UpdatePayment(ctx context.Context, paymentID stellar1.PaymentID) {
   194  	if p.done {
   195  		return
   196  	}
   197  
   198  	p.enqueuePayment(paymentID)
   199  }
   200  
   201  // UpdateRequest schedules a load for requestID. Gregor status notification handlers
   202  // should call this to update the request data.
   203  func (p *Loader) UpdateRequest(ctx context.Context, requestID stellar1.KeybaseRequestID) {
   204  	if p.done {
   205  		return
   206  	}
   207  
   208  	p.enqueueRequest(requestID)
   209  }
   210  
   211  // GetListener returns a channel and an ID for a payment status listener.  The ID
   212  // can be used to remove the listener from the loader.
   213  func (p *Loader) GetListener() (id string, ch chan PaymentStatusUpdate, err error) {
   214  	ch = make(chan PaymentStatusUpdate, 100)
   215  	id, err = libkb.RandString("", 8)
   216  	if err != nil {
   217  		return id, ch, err
   218  	}
   219  	p.Lock()
   220  	p.listeners[id] = ch
   221  	p.Unlock()
   222  
   223  	return id, ch, nil
   224  }
   225  
   226  // RemoveListener removes a listener from the loader when it is no longer needed.
   227  func (p *Loader) RemoveListener(id string) {
   228  	p.Lock()
   229  	delete(p.listeners, id)
   230  	p.Unlock()
   231  }
   232  
   233  func (p *Loader) Shutdown() error {
   234  	p.shutdownOnce.Do(func() {
   235  		p.Lock()
   236  		p.G().GetLog().Debug("shutting down stellar loader")
   237  		p.done = true
   238  		close(p.pqueue)
   239  		close(p.rqueue)
   240  		p.Unlock()
   241  	})
   242  	return nil
   243  }
   244  
   245  func (p *Loader) runPayments() {
   246  	for id := range p.pqueue {
   247  		if err := p.loadPayment(libkb.NewMetaContextTODO(p.G()), id); err != nil {
   248  			p.G().GetLog().CDebugf(context.TODO(), "Unable to load payment: %v", err)
   249  		}
   250  		p.cleanPayments(maxPayments)
   251  	}
   252  }
   253  
   254  func (p *Loader) runRequests() {
   255  	for id := range p.rqueue {
   256  		p.loadRequest(id)
   257  		p.cleanRequests(maxRequests)
   258  	}
   259  }
   260  
   261  func (p *Loader) LoadPaymentSync(ctx context.Context, paymentID stellar1.PaymentID) {
   262  	mctx := libkb.NewMetaContext(ctx, p.G())
   263  	defer mctx.Trace(fmt.Sprintf("LoadPaymentSync(%s)", paymentID), nil)()
   264  
   265  	backoffPolicy := libkb.BackoffPolicy{
   266  		Millis: []int{2000, 3000, 5000},
   267  	}
   268  	for i := 0; i <= 3; i++ {
   269  		err := p.loadPayment(mctx, paymentID)
   270  		if err == nil {
   271  			break
   272  		}
   273  		mctx.Debug("error on attempt %d to load payment %s: %s. sleep and retry.", i, paymentID, err)
   274  		time.Sleep(backoffPolicy.Duration(i))
   275  	}
   276  }
   277  
   278  func (p *Loader) loadPayment(mctx libkb.MetaContext, id stellar1.PaymentID) (err error) {
   279  	mctx, cancel := mctx.BackgroundWithLogTags().WithLogTag("LP").WithTimeout(15 * time.Second)
   280  	defer cancel()
   281  	defer mctx.Trace(fmt.Sprintf("loadPayment(%s)", id), nil)()
   282  
   283  	s := getGlobal(p.G())
   284  	details, err := s.remoter.PaymentDetailsGeneric(mctx.Ctx(), stellar1.TransactionIDFromPaymentID(id).String())
   285  	if err != nil {
   286  		mctx.Debug("error getting payment details for %s: %s", id, err)
   287  		return err
   288  	}
   289  
   290  	oc := NewOwnAccountLookupCache(mctx)
   291  	summary, err := TransformPaymentSummaryGeneric(mctx, details.Summary, oc)
   292  	if err != nil {
   293  		mctx.Debug("error transforming details for %s: %s", id, err)
   294  		return err
   295  	}
   296  
   297  	p.storePayment(id, summary)
   298  
   299  	p.sendPaymentNotification(mctx, id, summary)
   300  	return nil
   301  }
   302  
   303  func (p *Loader) loadRequest(id stellar1.KeybaseRequestID) {
   304  	ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
   305  	defer cancel()
   306  
   307  	m := libkb.NewMetaContext(ctx, p.G())
   308  	defer m.Trace(fmt.Sprintf("loadRequest(%s)", id), nil)()
   309  
   310  	s := getGlobal(p.G())
   311  	details, err := s.remoter.RequestDetails(ctx, id)
   312  	if err != nil {
   313  		m.Debug("error getting request details for %s: %s", id, err)
   314  		return
   315  	}
   316  	local, err := TransformRequestDetails(m, details)
   317  	if err != nil {
   318  		m.Debug("error transforming request details for %s: %s", id, err)
   319  		return
   320  	}
   321  
   322  	// must be a newly loaded request or the status changed for
   323  	// a notification to be sent below
   324  	isUpdate := p.storeRequest(id, local)
   325  
   326  	if isUpdate {
   327  		p.sendRequestNotification(m, id, local)
   328  	}
   329  }
   330  
   331  func (p *Loader) uiPaymentInfo(m libkb.MetaContext, summary *stellar1.PaymentLocal, msg chatMsg) *chat1.UIPaymentInfo {
   332  	info := chat1.UIPaymentInfo{
   333  		AccountID:         &summary.FromAccountID,
   334  		AmountDescription: summary.AmountDescription,
   335  		Worth:             summary.Worth,
   336  		WorthAtSendTime:   summary.WorthAtSendTime,
   337  		Delta:             summary.Delta,
   338  		Note:              utils.EscapeForDecorate(m.Ctx(), summary.Note),
   339  		IssuerDescription: summary.IssuerDescription,
   340  		PaymentID:         summary.Id,
   341  		SourceAmount:      summary.SourceAmountActual,
   342  		SourceAsset:       summary.SourceAsset,
   343  		Status:            summary.StatusSimplified,
   344  		StatusDescription: summary.StatusDescription,
   345  		StatusDetail:      summary.StatusDetail,
   346  		ShowCancel:        summary.ShowCancel,
   347  		FromUsername:      summary.FromUsername,
   348  		ToUsername:        summary.ToUsername,
   349  	}
   350  
   351  	info.Delta = stellar1.BalanceDelta_NONE
   352  
   353  	// Calculate the payment delta & relevant accountID
   354  	if summary.FromType == stellar1.ParticipantType_OWNACCOUNT && summary.ToType == stellar1.ParticipantType_OWNACCOUNT {
   355  		// This is a transfer between the user's own accounts.
   356  		info.Delta = stellar1.BalanceDelta_NONE
   357  	} else {
   358  		info.Delta = stellar1.BalanceDelta_INCREASE
   359  		if msg.sender != "" {
   360  			// this is related to a chat message
   361  			if msg.sender.Eq(p.G().ActiveDevice.Username(m)) {
   362  				info.Delta = stellar1.BalanceDelta_DECREASE
   363  			} else {
   364  				// switch the account ID to the recipient
   365  				info.AccountID = summary.ToAccountID
   366  			}
   367  		}
   368  	}
   369  
   370  	return &info
   371  }
   372  
   373  func (p *Loader) sendPaymentNotification(m libkb.MetaContext, id stellar1.PaymentID, summary *stellar1.PaymentLocal) {
   374  	p.Lock()
   375  	msg, ok := p.pmessages[id]
   376  	p.Unlock()
   377  
   378  	if !ok {
   379  		// this is ok: frontend only needs the payment ID
   380  		m.Debug("sending chat notification for payment %s using empty msg info", id)
   381  		msg = chatMsg{}
   382  	} else {
   383  		m.Debug("sending chat notification for payment %s to %s, %s", id, msg.convID, msg.msgID)
   384  	}
   385  
   386  	uid := p.G().ActiveDevice.UID()
   387  	info := p.uiPaymentInfo(m, summary, msg)
   388  
   389  	if info.AccountID != nil && summary.StatusSimplified != stellar1.PaymentStatus_PENDING {
   390  		// let WalletState know
   391  		err := p.G().GetStellar().RemovePendingTx(m, *info.AccountID, stellar1.TransactionIDFromPaymentID(id))
   392  		if err != nil {
   393  			m.Debug("ws.RemovePendingTx error: %s", err)
   394  		}
   395  		p.Lock()
   396  		for _, ch := range p.listeners {
   397  			ch <- PaymentStatusUpdate{AccountID: *info.AccountID, TxID: stellar1.TransactionIDFromPaymentID(id), Status: summary.StatusSimplified}
   398  		}
   399  		p.Unlock()
   400  	}
   401  
   402  	p.G().NotifyRouter.HandleChatPaymentInfo(m.Ctx(), uid, msg.convID, msg.msgID, *info)
   403  }
   404  
   405  func (p *Loader) uiRequestInfo(m libkb.MetaContext, details *stellar1.RequestDetailsLocal, msg chatMsg) *chat1.UIRequestInfo {
   406  	info := chat1.UIRequestInfo{
   407  		Amount:             details.Amount,
   408  		AmountDescription:  details.AmountDescription,
   409  		Asset:              details.Asset,
   410  		Currency:           details.Currency,
   411  		Status:             details.Status,
   412  		WorthAtRequestTime: details.WorthAtRequestTime,
   413  	}
   414  
   415  	return &info
   416  }
   417  
   418  func (p *Loader) sendRequestNotification(m libkb.MetaContext, id stellar1.KeybaseRequestID, details *stellar1.RequestDetailsLocal) {
   419  	p.Lock()
   420  	msg, ok := p.rmessages[id]
   421  	p.Unlock()
   422  
   423  	if !ok {
   424  		m.Debug("not sending request chat notification for %s (no associated convID, msgID)", id)
   425  		return
   426  	}
   427  
   428  	m.Debug("sending chat notification for request %s to %s, %s", id, msg.convID, msg.msgID)
   429  	uid := p.G().ActiveDevice.UID()
   430  	info := p.uiRequestInfo(m, details, msg)
   431  	p.G().NotifyRouter.HandleChatRequestInfo(m.Ctx(), uid, msg.convID, msg.msgID, *info)
   432  }
   433  
   434  func (p *Loader) enqueuePayment(paymentID stellar1.PaymentID) {
   435  	select {
   436  	case p.pqueue <- paymentID:
   437  	default:
   438  		p.G().Log.Debug("stellar.Loader payment queue full")
   439  	}
   440  }
   441  
   442  func (p *Loader) enqueueRequest(requestID stellar1.KeybaseRequestID) {
   443  	select {
   444  	case p.rqueue <- requestID:
   445  	default:
   446  		p.G().Log.Debug("stellar.Loader request queue full")
   447  	}
   448  }
   449  
   450  func (p *Loader) storePayment(id stellar1.PaymentID, payment *stellar1.PaymentLocal) {
   451  	p.Lock()
   452  	p.payments[id] = payment
   453  	p.plist = append(p.plist, id)
   454  	p.Unlock()
   455  }
   456  
   457  // storeRequest returns true if it updated an existing value.
   458  func (p *Loader) storeRequest(id stellar1.KeybaseRequestID, request *stellar1.RequestDetailsLocal) (isUpdate bool) {
   459  	p.Lock()
   460  	x, ok := p.requests[id]
   461  	if !ok || x.Status != request.Status {
   462  		isUpdate = true
   463  	}
   464  	p.requests[id] = request
   465  	p.rlist = append(p.rlist, id)
   466  	p.Unlock()
   467  
   468  	return isUpdate
   469  }
   470  
   471  func (p *Loader) PaymentsLen() int {
   472  	p.Lock()
   473  	defer p.Unlock()
   474  	return len(p.payments)
   475  }
   476  
   477  func (p *Loader) RequestsLen() int {
   478  	p.Lock()
   479  	defer p.Unlock()
   480  	return len(p.requests)
   481  }
   482  
   483  func (p *Loader) cleanPayments(n int) int {
   484  	p.Lock()
   485  	defer p.Unlock()
   486  
   487  	var deleted int
   488  	toDelete := len(p.payments) - n
   489  	if toDelete <= 0 {
   490  		return 0
   491  	}
   492  
   493  	for i := 0; i < toDelete; i++ {
   494  		delete(p.payments, p.plist[i])
   495  		delete(p.pmessages, p.plist[i])
   496  		deleted++
   497  	}
   498  
   499  	p.plist = p.plist[toDelete:]
   500  
   501  	return deleted
   502  }
   503  
   504  func (p *Loader) cleanRequests(n int) int {
   505  	p.Lock()
   506  	defer p.Unlock()
   507  
   508  	var deleted int
   509  	toDelete := len(p.requests) - n
   510  	if toDelete <= 0 {
   511  		return 0
   512  	}
   513  
   514  	for i := 0; i < toDelete; i++ {
   515  		delete(p.requests, p.rlist[i])
   516  		delete(p.rmessages, p.rlist[i])
   517  		deleted++
   518  	}
   519  
   520  	p.rlist = p.rlist[toDelete:]
   521  
   522  	return deleted
   523  }