github.com/mysteriumnetwork/node@v0.0.0-20240516044423-365054f76801/session/pingpong/consumer_balance_tracker.go (about)

     1  /*
     2   * Copyright (C) 2021 The "MysteriumNetwork/node" Authors.
     3   *
     4   * This program is free software: you can redistribute it and/or modify
     5   * it under the terms of the GNU General Public License as published by
     6   * the Free Software Foundation, either version 3 of the License, or
     7   * (at your option) any later version.
     8   *
     9   * This program is distributed in the hope that it will be useful,
    10   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    11   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    12   * GNU General Public License for more details.
    13   *
    14   * You should have received a copy of the GNU General Public License
    15   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    16   */
    17  
    18  package pingpong
    19  
    20  import (
    21  	"context"
    22  	"encoding/json"
    23  	"errors"
    24  	"fmt"
    25  	"math/big"
    26  	"sync"
    27  	"time"
    28  
    29  	"github.com/cenkalti/backoff/v4"
    30  	"github.com/ethereum/go-ethereum/common"
    31  	"github.com/rs/zerolog/log"
    32  
    33  	"github.com/mysteriumnetwork/node/config"
    34  	nodevent "github.com/mysteriumnetwork/node/core/node/event"
    35  	"github.com/mysteriumnetwork/node/eventbus"
    36  	"github.com/mysteriumnetwork/node/identity"
    37  	"github.com/mysteriumnetwork/node/identity/registry"
    38  	pevent "github.com/mysteriumnetwork/node/pilvytis"
    39  	"github.com/mysteriumnetwork/node/session/pingpong/event"
    40  	"github.com/mysteriumnetwork/payments/client"
    41  	"github.com/mysteriumnetwork/payments/units"
    42  )
    43  
    44  type balanceKey string
    45  
    46  func newBalanceKey(chainID int64, id identity.Identity) balanceKey {
    47  	return balanceKey(fmt.Sprintf("%v_%v", id.Address, chainID))
    48  }
    49  
    50  type balances struct {
    51  	sync.Mutex
    52  	valuesMap map[balanceKey]ConsumerBalance
    53  }
    54  
    55  type transactorBounties struct {
    56  	sync.Mutex
    57  	valuesMap map[balanceKey]*big.Int
    58  }
    59  
    60  // ConsumerBalanceTracker keeps track of consumer balances.
    61  // TODO: this needs to take into account the saved state.
    62  type ConsumerBalanceTracker struct {
    63  	balances           balances
    64  	transactorBounties transactorBounties
    65  
    66  	addressProvider                      addressProvider
    67  	registry                             registrationStatusProvider
    68  	consumerBalanceChecker               consumerBalanceChecker
    69  	bus                                  eventbus.EventBus
    70  	consumerGrandTotalsStorage           consumerTotalsStorage
    71  	consumerInfoGetter                   consumerInfoGetter
    72  	transactorRegistrationStatusProvider transactorRegistrationStatusProvider
    73  	blockchainInfoProvider               blockchainInfoProvider
    74  	stop                                 chan struct{}
    75  	once                                 sync.Once
    76  
    77  	fullBalanceUpdateThrottle map[string]struct{}
    78  	fullBalanceUpdateLock     sync.Mutex
    79  	balanceSyncer             *balanceSyncer
    80  
    81  	cfg ConsumerBalanceTrackerConfig
    82  }
    83  
    84  type transactorRegistrationStatusProvider interface {
    85  	FetchRegistrationFees(chainID int64) (registry.FeesResponse, error)
    86  	FetchRegistrationStatus(id string) ([]registry.TransactorStatusResponse, error)
    87  }
    88  
    89  type blockchainInfoProvider interface {
    90  	GetConsumerChannelsHermes(chainID int64, channelAddress common.Address) (client.ConsumersHermes, error)
    91  }
    92  
    93  // PollConfig sets the interval and timeout for polling.
    94  type PollConfig struct {
    95  	Interval time.Duration
    96  	Timeout  time.Duration
    97  }
    98  
    99  // ConsumerBalanceTrackerConfig represents the consumer balance tracker configuration.
   100  type ConsumerBalanceTrackerConfig struct {
   101  	FastSync PollConfig
   102  	LongSync PollConfig
   103  }
   104  
   105  // NewConsumerBalanceTracker creates a new instance
   106  func NewConsumerBalanceTracker(
   107  	publisher eventbus.EventBus,
   108  	consumerBalanceChecker consumerBalanceChecker,
   109  	consumerGrandTotalsStorage consumerTotalsStorage,
   110  	consumerInfoGetter consumerInfoGetter,
   111  	transactorRegistrationStatusProvider transactorRegistrationStatusProvider,
   112  	registry registrationStatusProvider,
   113  	addressProvider addressProvider,
   114  	blockchainInfoProvider blockchainInfoProvider,
   115  	cfg ConsumerBalanceTrackerConfig,
   116  ) *ConsumerBalanceTracker {
   117  	return &ConsumerBalanceTracker{
   118  		balances:                             balances{valuesMap: make(map[balanceKey]ConsumerBalance)},
   119  		transactorBounties:                   transactorBounties{valuesMap: make(map[balanceKey]*big.Int)},
   120  		consumerBalanceChecker:               consumerBalanceChecker,
   121  		bus:                                  publisher,
   122  		consumerGrandTotalsStorage:           consumerGrandTotalsStorage,
   123  		consumerInfoGetter:                   consumerInfoGetter,
   124  		transactorRegistrationStatusProvider: transactorRegistrationStatusProvider,
   125  		blockchainInfoProvider:               blockchainInfoProvider,
   126  		registry:                             registry,
   127  		addressProvider:                      addressProvider,
   128  		stop:                                 make(chan struct{}),
   129  		cfg:                                  cfg,
   130  		fullBalanceUpdateThrottle:            make(map[string]struct{}),
   131  		balanceSyncer:                        newBalanceSyncer(),
   132  	}
   133  }
   134  
   135  type consumerInfoGetter interface {
   136  	GetConsumerData(chainID int64, id string, cacheDuration time.Duration) (HermesUserInfo, error)
   137  }
   138  
   139  type consumerBalanceChecker interface {
   140  	GetConsumerChannel(chainID int64, addr common.Address, mystSCAddress common.Address) (client.ConsumerChannel, error)
   141  	GetMystBalance(chainID int64, mystAddress, identity common.Address) (*big.Int, error)
   142  }
   143  
   144  var errBalanceNotOffchain = errors.New("balance is not offchain, can't use hermes to check")
   145  
   146  // Subscribe subscribes the consumer balance tracker to relevant events
   147  func (cbt *ConsumerBalanceTracker) Subscribe(bus eventbus.Subscriber) error {
   148  	err := bus.SubscribeAsync(registry.AppTopicIdentityRegistration, cbt.handleRegistrationEvent)
   149  	if err != nil {
   150  		return err
   151  	}
   152  	err = bus.SubscribeAsync(string(nodevent.StatusStopped), cbt.handleStopEvent)
   153  	if err != nil {
   154  		return err
   155  	}
   156  	err = bus.SubscribeAsync(event.AppTopicGrandTotalChanged, cbt.handleGrandTotalChanged)
   157  	if err != nil {
   158  		return err
   159  	}
   160  	err = bus.SubscribeAsync(pevent.AppTopicOrderUpdated, cbt.handleOrderUpdatedEvent)
   161  	if err != nil {
   162  		return err
   163  	}
   164  	err = bus.SubscribeAsync(event.AppTopicSettlementComplete, cbt.handleSettlementComplete)
   165  	if err != nil {
   166  		return err
   167  	}
   168  	err = bus.SubscribeAsync(event.AppTopicWithdrawalRequested, cbt.handleWithdrawalRequested)
   169  	if err != nil {
   170  		return err
   171  	}
   172  	return bus.SubscribeAsync(identity.AppTopicIdentityUnlock, cbt.handleUnlockEvent)
   173  }
   174  
   175  // settlements increase balance on the chain they are settled on.
   176  func (cbt *ConsumerBalanceTracker) handleSettlementComplete(ev event.AppEventSettlementComplete) {
   177  	go cbt.aggressiveSync(ev.ChainID, ev.ProviderID, cbt.cfg.FastSync.Timeout, cbt.cfg.FastSync.Interval)
   178  }
   179  
   180  // withdrawals decrease balance on the from chain.
   181  func (cbt *ConsumerBalanceTracker) handleWithdrawalRequested(ev event.AppEventWithdrawalRequested) {
   182  	go cbt.aggressiveSync(ev.FromChain, ev.ProviderID, cbt.cfg.FastSync.Timeout, cbt.cfg.FastSync.Interval)
   183  }
   184  
   185  func (cbt *ConsumerBalanceTracker) handleOrderUpdatedEvent(ev pevent.AppEventOrderUpdated) {
   186  	if !ev.Status.Paid() {
   187  		return
   188  	}
   189  
   190  	go cbt.aggressiveSync(config.GetInt64(config.FlagChainID), identity.FromAddress(ev.IdentityAddress), cbt.cfg.FastSync.Timeout, cbt.cfg.FastSync.Interval)
   191  }
   192  
   193  // Performs a more aggresive type of sync on BC for the given identity on the given chain.
   194  // Should be used after events that cause a state change on blockchain.
   195  func (cbt *ConsumerBalanceTracker) aggressiveSync(chainID int64, id identity.Identity, timeout, frequency time.Duration) {
   196  	b, ok := cbt.getBalance(chainID, id)
   197  	if ok && b.IsOffchain {
   198  		log.Info().Bool("is_offchain", b.IsOffchain).Msg("skipping aggresive sync")
   199  		return
   200  	}
   201  
   202  	cbt.startJob(chainID, id, timeout, frequency)
   203  }
   204  
   205  func (cbt *ConsumerBalanceTracker) formJobSyncKey(chainID int64, id identity.Identity, timeout, frequency time.Duration) string {
   206  	return fmt.Sprintf("%v%v%v%v", chainID, id.ToCommonAddress().Hex(), timeout, frequency)
   207  }
   208  
   209  // NeedsForceSync returns true if balance needs to be force synced.
   210  func (cbt *ConsumerBalanceTracker) NeedsForceSync(chainID int64, id identity.Identity) bool {
   211  	v, ok := cbt.getBalance(chainID, id)
   212  	if !ok {
   213  		return true
   214  	}
   215  
   216  	// Offchain balances expire after configured amount of time and need to be resynced.
   217  	if v.OffchainNeedsSync() {
   218  		return true
   219  	}
   220  
   221  	// Balance doesn't always go to 0 but connections can still fail.
   222  	if v.BCBalance.Cmp(units.SingleGweiInWei()) < 0 {
   223  		return true
   224  	}
   225  
   226  	return false
   227  }
   228  
   229  // GetBalance gets the current balance for given identity
   230  func (cbt *ConsumerBalanceTracker) GetBalance(chainID int64, id identity.Identity) *big.Int {
   231  	if v, ok := cbt.getBalance(chainID, id); ok {
   232  		return v.GetBalance()
   233  	}
   234  	return new(big.Int)
   235  }
   236  
   237  func (cbt *ConsumerBalanceTracker) publishChangeEvent(id identity.Identity, before, after *big.Int) {
   238  	if before == nil || after == nil || before.Cmp(after) == 0 {
   239  		return
   240  	}
   241  
   242  	cbt.bus.Publish(event.AppTopicBalanceChanged, event.AppEventBalanceChanged{
   243  		Identity: id,
   244  		Previous: before,
   245  		Current:  after,
   246  	})
   247  }
   248  
   249  func (cbt *ConsumerBalanceTracker) handleUnlockEvent(data identity.AppEventIdentityUnlock) {
   250  	err := cbt.recoverGrandTotalPromised(data.ChainID, data.ID)
   251  	if err != nil {
   252  		log.Error().Err(err).Msg("Could not recover Grand Total Promised")
   253  	}
   254  
   255  	status, err := cbt.registry.GetRegistrationStatus(data.ChainID, data.ID)
   256  	if err != nil {
   257  		log.Error().Err(err).Msg("Could not recover get registration status")
   258  	}
   259  
   260  	switch status {
   261  	case registry.InProgress:
   262  		cbt.alignWithTransactor(data.ChainID, data.ID)
   263  	default:
   264  		cbt.ForceBalanceUpdate(data.ChainID, data.ID)
   265  	}
   266  
   267  	go cbt.lifetimeBCSync(data.ChainID, data.ID)
   268  }
   269  
   270  func (cbt *ConsumerBalanceTracker) handleGrandTotalChanged(ev event.AppEventGrandTotalChanged) {
   271  	if _, ok := cbt.getBalance(ev.ChainID, ev.ConsumerID); !ok {
   272  		cbt.ForceBalanceUpdate(ev.ChainID, ev.ConsumerID)
   273  		return
   274  	}
   275  
   276  	cbt.updateGrandTotal(ev.ChainID, ev.ConsumerID, ev.Current)
   277  }
   278  
   279  func (cbt *ConsumerBalanceTracker) getUnregisteredChannelBalance(chainID int64, id identity.Identity) (*big.Int, error) {
   280  	addr, err := cbt.addressProvider.GetActiveChannelAddress(chainID, id.ToCommonAddress())
   281  	if err != nil {
   282  		return new(big.Int), err
   283  	}
   284  
   285  	myst, err := cbt.addressProvider.GetMystAddress(chainID)
   286  	if err != nil {
   287  		return new(big.Int), err
   288  	}
   289  
   290  	balance, err := cbt.consumerBalanceChecker.GetMystBalance(chainID, myst, addr)
   291  	if err != nil {
   292  		return new(big.Int), err
   293  	}
   294  	return balance, nil
   295  }
   296  
   297  func (cbt *ConsumerBalanceTracker) lifetimeBCSync(chainID int64, id identity.Identity) {
   298  	b, ok := cbt.getBalance(chainID, id)
   299  	if ok && b.IsOffchain {
   300  		log.Info().Bool("is_offchain", b.IsOffchain).Msg("skipping external channel top-up tracking")
   301  		return
   302  	}
   303  
   304  	// 100 years should be close enough to never
   305  	timeout := time.Hour * 24 * 365 * 100
   306  	cbt.startJob(chainID, id, timeout, cbt.cfg.LongSync.Interval)
   307  }
   308  
   309  func (cbt *ConsumerBalanceTracker) startJob(chainID int64, id identity.Identity, timeout, frequency time.Duration) {
   310  	job, exists := cbt.balanceSyncer.PeriodiclySyncBalance(
   311  		cbt.formJobSyncKey(chainID, id, timeout, frequency),
   312  		func(stop <-chan struct{}) {
   313  			cbt.periodicSync(stop, chainID, id, frequency)
   314  		},
   315  		timeout,
   316  	)
   317  
   318  	if exists {
   319  		return
   320  	}
   321  
   322  	go func() {
   323  		select {
   324  		case <-cbt.stop:
   325  			job.Stop()
   326  			return
   327  		case <-job.Done():
   328  			return
   329  		}
   330  	}()
   331  }
   332  
   333  func (cbt *ConsumerBalanceTracker) periodicSync(stop <-chan struct{}, chainID int64, id identity.Identity, syncPeriod time.Duration) {
   334  	for {
   335  		select {
   336  		case <-stop:
   337  			return
   338  		case <-time.After(syncPeriod):
   339  			_ = cbt.ForceBalanceUpdate(chainID, id)
   340  		}
   341  	}
   342  }
   343  
   344  func (cbt *ConsumerBalanceTracker) alignWithHermes(chainID int64, id identity.Identity) (*big.Int, *big.Int, error) {
   345  	var boff backoff.BackOff
   346  	eback := backoff.NewExponentialBackOff()
   347  	eback.MaxElapsedTime = time.Second * 15
   348  	eback.InitialInterval = time.Second * 1
   349  
   350  	boff = backoff.WithMaxRetries(eback, 5)
   351  	ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
   352  	defer cancel()
   353  
   354  	go func() {
   355  		select {
   356  		case <-cbt.stop:
   357  			cancel()
   358  		case <-ctx.Done():
   359  		}
   360  	}()
   361  
   362  	boff = backoff.WithContext(boff, ctx)
   363  	balance := cbt.GetBalance(chainID, id)
   364  	promised := new(big.Int)
   365  	alignBalance := func() error {
   366  		consumer, err := cbt.consumerInfoGetter.GetConsumerData(chainID, id.Address, 5*time.Second)
   367  		if err != nil {
   368  			var syntax *json.SyntaxError
   369  			if errors.As(err, &syntax) {
   370  				cancel()
   371  				log.Err(err).Msg("hermes response is malformed JSON can't check if offchain")
   372  				return err
   373  			}
   374  
   375  			if errors.Is(err, ErrHermesNotFound) {
   376  				// Hermes doesn't know about this identity meaning it's not offchain. Cancel.
   377  				cancel()
   378  				return errBalanceNotOffchain
   379  			}
   380  
   381  			return err
   382  		}
   383  		if !consumer.IsOffchain {
   384  			// Hermes knows about this identity, but it's not offchain. Cancel.
   385  			cancel()
   386  			return errBalanceNotOffchain
   387  		}
   388  
   389  		if consumer.LatestPromise.Amount != nil {
   390  			promised = consumer.LatestPromise.Amount
   391  		}
   392  
   393  		if isSettledBiggerThanPromised(consumer.Settled, promised) {
   394  			promised, err = cbt.getPromisedWhenSettledIsBigger(consumer, promised, chainID, id.ToCommonAddress())
   395  			if err != nil {
   396  				return err
   397  			}
   398  		}
   399  
   400  		previous, _ := cbt.getBalance(chainID, id)
   401  		cbt.setBalance(chainID, id, ConsumerBalance{
   402  			BCBalance:          consumer.Balance,
   403  			BCSettled:          consumer.Settled,
   404  			GrandTotalPromised: promised,
   405  			IsOffchain:         true,
   406  			LastOffchainSync:   time.Now().UTC(),
   407  		})
   408  
   409  		currentBalance, _ := cbt.getBalance(chainID, id)
   410  		go cbt.publishChangeEvent(id, previous.GetBalance(), currentBalance.GetBalance())
   411  		balance = consumer.Balance
   412  		return nil
   413  	}
   414  
   415  	return balance, promised, backoff.Retry(alignBalance, boff)
   416  }
   417  
   418  // ForceBalanceUpdateCached forces a balance update for the given identity only if the last call to this func was done no sooner than a minute ago.
   419  func (cbt *ConsumerBalanceTracker) ForceBalanceUpdateCached(chainID int64, id identity.Identity) *big.Int {
   420  	cbt.fullBalanceUpdateLock.Lock()
   421  	defer cbt.fullBalanceUpdateLock.Unlock()
   422  
   423  	key := getKeyForForceBalanceCache(chainID, id)
   424  	_, ok := cbt.fullBalanceUpdateThrottle[key]
   425  	if ok {
   426  		return cbt.GetBalance(chainID, id)
   427  	}
   428  
   429  	currentBalance := cbt.ForceBalanceUpdate(chainID, id)
   430  	cbt.fullBalanceUpdateThrottle[key] = struct{}{}
   431  
   432  	go func() {
   433  		select {
   434  		case <-time.After(time.Minute):
   435  			cbt.deleteCachedForceBalance(key)
   436  		case <-cbt.stop:
   437  			return
   438  		}
   439  	}()
   440  
   441  	return currentBalance
   442  }
   443  
   444  func (cbt *ConsumerBalanceTracker) deleteCachedForceBalance(key string) {
   445  	cbt.fullBalanceUpdateLock.Lock()
   446  	defer cbt.fullBalanceUpdateLock.Unlock()
   447  
   448  	delete(cbt.fullBalanceUpdateThrottle, key)
   449  }
   450  
   451  func getKeyForForceBalanceCache(chainID int64, id identity.Identity) string {
   452  	return fmt.Sprintf("%v_%v", id.ToCommonAddress().Hex(), chainID)
   453  }
   454  
   455  // ForceBalanceUpdate forces a balance update and returns the updated balance
   456  func (cbt *ConsumerBalanceTracker) ForceBalanceUpdate(chainID int64, id identity.Identity) *big.Int {
   457  	fallback, ok := cbt.getBalance(chainID, id)
   458  	if !ok {
   459  		fallback.BCBalance = big.NewInt(0)
   460  	}
   461  
   462  	addr, err := cbt.addressProvider.GetActiveChannelAddress(chainID, id.ToCommonAddress())
   463  	if err != nil {
   464  		log.Error().Err(err).Msg("Could not calculate channel address")
   465  		return fallback.BCBalance
   466  	}
   467  
   468  	myst, err := cbt.addressProvider.GetMystAddress(chainID)
   469  	if err != nil {
   470  		log.Error().Err(err).Msg("could not get myst address")
   471  		return new(big.Int)
   472  	}
   473  
   474  	balance, lastPromised, err := cbt.alignWithHermes(chainID, id)
   475  	if err != nil {
   476  		if !errors.Is(err, errBalanceNotOffchain) {
   477  			log.Error().Err(err).Msg("align with hermes failed with a critical error, offchain balance out of sync")
   478  		}
   479  		if !errors.Is(err, errBalanceNotOffchain) && fallback.IsOffchain {
   480  			log.Warn().Msg("offchain sync failed but found a cache entry, will return that")
   481  			return fallback.BCBalance
   482  		}
   483  	} else {
   484  		return balance
   485  	}
   486  
   487  	cc, err := cbt.consumerBalanceChecker.GetConsumerChannel(chainID, addr, myst)
   488  	if err != nil {
   489  		// This indicates we're not registered, check for transactor bounty first and then unregistered balance.
   490  		log.Warn().Err(err).Msg("Could not get consumer channel")
   491  		if client.IsErrConnectionFailed(err) {
   492  			log.Debug().Msg("tried to get consumer channel and got a connection error, will return last known balance")
   493  			return fallback.BCBalance
   494  		}
   495  
   496  		var unregisteredBalance *big.Int
   497  		// If registration is in progress, check transactor for bounty amount.
   498  		bountyAmount, ok := cbt.getTransactorBounty(chainID, id)
   499  		if ok {
   500  			// if bounty from transactor is 0 it will be the unregistered balance of the channel.
   501  			unregisteredBalance = bountyAmount
   502  		} else {
   503  			// If error was not for connection it indicates we're not registered, check for unregistered balance.
   504  			unregisteredBalance, err = cbt.getUnregisteredChannelBalance(chainID, id)
   505  			if err != nil {
   506  				log.Error().Err(err).Msg("could not get unregistered balance")
   507  				return fallback.BCBalance
   508  			}
   509  		}
   510  
   511  		cbt.setBalance(chainID, id, ConsumerBalance{
   512  			BCBalance:          unregisteredBalance,
   513  			BCSettled:          new(big.Int),
   514  			GrandTotalPromised: new(big.Int),
   515  		})
   516  
   517  		currentBalance, _ := cbt.getBalance(chainID, id)
   518  		go cbt.publishChangeEvent(id, new(big.Int), currentBalance.GetBalance())
   519  		return unregisteredBalance
   520  	}
   521  
   522  	hermes, err := cbt.addressProvider.GetActiveHermes(chainID)
   523  	if err != nil {
   524  		log.Error().Err(err).Msg("could not get active hermes address")
   525  		return fallback.BCBalance
   526  	}
   527  
   528  	grandTotal, err := cbt.consumerGrandTotalsStorage.Get(chainID, id, hermes)
   529  	if errors.Is(err, ErrNotFound) || (err == nil && lastPromised != nil && grandTotal.Cmp(lastPromised) == -1) {
   530  		err := cbt.consumerGrandTotalsStorage.Store(chainID, id, hermes, lastPromised)
   531  		if err != nil {
   532  			log.Error().Err(err).Msg("Could not recover Grand Total Promised")
   533  		}
   534  		grandTotal = lastPromised
   535  	}
   536  	if err != nil && !errors.Is(err, ErrNotFound) {
   537  		log.Error().Err(err).Msg("Could not get consumer grand total promised")
   538  		return fallback.BCBalance
   539  	}
   540  
   541  	before := new(big.Int)
   542  	if v, ok := cbt.getBalance(chainID, id); ok {
   543  		before = v.GetBalance()
   544  	}
   545  
   546  	cbt.setBalance(chainID, id, ConsumerBalance{
   547  		BCBalance:          cc.Balance,
   548  		BCSettled:          cc.Settled,
   549  		GrandTotalPromised: grandTotal,
   550  	})
   551  
   552  	currentBalance, _ := cbt.getBalance(chainID, id)
   553  	go cbt.publishChangeEvent(id, before, currentBalance.GetBalance())
   554  	return currentBalance.GetBalance()
   555  }
   556  
   557  func (cbt *ConsumerBalanceTracker) handleRegistrationEvent(event registry.AppEventIdentityRegistration) {
   558  	switch event.Status {
   559  	case registry.InProgress:
   560  		cbt.alignWithTransactor(event.ChainID, event.ID)
   561  	case registry.Registered:
   562  		cbt.removeTransactorBounty(event.ChainID, event.ID)
   563  		cbt.ForceBalanceUpdate(event.ChainID, event.ID)
   564  	}
   565  }
   566  
   567  func (cbt *ConsumerBalanceTracker) alignWithTransactor(chainID int64, id identity.Identity) {
   568  	balance, ok := cbt.getBalance(chainID, id)
   569  	if ok {
   570  		// do not override existing values with transactor data if it is not 0
   571  		if balance.BCBalance.Cmp(big.NewInt(0)) != 0 {
   572  			return
   573  		}
   574  	}
   575  
   576  	ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
   577  	defer cancel()
   578  
   579  	go func() {
   580  		select {
   581  		case <-cbt.stop:
   582  			cancel()
   583  		case <-ctx.Done():
   584  		}
   585  	}()
   586  
   587  	bountyAmount := cbt.getTransactorBalance(ctx, chainID, id)
   588  	if bountyAmount == nil {
   589  		return
   590  	}
   591  
   592  	c := ConsumerBalance{
   593  		BCBalance:          bountyAmount,
   594  		BCSettled:          new(big.Int),
   595  		GrandTotalPromised: new(big.Int),
   596  	}
   597  
   598  	cbt.setBalance(chainID, id, c)
   599  	cbt.setTransactorBounty(chainID, id, bountyAmount)
   600  	go cbt.publishChangeEvent(id, balance.GetBalance(), c.GetBalance())
   601  }
   602  
   603  func (cbt *ConsumerBalanceTracker) getTransactorBalance(ctx context.Context, chainID int64, id identity.Identity) *big.Int {
   604  	data, err := cbt.identityRegistrationStatus(ctx, id, chainID)
   605  	if err != nil {
   606  		log.Error().Err(fmt.Errorf("could not fetch registration status from transactor: %w", err))
   607  		return nil
   608  	}
   609  
   610  	if data.Status != registry.TransactorRegistrationEntryStatusCreated &&
   611  		data.Status != registry.TransactorRegistrationEntryStatusPriceIncreased {
   612  		return nil
   613  	}
   614  
   615  	if data.BountyAmount == nil || data.BountyAmount.Cmp(big.NewInt(0)) == 0 {
   616  		// if we've got no bounty, get myst balance from BC and use that as bounty
   617  		b, err := cbt.getUnregisteredChannelBalance(chainID, id)
   618  		if err != nil {
   619  			log.Error().Err(err).Msg("could not get unregistered balance")
   620  			return nil
   621  		}
   622  
   623  		data.BountyAmount = b
   624  	}
   625  
   626  	log.Debug().Msgf("Loaded transactor state, current balance: %v MYST", data.BountyAmount)
   627  	return data.BountyAmount
   628  }
   629  
   630  func (cbt *ConsumerBalanceTracker) recoverGrandTotalPromised(chainID int64, identity identity.Identity) error {
   631  	var boff backoff.BackOff
   632  	eback := backoff.NewExponentialBackOff()
   633  	eback.MaxElapsedTime = time.Second * 20
   634  	eback.InitialInterval = time.Second * 2
   635  
   636  	boff = backoff.WithMaxRetries(eback, 10)
   637  	ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
   638  	defer cancel()
   639  
   640  	go func() {
   641  		select {
   642  		case <-cbt.stop:
   643  			cancel()
   644  		case <-ctx.Done():
   645  		}
   646  	}()
   647  
   648  	var data HermesUserInfo
   649  	boff = backoff.WithContext(boff, ctx)
   650  	toRetry := func() error {
   651  		d, err := cbt.consumerInfoGetter.GetConsumerData(chainID, identity.Address, time.Minute)
   652  		if err != nil {
   653  			if !errors.Is(err, ErrHermesNotFound) {
   654  				return err
   655  			}
   656  			log.Debug().Msgf("No previous invoice grand total, assuming zero")
   657  			return nil
   658  		}
   659  		data = d
   660  		return nil
   661  	}
   662  
   663  	if err := backoff.Retry(toRetry, boff); err != nil {
   664  		return err
   665  	}
   666  
   667  	latestPromised := big.NewInt(0)
   668  	if data.LatestPromise.Amount != nil {
   669  		latestPromised = data.LatestPromise.Amount
   670  	}
   671  
   672  	if isSettledBiggerThanPromised(data.Settled, latestPromised) {
   673  		var err error
   674  		latestPromised, err = cbt.getPromisedWhenSettledIsBigger(data, latestPromised, chainID, identity.ToCommonAddress())
   675  		if err != nil {
   676  			return err
   677  		}
   678  	}
   679  
   680  	log.Debug().Msgf("Loaded hermes state: already promised: %v", latestPromised)
   681  
   682  	hermes, err := cbt.addressProvider.GetActiveHermes(chainID)
   683  	if err != nil {
   684  		log.Error().Err(err).Msg("could not get hermes address")
   685  		return err
   686  	}
   687  
   688  	return cbt.consumerGrandTotalsStorage.Store(chainID, identity, hermes, latestPromised)
   689  }
   690  
   691  func (cbt *ConsumerBalanceTracker) handleStopEvent() {
   692  	cbt.once.Do(func() {
   693  		close(cbt.stop)
   694  	})
   695  }
   696  
   697  func (cbt *ConsumerBalanceTracker) getBalance(chainID int64, id identity.Identity) (ConsumerBalance, bool) {
   698  	cbt.balances.Lock()
   699  	defer cbt.balances.Unlock()
   700  
   701  	if v, ok := cbt.balances.valuesMap[newBalanceKey(chainID, id)]; ok {
   702  		return v, true
   703  	}
   704  
   705  	return ConsumerBalance{
   706  		BCBalance:          new(big.Int),
   707  		BCSettled:          new(big.Int),
   708  		GrandTotalPromised: new(big.Int),
   709  	}, false
   710  }
   711  
   712  func (cbt *ConsumerBalanceTracker) setBalance(chainID int64, id identity.Identity, balance ConsumerBalance) {
   713  	cbt.balances.Lock()
   714  	defer cbt.balances.Unlock()
   715  
   716  	cbt.balances.valuesMap[newBalanceKey(chainID, id)] = balance
   717  }
   718  
   719  func (cbt *ConsumerBalanceTracker) getTransactorBounty(chainID int64, id identity.Identity) (*big.Int, bool) {
   720  	cbt.transactorBounties.Lock()
   721  	defer cbt.transactorBounties.Unlock()
   722  
   723  	if v, ok := cbt.transactorBounties.valuesMap[newBalanceKey(chainID, id)]; ok {
   724  		return v, true
   725  	}
   726  
   727  	return nil, false
   728  }
   729  
   730  func (cbt *ConsumerBalanceTracker) setTransactorBounty(chainID int64, id identity.Identity, bountyAmount *big.Int) {
   731  	cbt.transactorBounties.Lock()
   732  	defer cbt.transactorBounties.Unlock()
   733  
   734  	cbt.transactorBounties.valuesMap[newBalanceKey(chainID, id)] = bountyAmount
   735  }
   736  
   737  func (cbt *ConsumerBalanceTracker) removeTransactorBounty(chainID int64, id identity.Identity) {
   738  	cbt.transactorBounties.Lock()
   739  	defer cbt.transactorBounties.Unlock()
   740  
   741  	delete(cbt.transactorBounties.valuesMap, newBalanceKey(chainID, id))
   742  }
   743  
   744  func (cbt *ConsumerBalanceTracker) updateGrandTotal(chainID int64, id identity.Identity, current *big.Int) {
   745  	b, ok := cbt.getBalance(chainID, id)
   746  	if !ok || b.OffchainNeedsSync() {
   747  		cbt.ForceBalanceUpdate(chainID, id)
   748  		return
   749  	}
   750  
   751  	before := b.BCBalance
   752  	b.GrandTotalPromised = current
   753  	cbt.setBalance(chainID, id, b)
   754  
   755  	after, _ := cbt.getBalance(chainID, id)
   756  	go cbt.publishChangeEvent(id, before, after.GetBalance())
   757  }
   758  
   759  // identityRegistrationStatus returns the registration status of a given identity.
   760  func (cbt *ConsumerBalanceTracker) identityRegistrationStatus(ctx context.Context, id identity.Identity, chainID int64) (registry.TransactorStatusResponse, error) {
   761  	var data registry.TransactorStatusResponse
   762  	boff := backoff.WithContext(backoff.NewConstantBackOff(time.Millisecond*500), ctx)
   763  	toRetry := func() error {
   764  		resp, err := cbt.transactorRegistrationStatusProvider.FetchRegistrationStatus(id.Address)
   765  		if err != nil {
   766  			return err
   767  		}
   768  
   769  		var status *registry.TransactorStatusResponse
   770  		for _, v := range resp {
   771  			if v.ChainID == chainID {
   772  				status = &v
   773  				break
   774  			}
   775  		}
   776  
   777  		if status == nil {
   778  			err := fmt.Errorf("got response but failed to find status for id '%s' on chain '%d'", id.Address, chainID)
   779  			return backoff.Permanent(err)
   780  		}
   781  
   782  		data = *status
   783  		return nil
   784  	}
   785  
   786  	return data, backoff.Retry(toRetry, boff)
   787  }
   788  
   789  func (cbt *ConsumerBalanceTracker) getPromisedWhenSettledIsBigger(data HermesUserInfo, latestPromised *big.Int, chainID int64, identityAddress common.Address) (*big.Int, error) {
   790  	if data.IsOffchain {
   791  		return data.Settled, nil
   792  	}
   793  
   794  	activeChannelAddress, err := cbt.addressProvider.GetActiveChannelAddress(chainID, identityAddress)
   795  	if err != nil {
   796  		return nil, fmt.Errorf("error getting active channel address: %w", err)
   797  	}
   798  
   799  	consumerHermes, err := cbt.blockchainInfoProvider.GetConsumerChannelsHermes(chainID, activeChannelAddress)
   800  	if err != nil {
   801  		return nil, fmt.Errorf("error getting consumer channels hermes: %w", err)
   802  	}
   803  
   804  	return consumerHermes.Settled, nil
   805  }
   806  
   807  func safeSub(a, b *big.Int) *big.Int {
   808  	if a == nil || b == nil {
   809  		return new(big.Int)
   810  	}
   811  
   812  	if a.Cmp(b) >= 0 {
   813  		return new(big.Int).Sub(a, b)
   814  	}
   815  	return new(big.Int)
   816  }
   817  
   818  func isSettledBiggerThanPromised(settled, promised *big.Int) bool {
   819  	return settled != nil && settled.Cmp(promised) == 1
   820  }
   821  
   822  // ConsumerBalance represents the consumer balance
   823  type ConsumerBalance struct {
   824  	BCBalance          *big.Int
   825  	BCSettled          *big.Int
   826  	GrandTotalPromised *big.Int
   827  
   828  	// IsOffchain is an optional indicator which marks an offchain balanace.
   829  	// Offchain balances receive no updates on the blockchain and their
   830  	// actual remaining balance should be retreived from hermes.
   831  	IsOffchain       bool
   832  	LastOffchainSync time.Time
   833  }
   834  
   835  // OffchainNeedsSync returns true if balance is offchain and should be synced.
   836  func (cb ConsumerBalance) OffchainNeedsSync() bool {
   837  	if !cb.IsOffchain {
   838  		return false
   839  	}
   840  
   841  	if cb.LastOffchainSync.IsZero() {
   842  		return true
   843  	}
   844  
   845  	expiresAfter := config.GetDuration(config.FlagOffchainBalanceExpiration)
   846  	now := time.Now().UTC()
   847  	return cb.LastOffchainSync.Add(expiresAfter).Before(now)
   848  }
   849  
   850  // GetBalance returns the current balance
   851  func (cb ConsumerBalance) GetBalance() *big.Int {
   852  	// Balance (to spend) = BCBalance - (hermesPromised - BCSettled)
   853  	return safeSub(cb.BCBalance, safeSub(cb.GrandTotalPromised, cb.BCSettled))
   854  }