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 }