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 }