/
githubmirror
/
client
Обзор
Документация
Войти
/
githubmirror
/
client
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
go/stellar/loader.go
524 строки
14 KB
chrisnojima-zoom
Version 670 - clean2 (#29122)
08 июн 2026, 19:31
Не верифицирован
08 июн 2026, 19:31
1943197
Код
Авторство
О чём код?
package stellar import ( "context" "fmt" "sync" "time" "github.com/keybase/client/go/chat/utils" "github.com/keybase/client/go/libkb" "github.com/keybase/client/go/protocol/chat1" "github.com/keybase/client/go/protocol/stellar1" ) type chatMsg struct { convID chat1.ConversationID msgID chat1.MessageID sender libkb.NormalizedUsername } type PaymentStatusUpdate struct { AccountID stellar1.AccountID TxID stellar1.TransactionID Status stellar1.PaymentStatus } const ( maxPayments = 1000 maxRequests = 1000 ) type Loader struct { libkb.Contextified payments map[stellar1.PaymentID]*stellar1.PaymentLocal pmessages map[stellar1.PaymentID]chatMsg pqueue chan stellar1.PaymentID plist []stellar1.PaymentID requests map[stellar1.KeybaseRequestID]*stellar1.RequestDetailsLocal rmessages map[stellar1.KeybaseRequestID]chatMsg rqueue chan stellar1.KeybaseRequestID rlist []stellar1.KeybaseRequestID listeners map[string]chan PaymentStatusUpdate shutdownOnce sync.Once done bool sync.Mutex } var ( defaultLoader *Loader defaultLock sync.Mutex ) func NewLoader(g *libkb.GlobalContext) *Loader { p := &Loader{ Contextified: libkb.NewContextified(g), payments: make(map[stellar1.PaymentID]*stellar1.PaymentLocal), pmessages: make(map[stellar1.PaymentID]chatMsg), pqueue: make(chan stellar1.PaymentID, 100), requests: make(map[stellar1.KeybaseRequestID]*stellar1.RequestDetailsLocal), rmessages: make(map[stellar1.KeybaseRequestID]chatMsg), rqueue: make(chan stellar1.KeybaseRequestID, 100), listeners: make(map[string]chan PaymentStatusUpdate), } go p.runPayments() go p.runRequests() return p } func DefaultLoader(g *libkb.GlobalContext) *Loader { defaultLock.Lock() defer defaultLock.Unlock() if defaultLoader == nil { defaultLoader = NewLoader(g) g.PushShutdownHook(func(mctx libkb.MetaContext) error { defaultLock.Lock() err := defaultLoader.Shutdown() defaultLoader = nil defaultLock.Unlock() return err }) } return defaultLoader } func (p *Loader) GetPaymentLocal(ctx context.Context, paymentID stellar1.PaymentID) (*stellar1.PaymentLocal, bool) { p.Lock() defer p.Unlock() return p.getPaymentLocalLocked(ctx, paymentID) } func (p *Loader) getPaymentLocalLocked(ctx context.Context, paymentID stellar1.PaymentID) (*stellar1.PaymentLocal, bool) { pmt, ok := p.payments[paymentID] return pmt, ok } func (p *Loader) LoadPayment(ctx context.Context, convID chat1.ConversationID, msgID chat1.MessageID, senderUsername string, paymentID stellar1.PaymentID) *chat1.UIPaymentInfo { defer p.G().CTrace(ctx, fmt.Sprintf("Loader.LoadPayment(cid=%s,mid=%s,pid=%s)", convID, msgID, paymentID), nil)() p.Lock() defer p.Unlock() m := libkb.NewMetaContext(ctx, p.G()) if p.done { m.Debug("loader shutdown, not loading payment %s", paymentID) return nil } if len(paymentID) == 0 { m.Debug("LoadPayment called with empty paymentID for %s/%s", convID, msgID) return nil } msg, ok := p.pmessages[paymentID] // store the msg info if necessary if !ok { msg = chatMsg{ convID: convID, msgID: msgID, sender: libkb.NewNormalizedUsername(senderUsername), } p.pmessages[paymentID] = msg } else if !msg.convID.Eq(convID) || msg.msgID != msgID { m.Warning("existing payment message info does not match load info: (%v, %v) != (%v, %v)", msg.convID, msg.msgID, convID, msgID) } payment, ok := p.getPaymentLocalLocked(ctx, paymentID) if ok { info := p.uiPaymentInfo(m, payment, msg) p.G().NotifyRouter.HandleChatPaymentInfo(m.Ctx(), p.G().ActiveDevice.UID(), convID, msgID, *info) if info.Status != stellar1.PaymentStatus_COMPLETED { // to be safe, schedule a reload of the payment in case it has // changed since stored p.enqueuePayment(paymentID) } return info } // not found, need to load payment in background p.enqueuePayment(paymentID) return nil } func (p *Loader) LoadRequest(ctx context.Context, convID chat1.ConversationID, msgID chat1.MessageID, senderUsername string, requestID stellar1.KeybaseRequestID) *chat1.UIRequestInfo { defer p.G().CTrace(ctx, fmt.Sprintf("Loader.LoadRequest(cid=%s,mid=%s,rid=%s)", convID, msgID, requestID), nil)() p.Lock() defer p.Unlock() m := libkb.NewMetaContext(ctx, p.G()) if p.done { m.Debug("loader shutdown, not loading request %s", requestID) return nil } msg, ok := p.rmessages[requestID] // store the msg info if necessary if !ok { msg = chatMsg{ convID: convID, msgID: msgID, sender: libkb.NewNormalizedUsername(senderUsername), } p.rmessages[requestID] = msg } else if !msg.convID.Eq(convID) || msg.msgID != msgID { m.Warning("existing request message info does not match load info: (%v, %v) != (%v, %v)", msg.convID, msg.msgID, convID, msgID) } request, ok := p.requests[requestID] var info *chat1.UIRequestInfo if ok { info = p.uiRequestInfo(m, request, msg) } // always load request in background (even if found) to make sure stored value is up-to-date. p.enqueueRequest(requestID) return info } // UpdatePayment schedules a load of paymentID. Gregor status notification handlers // should call this to update the payment data. func (p *Loader) UpdatePayment(ctx context.Context, paymentID stellar1.PaymentID) { if p.done { return } p.enqueuePayment(paymentID) } // UpdateRequest schedules a load for requestID. Gregor status notification handlers // should call this to update the request data. func (p *Loader) UpdateRequest(ctx context.Context, requestID stellar1.KeybaseRequestID) { if p.done { return } p.enqueueRequest(requestID) } // GetListener returns a channel and an ID for a payment status listener. The ID // can be used to remove the listener from the loader. func (p *Loader) GetListener() (id string, ch chan PaymentStatusUpdate, err error) { ch = make(chan PaymentStatusUpdate, 100) id, err = libkb.RandString("", 8) if err != nil { return id, ch, err } p.Lock() p.listeners[id] = ch p.Unlock() return id, ch, nil } // RemoveListener removes a listener from the loader when it is no longer needed. func (p *Loader) RemoveListener(id string) { p.Lock() delete(p.listeners, id) p.Unlock() } func (p *Loader) Shutdown() error { p.shutdownOnce.Do(func() { p.Lock() p.G().GetLog().Debug("shutting down stellar loader") p.done = true close(p.pqueue) close(p.rqueue) p.Unlock() }) return nil } func (p *Loader) runPayments() { for id := range p.pqueue { if err := p.loadPayment(libkb.NewMetaContextTODO(p.G()), id); err != nil { p.G().GetLog().CDebugf(context.TODO(), "Unable to load payment: %v", err) } p.cleanPayments(maxPayments) } } func (p *Loader) runRequests() { for id := range p.rqueue { p.loadRequest(id) p.cleanRequests(maxRequests) } } func (p *Loader) LoadPaymentSync(ctx context.Context, paymentID stellar1.PaymentID) { mctx := libkb.NewMetaContext(ctx, p.G()) defer mctx.Trace(fmt.Sprintf("LoadPaymentSync(%s)", paymentID), nil)() backoffPolicy := libkb.BackoffPolicy{ Millis: []int{2000, 3000, 5000}, } for i := 0; i <= 3; i++ { err := p.loadPayment(mctx, paymentID) if err == nil { break } mctx.Debug("error on attempt %d to load payment %s: %s. sleep and retry.", i, paymentID, err) time.Sleep(backoffPolicy.Duration(i)) } } func (p *Loader) loadPayment(mctx libkb.MetaContext, id stellar1.PaymentID) (err error) { mctx, cancel := mctx.BackgroundWithLogTags().WithLogTag("LP").WithTimeout(15 * time.Second) defer cancel() defer mctx.Trace(fmt.Sprintf("loadPayment(%s)", id), nil)() s := getGlobal(p.G()) details, err := s.remoter.PaymentDetailsGeneric(mctx.Ctx(), stellar1.TransactionIDFromPaymentID(id).String()) if err != nil { mctx.Debug("error getting payment details for %s: %s", id, err) return err } oc := NewOwnAccountLookupCache(mctx) summary, err := TransformPaymentSummaryGeneric(mctx, details.Summary, oc) if err != nil { mctx.Debug("error transforming details for %s: %s", id, err) return err } p.storePayment(id, summary) p.sendPaymentNotification(mctx, id, summary) return nil } func (p *Loader) loadRequest(id stellar1.KeybaseRequestID) { ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() m := libkb.NewMetaContext(ctx, p.G()) defer m.Trace(fmt.Sprintf("loadRequest(%s)", id), nil)() s := getGlobal(p.G()) details, err := s.remoter.RequestDetails(ctx, id) if err != nil { m.Debug("error getting request details for %s: %s", id, err) return } local, err := TransformRequestDetails(m, details) if err != nil { m.Debug("error transforming request details for %s: %s", id, err) return } // must be a newly loaded request or the status changed for // a notification to be sent below isUpdate := p.storeRequest(id, local) if isUpdate { p.sendRequestNotification(m, id, local) } } func (p *Loader) uiPaymentInfo(m libkb.MetaContext, summary *stellar1.PaymentLocal, msg chatMsg) *chat1.UIPaymentInfo { info := chat1.UIPaymentInfo{ AccountID: &summary.FromAccountID, AmountDescription: summary.AmountDescription, Worth: summary.Worth, WorthAtSendTime: summary.WorthAtSendTime, Delta: summary.Delta, Note: utils.EscapeForDecorate(m.Ctx(), summary.Note), IssuerDescription: summary.IssuerDescription, PaymentID: summary.Id, SourceAmount: summary.SourceAmountActual, SourceAsset: summary.SourceAsset, Status: summary.StatusSimplified, StatusDescription: summary.StatusDescription, StatusDetail: summary.StatusDetail, ShowCancel: summary.ShowCancel, FromUsername: summary.FromUsername, ToUsername: summary.ToUsername, } info.Delta = stellar1.BalanceDelta_NONE // Calculate the payment delta & relevant accountID if summary.FromType == stellar1.ParticipantType_OWNACCOUNT && summary.ToType == stellar1.ParticipantType_OWNACCOUNT { // This is a transfer between the user's own accounts. info.Delta = stellar1.BalanceDelta_NONE } else { info.Delta = stellar1.BalanceDelta_INCREASE if msg.sender != "" { // this is related to a chat message if msg.sender.Eq(p.G().ActiveDevice.Username(m)) { info.Delta = stellar1.BalanceDelta_DECREASE } else { // switch the account ID to the recipient info.AccountID = summary.ToAccountID } } } return &info } func (p *Loader) sendPaymentNotification(m libkb.MetaContext, id stellar1.PaymentID, summary *stellar1.PaymentLocal) { p.Lock() msg, ok := p.pmessages[id] p.Unlock() if !ok { // this is ok: frontend only needs the payment ID m.Debug("sending chat notification for payment %s using empty msg info", id) msg = chatMsg{} } else { m.Debug("sending chat notification for payment %s to %s, %s", id, msg.convID, msg.msgID) } uid := p.G().ActiveDevice.UID() info := p.uiPaymentInfo(m, summary, msg) if info.AccountID != nil && summary.StatusSimplified != stellar1.PaymentStatus_PENDING { // let WalletState know err := p.G().GetStellar().RemovePendingTx(m, *info.AccountID, stellar1.TransactionIDFromPaymentID(id)) if err != nil { m.Debug("ws.RemovePendingTx error: %s", err) } p.Lock() for _, ch := range p.listeners { ch <- PaymentStatusUpdate{AccountID: *info.AccountID, TxID: stellar1.TransactionIDFromPaymentID(id), Status: summary.StatusSimplified} } p.Unlock() } p.G().NotifyRouter.HandleChatPaymentInfo(m.Ctx(), uid, msg.convID, msg.msgID, *info) } func (p *Loader) uiRequestInfo(m libkb.MetaContext, details *stellar1.RequestDetailsLocal, msg chatMsg) *chat1.UIRequestInfo { info := chat1.UIRequestInfo{ Amount: details.Amount, AmountDescription: details.AmountDescription, Asset: details.Asset, Currency: details.Currency, Status: details.Status, WorthAtRequestTime: details.WorthAtRequestTime, } return &info } func (p *Loader) sendRequestNotification(m libkb.MetaContext, id stellar1.KeybaseRequestID, details *stellar1.RequestDetailsLocal) { p.Lock() msg, ok := p.rmessages[id] p.Unlock() if !ok { m.Debug("not sending request chat notification for %s (no associated convID, msgID)", id) return } m.Debug("sending chat notification for request %s to %s, %s", id, msg.convID, msg.msgID) uid := p.G().ActiveDevice.UID() info := p.uiRequestInfo(m, details, msg) p.G().NotifyRouter.HandleChatRequestInfo(m.Ctx(), uid, msg.convID, msg.msgID, *info) } func (p *Loader) enqueuePayment(paymentID stellar1.PaymentID) { select { case p.pqueue <- paymentID: default: p.G().Log.Debug("stellar.Loader payment queue full") } } func (p *Loader) enqueueRequest(requestID stellar1.KeybaseRequestID) { select { case p.rqueue <- requestID: default: p.G().Log.Debug("stellar.Loader request queue full") } } func (p *Loader) storePayment(id stellar1.PaymentID, payment *stellar1.PaymentLocal) { p.Lock() p.payments[id] = payment p.plist = append(p.plist, id) p.Unlock() } // storeRequest returns true if it updated an existing value. func (p *Loader) storeRequest(id stellar1.KeybaseRequestID, request *stellar1.RequestDetailsLocal) (isUpdate bool) { p.Lock() x, ok := p.requests[id] if !ok || x.Status != request.Status { isUpdate = true } p.requests[id] = request p.rlist = append(p.rlist, id) p.Unlock() return isUpdate } func (p *Loader) PaymentsLen() int { p.Lock() defer p.Unlock() return len(p.payments) } func (p *Loader) RequestsLen() int { p.Lock() defer p.Unlock() return len(p.requests) } func (p *Loader) cleanPayments(n int) int { p.Lock() defer p.Unlock() var deleted int toDelete := len(p.payments) - n if toDelete <= 0 { return 0 } for i := range toDelete { delete(p.payments, p.plist[i]) delete(p.pmessages, p.plist[i]) deleted++ } p.plist = p.plist[toDelete:] return deleted } func (p *Loader) cleanRequests(n int) int { p.Lock() defer p.Unlock() var deleted int toDelete := len(p.requests) - n if toDelete <= 0 { return 0 } for i := range toDelete { delete(p.requests, p.rlist[i]) delete(p.rmessages, p.rlist[i]) deleted++ } p.rlist = p.rlist[toDelete:] return deleted }