/
githubmirror
/
client
Обзор
Документация
Войти
/
githubmirror
/
client
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
go/stellar/global.go
713 строк
21 KB
chrisnojima-zoom
Version 670 - clean2 (#29122)
08 июн 2026, 19:31
Не верифицирован
08 июн 2026, 19:31
1943197
Код
Авторство
О чём код?
package stellar import ( "context" "encoding/json" "errors" "fmt" "strings" "sync" "time" "github.com/keybase/client/go/badges" "github.com/keybase/client/go/gregor" "github.com/keybase/client/go/libkb" "github.com/keybase/client/go/protocol/chat1" "github.com/keybase/client/go/protocol/keybase1" "github.com/keybase/client/go/protocol/stellar1" "github.com/keybase/client/go/slotctx" "github.com/keybase/client/go/stellar/remote" "github.com/keybase/stellarnet" "github.com/stellar/go/build" "github.com/stellar/go/clients/federation" // nolint "github.com/stellar/go/clients/horizon" ) func ServiceInit(g *libkb.GlobalContext, walletState *WalletState, badger *badges.Badger) { if g.Env.GetRunMode() != libkb.ProductionRunMode { stellarnet.SetClientAndNetwork(horizon.DefaultTestNetClient, build.TestNetwork) } s := NewStellar(g, walletState, badger) g.SetStellar(s) g.AddLogoutHook(s, "stellar") g.AddDbNukeHook(s, "stellar") g.PushShutdownHook(s.Shutdown) } type Stellar struct { libkb.Contextified remoter remote.Remoter walletState *WalletState serverConfLock sync.Mutex cachedServerConf stellar1.StellarServerDefinitions autoClaimRunnerLock sync.Mutex autoClaimRunner *AutoClaimRunner // often nil hasWalletCacheLock sync.Mutex hasWalletCache map[keybase1.UserVersion]bool federationClient federation.ClientInterface bidLock sync.Mutex bids []*buildPaymentEntry bpcLock sync.Mutex bpc BuildPaymentCache disclaimerLock sync.Mutex disclaimerAccepted *keybase1.UserVersion // A UV who has accepted the disclaimer. accountsLock sync.Mutex accounts *AccountsCache // Slot for build payments that do not use BuildPaymentID. buildPaymentSlot *slotctx.PrioritySlot reconnectSlot *slotctx.Slot badger *badges.Badger } var _ libkb.Stellar = (*Stellar)(nil) func NewStellar(g *libkb.GlobalContext, walletState *WalletState, badger *badges.Badger) *Stellar { return &Stellar{ Contextified: libkb.NewContextified(g), remoter: walletState, walletState: walletState, hasWalletCache: make(map[keybase1.UserVersion]bool), federationClient: getFederationClient(g), buildPaymentSlot: slotctx.NewPriority(), reconnectSlot: slotctx.New(), badger: badger, } } type AccountsCache struct { Stored time.Time Revision stellar1.BundleRevision Accounts []stellar1.BundleEntry } func (s *Stellar) CreateWalletSoft(ctx context.Context) { CreateWalletSoft(libkb.NewMetaContext(ctx, s.G())) } func (s *Stellar) Upkeep(ctx context.Context) error { return Upkeep(libkb.NewMetaContext(ctx, s.G())) } func (s *Stellar) OnLogout(mctx libkb.MetaContext) error { s.Clear(mctx) return nil } func (s *Stellar) OnDbNuke(mctx libkb.MetaContext) error { s.Clear(mctx) return nil } func (s *Stellar) Shutdown(mctx libkb.MetaContext) error { s.Clear(mctx) return nil } func (s *Stellar) Clear(mctx libkb.MetaContext) { s.shutdownAutoClaimRunner() s.deleteBpc() s.deleteDisclaimer() s.clearBids() s.clearAccounts() } func (s *Stellar) shutdownAutoClaimRunner() { s.autoClaimRunnerLock.Lock() defer s.autoClaimRunnerLock.Unlock() // Shutdown and delete the ACR. if acr := s.autoClaimRunner; acr != nil { acr.Shutdown(libkb.NewMetaContextBackground(s.G())) } s.autoClaimRunner = nil } func (s *Stellar) deleteBpc() { s.bpcLock.Lock() defer s.bpcLock.Unlock() s.bpc = nil } func (s *Stellar) deleteDisclaimer() { s.disclaimerLock.Lock() defer s.disclaimerLock.Unlock() s.disclaimerAccepted = nil } func (s *Stellar) clearBids() { s.buildPaymentSlot.Stop() s.bidLock.Lock() defer s.bidLock.Unlock() for _, bid := range s.bids { bid.Slot.Stop() } s.bids = nil } func (s *Stellar) clearAccounts() { s.accountsLock.Lock() defer s.accountsLock.Unlock() s.accounts = nil } func (s *Stellar) GetServerDefinitions(ctx context.Context) (ret stellar1.StellarServerDefinitions, err error) { s.serverConfLock.Lock() defer s.serverConfLock.Unlock() if s.cachedServerConf.Revision == 0 { // check if still 0, we might have waited for other thread // to finish fetching. if ret, err = remote.FetchServerConfig(ctx, s.G()); err != nil { return ret, err } s.cachedServerConf = ret } return s.cachedServerConf, nil } func (s *Stellar) KnownCurrencyCodeInstant(ctx context.Context, code string) (known, ok bool) { code = strings.ToUpper(code) if code == "XLM" { return true, true } s.serverConfLock.Lock() defer s.serverConfLock.Unlock() if s.cachedServerConf.Revision == 0 { return false, false } _, known = s.cachedServerConf.Currencies[stellar1.OutsideCurrencyCode(code)] return known, true } // `trigger` is optional, and is of the gregor message that caused the kick. func (s *Stellar) KickAutoClaimRunner(mctx libkb.MetaContext, trigger gregor.MsgID) { // Create the ACR if one does not exist. mctx.Debug("KickAutoClaimRunner(trigger:%v)", trigger) s.autoClaimRunnerLock.Lock() defer s.autoClaimRunnerLock.Unlock() if s.autoClaimRunner == nil { s.autoClaimRunner = NewAutoClaimRunner(s.walletState) } s.autoClaimRunner.Kick(mctx, trigger) } func (s *Stellar) InformHasWallet(ctx context.Context, uv keybase1.UserVersion) { if uv.Uid.IsNil() { s.G().Log.CErrorf(ctx, "Stellar.InformHasWallet called with nil UID") return } if uv.EldestSeqno <= 0 { // It is not possible for such a user to have a wallet. s.G().Log.CErrorf(ctx, "Stellar.InformHasWallet called with %v EldestSeqno", uv.EldestSeqno) return } s.hasWalletCacheLock.Lock() defer s.hasWalletCacheLock.Unlock() s.G().Log.CDebugf(ctx, "Stellar.InformHasWallet(%v)", uv) s.hasWalletCache[uv] = true } func (s *Stellar) CachedHasWallet(ctx context.Context, uv keybase1.UserVersion) bool { s.hasWalletCacheLock.Lock() defer s.hasWalletCacheLock.Unlock() has := s.hasWalletCache[uv] s.G().Log.CDebugf(ctx, "Stellar.CachedHasWallet(%v) -> %v", uv, has) return has } func (s *Stellar) SetFederationClientForTest(cli federation.ClientInterface) { s.federationClient = cli } func (s *Stellar) getBuildPaymentCache() BuildPaymentCache { s.bpcLock.Lock() defer s.bpcLock.Unlock() if s.bpc == nil { s.bpc = newBuildPaymentCache(s.remoter) } return s.bpc } // UpdateUnreadCount will take the unread count for an account id and // update the badger. func (s *Stellar) UpdateUnreadCount(ctx context.Context, accountID stellar1.AccountID, unread int) error { if s.badger == nil { s.G().Log.CDebugf(ctx, "Stellar Global has no badger") return nil } s.badger.SetWalletAccountUnreadCount(ctx, accountID, unread) return nil } // SendMiniChatPayments sends multiple payments from one sender to multiple // different recipients as fast as it can. These come from chat messages // like "+1XLM@alice +2XLM@charlie". func (s *Stellar) SendMiniChatPayments(mctx libkb.MetaContext, convID chat1.ConversationID, payments []libkb.MiniChatPayment) ([]libkb.MiniChatPaymentResult, error) { return SendMiniChatPayments(mctx, s.walletState, convID, payments) } // SpecMiniChatPayments creates a summary of the amounts that a list of MiniChatPayments will // result in. func (s *Stellar) SpecMiniChatPayments(mctx libkb.MetaContext, payments []libkb.MiniChatPayment) (*libkb.MiniChatPaymentSummary, error) { return SpecMiniChatPayments(mctx, s.walletState, payments) } // HandleOobm will handle any out of band gregor messages for stellar. func (s *Stellar) HandleOobm(ctx context.Context, obm gregor.OutOfBandMessage) (bool, error) { if obm.System() == nil { return false, errors.New("nil system in out of band message") } // make a new background context for the handlers mctx := libkb.NewMetaContextBackground(s.G()).WithLogTag("WAOOBM") // all of these handlers should be in goroutines so they don't block the // oobm handler thread. switch obm.System().String() { case "internal.reconnect": go s.handleReconnect(mctx) // returning false, nil here so that others can handle this one too return false, nil case stellar1.PushPaymentStatus: go s.handlePaymentStatus(mctx, obm) return true, nil case stellar1.PushPaymentNotification: go s.handlePaymentNotification(mctx, obm) return true, nil case stellar1.PushRequestStatus: go s.handleRequestStatus(mctx, obm) return true, nil } return false, nil } func (s *Stellar) handleReconnect(mctx libkb.MetaContext) { defer mctx.Trace("Stellar.handleReconnect", nil)() mctx.Debug("stellar received reconnect msg, doing delayed wallet refresh") mctx = mctx.WithCtx(s.reconnectSlot.Use(mctx.Ctx())) mctx, cancel := cancelOnMobileBackground(mctx) defer cancel() if err := libkb.Sleep(mctx.Ctx(), libkb.RandomJitter(4*time.Second)); err != nil { mctx.Debug("Stellar.handleReconnect canceled") return } if libkb.IsMobilePlatform() { // sleep some more on mobile if err := libkb.Sleep(mctx.Ctx(), libkb.RandomJitter(4*time.Second)); err != nil { mctx.Debug("Stellar.handleReconnect canceled") return } } mctx.Debug("Stellar.handleReconnect delay complete, refreshing wallet state") if err := s.walletState.RefreshAll(mctx, "reconnect"); err != nil { mctx.Debug("Stellar.handleReconnect RefreshAll error: %s", err) } } func (s *Stellar) handlePaymentStatus(mctx libkb.MetaContext, obm gregor.OutOfBandMessage) { var err error defer mctx.Trace("Stellar.handlePaymentStatus", &err)() var msg stellar1.PaymentStatusMsg if err = json.Unmarshal(obm.Body().Bytes(), &msg); err != nil { mctx.Debug("error unmarshaling obm PaymentStatusMsg: %s", err) return } paymentID := stellar1.NewPaymentID(msg.TxID) notifiedAccountID, err := s.refreshPaymentFromNotification(mctx, msg.AccountID, paymentID) if err != nil { mctx.Debug("refreshPaymentFromNotification error: %s", err) return } s.G().NotifyRouter.HandleWalletPaymentStatusNotification(mctx.Ctx(), notifiedAccountID, paymentID) } func (s *Stellar) handlePaymentNotification(mctx libkb.MetaContext, obm gregor.OutOfBandMessage) { var err error defer mctx.Trace("Stellar.handlePaymentNotification", &err)() var msg stellar1.PaymentNotificationMsg if err = json.Unmarshal(obm.Body().Bytes(), &msg); err != nil { mctx.Debug("error unmarshaling obm PaymentNotificationMsg: %s", err) return } notifiedAccountID, err := s.refreshPaymentFromNotification(mctx, msg.AccountID, msg.PaymentID) if err != nil { mctx.Debug("refreshPaymentFromNotification error: %s", err) return } s.G().NotifyRouter.HandleWalletPaymentNotification(mctx.Ctx(), notifiedAccountID, msg.PaymentID) } func (s *Stellar) findAccountFromPayment(mctx libkb.MetaContext, accountID stellar1.AccountID, payment *stellar1.PaymentLocal) (stellar1.AccountID, error) { var emptyAccountID stellar1.AccountID // double-check that the accountID from the notification matches one of the accountIDs // in the payment if accountID == payment.FromAccountID || (payment.ToAccountID != nil && accountID == *payment.ToAccountID) { ok, _, err := getGlobal(mctx.G()).OwnAccountCached(mctx, accountID) if err != nil { return emptyAccountID, err } if ok { // the user owns the accountID in the notification return accountID, nil } } // check if the user owns either from or to accountID: ok, _, err := getGlobal(mctx.G()).OwnAccountCached(mctx, payment.FromAccountID) if err != nil { return emptyAccountID, err } if ok { // the running user is the sender of this payment return payment.FromAccountID, nil } if payment.ToAccountID == nil { return emptyAccountID, ErrAccountNotFound } ok, _, err = getGlobal(mctx.G()).OwnAccountCached(mctx, *payment.ToAccountID) if err != nil { return emptyAccountID, err } if ok { // the running user is the recipient of this payment return *payment.ToAccountID, nil } return emptyAccountID, ErrAccountNotFound } func (s *Stellar) refreshPaymentFromNotification(mctx libkb.MetaContext, accountID stellar1.AccountID, paymentID stellar1.PaymentID) (notifiedAccountID stellar1.AccountID, err error) { var emptyAccountID stellar1.AccountID // load the payment loader := DefaultLoader(s.G()) loader.LoadPaymentSync(mctx.Ctx(), paymentID) payment, ok := loader.GetPaymentLocal(mctx.Ctx(), paymentID) if !ok { return emptyAccountID, fmt.Errorf("couldn't find the payment immediately after loading it %v", paymentID) } // find the accountID for the running user in the payment (could be sender, recipient, neither) notifiedAccountID, err = s.findAccountFromPayment(mctx, accountID, payment) if err != nil { return emptyAccountID, err } // refresh the wallet state for this accountID if err := s.walletState.Refresh(mctx, notifiedAccountID, "notification received"); err != nil { return notifiedAccountID, err } return notifiedAccountID, nil } func (s *Stellar) handleRequestStatus(mctx libkb.MetaContext, obm gregor.OutOfBandMessage) { var err error defer mctx.Trace("Stellar.handleRequestStatus", &err)() var msg stellar1.RequestStatusMsg if err = json.Unmarshal(obm.Body().Bytes(), &msg); err != nil { mctx.Debug("error unmarshaling obm RequestStatusMsg: %s", err) return } mctx.G().NotifyRouter.HandleWalletRequestStatusNotification(mctx.Ctx(), msg.ReqID) DefaultLoader(mctx.G()).UpdateRequest(mctx.Ctx(), msg.ReqID) } type hasAcceptedDisclaimerDBEntry struct { Version int // 1 Accepted bool } // For a UV, accepted starts out false and transitions to true. It never becomes false again. // A cached true is returned, but a false always hits the server. func (s *Stellar) hasAcceptedDisclaimer(ctx context.Context) (bool, error) { log := func(format string, args ...any) { s.G().Log.CDebugf(ctx, "Stellar.hasAcceptedDisclaimer "+format, args...) } uv, err := s.G().GetMeUV(ctx) if err != nil { return false, err } s.disclaimerLock.Lock() defer s.disclaimerLock.Unlock() // Check memory memAccepted := s.disclaimerAccepted != nil && s.disclaimerAccepted.Eq(uv) log("mem -> %v", memAccepted) if memAccepted { return true, nil } // Check disk dbKey := libkb.DbKey{ Typ: libkb.DBStellarDisclaimer, Key: uv.String(), } var dbEntry hasAcceptedDisclaimerDBEntry found, err := s.G().LocalDb.GetInto(&dbEntry, dbKey) log("disk -> [found:%v err:(%v) v:%v accepted:%v]", found, err, dbEntry.Version, dbEntry.Accepted) if err == nil && found && dbEntry.Version == 1 && dbEntry.Accepted { err = s.informAcceptedDisclaimerLocked(ctx) if err != nil { log("store -> err:(%v)", err) } return true, nil } // Check remote accepted, err := remote.GetAcceptedDisclaimer(ctx, s.G()) log("remote -> [err:(%v) accepted:%v]", err, accepted) if err != nil { return false, err } if accepted { err = s.informAcceptedDisclaimerLocked(ctx) if err != nil { log("store -> err:(%v)", err) } } return accepted, nil } func (s *Stellar) informAcceptedDisclaimer(ctx context.Context) { s.disclaimerLock.Lock() defer s.disclaimerLock.Unlock() _ = s.informAcceptedDisclaimerLocked(ctx) } func (s *Stellar) informAcceptedDisclaimerLocked(ctx context.Context) (err error) { defer s.G().CTrace(ctx, "Stellar.informAcceptedDisclaimer", &err)() uv, err := s.G().GetMeUV(ctx) if err != nil { return err } // Store memory s.disclaimerAccepted = &uv // Store disk return s.G().LocalDb.PutObj(libkb.DbKey{ Typ: libkb.DBStellarDisclaimer, Key: uv.String(), }, nil, hasAcceptedDisclaimerDBEntry{ Version: 1, Accepted: true, }) } func (s *Stellar) startBuildPayment(mctx libkb.MetaContext) (bid stellar1.BuildPaymentID, err error) { defer func() { x := bid.String() if err != nil { x = fmt.Sprintf("ERR(%v)", err.Error()) } mctx.Debug("Stellar.startBuildPayment -> %v", x) }() bid, err = RandomBuildPaymentID() if err != nil { return "", err } s.bidLock.Lock() defer s.bidLock.Unlock() s.bids = append(s.bids, newBuildPaymentEntry(bid)) const maxConcurrentBuilds = 20 if len(s.bids) > maxConcurrentBuilds { // Too many open payment builds. Drop the oldest ones at the beginning of the list. // Leave the newest ones at the end of the list. for i := 0; i < len(s.bids)-maxConcurrentBuilds; i++ { entry := s.bids[i] entry.Slot.Shutdown() } s.bids = s.bids[len(s.bids)-maxConcurrentBuilds:] } return bid, nil } // stopBuildPayment stops a bid forever. func (s *Stellar) stopBuildPayment(mctx libkb.MetaContext, bid stellar1.BuildPaymentID) { mctx.Debug("Stellar.stopBuildPayment(%v)", bid) if bid.IsNil() { s.buildPaymentSlot.Stop() return } s.bidLock.Lock() defer s.bidLock.Unlock() for _, entry := range s.bids { if entry.Bid.Eq(bid) { if entry.Stopped { mctx.Debug("payment already stopped") return } entry.Slot.Shutdown() entry.Stopped = true mctx.Debug("payment shutdown") return } } mctx.Debug("payment not found to stop") } // acquireBuildPayment takes ownership of a payment build. // Returns a new `mctx` that the caller should switch to. Because it runs within the slot. // When err=nil the caller owns `data` and must release it with `release` when finished. // When err!=nil data is nil. // `release` can be called even when err!=nil. // `mctx` can also be used if err!=nil. // Callers should `release` soon after their context is canceled. func (s *Stellar) acquireBuildPayment(mctx1 libkb.MetaContext, bid stellar1.BuildPaymentID, sessionID int) ( mctx libkb.MetaContext, data *buildPaymentData, release func(), err error, ) { mctx = mctx1 mctx.Debug("Stellar.acquireBuildPayment(%v)", bid) release = func() {} s.bidLock.Lock() defer s.bidLock.Unlock() for _, entry := range s.bids { if !entry.Bid.Eq(bid) { continue } if entry.Stopped { return mctx, nil, release, fmt.Errorf("This payment might have already been sent. Check your recent payments before trying again.") } mctx = mctx.WithCtx(entry.Slot.Use(mctx.Ctx(), sessionID)) if err = mctx.Ctx().Err(); err != nil { return mctx, nil, release, err } err = libkb.AcquireWithContextAndTimeout(mctx.Ctx(), &entry.DataLock, 5*time.Second) if err != nil { mctx.Debug("error while attempting to acquire data lock: %v", err) return mctx, nil, release, err } release = libkb.Once(func() { entry.DataLock.Unlock() }) return mctx, &entry.Data, release, nil } return mctx, nil, release, fmt.Errorf("payment build not found") } // finalizeBuildPayment stops a bid forever and returns its data. func (s *Stellar) finalizeBuildPayment(mctx libkb.MetaContext, bid stellar1.BuildPaymentID) (res *buildPaymentData, err error) { mctx.Debug("Stellar.finalizeBuildPayment(%v)", bid) s.bidLock.Lock() defer s.bidLock.Unlock() for _, entry := range s.bids { if !entry.Bid.Eq(bid) { continue } if entry.Stopped { return nil, fmt.Errorf("This payment might have already been sent. Check your recent payments before trying again.") } entry.Slot.Shutdown() entry.Stopped = true err = libkb.AcquireWithContextAndTimeout(mctx.Ctx(), &entry.DataLock, 5*time.Second) if err != nil { // This likely means something in the Slot is not yielding to its context or forgot to release the lock. mctx.Debug("error while attempting to acquire data lock: %v", err) return nil, err } res = &entry.Data entry.DataLock.Unlock() return res, nil } return nil, fmt.Errorf("payment build not found") } func (s *Stellar) WalletStateForTest() *WalletState { return s.walletState } func (s *Stellar) RemovePendingTx(mctx libkb.MetaContext, accountID stellar1.AccountID, txID stellar1.TransactionID) error { return s.walletState.RemovePendingTx(mctx.Ctx(), accountID, txID) } // BaseFee returns the server-suggested base fee per operation. func (s *Stellar) BaseFee(mctx libkb.MetaContext) uint64 { return s.walletState.BaseFee(mctx) } func (s *Stellar) InformBundle(mctx libkb.MetaContext, rev stellar1.BundleRevision, accounts []stellar1.BundleEntry) { go func() { err := libkb.AcquireWithContextAndTimeout(mctx.Ctx(), &s.accountsLock, 5*time.Second) if err != nil { mctx.Debug("InformBundle: error acquiring lock") return } defer s.accountsLock.Unlock() if s.accounts != nil && rev < s.accounts.Revision { return } s.accounts = &AccountsCache{ Stored: mctx.G().Clock().Now(), Revision: rev, Accounts: accounts, } }() } func (s *Stellar) InformDefaultCurrencyChange(mctx libkb.MetaContext) { go func() { s.getBuildPaymentCache().InformDefaultCurrencyChange(mctx) }() } func (s *Stellar) OwnAccountCached(mctx libkb.MetaContext, accountID stellar1.AccountID) (own, isPrimary bool, err error) { own, isPrimary, _, err = s.OwnAccountPlusNameCached(mctx, accountID) return own, isPrimary, err } func (s *Stellar) OwnAccountPlusNameCached(mctx libkb.MetaContext, accountID stellar1.AccountID) (own, isPrimary bool, accountName string, err error) { err = libkb.AcquireWithContextAndTimeout(mctx.Ctx(), &s.accountsLock, 5*time.Second) if err != nil { mctx.Debug("OwnAccountPlusNameCached: error acquiring lock") return } if s.accounts != nil && mctx.G().Clock().Now().Sub(s.accounts.Stored.Round(0)) < 2*time.Minute { for _, acc := range s.accounts.Accounts { if acc.AccountID.Eq(accountID) { s.accountsLock.Unlock() return true, acc.IsPrimary, acc.Name, nil } } s.accountsLock.Unlock() return false, false, "", nil } s.accountsLock.Unlock() return OwnAccountPlusName(mctx, accountID) } func (s *Stellar) Refresh(mctx libkb.MetaContext, reason string) { if err := s.walletState.RefreshAll(mctx, reason); err != nil { mctx.Debug("Stellar.Refresh(%s) ws.RefreshAll error: %s", reason, err) } } // getFederationClient is a helper function used during // initialization. func getFederationClient(g *libkb.GlobalContext) federation.ClientInterface { if g.Env.GetRunMode() != libkb.ProductionRunMode { return federation.DefaultTestNetClient } return federation.DefaultPublicNetClient } // getGlobal gets the libkb.Stellar off of G and asserts it into a stellar.Stellar func getGlobal(g *libkb.GlobalContext) *Stellar { return g.GetStellar().(*Stellar) }