github.com/mysteriumnetwork/node@v0.0.0-20240516044423-365054f76801/pilvytis/order_status_tracker.go (about)

     1  /*
     2   * Copyright (C) 2020 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 pilvytis
    19  
    20  import (
    21  	"fmt"
    22  	"sync"
    23  	"time"
    24  
    25  	"github.com/mysteriumnetwork/node/eventbus"
    26  	"github.com/mysteriumnetwork/node/identity"
    27  	"github.com/rs/zerolog/log"
    28  )
    29  
    30  type orderProvider interface {
    31  	GetPaymentGatewayOrders(id identity.Identity) ([]GatewayOrderResponse, error)
    32  }
    33  
    34  type identityProvider interface {
    35  	GetIdentities() []identity.Identity
    36  	IsUnlocked(address string) bool
    37  }
    38  
    39  // StatusTracker tracks payment order status.
    40  type StatusTracker struct {
    41  	api              orderProvider
    42  	identityProvider identityProvider
    43  	eventBus         eventbus.Publisher
    44  	orders           map[string]map[string]OrderSummary
    45  	failedSyncs      map[identity.Identity]struct{}
    46  
    47  	updateInterval time.Duration
    48  	forceSync      chan identity.Identity
    49  	stopCh         chan struct{}
    50  	once           sync.Once
    51  }
    52  
    53  // NewStatusTracker constructs a StatusTracker.
    54  func NewStatusTracker(api orderProvider, identityProvider identityProvider, eventBus eventbus.Publisher, updateInterval time.Duration) *StatusTracker {
    55  	return &StatusTracker{
    56  		api:              api,
    57  		identityProvider: identityProvider,
    58  		eventBus:         eventBus,
    59  		orders:           make(map[string]map[string]OrderSummary),
    60  		failedSyncs:      make(map[identity.Identity]struct{}),
    61  		forceSync:        make(chan identity.Identity),
    62  		updateInterval:   updateInterval,
    63  		stopCh:           make(chan struct{}),
    64  	}
    65  }
    66  
    67  // SubscribeAsync subscribes to event to listen for unlock events.
    68  func (t *StatusTracker) SubscribeAsync(bus eventbus.Subscriber) error {
    69  	handleUnlockEvent := func(data identity.AppEventIdentityUnlock) {
    70  		t.UpdateOrdersFor(data.ID)
    71  	}
    72  
    73  	// Handle the unlock event so that we load any orders for identities that just launched without
    74  	// waiting for the thread to re-execute.
    75  	return bus.SubscribeAsync(identity.AppTopicIdentityUnlock, handleUnlockEvent)
    76  }
    77  
    78  // Track will block and start tracking orders.
    79  func (t *StatusTracker) Track() {
    80  	for {
    81  		select {
    82  		case <-t.stopCh:
    83  			return
    84  		case id := <-t.forceSync:
    85  			t.refreshAndUpdate(id)
    86  		case <-time.After(t.updateInterval):
    87  			for _, id := range t.identityProvider.GetIdentities() {
    88  				if !t.identityProvider.IsUnlocked(id.Address) {
    89  					continue
    90  				}
    91  				if !t.needsRefresh(id) {
    92  					continue
    93  				}
    94  
    95  				t.refreshAndUpdate(id)
    96  			}
    97  		}
    98  	}
    99  }
   100  
   101  // UpdateOrdersFor sends a notification to the main running thread to
   102  // sync orders for the given identity.
   103  func (t *StatusTracker) UpdateOrdersFor(id identity.Identity) {
   104  	t.forceSync <- id
   105  }
   106  
   107  // Stop stops the status tracker
   108  func (t *StatusTracker) Stop() {
   109  	t.once.Do(func() {
   110  		close(t.stopCh)
   111  	})
   112  }
   113  
   114  func (t *StatusTracker) needsRefresh(id identity.Identity) bool {
   115  	_, ok := t.failedSyncs[id]
   116  	if ok {
   117  		return true
   118  	}
   119  
   120  	orders, ok := t.orders[id.Address]
   121  	if !ok {
   122  		return true
   123  	}
   124  
   125  	for _, order := range orders {
   126  		if order.Status.Incomplete() {
   127  			return true
   128  		}
   129  	}
   130  
   131  	return false
   132  }
   133  
   134  func (t *StatusTracker) refreshAndUpdate(id identity.Identity) {
   135  	// If we fail to sync or only sync partialy we must force a repeat
   136  	t.failedSyncs[id] = struct{}{}
   137  	defer delete(t.failedSyncs, id)
   138  
   139  	newOrders, err := t.refresh(id)
   140  	if err != nil {
   141  		log.Err(err).Str("identity", id.Address).Msg("Could not update orders")
   142  		return
   143  	}
   144  
   145  	if len(newOrders) == 0 {
   146  		if _, ok := t.orders[id.Address]; !ok {
   147  			t.orders[id.Address] = map[string]OrderSummary{}
   148  		}
   149  		return
   150  	}
   151  
   152  	t.compareAndUpdate(id, newOrders)
   153  }
   154  
   155  func (t *StatusTracker) compareAndUpdate(id identity.Identity, newOrders map[string]OrderSummary) {
   156  	old, ok := t.orders[id.Address]
   157  	if !ok || len(old) == 0 {
   158  		t.orders[id.Address] = newOrders
   159  		return
   160  	}
   161  
   162  	updated := make(map[string]OrderSummary)
   163  	for _, no := range newOrders {
   164  		old, ok := old[no.ID]
   165  		if !ok {
   166  			updated[no.ID] = no
   167  			if no.Status.Incomplete() {
   168  				continue
   169  			}
   170  
   171  			// If the entry is new but already completed, send an update about it
   172  			t.eventBus.Publish(AppTopicOrderUpdated, AppEventOrderUpdated{no})
   173  		}
   174  
   175  		newEntry, changed := applyChanges(old, no)
   176  		if changed {
   177  			t.eventBus.Publish(AppTopicOrderUpdated, AppEventOrderUpdated{newEntry})
   178  		}
   179  		updated[no.ID] = newEntry
   180  	}
   181  
   182  	t.orders[id.Address] = updated
   183  }
   184  
   185  func (t *StatusTracker) refresh(id identity.Identity) (map[string]OrderSummary, error) {
   186  	result := make(map[string]OrderSummary)
   187  	gwOrders, err := t.api.GetPaymentGatewayOrders(id)
   188  	if err != nil {
   189  		return nil, err
   190  	}
   191  
   192  	for _, o := range gwOrders {
   193  		result[o.ID] = OrderSummary{
   194  			ID:              o.ID,
   195  			IdentityAddress: o.Identity,
   196  			Status:          o.Status,
   197  			PayAmount:       o.PayAmount,
   198  			PayCurrency:     o.PayCurrency,
   199  		}
   200  	}
   201  
   202  	return result, nil
   203  }
   204  
   205  // applyChanges applies changes to the OrderSummary from an OrderResponse. Returns true if changed.
   206  func applyChanges(order OrderSummary, newOrder OrderSummary) (OrderSummary, bool) {
   207  	changed := false
   208  	if order.Status != newOrder.Status {
   209  		order.Status = newOrder.Status
   210  		changed = true
   211  	}
   212  	if order.PayAmount != newOrder.PayAmount {
   213  		order.PayAmount = newOrder.PayAmount
   214  		changed = true
   215  	}
   216  	if order.PayCurrency != newOrder.PayCurrency {
   217  		order.PayCurrency = newOrder.PayCurrency
   218  		changed = true
   219  	}
   220  
   221  	return order, changed
   222  }
   223  
   224  // OrderSummary is a subset of an OrderResponse stored by the StatusTracker.
   225  type OrderSummary struct {
   226  	ID              string
   227  	IdentityAddress string
   228  	Status          CompletionProvider
   229  	PayAmount       string
   230  	PayCurrency     string
   231  }
   232  
   233  // CompletionProvider is a temporary interface to make
   234  // any order work with the tracker.
   235  // TODO: Remove after legacy payments are removed.
   236  type CompletionProvider interface {
   237  	Incomplete() bool
   238  	Status() string
   239  	Paid() bool
   240  }
   241  
   242  func (o OrderSummary) String() string {
   243  	return fmt.Sprintf("ID: %v, IdentityAddress: %v, Status: %v, PayAmount: %v, PayCurrency: %v", o.ID, o.IdentityAddress, o.Status, o.PayAmount, o.PayCurrency)
   244  }