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 }