decred.org/dcrdex@v1.0.5/client/core/notification.go (about)

     1  // This code is available on the terms of the project LICENSE.md file,
     2  // also available online at https://blueoakcouncil.org/license/1.0.0.
     3  
     4  package core
     5  
     6  import (
     7  	"fmt"
     8  	"sync/atomic"
     9  
    10  	"decred.org/dcrdex/client/asset"
    11  	"decred.org/dcrdex/client/comms"
    12  	"decred.org/dcrdex/client/db"
    13  	"decred.org/dcrdex/dex"
    14  	"decred.org/dcrdex/dex/msgjson"
    15  	"decred.org/dcrdex/dex/order"
    16  	"decred.org/dcrdex/server/account"
    17  )
    18  
    19  // Notifications should use the following note type strings.
    20  const (
    21  	NoteTypeFeePayment     = "feepayment"
    22  	NoteTypeBondPost       = "bondpost"
    23  	NoteTypeBondRefund     = "bondrefund"
    24  	NoteTypeUnknownBond    = "unknownbond"
    25  	NoteTypeSend           = "send"
    26  	NoteTypeOrder          = "order"
    27  	NoteTypeMatch          = "match"
    28  	NoteTypeEpoch          = "epoch"
    29  	NoteTypeConnEvent      = "conn"
    30  	NoteTypeBalance        = "balance"
    31  	NoteTypeSpots          = "spots"
    32  	NoteTypeWalletConfig   = "walletconfig"
    33  	NoteTypeWalletState    = "walletstate"
    34  	NoteTypeWalletSync     = "walletsync"
    35  	NoteTypeServerNotify   = "notify"
    36  	NoteTypeSecurity       = "security"
    37  	NoteTypeUpgrade        = "upgrade"
    38  	NoteTypeBot            = "bot"
    39  	NoteTypeDEXAuth        = "dex_auth"
    40  	NoteTypeFiatRates      = "fiatrateupdate"
    41  	NoteTypeCreateWallet   = "createwallet"
    42  	NoteTypeLogin          = "login"
    43  	NoteTypeWalletNote     = "walletnote"
    44  	NoteTypeReputation     = "reputation"
    45  	NoteTypeActionRequired = "actionrequired"
    46  )
    47  
    48  var noteChanCounter uint64
    49  
    50  func (c *Core) logNote(n Notification) {
    51  	// Do not log certain spammy note types that have no value in logs.
    52  	switch n.Type() {
    53  	case NoteTypeSpots: // expand this case as needed
    54  		return
    55  	default:
    56  	}
    57  	if n.Subject() == "" && n.Details() == "" {
    58  		return
    59  	}
    60  
    61  	logFun := c.log.Warnf // default in case the Severity level is unknown to notify
    62  	switch n.Severity() {
    63  	case db.Data:
    64  		logFun = c.log.Tracef
    65  	case db.Poke:
    66  		logFun = c.log.Debugf
    67  	case db.Success:
    68  		logFun = c.log.Infof
    69  	case db.WarningLevel:
    70  		logFun = c.log.Warnf
    71  	case db.ErrorLevel:
    72  		logFun = c.log.Errorf
    73  	}
    74  
    75  	logFun("notify: %v", n)
    76  }
    77  
    78  func (c *Core) Broadcast(n Notification) {
    79  	c.notify(n)
    80  }
    81  
    82  // notify sends a notification to all subscribers. If the notification is of
    83  // sufficient severity, it is stored in the database.
    84  func (c *Core) notify(n Notification) {
    85  	if n.Severity() >= db.Success {
    86  		c.db.SaveNotification(n.DBNote())
    87  	} else if n.Severity() == db.Poke {
    88  		c.pokesCache.add(n.DBNote())
    89  	}
    90  
    91  	c.logNote(n)
    92  
    93  	c.noteMtx.RLock()
    94  	for _, ch := range c.noteChans {
    95  		select {
    96  		case ch <- n:
    97  		default:
    98  			c.log.Errorf("blocking notification channel")
    99  		}
   100  	}
   101  	c.noteMtx.RUnlock()
   102  }
   103  
   104  // NoteFeed contains a receiving channel for notifications.
   105  type NoteFeed struct {
   106  	C      <-chan Notification
   107  	closer func()
   108  }
   109  
   110  // ReturnFeed should be called when the channel is no longer needed.
   111  func (c *NoteFeed) ReturnFeed() {
   112  	if c.closer != nil {
   113  		c.closer()
   114  	}
   115  }
   116  
   117  // NotificationFeed returns a new receiving channel for notifications. The
   118  // channel has capacity 1024, and should be monitored for the lifetime of the
   119  // Core. Blocking channels are silently ignored.
   120  func (c *Core) NotificationFeed() *NoteFeed {
   121  	id, ch := c.notificationFeed()
   122  	return &NoteFeed{
   123  		C:      ch,
   124  		closer: func() { c.returnFeed(id) },
   125  	}
   126  }
   127  
   128  func (c *Core) notificationFeed() (uint64, <-chan Notification) {
   129  	ch := make(chan Notification, 1024)
   130  	cid := atomic.AddUint64(&noteChanCounter, 1)
   131  	c.noteMtx.Lock()
   132  	c.noteChans[cid] = ch
   133  	c.noteMtx.Unlock()
   134  	return cid, ch
   135  }
   136  
   137  func (c *Core) returnFeed(channelID uint64) {
   138  	c.noteMtx.Lock()
   139  	delete(c.noteChans, channelID)
   140  	c.noteMtx.Unlock()
   141  }
   142  
   143  // AckNotes sets the acknowledgement field for the notifications.
   144  func (c *Core) AckNotes(ids []dex.Bytes) {
   145  	for _, id := range ids {
   146  		err := c.db.AckNotification(id)
   147  		if err != nil {
   148  			c.log.Errorf("error saving notification acknowledgement for %s: %v", id, err)
   149  		}
   150  	}
   151  }
   152  
   153  func (c *Core) formatDetails(topic Topic, args ...any) (translatedSubject, details string) {
   154  	locale := c.locale()
   155  	trans, found := locale.m[topic]
   156  	if !found {
   157  		c.log.Errorf("No translation found for topic %q", topic)
   158  		originTrans, found := originLocale[topic]
   159  		if !found {
   160  			return string(topic), "translation error"
   161  		}
   162  		return originTrans.subject.T, fmt.Sprintf(originTrans.template.T, args...)
   163  	}
   164  	return trans.subject.T, locale.printer.Sprintf(string(topic), args...)
   165  }
   166  
   167  func makeCoinIDToken(txHash string, assetID uint32) string {
   168  	return fmt.Sprintf("{{{%d|%s}}}", assetID, txHash)
   169  }
   170  
   171  func makeOrderToken(orderToken string) string {
   172  	return fmt.Sprintf("{{{order|%s}}}", orderToken)
   173  }
   174  
   175  // Notification is an interface for a user notification. Notification is
   176  // satisfied by db.Notification, so concrete types can embed the db type.
   177  type Notification interface {
   178  	// Type is a string ID unique to the concrete type.
   179  	Type() string
   180  	// Topic is a string ID unique to the message subject. Since subjects must
   181  	// be translated, we cannot rely on the subject to programmatically identify
   182  	// the message.
   183  	Topic() Topic
   184  	// Subject is a short description of the notification contents. When displayed
   185  	// to the user, the Subject will typically be given visual prominence. For
   186  	// notifications with Severity < Poke (not meant for display), the Subject
   187  	// field may be repurposed as a second-level category ID.
   188  	Subject() string
   189  	// Details should contain more detailed information.
   190  	Details() string
   191  	// Severity is the notification severity.
   192  	Severity() db.Severity
   193  	// Time is the notification timestamp. The timestamp is set in
   194  	// db.NewNotification. Time is a UNIX timestamp, in milliseconds.
   195  	Time() uint64
   196  	// Acked is true if the user has seen the notification. Acknowledgement is
   197  	// recorded with (*Core).AckNotes.
   198  	Acked() bool
   199  	// ID should be unique, except in the case of identical copies of
   200  	// db.Notification where the IDs should be the same.
   201  	ID() dex.Bytes
   202  	// Stamp sets the notification timestamp. If db.NewNotification is used to
   203  	// construct the db.Notification, the timestamp will already be set.
   204  	Stamp()
   205  	// DBNote returns the underlying *db.Notification.
   206  	DBNote() *db.Notification
   207  	// String generates a compact human-readable representation of the
   208  	// Notification that is suitable for logging.
   209  	String() string
   210  }
   211  
   212  // Topic is a language-independent unique ID for a Notification.
   213  type Topic = db.Topic
   214  
   215  // SecurityNote is a note regarding application security, credentials, or
   216  // authentication.
   217  type SecurityNote struct {
   218  	db.Notification
   219  }
   220  
   221  const (
   222  	TopicSeedNeedsSaving Topic = "SeedNeedsSaving"
   223  	TopicUpgradedToSeed  Topic = "UpgradedToSeed"
   224  )
   225  
   226  func newSecurityNote(topic Topic, subject, details string, severity db.Severity) *SecurityNote {
   227  	return &SecurityNote{
   228  		Notification: db.NewNotification(NoteTypeSecurity, topic, subject, details, severity),
   229  	}
   230  }
   231  
   232  const (
   233  	TopicFeePaymentInProgress    Topic = "FeePaymentInProgress"
   234  	TopicFeePaymentError         Topic = "FeePaymentError"
   235  	TopicFeeCoinError            Topic = "FeeCoinError"
   236  	TopicRegUpdate               Topic = "RegUpdate"
   237  	TopicBondConfirming          Topic = "BondConfirming"
   238  	TopicBondRefunded            Topic = "BondRefunded"
   239  	TopicBondPostError           Topic = "BondPostError"
   240  	TopicBondPostErrorConfirm    Topic = "BondPostErrorConfirm"
   241  	TopicBondCoinError           Topic = "BondCoinError"
   242  	TopicAccountRegistered       Topic = "AccountRegistered"
   243  	TopicAccountUnlockError      Topic = "AccountUnlockError"
   244  	TopicWalletConnectionWarning Topic = "WalletConnectionWarning"
   245  	TopicWalletUnlockError       Topic = "WalletUnlockError"
   246  	TopicWalletCommsWarning      Topic = "WalletCommsWarning"
   247  	TopicWalletPeersRestored     Topic = "WalletPeersRestored"
   248  )
   249  
   250  // FeePaymentNote is a notification regarding registration fee payment.
   251  type FeePaymentNote struct {
   252  	db.Notification
   253  	Asset         *uint32 `json:"asset,omitempty"`
   254  	Confirmations *uint32 `json:"confirmations,omitempty"`
   255  	Dex           string  `json:"dex,omitempty"`
   256  }
   257  
   258  func newFeePaymentNote(topic Topic, subject, details string, severity db.Severity, dexAddr string) *FeePaymentNote {
   259  	host, _ := addrHost(dexAddr)
   260  	return &FeePaymentNote{
   261  		Notification: db.NewNotification(NoteTypeFeePayment, topic, subject, details, severity),
   262  		Dex:          host,
   263  	}
   264  }
   265  
   266  func newFeePaymentNoteWithConfirmations(topic Topic, subject, details string, severity db.Severity, asset, currConfs uint32, dexAddr string) *FeePaymentNote {
   267  	feePmtNt := newFeePaymentNote(topic, subject, details, severity, dexAddr)
   268  	feePmtNt.Asset = &asset
   269  	feePmtNt.Confirmations = &currConfs
   270  	return feePmtNt
   271  }
   272  
   273  // BondRefundNote is a notification regarding bond refunds.
   274  type BondRefundNote struct {
   275  	db.Notification
   276  }
   277  
   278  func newBondRefundNote(topic Topic, subject, details string, severity db.Severity) *BondRefundNote {
   279  	return &BondRefundNote{
   280  		Notification: db.NewNotification(NoteTypeBondRefund, topic, subject, details, severity),
   281  	}
   282  }
   283  
   284  const (
   285  	TopicBondAuthUpdate Topic = "BondAuthUpdate"
   286  )
   287  
   288  // BondPostNote is a notification regarding bond posting.
   289  type BondPostNote struct {
   290  	db.Notification
   291  	Asset         *uint32       `json:"asset,omitempty"`
   292  	Confirmations *int32        `json:"confirmations,omitempty"`
   293  	BondedTier    *int64        `json:"bondedTier,omitempty"`
   294  	CoinID        *string       `json:"coinID,omitempty"`
   295  	Dex           string        `json:"dex,omitempty"`
   296  	Auth          *ExchangeAuth `json:"auth,omitempty"`
   297  }
   298  
   299  func newBondPostNote(topic Topic, subject, details string, severity db.Severity, dexAddr string) *BondPostNote {
   300  	host, _ := addrHost(dexAddr)
   301  	return &BondPostNote{
   302  		Notification: db.NewNotification(NoteTypeBondPost, topic, subject, details, severity),
   303  		Dex:          host,
   304  	}
   305  }
   306  
   307  func newBondPostNoteWithConfirmations(
   308  	topic Topic,
   309  	subject string,
   310  	details string,
   311  	severity db.Severity,
   312  	asset uint32,
   313  	coinID string,
   314  	currConfs int32,
   315  	host string,
   316  	auth *ExchangeAuth,
   317  ) *BondPostNote {
   318  
   319  	bondPmtNt := newBondPostNote(topic, subject, details, severity, host)
   320  	bondPmtNt.Asset = &asset
   321  	bondPmtNt.CoinID = &coinID
   322  	bondPmtNt.Confirmations = &currConfs
   323  	bondPmtNt.Auth = auth
   324  	return bondPmtNt
   325  }
   326  
   327  func newBondPostNoteWithTier(topic Topic, subject, details string, severity db.Severity, dexAddr string, bondedTier int64, auth *ExchangeAuth) *BondPostNote {
   328  	bondPmtNt := newBondPostNote(topic, subject, details, severity, dexAddr)
   329  	bondPmtNt.BondedTier = &bondedTier
   330  	bondPmtNt.Auth = auth
   331  	return bondPmtNt
   332  }
   333  
   334  func newBondAuthUpdate(host string, auth *ExchangeAuth) *BondPostNote {
   335  	n := newBondPostNote(TopicBondAuthUpdate, "", "", db.Data, host)
   336  	n.Auth = auth
   337  	return n
   338  }
   339  
   340  // SendNote is a notification regarding a requested send or withdraw.
   341  type SendNote struct {
   342  	db.Notification
   343  }
   344  
   345  const (
   346  	TopicSendError   Topic = "SendError"
   347  	TopicSendSuccess Topic = "SendSuccess"
   348  )
   349  
   350  func newSendNote(topic Topic, subject, details string, severity db.Severity) *SendNote {
   351  	return &SendNote{
   352  		Notification: db.NewNotification(NoteTypeSend, topic, subject, details, severity),
   353  	}
   354  }
   355  
   356  // OrderNote is a notification about an order or a match.
   357  type OrderNote struct {
   358  	db.Notification
   359  	Order       *Order `json:"order"`
   360  	TemporaryID uint64 `json:"tempID,omitempty"`
   361  }
   362  
   363  const (
   364  	TopicOrderLoadFailure     Topic = "OrderLoadFailure"
   365  	TopicOrderResumeFailure   Topic = "OrderResumeFailure"
   366  	TopicBuyOrderPlaced       Topic = "BuyOrderPlaced"
   367  	TopicSellOrderPlaced      Topic = "SellOrderPlaced"
   368  	TopicYoloPlaced           Topic = "YoloPlaced"
   369  	TopicMissingMatches       Topic = "MissingMatches"
   370  	TopicWalletMissing        Topic = "WalletMissing"
   371  	TopicMatchErrorCoin       Topic = "MatchErrorCoin"
   372  	TopicMatchErrorContract   Topic = "MatchErrorContract"
   373  	TopicMatchRecoveryError   Topic = "MatchRecoveryError"
   374  	TopicOrderCoinError       Topic = "OrderCoinError"
   375  	TopicOrderCoinFetchError  Topic = "OrderCoinFetchError"
   376  	TopicPreimageSent         Topic = "PreimageSent"
   377  	TopicCancelPreimageSent   Topic = "CancelPreimageSent"
   378  	TopicMissedCancel         Topic = "MissedCancel"
   379  	TopicOrderBooked          Topic = "OrderBooked"
   380  	TopicNoMatch              Topic = "NoMatch"
   381  	TopicBuyOrderCanceled     Topic = "BuyOrderCanceled"
   382  	TopicSellOrderCanceled    Topic = "SellOrderCanceled"
   383  	TopicCancel               Topic = "Cancel"
   384  	TopicBuyMatchesMade       Topic = "BuyMatchesMade"
   385  	TopicSellMatchesMade      Topic = "SellMatchesMade"
   386  	TopicSwapSendError        Topic = "SwapSendError"
   387  	TopicInitError            Topic = "InitError"
   388  	TopicReportRedeemError    Topic = "ReportRedeemError"
   389  	TopicSwapsInitiated       Topic = "SwapsInitiated"
   390  	TopicRedemptionError      Topic = "RedemptionError"
   391  	TopicMatchComplete        Topic = "MatchComplete"
   392  	TopicRefundFailure        Topic = "RefundFailure"
   393  	TopicMatchesRefunded      Topic = "MatchesRefunded"
   394  	TopicMatchRevoked         Topic = "MatchRevoked"
   395  	TopicOrderRevoked         Topic = "OrderRevoked"
   396  	TopicOrderAutoRevoked     Topic = "OrderAutoRevoked"
   397  	TopicMatchRecovered       Topic = "MatchRecovered"
   398  	TopicCancellingOrder      Topic = "CancellingOrder"
   399  	TopicOrderStatusUpdate    Topic = "OrderStatusUpdate"
   400  	TopicMatchResolutionError Topic = "MatchResolutionError"
   401  	TopicFailedCancel         Topic = "FailedCancel"
   402  	TopicOrderLoaded          Topic = "OrderLoaded"
   403  	TopicOrderRetired         Topic = "OrderRetired"
   404  	TopicAsyncOrderFailure    Topic = "AsyncOrderFailure"
   405  	TopicAsyncOrderSubmitted  Topic = "AsyncOrderSubmitted"
   406  	TopicOrderQuantityTooHigh Topic = "OrderQuantityTooHigh"
   407  )
   408  
   409  func newOrderNote(topic Topic, subject, details string, severity db.Severity, corder *Order) *OrderNote {
   410  	return &OrderNote{
   411  		Notification: db.NewNotification(NoteTypeOrder, topic, subject, details, severity),
   412  		Order:        corder,
   413  	}
   414  }
   415  
   416  func newOrderNoteWithTempID(topic Topic, subject, details string, severity db.Severity, corder *Order, tempID uint64) *OrderNote {
   417  	note := newOrderNote(topic, subject, details, severity, corder)
   418  	note.TemporaryID = tempID
   419  	return note
   420  }
   421  
   422  // MatchNote is a notification about a match.
   423  type MatchNote struct {
   424  	db.Notification
   425  	OrderID  dex.Bytes `json:"orderID"`
   426  	Match    *Match    `json:"match"`
   427  	Host     string    `json:"host"`
   428  	MarketID string    `json:"marketID"`
   429  }
   430  
   431  const (
   432  	TopicAudit                 Topic = "Audit"
   433  	TopicAuditTrouble          Topic = "AuditTrouble"
   434  	TopicNewMatch              Topic = "NewMatch"
   435  	TopicCounterConfirms       Topic = "CounterConfirms"
   436  	TopicConfirms              Topic = "Confirms"
   437  	TopicRedemptionResubmitted Topic = "RedemptionResubmitted"
   438  	TopicSwapRefunded          Topic = "SwapRefunded"
   439  	TopicRedemptionConfirmed   Topic = "RedemptionConfirmed"
   440  )
   441  
   442  func newMatchNote(topic Topic, subject, details string, severity db.Severity, t *trackedTrade, match *matchTracker) *MatchNote {
   443  	swapConfs, counterConfs := match.confirms()
   444  	if counterConfs < 0 {
   445  		// This can be -1 before it is actually checked, but for purposes of the
   446  		// match note, it should be non-negative.
   447  		counterConfs = 0
   448  	}
   449  	return &MatchNote{
   450  		Notification: db.NewNotification(NoteTypeMatch, topic, subject, details, severity),
   451  		OrderID:      t.ID().Bytes(),
   452  		Match: matchFromMetaMatchWithConfs(t.Order, &match.MetaMatch, swapConfs,
   453  			int64(t.metaData.FromSwapConf), counterConfs, int64(t.metaData.ToSwapConf),
   454  			int64(match.redemptionConfs), int64(match.redemptionConfsReq)),
   455  		Host:     t.dc.acct.host,
   456  		MarketID: marketName(t.Base(), t.Quote()),
   457  	}
   458  }
   459  
   460  // String supplements db.Notification's Stringer with the Order's ID, if the
   461  // Order is not nil.
   462  func (on *OrderNote) String() string {
   463  	base := on.Notification.String()
   464  	if on.Order == nil {
   465  		return base
   466  	}
   467  	return fmt.Sprintf("%s - Order: %s", base, on.Order.ID)
   468  }
   469  
   470  // EpochNotification is a data notification that a new epoch has begun.
   471  type EpochNotification struct {
   472  	db.Notification
   473  	Host     string `json:"host"`
   474  	MarketID string `json:"marketID"`
   475  	Epoch    uint64 `json:"epoch"`
   476  }
   477  
   478  const TopicEpoch Topic = "Epoch"
   479  
   480  func newEpochNotification(host, mktID string, epochIdx uint64) *EpochNotification {
   481  	return &EpochNotification{
   482  		Host:         host,
   483  		MarketID:     mktID,
   484  		Notification: db.NewNotification(NoteTypeEpoch, TopicEpoch, "", "", db.Data),
   485  		Epoch:        epochIdx,
   486  	}
   487  }
   488  
   489  // String supplements db.Notification's Stringer with the Epoch index.
   490  func (on *EpochNotification) String() string {
   491  	return fmt.Sprintf("%s - Index: %d", on.Notification.String(), on.Epoch)
   492  }
   493  
   494  // ConnEventNote is a notification regarding individual DEX connection status.
   495  type ConnEventNote struct {
   496  	db.Notification
   497  	Host             string                 `json:"host"`
   498  	ConnectionStatus comms.ConnectionStatus `json:"connectionStatus"`
   499  }
   500  
   501  const (
   502  	TopicDEXConnected    Topic = "DEXConnected"
   503  	TopicDEXDisconnected Topic = "DEXDisconnected"
   504  	TopicDexConnectivity Topic = "DEXConnectivity"
   505  	TopicDEXDisabled     Topic = "DEXDisabled"
   506  	TopicDEXEnabled      Topic = "DEXEnabled"
   507  )
   508  
   509  func newConnEventNote(topic Topic, subject, host string, status comms.ConnectionStatus, details string, severity db.Severity) *ConnEventNote {
   510  	return &ConnEventNote{
   511  		Notification:     db.NewNotification(NoteTypeConnEvent, topic, subject, details, severity),
   512  		Host:             host,
   513  		ConnectionStatus: status,
   514  	}
   515  }
   516  
   517  // FiatRatesNote is an update of fiat rate data for assets.
   518  type FiatRatesNote struct {
   519  	db.Notification
   520  	FiatRates map[uint32]float64 `json:"fiatRates"`
   521  }
   522  
   523  const TopicFiatRatesUpdate Topic = "fiatrateupdate"
   524  
   525  func newFiatRatesUpdate(rates map[uint32]float64) *FiatRatesNote {
   526  	return &FiatRatesNote{
   527  		Notification: db.NewNotification(NoteTypeFiatRates, TopicFiatRatesUpdate, "", "", db.Data),
   528  		FiatRates:    rates,
   529  	}
   530  }
   531  
   532  // BalanceNote is an update to a wallet's balance.
   533  type BalanceNote struct {
   534  	db.Notification
   535  	AssetID uint32         `json:"assetID"`
   536  	Balance *WalletBalance `json:"balance"`
   537  }
   538  
   539  const TopicBalanceUpdated Topic = "BalanceUpdated"
   540  
   541  func newBalanceNote(assetID uint32, bal *WalletBalance) *BalanceNote {
   542  	return &BalanceNote{
   543  		Notification: db.NewNotification(NoteTypeBalance, TopicBalanceUpdated, "", "", db.Data),
   544  		AssetID:      assetID,
   545  		Balance:      bal, // Once created, balance is never modified by Core.
   546  	}
   547  }
   548  
   549  // SpotPriceNote is a notification of an update to the market's spot price.
   550  type SpotPriceNote struct {
   551  	db.Notification
   552  	Host  string                   `json:"host"`
   553  	Spots map[string]*msgjson.Spot `json:"spots"`
   554  }
   555  
   556  const TopicSpotsUpdate Topic = "SpotsUpdate"
   557  
   558  func newSpotPriceNote(host string, spots map[string]*msgjson.Spot) *SpotPriceNote {
   559  	return &SpotPriceNote{
   560  		Notification: db.NewNotification(NoteTypeSpots, TopicSpotsUpdate, "", "", db.Data),
   561  		Host:         host,
   562  		Spots:        spots,
   563  	}
   564  }
   565  
   566  // DEXAuthNote is a notification regarding individual DEX authentication status.
   567  type DEXAuthNote struct {
   568  	db.Notification
   569  	Host          string `json:"host"`
   570  	Authenticated bool   `json:"authenticated"`
   571  }
   572  
   573  const (
   574  	TopicDexAuthError     Topic = "DexAuthError"
   575  	TopicDexAuthErrorBond Topic = "DexAuthErrorBond"
   576  	TopicUnknownOrders    Topic = "UnknownOrders"
   577  	TopicOrdersReconciled Topic = "OrdersReconciled"
   578  	TopicBondConfirmed    Topic = "BondConfirmed"
   579  	TopicBondExpired      Topic = "BondExpired"
   580  	TopicAccountRegTier   Topic = "AccountRegTier"
   581  )
   582  
   583  func newDEXAuthNote(topic Topic, subject, host string, authenticated bool, details string, severity db.Severity) *DEXAuthNote {
   584  	return &DEXAuthNote{
   585  		Notification:  db.NewNotification(NoteTypeDEXAuth, topic, subject, details, severity),
   586  		Host:          host,
   587  		Authenticated: authenticated,
   588  	}
   589  }
   590  
   591  // WalletConfigNote is a notification regarding a change in wallet
   592  // configuration.
   593  type WalletConfigNote struct {
   594  	db.Notification
   595  	Wallet *WalletState `json:"wallet"`
   596  }
   597  
   598  const (
   599  	TopicWalletConfigurationUpdated Topic = "WalletConfigurationUpdated"
   600  	TopicWalletPasswordUpdated      Topic = "WalletPasswordUpdated"
   601  	TopicWalletPeersWarning         Topic = "WalletPeersWarning"
   602  	TopicWalletTypeDeprecated       Topic = "WalletTypeDeprecated"
   603  	TopicWalletPeersUpdate          Topic = "WalletPeersUpdate"
   604  	TopicBondWalletNotConnected     Topic = "BondWalletNotConnected"
   605  )
   606  
   607  func newWalletConfigNote(topic Topic, subject, details string, severity db.Severity, walletState *WalletState) *WalletConfigNote {
   608  	return &WalletConfigNote{
   609  		Notification: db.NewNotification(NoteTypeWalletConfig, topic, subject, details, severity),
   610  		Wallet:       walletState,
   611  	}
   612  }
   613  
   614  // WalletStateNote is a notification regarding a change in wallet state,
   615  // including: creation, locking, unlocking, connect, disabling and enabling. This
   616  // is intended to be a Data Severity notification.
   617  type WalletStateNote WalletConfigNote
   618  
   619  const TopicWalletState Topic = "WalletState"
   620  const TopicTokenApproval Topic = "TokenApproval"
   621  
   622  func newTokenApprovalNote(walletState *WalletState) *WalletStateNote {
   623  	return &WalletStateNote{
   624  		Notification: db.NewNotification(NoteTypeWalletState, TopicTokenApproval, "", "", db.Data),
   625  		Wallet:       walletState,
   626  	}
   627  }
   628  
   629  func newWalletStateNote(walletState *WalletState) *WalletStateNote {
   630  	return &WalletStateNote{
   631  		Notification: db.NewNotification(NoteTypeWalletState, TopicWalletState, "", "", db.Data),
   632  		Wallet:       walletState,
   633  	}
   634  }
   635  
   636  // WalletSyncNote is a notification of the wallet sync status.
   637  type WalletSyncNote struct {
   638  	db.Notification
   639  	AssetID      uint32            `json:"assetID"`
   640  	SyncStatus   *asset.SyncStatus `json:"syncStatus"`
   641  	SyncProgress float32           `json:"syncProgress"`
   642  }
   643  
   644  const TopicWalletSync = "WalletSync"
   645  
   646  func newWalletSyncNote(assetID uint32, ss *asset.SyncStatus) *WalletSyncNote {
   647  	return &WalletSyncNote{
   648  		Notification: db.NewNotification(NoteTypeWalletSync, TopicWalletState, "", "", db.Data),
   649  		AssetID:      assetID,
   650  		SyncStatus:   ss,
   651  		SyncProgress: ss.BlockProgress(),
   652  	}
   653  }
   654  
   655  // ServerNotifyNote is a notification containing a server-originating message.
   656  type ServerNotifyNote struct {
   657  	db.Notification
   658  }
   659  
   660  const (
   661  	TopicMarketSuspendScheduled   Topic = "MarketSuspendScheduled"
   662  	TopicMarketSuspended          Topic = "MarketSuspended"
   663  	TopicMarketSuspendedWithPurge Topic = "MarketSuspendedWithPurge"
   664  	TopicMarketResumeScheduled    Topic = "MarketResumeScheduled"
   665  	TopicMarketResumed            Topic = "MarketResumed"
   666  	TopicPenalized                Topic = "Penalized"
   667  	TopicDEXNotification          Topic = "DEXNotification"
   668  )
   669  
   670  func newServerNotifyNote(topic Topic, subject, details string, severity db.Severity) *ServerNotifyNote {
   671  	return &ServerNotifyNote{
   672  		Notification: db.NewNotification(NoteTypeServerNotify, topic, subject, details, severity),
   673  	}
   674  }
   675  
   676  // UpgradeNote is a notification regarding an outdated client.
   677  type UpgradeNote struct {
   678  	db.Notification
   679  }
   680  
   681  const (
   682  	TopicUpgradeNeeded Topic = "UpgradeNeeded"
   683  )
   684  
   685  func newUpgradeNote(topic Topic, subject, details string, severity db.Severity) *UpgradeNote {
   686  	return &UpgradeNote{
   687  		Notification: db.NewNotification(NoteTypeUpgrade, topic, subject, details, severity),
   688  	}
   689  }
   690  
   691  // ServerConfigUpdateNote is sent when a server's configuration is updated.
   692  type ServerConfigUpdateNote struct {
   693  	db.Notification
   694  	Host string `json:"host"`
   695  }
   696  
   697  const TopicServerConfigUpdate Topic = "ServerConfigUpdate"
   698  
   699  func newServerConfigUpdateNote(host string) *ServerConfigUpdateNote {
   700  	return &ServerConfigUpdateNote{
   701  		Notification: db.NewNotification(NoteTypeServerNotify, TopicServerConfigUpdate, "", "", db.Data),
   702  		Host:         host,
   703  	}
   704  }
   705  
   706  // WalletCreationNote is a notification regarding asynchronous wallet creation.
   707  type WalletCreationNote struct {
   708  	db.Notification
   709  	AssetID uint32 `json:"assetID"`
   710  }
   711  
   712  const (
   713  	TopicQueuedCreationFailed  Topic = "QueuedCreationFailed"
   714  	TopicQueuedCreationSuccess Topic = "QueuedCreationSuccess"
   715  	TopicCreationQueued        Topic = "CreationQueued"
   716  )
   717  
   718  func newWalletCreationNote(topic Topic, subject, details string, severity db.Severity, assetID uint32) *WalletCreationNote {
   719  	return &WalletCreationNote{
   720  		Notification: db.NewNotification(NoteTypeCreateWallet, topic, subject, details, severity),
   721  		AssetID:      assetID,
   722  	}
   723  }
   724  
   725  // LoginNote is a notification with the recent login status.
   726  type LoginNote struct {
   727  	db.Notification
   728  }
   729  
   730  const TopicLoginStatus Topic = "LoginStatus"
   731  
   732  func newLoginNote(message string) *LoginNote {
   733  	return &LoginNote{
   734  		Notification: db.NewNotification(NoteTypeLogin, TopicLoginStatus, "", message, db.Data),
   735  	}
   736  }
   737  
   738  // WalletNote is a notification originating from a wallet.
   739  type WalletNote struct {
   740  	db.Notification
   741  	Payload asset.WalletNotification `json:"payload"`
   742  }
   743  
   744  const TopicWalletNotification Topic = "WalletNotification"
   745  
   746  func newWalletNote(n asset.WalletNotification) *WalletNote {
   747  	return &WalletNote{
   748  		Notification: db.NewNotification(NoteTypeWalletNote, TopicWalletNotification, "", "", db.Data),
   749  		Payload:      n,
   750  	}
   751  }
   752  
   753  type ReputationNote struct {
   754  	db.Notification
   755  	Host       string             `json:"host"`
   756  	Reputation account.Reputation `json:"rep"`
   757  }
   758  
   759  const TopicReputationUpdate = "ReputationUpdate"
   760  
   761  func newReputationNote(host string, rep account.Reputation) *ReputationNote {
   762  	return &ReputationNote{
   763  		Notification: db.NewNotification(NoteTypeReputation, TopicReputationUpdate, "", "", db.Data),
   764  		Host:         host,
   765  		Reputation:   rep,
   766  	}
   767  }
   768  
   769  const TopicUnknownBondTierZero = "UnknownBondTierZero"
   770  
   771  // newUnknownBondTierZeroNote is used when unknown bonds are reported by the
   772  // server while at target tier zero.
   773  func newUnknownBondTierZeroNote(subject, details string) *db.Notification {
   774  	note := db.NewNotification(NoteTypeUnknownBond, TopicUnknownBondTierZero, subject, details, db.WarningLevel)
   775  	return &note
   776  }
   777  
   778  const (
   779  	ActionIDRedeemRejected = "redeemRejected"
   780  	TopicRedeemRejected    = "RedeemRejected"
   781  )
   782  
   783  func newActionRequiredNote(actionID, uniqueID string, payload any) *asset.ActionRequiredNote {
   784  	n := &asset.ActionRequiredNote{
   785  		UniqueID: uniqueID,
   786  		ActionID: actionID,
   787  		Payload:  payload,
   788  	}
   789  	const routeNotNeededCuzCoreHasNoteType = ""
   790  	n.Route = routeNotNeededCuzCoreHasNoteType
   791  	return n
   792  }
   793  
   794  type RejectedRedemptionData struct {
   795  	OrderID dex.Bytes `json:"orderID"`
   796  	CoinID  dex.Bytes `json:"coinID"`
   797  	AssetID uint32    `json:"assetID"`
   798  	CoinFmt string    `json:"coinFmt"`
   799  }
   800  
   801  // ActionRequiredNote is structured like a WalletNote. The payload will be
   802  // an *asset.ActionRequiredNote. This is done for compatibility reasons.
   803  type ActionRequiredNote WalletNote
   804  
   805  func newRejectedRedemptionNote(assetID uint32, oid order.OrderID, coinID []byte) (*asset.ActionRequiredNote, *ActionRequiredNote) {
   806  	data := &RejectedRedemptionData{
   807  		AssetID: assetID,
   808  		OrderID: oid[:],
   809  		CoinID:  coinID,
   810  		CoinFmt: coinIDString(assetID, coinID),
   811  	}
   812  	uniqueID := dex.Bytes(coinID).String()
   813  	actionNote := newActionRequiredNote(ActionIDRedeemRejected, uniqueID, data)
   814  	coreNote := &ActionRequiredNote{
   815  		Notification: db.NewNotification(NoteTypeActionRequired, TopicRedeemRejected, "", "", db.Data),
   816  		Payload:      actionNote,
   817  	}
   818  	return actionNote, coreNote
   819  }