diff --git a/client/core/bookie.go b/client/core/bookie.go index 86cfd53fdf..43fc3b3da8 100644 --- a/client/core/bookie.go +++ b/client/core/bookie.go @@ -482,17 +482,17 @@ func handleTradeSuspensionMsg(c *Core, dc *dexConnection, msg *msgjson.Message) if sp.SuspendTime != 0 { // This is just a warning about a scheduled suspension. suspendTime := encode.UnixTimeMilli(int64(sp.SuspendTime)) - subject, detail := c.formatDetails(SubjectMarketSuspendScheduled, sp.MarketID, dc.acct.host, suspendTime) - c.notify(newServerNotifyNote(subject, detail, db.WarningLevel)) + subject, detail := c.formatDetails(TopicMarketSuspendScheduled, sp.MarketID, dc.acct.host, suspendTime) + c.notify(newServerNotifyNote(TopicMarketSuspendScheduled, subject, detail, db.WarningLevel)) return nil } - subject := SubjectMarketSuspended + topic := TopicMarketSuspended if !sp.Persist { - subject = SubjectMarketSuspendedWithPurge + topic = TopicMarketSuspendedWithPurge } - subject, detail := c.formatDetails(subject, sp.MarketID, dc.acct.host) - c.notify(newServerNotifyNote(subject, detail, db.WarningLevel)) + subject, detail := c.formatDetails(topic, sp.MarketID, dc.acct.host) + c.notify(newServerNotifyNote(topic, subject, detail, db.WarningLevel)) if sp.Persist { // No book changes. Just wait for more order notes. @@ -522,8 +522,8 @@ func handleTradeSuspensionMsg(c *Core, dc *dexConnection, msg *msgjson.Message) tracker.metaData.Host == dc.acct.host && tracker.metaData.Status == order.OrderStatusBooked { // Locally revoke the purged book order. tracker.revoke() - subject, details := c.formatDetails(SubjectOrderAutoRevoked, tracker.token(), sp.MarketID, dc.acct.host) - c.notify(newOrderNote(subject, details, db.WarningLevel, tracker.coreOrderInternal())) + subject, details := c.formatDetails(TopicOrderAutoRevoked, tracker.token(), sp.MarketID, dc.acct.host) + c.notify(newOrderNote(TopicOrderAutoRevoked, subject, details, db.WarningLevel, tracker.coreOrderInternal())) updatedAssets.count(tracker.fromAssetID) } } @@ -569,8 +569,8 @@ func handleTradeResumptionMsg(c *Core, dc *dexConnection, msg *msgjson.Message) // This is just a notice about a scheduled resumption. dc.setMarketStartEpoch(rs.MarketID, rs.StartEpoch, false) // set the start epoch, leaving any final/persist data resTime := encode.UnixTimeMilli(int64(rs.ResumeTime)) - subject, detail := c.formatDetails(SubjectMarketResumeScheduled, rs.MarketID, dc.acct.host, resTime) - c.notify(newServerNotifyNote(subject, detail, db.WarningLevel)) + subject, detail := c.formatDetails(TopicMarketResumeScheduled, rs.MarketID, dc.acct.host, resTime) + c.notify(newServerNotifyNote(TopicMarketResumeScheduled, subject, detail, db.WarningLevel)) return nil } @@ -587,8 +587,8 @@ func handleTradeResumptionMsg(c *Core, dc *dexConnection, msg *msgjson.Message) // Fetch the updated DEX configuration. // dc.refreshServerConfig() - subject, detail := c.formatDetails(SubjectMarketResumed, rs.MarketID, dc.acct.host, rs.StartEpoch) - c.notify(newServerNotifyNote(subject, detail, db.Success)) + subject, detail := c.formatDetails(TopicMarketResumed, rs.MarketID, dc.acct.host, rs.StartEpoch) + c.notify(newServerNotifyNote(TopicMarketResumed, subject, detail, db.Success)) // Book notes may resume at any time. Seq not set since no book changes. diff --git a/client/core/core.go b/client/core/core.go index a41a49d70b..7ee0273198 100644 --- a/client/core/core.go +++ b/client/core/core.go @@ -444,8 +444,8 @@ func (c *Core) tryCancelTrade(dc *dexConnection, tracker *trackedTrade) error { c.log.Infof("Cancel order %s targeting order %s at %s has been placed", co.ID(), oid, dc.acct.host) - subject, details := c.formatDetails(SubjectCancellingOrder, tracker.token()) - c.notify(newOrderNote(subject, details, db.Poke, tracker.coreOrderInternal())) + subject, details := c.formatDetails(TopicCancellingOrder, tracker.token()) + c.notify(newOrderNote(TopicCancellingOrder, subject, details, db.Poke, tracker.coreOrderInternal())) return nil } @@ -845,8 +845,8 @@ func (dc *dexConnection) reconcileTrades(srvOrderStatuses []*msgjson.OrderStatus oid, previousStatus, newStatus, dc.acct.host) } - subject, details := trade.formatDetails(SubjectOrderStatusUpdate, trade.token(), previousStatus, newStatus) - dc.notify(newOrderNote(subject, details, db.WarningLevel, trade.coreOrderInternal())) + subject, details := trade.formatDetails(TopicOrderStatusUpdate, trade.token(), previousStatus, newStatus) + dc.notify(newOrderNote(TopicOrderStatusUpdate, subject, details, db.WarningLevel, trade.coreOrderInternal())) } // Compare the status reported by the server for each known active trade. Orders @@ -1113,6 +1113,8 @@ type Core struct { net dex.Network lockTimeTaker time.Duration lockTimeMaker time.Duration + + locale map[Topic]*translation localePrinter *message.Printer credMtx sync.RWMutex @@ -1170,6 +1172,11 @@ func New(cfg *Config) (*Core, error) { } } + locale, found := locales[lang.String()] + if !found { + return nil, fmt.Errorf("No translations for language %s", lang) + } + // Try to get the primary credentials, but ignore no-credentials error here // because the client may not be initialized. creds, err := boltDB.PrimaryCredentials() @@ -1198,6 +1205,7 @@ func New(cfg *Config) (*Core, error) { reCrypter: encrypt.Deserialize, latencyQ: wait.NewTickerQueue(recheckInterval), + locale: locale, localePrinter: message.NewPrinter(lang), } @@ -2141,8 +2149,8 @@ func (c *Core) ReconfigureWallet(appPW, newWalletPW []byte, assetID uint32, cfg } c.notify(newBalanceNote(assetID, balances)) // redundant with wallet config note? - subject, details := c.formatDetails(SubjectWalletConfigurationUpdated, unbip(assetID), wallet.address) - c.notify(newWalletConfigNote(subject, details, db.Success, wallet.state())) + subject, details := c.formatDetails(TopicWalletConfigurationUpdated, unbip(assetID), wallet.address) + c.notify(newWalletConfigNote(TopicWalletConfigurationUpdated, subject, details, db.Success, wallet.state())) // Clear any existing tickGovernors for suspect matches. for _, dc := range c.dexConnections() { @@ -2245,8 +2253,8 @@ func (c *Core) setWalletPassword(wallet *xcWallet, newPW []byte, crypter encrypt // Do not disconnect because the Wallet may not allow reconnection. - subject, details := c.formatDetails(SubjectWalletPasswordUpdated, unbip(wallet.AssetID)) - c.notify(newWalletConfigNote(subject, details, db.Success, wallet.state())) + subject, details := c.formatDetails(TopicWalletPasswordUpdated, unbip(wallet.AssetID)) + c.notify(newWalletConfigNote(TopicWalletPasswordUpdated, subject, details, db.Success, wallet.state())) return nil } @@ -2609,8 +2617,8 @@ func (c *Core) Register(form *RegisterForm) (*RegisterResult, error) { c.updateAssetBalance(regFeeAssetID) - subject, details := c.formatDetails(SubjectFeePaymentInProgress, requiredConfs, dc.acct.host) - c.notify(newFeePaymentNote(subject, details, db.Success, dc.acct.host)) + subject, details := c.formatDetails(TopicFeePaymentInProgress, requiredConfs, dc.acct.host) + c.notify(newFeePaymentNote(TopicFeePaymentInProgress, subject, details, db.Success, dc.acct.host)) // Set up the coin waiter, which waits for the required number of // confirmations to notify the DEX and establish an authenticated @@ -2685,8 +2693,8 @@ func (c *Core) verifyRegistrationFee(assetID uint32, dc *dexConnection, coinID [ if confs < reqConfs { dc.setRegConfirms(confs) - subject, details := c.formatDetails(SubjectRegUpdate, confs, reqConfs) - c.notify(newFeePaymentNoteWithConfirmations(subject, details, db.Data, confs, dc.acct.host)) + subject, details := c.formatDetails(TopicRegUpdate, confs, reqConfs) + c.notify(newFeePaymentNoteWithConfirmations(TopicRegUpdate, subject, details, db.Data, confs, dc.acct.host)) } return confs >= reqConfs, nil @@ -2697,12 +2705,12 @@ func (c *Core) verifyRegistrationFee(assetID uint32, dc *dexConnection, coinID [ c.log.Debugf("Registration fee txn %s now has %d confirmations.", coinIDString(wallet.AssetID, coinID), reqConfs) defer func() { if err != nil { - subject, details := c.formatDetails(SubjectFeePaymentError, dc.acct.host, err) - c.notify(newFeePaymentNote(subject, details, db.ErrorLevel, dc.acct.host)) + subject, details := c.formatDetails(TopicFeePaymentError, dc.acct.host, err) + c.notify(newFeePaymentNote(TopicFeePaymentError, subject, details, db.ErrorLevel, dc.acct.host)) } else { - subject, details := c.formatDetails(SubjectAccountRegistered, dc.acct.host) dc.setRegConfirms(regConfirmationsPaid) - c.notify(newFeePaymentNote(subject, details, db.Success, dc.acct.host)) + subject, details := c.formatDetails(TopicAccountRegistered, dc.acct.host) + c.notify(newFeePaymentNote(TopicAccountRegistered, subject, details, db.Success, dc.acct.host)) } }() if err != nil { @@ -2748,8 +2756,8 @@ func (c *Core) InitializeClient(pw, restorationSeed []byte) error { c.setCredentials(creds) if len(restorationSeed) == 0 { - msg := "A new application seed has been created. Make a back up now in the settings view." - c.notify(newSecurityNote(SubjectSeedNeedsSaving, msg, db.Success)) + subject, details := c.formatDetails(TopicSeedNeedsSaving) + c.notify(newSecurityNote(TopicSeedNeedsSaving, subject, details, db.Success)) } return nil @@ -2914,8 +2922,8 @@ func (c *Core) initializePrimaryCredentials(pw []byte, oldKeyParams []byte) erro c.setCredentials(creds) - msg := "The client has been upgraded to use an application seed. Back up the seed now in the settings view." - c.notify(newSecurityNote(SubjectUpgradedToSeed, msg, db.WarningLevel)) + subject, details := c.formatDetails(TopicUpgradedToSeed) + c.notify(newSecurityNote(TopicUpgradedToSeed, subject, details, db.WarningLevel)) for assetID, newEncPW := range walletUpdates { w, found := c.wallet(assetID) @@ -3255,8 +3263,8 @@ func (c *Core) initializeDEXConnections(crypter encrypt.Crypter) []*DEXBrief { // locked, and needs unlocked. err := dc.acct.unlock(crypter) if err != nil { - subject, details := c.formatDetails(SubjectAccountUnlockError, dc.acct.host, err) - c.notify(newFeePaymentNote(subject, details, db.ErrorLevel, dc.acct.host)) + subject, details := c.formatDetails(TopicAccountUnlockError, dc.acct.host, err) + c.notify(newFeePaymentNote(TopicAccountUnlockError, subject, details, db.ErrorLevel, dc.acct.host)) result.AuthErr = details continue } @@ -3269,8 +3277,8 @@ func (c *Core) initializeDEXConnections(crypter encrypt.Crypter) []*DEXBrief { dcrID, _ := dex.BipSymbolID(regFeeAssetSymbol) if !dc.acct.feePaid() { if len(dc.acct.feeCoin) == 0 { - subject, details := c.formatDetails(SubjectFeeCoinError, dc.acct.host) - c.notify(newFeePaymentNote(subject, details, db.ErrorLevel, dc.acct.host)) + subject, details := c.formatDetails(TopicFeeCoinError, dc.acct.host) + c.notify(newFeePaymentNote(TopicFeeCoinError, subject, details, db.ErrorLevel, dc.acct.host)) result.AuthErr = details continue } @@ -3279,16 +3287,16 @@ func (c *Core) initializeDEXConnections(crypter encrypt.Crypter) []*DEXBrief { dcrWallet, err := c.connectedWallet(dcrID) if err != nil { c.log.Debugf("Failed to connect for reFee at %s with error: %v", dc.acct.host, err) - subject, details := c.formatDetails(SubjectWalletConnectionWarning, dc.acct.host) - c.notify(newFeePaymentNote(subject, details, db.WarningLevel, dc.acct.host)) + subject, details := c.formatDetails(TopicWalletConnectionWarning, dc.acct.host) + c.notify(newFeePaymentNote(TopicWalletConnectionWarning, subject, details, db.WarningLevel, dc.acct.host)) result.AuthErr = details continue } if !dcrWallet.unlocked() { err = dcrWallet.Unlock(crypter) if err != nil { - subject, details := c.formatDetails(SubjectWalletUnlockError, dc.acct.host, err) - c.notify(newFeePaymentNote(subject, details, db.ErrorLevel, dc.acct.host)) + subject, details := c.formatDetails(TopicWalletUnlockError, dc.acct.host, err) + c.notify(newFeePaymentNote(TopicWalletUnlockError, subject, details, db.ErrorLevel, dc.acct.host)) result.AuthErr = details continue } @@ -3301,8 +3309,8 @@ func (c *Core) initializeDEXConnections(crypter encrypt.Crypter) []*DEXBrief { defer wg.Done() err := c.authDEX(dc) if err != nil { - subject, details := c.formatDetails(SubjectDexAuthError, dc.acct.host, err) - c.notify(newDEXAuthNote(subject, dc.acct.host, false, details, db.ErrorLevel)) + subject, details := c.formatDetails(TopicDexAuthError, dc.acct.host, err) + c.notify(newDEXAuthNote(TopicDexAuthError, subject, dc.acct.host, false, details, db.ErrorLevel)) result.AuthErr = details // Disable account on AccountNotFoundError. var mErr *msgjson.Error @@ -3338,8 +3346,8 @@ func (c *Core) resolveActiveTrades(crypter encrypt.Crypter) (loaded int) { // loadDBTrades can add to the failed map. ready, err := c.loadDBTrades(dc, crypter, failed) if err != nil { - subject, details := c.formatDetails(SubjectOrderLoadFailure, err) - c.notify(newOrderNote(subject, details, db.ErrorLevel, nil)) + subject, details := c.formatDetails(TopicOrderLoadFailure, err) + c.notify(newOrderNote(TopicOrderLoadFailure, subject, details, db.ErrorLevel, nil)) // Keep going since some trades may still have loaded. } if len(ready) > 0 { @@ -3444,13 +3452,13 @@ func (c *Core) Withdraw(pw []byte, assetID uint32, value uint64, address string) } coin, err := wallet.Withdraw(address, value) if err != nil { - subject, details := c.formatDetails(SubjectWithdrawError, unbip(assetID), err) - c.notify(newWithdrawNote(subject, details, db.ErrorLevel)) + subject, details := c.formatDetails(TopicWithdrawError, unbip(assetID), err) + c.notify(newWithdrawNote(TopicWithdrawError, subject, details, db.ErrorLevel)) return nil, err } - subject, details := c.formatDetails(SubjectWithdrawSend, unbip(assetID), coin) - c.notify(newWithdrawNote(subject, details, db.Success)) + subject, details := c.formatDetails(TopicWithdrawSend, unbip(assetID), coin) + c.notify(newWithdrawNote(TopicWithdrawSend, subject, details, db.Success)) c.updateAssetBalance(assetID) return coin, nil @@ -3844,17 +3852,17 @@ func (c *Core) prepareTrackedTrade(dc *dexConnection, form *TradeForm, crypter e // Send a low-priority notification. corder := tracker.coreOrder() if !form.IsLimit && !form.Sell { - subject, details := c.formatDetails(SubjectYoloPlaced, + subject, details := c.formatDetails(TopicYoloPlaced, float64(corder.Qty)/conversionFactor, unbip(form.Quote), tracker.token()) - c.notify(newOrderNote(subject, details, db.Poke, corder)) + c.notify(newOrderNote(TopicYoloPlaced, subject, details, db.Poke, corder)) } else { rateString := "market" if form.IsLimit { rateString = strconv.FormatFloat(float64(corder.Rate)/conversionFactor, 'f', 8, 64) } - subject, details := c.formatDetails(SubjectOrderPlaced, + subject, details := c.formatDetails(TopicOrderPlaced, sellString(corder.Sell), float64(corder.Qty)/conversionFactor, unbip(form.Base), rateString, tracker.token()) - c.notify(newOrderNote(subject, details, db.Poke, corder)) + c.notify(newOrderNote(TopicOrderPlaced, subject, details, db.Poke, corder)) } return corder, wallets.fromWallet.AssetID, nil @@ -4034,9 +4042,9 @@ func (c *Core) authDEX(dc *dexConnection) error { updatedAssets.count(trade.wallets.fromAsset.ID) } - subject, details := c.formatDetails(SubjectMissingMatches, + subject, details := c.formatDetails(TopicMissingMatches, len(missing), trade.token(), dc.acct.host) - c.notify(newOrderNote(subject, details, db.ErrorLevel, trade.coreOrderInternal())) + c.notify(newOrderNote(TopicMissingMatches, subject, details, db.ErrorLevel, trade.coreOrderInternal())) } // Start negotiation for extra matches for this trade. @@ -4045,8 +4053,8 @@ func (c *Core) authDEX(dc *dexConnection) error { if err != nil { c.log.Errorf("Error negotiating one or more previously unknown matches for order %s reported by %s on connect: %v", oid, dc.acct.host, err) - subject, details := c.formatDetails(SubjectMatchResolutionError, len(extras), dc.acct.host, trade.token()) - c.notify(newOrderNote(subject, details, db.ErrorLevel, trade.coreOrderInternal())) + subject, details := c.formatDetails(TopicMatchResolutionError, len(extras), dc.acct.host, trade.token()) + c.notify(newOrderNote(TopicMatchResolutionError, subject, details, db.ErrorLevel, trade.coreOrderInternal())) } else { // For taker matches in MakerSwapCast, queue up match status // resolution to retrieve the maker's contract and coin. @@ -4083,12 +4091,12 @@ func (c *Core) authDEX(dc *dexConnection) error { // used to properly set order statuses and filled amount. unknownOrdersCount, reconciledOrdersCount := dc.reconcileTrades(result.ActiveOrderStatuses) if unknownOrdersCount > 0 { - subject, details := c.formatDetails(SubjectUnknownOrders, unknownOrdersCount, dc.acct.host) - c.notify(newDEXAuthNote(subject, dc.acct.host, false, details, db.Poke)) + subject, details := c.formatDetails(TopicUnknownOrders, unknownOrdersCount, dc.acct.host) + c.notify(newDEXAuthNote(TopicUnknownOrders, subject, dc.acct.host, false, details, db.Poke)) } if reconciledOrdersCount > 0 { - subject, details := c.formatDetails(SubjectOrdersReconciled, reconciledOrdersCount) - c.notify(newDEXAuthNote(subject, dc.acct.host, false, details, db.Poke)) + subject, details := c.formatDetails(TopicOrdersReconciled, reconciledOrdersCount) + c.notify(newDEXAuthNote(TopicOrdersReconciled, subject, dc.acct.host, false, details, db.Poke)) } if len(matchConflicts) > 0 { @@ -4315,12 +4323,12 @@ func (c *Core) reFee(dcrWallet *xcWallet, dc *dexConnection) { err := c.notifyFee(dc, acctInfo.FeeCoin) if err != nil { c.log.Errorf("reFee %s - notifyfee error: %v", dc.acct.host, err) - subject, details := c.formatDetails(SubjectFeePaymentError, dc.acct.host, err) - c.notify(newFeePaymentNote(subject, details, db.ErrorLevel, dc.acct.host)) + subject, details := c.formatDetails(TopicFeePaymentError, dc.acct.host, err) + c.notify(newFeePaymentNote(TopicFeePaymentError, subject, details, db.ErrorLevel, dc.acct.host)) } else { c.log.Infof("Fee paid at %s", dc.acct.host) - subject, details := c.formatDetails(SubjectAccountRegistered, dc.acct.host) - c.notify(newFeePaymentNote(subject, details, db.Success, dc.acct.host)) + subject, details := c.formatDetails(TopicAccountRegistered, dc.acct.host) + c.notify(newFeePaymentNote(TopicAccountRegistered, subject, details, db.Success, dc.acct.host)) // dc.acct.pay() and c.authDEX???? dc.acct.markFeePaid() err = c.authDEX(dc) @@ -4554,9 +4562,9 @@ func (c *Core) loadDBTrades(dc *dexConnection, crypter encrypt.Crypter, failed m // new matches on an order. func (c *Core) resumeTrades(dc *dexConnection, trackers []*trackedTrade) assetMap { var tracker *trackedTrade - notifyErr := func(subject string, args ...interface{}) { - subject, detail := c.formatDetails(subject, args...) - c.notify(newOrderNote(subject, detail, db.ErrorLevel, tracker.coreOrder())) + notifyErr := func(topic Topic, args ...interface{}) { + subject, detail := c.formatDetails(topic, args...) + c.notify(newOrderNote(topic, subject, detail, db.ErrorLevel, tracker.coreOrder())) } // markUnfunded is used to allow an unfunded order to enter the trades map @@ -4594,7 +4602,7 @@ func (c *Core) resumeTrades(dc *dexConnection, trackers []*trackedTrade) assetMa // Make sure we have the necessary wallets. wallets, err := c.walletSet(dc, tracker.Base(), tracker.Quote(), trade.Sell) if err != nil { - notifyErr(SubjectWalletMissing, tracker.token(), err) + notifyErr(TopicWalletMissing, tracker.token(), err) continue } @@ -4630,13 +4638,13 @@ func (c *Core) resumeTrades(dc *dexConnection, trackers []*trackedTrade) assetMa // Check for unresolvable states. if len(counterSwap) == 0 { match.swapErr = fmt.Errorf("missing counter-swap, order %s, match %s", tracker.ID(), match) - notifyErr(SubjectMatchErrorCoin, match.Side, tracker.token(), match.Status) + notifyErr(TopicMatchErrorCoin, match.Side, tracker.token(), match.Status) continue } counterContract := match.MetaData.Proof.CounterContract if len(counterContract) == 0 { match.swapErr = fmt.Errorf("missing counter-contract, order %s, match %s", tracker.ID(), match) - notifyErr(SubjectMatchErrorContract, match.Side, tracker.token(), match.Status) + notifyErr(TopicMatchErrorContract, match.Side, tracker.token(), match.Status) continue } counterTxData := match.MetaData.Proof.CounterTxData @@ -4663,7 +4671,7 @@ func (c *Core) resumeTrades(dc *dexConnection, trackers []*trackedTrade) assetMa c.log.Debugf("AuditContract error for match %v status %v, refunded = %v, revoked = %v: %v", match, match.Status, len(match.MetaData.Proof.RefundCoin) > 0, match.MetaData.Proof.IsRevoked(), err) - notifyErr(SubjectMatchRecoveryError, unbip(wallets.toAsset.ID), contractStr, tracker.token(), err) + notifyErr(TopicMatchRecoveryError, unbip(wallets.toAsset.ID), contractStr, tracker.token(), err) // The match may become revoked by server. return } @@ -4690,7 +4698,7 @@ func (c *Core) resumeTrades(dc *dexConnection, trackers []*trackedTrade) assetMa } tracker.coins = map[string]asset.Coin{} // should already be if len(coinIDs) == 0 { - notifyErr(SubjectOrderCoinError, tracker.token()) + notifyErr(TopicOrderCoinError, tracker.token()) markUnfunded(tracker, matchesNeedingCoins) // bug - no user resolution } else { byteIDs := make([]dex.Bytes, 0, len(coinIDs)) @@ -4699,7 +4707,7 @@ func (c *Core) resumeTrades(dc *dexConnection, trackers []*trackedTrade) assetMa } coins, err := wallets.fromWallet.FundingCoins(byteIDs) if err != nil || len(coins) == 0 { - notifyErr(SubjectOrderCoinFetchError, tracker.token(), unbip(wallets.fromAsset.ID), err) + notifyErr(TopicOrderCoinFetchError, tracker.token(), unbip(wallets.fromAsset.ID), err) // Block matches needing funding coins. markUnfunded(tracker, matchesNeedingCoins) // Note: tracker is still added to trades map for (1) status @@ -4729,7 +4737,7 @@ func (c *Core) resumeTrades(dc *dexConnection, trackers []*trackedTrade) assetMa } dc.trades[tracker.ID()] = tracker - c.notify(newOrderNote(SubjectOrderLoaded, "", db.Data, tracker.coreOrder())) + c.notify(newOrderNote(TopicOrderLoaded, "", "", db.Data, tracker.coreOrder())) } return relocks } @@ -4836,8 +4844,8 @@ func (c *Core) runMatches(tradeMatches map[order.OrderID]*serverMatches) (assetM // sendOutdatedClientNotification will send a notification to the UI that // indicates the client should be updated to be used with this DEX server. func sendOutdatedClientNotification(c *Core, dc *dexConnection) { - subject, details := c.formatDetails(SubjectUpgradeNeeded, dc.acct.host) - c.notify(newUpgradeNote(subject, details, db.WarningLevel)) + subject, details := c.formatDetails(TopicUpgradeNeeded, dc.acct.host) + c.notify(newUpgradeNote(TopicUpgradeNeeded, subject, details, db.WarningLevel)) } // connectDEX establishes a ws connection to a DEX server using the provided @@ -5044,15 +5052,15 @@ func (dc *dexConnection) broadcastingConnect() bool { // NOTE: Disconnect event notifications may lag behind actual disconnections. func (c *Core) handleConnectEvent(dc *dexConnection, connected bool) { var v uint32 - subject := SubjectDEXConnected + topic := TopicDEXConnected if connected { v = 1 - subject = SubjectDEXDisconnected + topic = TopicDEXDisconnected } atomic.StoreUint32(&dc.connected, v) if dc.broadcastingConnect() { - subject, details := c.formatDetails(subject, dc.acct.host) - dc.notify(newConnEventNote(subject, dc.acct.host, connected, details, db.Poke)) + subject, details := c.formatDetails(topic, dc.acct.host) + dc.notify(newConnEventNote(topic, subject, dc.acct.host, connected, details, db.Poke)) } } @@ -5119,8 +5127,8 @@ func handleRevokeOrderMsg(c *Core, dc *dexConnection, msg *msgjson.Message) erro tracker.revoke() - subject, details := c.formatDetails(SubjectOrderRevoked, tracker.token(), tracker.mktID, dc.acct.host) - c.notify(newOrderNote(subject, details, db.ErrorLevel, tracker.coreOrder())) + subject, details := c.formatDetails(TopicOrderRevoked, tracker.token(), tracker.mktID, dc.acct.host) + c.notify(newOrderNote(TopicOrderRevoked, subject, details, db.ErrorLevel, tracker.coreOrder())) // Update market orders, and the balance to account for unlocked coins. c.updateAssetBalance(tracker.fromAssetID) @@ -5169,8 +5177,8 @@ func handleNotifyMsg(c *Core, dc *dexConnection, msg *msgjson.Message) error { if err != nil { return fmt.Errorf("notify unmarshal error: %w", err) } - txt = fmt.Sprintf("Message from DEX at %s:\n\n\"%s\"\n", dc.acct.host, txt) - c.notify(newServerNotifyNote(dc.acct.host, txt, db.WarningLevel)) + subject, details := c.formatDetails(TopicDEXNotification, dc.acct.host, txt) + c.notify(newServerNotifyNote(TopicDEXNotification, subject, details, db.WarningLevel)) return nil } @@ -5191,8 +5199,8 @@ func handlePenaltyMsg(c *Core, dc *dexConnection, msg *msgjson.Message) error { t := encode.UnixTimeMilli(int64(note.Penalty.Time)) // d := time.Duration(note.Penalty.Duration) * time.Millisecond - subject, details := c.formatDetails(SubjectPenalized, dc.acct.host, note.Penalty.Rule, t, note.Penalty.Details) - c.notify(newServerNotifyNote(subject, details, db.WarningLevel)) + subject, details := c.formatDetails(TopicPenalized, dc.acct.host, note.Penalty.Rule, t, note.Penalty.Details) + c.notify(newServerNotifyNote(TopicPenalized, subject, details, db.WarningLevel)) return nil } @@ -5296,7 +5304,7 @@ func (c *Core) listen(dc *dexConnection) { if len(doneTrades) > 0 { dc.tradeMtx.Lock() for _, trade := range doneTrades { - c.notify(newOrderNote(SubjectOrderRetired, "", db.Data, trade.coreOrder())) + c.notify(newOrderNote(TopicOrderRetired, "", "", db.Data, trade.coreOrder())) delete(dc.trades, trade.ID()) } dc.tradeMtx.Unlock() @@ -5539,11 +5547,11 @@ func processPreimageRequest(c *Core, dc *dexConnection, reqID uint64, oid order. if err != nil { return fmt.Errorf("preimage send error: %w", err) } - subject := SubjectPreimageSent + topic := TopicPreimageSent if isCancel { - subject = SubjectCancelPreimageSent + topic = TopicCancelPreimageSent } - c.notify(newOrderNote(subject, "", db.Data, tracker.coreOrder())) + c.notify(newOrderNote(topic, "", "", db.Data, tracker.coreOrder())) return nil } diff --git a/client/core/core_test.go b/client/core/core_test.go index 305a7f896d..77c73f75a1 100644 --- a/client/core/core_test.go +++ b/client/core/core_test.go @@ -903,8 +903,10 @@ func newTestRig() *testRig { // which may have been previously "disconnected". return conn, nil }, - newCrypter: func([]byte) encrypt.Crypter { return crypter }, - reCrypter: func([]byte, []byte) (encrypt.Crypter, error) { return crypter, crypter.recryptErr }, + newCrypter: func([]byte) encrypt.Crypter { return crypter }, + reCrypter: func([]byte, []byte) (encrypt.Crypter, error) { return crypter, crypter.recryptErr }, + + locale: enUS, localePrinter: message.NewPrinter(language.AmericanEnglish), }, db: tdb, @@ -2670,8 +2672,8 @@ func TestHandlePreimageRequest(t *testing.T) { select { case note := <-notes: - if note.Subject() != SubjectPreimageSent { - t.Fatalf("note subject is %v, not %v", note.Subject(), SubjectPreimageSent) + if note.Topic() != TopicPreimageSent { + t.Fatalf("note subject is %v, not %v", note.Topic(), TopicPreimageSent) } case <-time.After(time.Second): t.Fatal("no order note from preimage request handling") @@ -2762,8 +2764,8 @@ func TestHandlePreimageRequest(t *testing.T) { select { case note := <-notes: - if note.Subject() != SubjectPreimageSent { - t.Fatalf("note subject is %v, not %v", note.Subject(), SubjectPreimageSent) + if note.Topic() != TopicPreimageSent { + t.Fatalf("note subject is %v, not %v", note.Topic(), TopicPreimageSent) } case <-time.After(time.Second): t.Fatal("no order note from preimage request handling") @@ -2902,8 +2904,8 @@ func TestHandlePreimageRequest(t *testing.T) { select { case note := <-notes: - if note.Subject() != SubjectPreimageSent { - t.Fatalf("note subject is %v, not %v", note.Subject(), SubjectPreimageSent) + if note.Topic() != TopicPreimageSent { + t.Fatalf("note subject is %v, not %v", note.Topic(), TopicPreimageSent) } case <-time.After(time.Second): t.Fatal("no order note from preimage request handling") @@ -2981,8 +2983,8 @@ func TestHandlePreimageRequest(t *testing.T) { select { case note := <-notes: - if note.Subject() != SubjectCancelPreimageSent { - t.Fatalf("note subject is %v, not %v", note.Subject(), SubjectCancelPreimageSent) + if note.Topic() != TopicCancelPreimageSent { + t.Fatalf("note subject is %v, not %v", note.Topic(), TopicCancelPreimageSent) } case <-time.After(time.Second): t.Fatal("no order note from preimage request handling") @@ -3146,8 +3148,8 @@ func TestHandlePreimageRequest(t *testing.T) { select { case note := <-notes: - if note.Subject() != SubjectCancelPreimageSent { - t.Fatalf("note subject is %v, not %v", note.Subject(), SubjectPreimageSent) + if note.Topic() != TopicCancelPreimageSent { + t.Fatalf("note subject is %v, not %v", note.Topic(), TopicCancelPreimageSent) } case <-time.After(time.Second): t.Fatal("no order note from preimage request handling") @@ -3230,7 +3232,7 @@ func TestHandleRevokeOrderMsg(t *testing.T) { t.Fatalf("handleRevokeOrderMsg error: %v", err) } - verifyRevokeNotification(orderNotes, SubjectOrderRevoked, t) + verifyRevokeNotification(orderNotes, TopicOrderRevoked, t) if tracker.metaData.Status != order.OrderStatusRevoked { t.Errorf("expected order status %v, got %v", order.OrderStatusRevoked, tracker.metaData.Status) @@ -3372,8 +3374,8 @@ func TestTradeTracking(t *testing.T) { for { select { case note := <-notes: - if note.Severity() == db.ErrorLevel && (note.Subject() == SubjectSwapSendError || - note.Subject() == SubjectInitError || note.Subject() == SubjectReportRedeemError) { + if note.Severity() == db.ErrorLevel && (note.Topic() == TopicSwapSendError || + note.Topic() == TopicInitError || note.Topic() == TopicReportRedeemError) { return note } @@ -4336,7 +4338,7 @@ func TestNotifications(t *testing.T) { defer rig.shutdown() // Insert a notification into the database. - typedNote := newOrderNote("abc", "def", 100, nil) + typedNote := newOrderNote("123", "abc", "def", 100, nil) tCore := rig.core ch := tCore.NotificationFeed() @@ -5334,7 +5336,7 @@ func TestHandleTradeSuspensionMsg(t *testing.T) { t.Fatalf("[handleTradeSuspensionMsg] unexpected error: %v", err) } - verifyRevokeNotification(orderNotes, SubjectOrderAutoRevoked, t) + verifyRevokeNotification(orderNotes, TopicOrderAutoRevoked, t) // Check that the funding coin was returned. Use the tradeMtx for // synchronization. @@ -5443,12 +5445,12 @@ func orderNoteFeed(tCore *Core) (orderNotes chan *OrderNote, done func()) { return orderNotes, done } -func verifyRevokeNotification(ch chan *OrderNote, expectedSubject string, t *testing.T) { +func verifyRevokeNotification(ch chan *OrderNote, expectedTopic Topic, t *testing.T) { select { case actualOrderNote := <-ch: - if expectedSubject != actualOrderNote.SubjectText { - t.Fatalf("SubjectText mismatch. %s != %s", actualOrderNote.SubjectText, - expectedSubject) + if expectedTopic != actualOrderNote.TopicID { + t.Fatalf("SubjectText mismatch. %s != %s", actualOrderNote.TopicID, + expectedTopic) } return case <-tCtx.Done(): diff --git a/client/core/locale_en.go b/client/core/locale_en.go index 0b7233c35d..a1f11e0f00 100644 --- a/client/core/locale_en.go +++ b/client/core/locale_en.go @@ -5,147 +5,305 @@ import ( "golang.org/x/text/message" ) -type Translation struct { - Subject string - Template string +type translation struct { + subject string + template string } -var TemplateKeys = map[string]string{ +// enUS is the American English translations. +var enUS = map[Topic]*translation{ // [host] - SubjectAccountRegistered: "You may now trade at %s", + TopicAccountRegistered: { + subject: "Account registered", + template: "You may now trade at %s", + }, // [confs, host] - SubjectFeePaymentInProgress: "Waiting for %d confirmations before trading at %s", + TopicFeePaymentInProgress: { + subject: "Fee payment in progress", + template: "Waiting for %d confirmations before trading at %s", + }, // [confs, required confs] - SubjectRegUpdate: "Fee payment confirmations %v/%v", + TopicRegUpdate: { + subject: "regupdate", + template: "Fee payment confirmations %v/%v", + }, // [host, error] - SubjectFeePaymentError: "Error encountered while paying fees to %s: %v", + TopicFeePaymentError: { + subject: "Fee payment error", + template: "Error encountered while paying fees to %s: %v", + }, // [host, error] - SubjectAccountUnlockError: "error unlocking account for %s: %v", + TopicAccountUnlockError: { + subject: "Account unlock error", + template: "error unlocking account for %s: %v", + }, // [host] - SubjectFeeCoinError: "Empty fee coin for %s.", + TopicFeeCoinError: { + subject: "Fee coin error", + template: "Empty fee coin for %s.", + }, // [host] - SubjectWalletConnectionWarning: "Incomplete registration detected for %s, but failed to connect to the Decred wallet", + TopicWalletConnectionWarning: { + subject: "Wallet connection warning", + template: "Incomplete registration detected for %s, but failed to connect to the Decred wallet", + }, // [host, error] - SubjectWalletUnlockError: "Connected to Decred wallet to complete registration at %s, but failed to unlock: %v", + TopicWalletUnlockError: { + subject: "Wallet unlock error", + template: "Connected to Decred wallet to complete registration at %s, but failed to unlock: %v", + }, // [ticker, error] - SubjectWithdrawError: "Error encountered during %s withdraw: %v", + TopicWithdrawError: { + subject: "Withdraw error", + template: "Error encountered during %s withdraw: %v", + }, // [ticker, coin ID] - SubjectWithdrawSend: "Withdraw of %s has completed successfully. Coin ID = %s", + TopicWithdrawSend: { + subject: "Withdraw sent", + template: "Withdraw of %s has completed successfully. Coin ID = %s", + }, // [error] - SubjectOrderLoadFailure: "Some orders failed to load from the database: %v", + TopicOrderLoadFailure: { + subject: "Order load failure", + template: "Some orders failed to load from the database: %v", + }, // [qty, ticker, token] - SubjectYoloPlaced: "selling %.8f %s at market rate (%s)", + TopicYoloPlaced: { + subject: "Market order placed", + template: "selling %.8f %s at market rate (%s)", + }, // [sell string, qty, ticker, rate string, token] - SubjectOrderPlaced: "%sing %.8f %s, rate = %s (%s)", + TopicOrderPlaced: { + subject: "Order placed", + template: "%sing %.8f %s, rate = %s (%s)", + }, // [missing count, token, host] - SubjectMissingMatches: "%d matches for order %s were not reported by %q and are considered revoked", + TopicMissingMatches: { + subject: "Missing matches", + template: "%d matches for order %s were not reported by %q and are considered revoked", + }, // [token, error] - SubjectWalletMissing: "Wallet retrieval error for active order %s: %v", + TopicWalletMissing: { + subject: "Wallet missing", + template: "Wallet retrieval error for active order %s: %v", + }, // [side, token, match status] - SubjectMatchErrorCoin: "Match %s for order %s is in state %s, but has no maker swap coin.", + TopicMatchErrorCoin: { + subject: "Match coin error", + template: "Match %s for order %s is in state %s, but has no maker swap coin.", + }, // [side, token, match status] - SubjectMatchErrorContract: "Match %s for order %s is in state %s, but has no maker swap contract.", + TopicMatchErrorContract: { + subject: "Match contract error", + template: "Match %s for order %s is in state %s, but has no maker swap contract.", + }, // [ticker, contract, token, error] - SubjectMatchRecoveryError: "Error auditing counter-party's swap contract (%s %v) during swap recovery on order %s: %v", + TopicMatchRecoveryError: { + subject: "Match recovery error", + template: "Error auditing counter-party's swap contract (%s %v) during swap recovery on order %s: %v", + }, // [token] - SubjectOrderCoinError: "No funding coins recorded for active order %s", + TopicOrderCoinError: { + subject: "Order coin error", + template: "No funding coins recorded for active order %s", + }, // [token, ticker, error] - SubjectOrderCoinFetchError: "Source coins retrieval error for order %s (%s): %v", + TopicOrderCoinFetchError: { + subject: "Order coin fetch error", + template: "Source coins retrieval error for order %s (%s): %v", + }, // [token, ticker, error] - SubjectMissedCancel: "Cancel order did not match for order %s. This can happen if the cancel order is submitted in the same epoch as the trade or if the target order is fully executed before matching with the cancel order.", + TopicMissedCancel: { + subject: "Missed cancel", + template: "Cancel order did not match for order %s. This can happen if the cancel order is submitted in the same epoch as the trade or if the target order is fully executed before matching with the cancel order.", + }, // [capitalized sell string, base ticker, quote ticker, host, token] - SubjectOrderCanceled: "%s order on %s-%s at %s has been canceled (%s)", + TopicOrderCanceled: { + subject: "Order canceled", + template: "%s order on %s-%s at %s has been canceled (%s)", + }, // [capitalized sell string, base ticker, quote ticker, fill percent, token] - SubjectMatchesMade: "%s order on %s-%s %.1f%% filled (%s)", + TopicMatchesMade: { + subject: "Matches made", + template: "%s order on %s-%s %.1f%% filled (%s)", + }, // [qty, ticker, token] - SubjectSwapSendError: "Error encountered sending a swap output(s) worth %.8f %s on order %s", + TopicSwapSendError: { + subject: "Swap send error", + template: "Error encountered sending a swap output(s) worth %.8f %s on order %s", + }, // [match, error] - SubjectInitError: "Error notifying DEX of swap for match %s: %v", + TopicInitError: { + subject: "Swap reporting error", + template: "Error notifying DEX of swap for match %s: %v", + }, // [match, error] - SubjectReportRedeemError: "Error notifying DEX of redemption for match %s: %v", + TopicReportRedeemError: { + subject: "Redeem reporting error", + template: "Error notifying DEX of redemption for match %s: %v", + }, // [qty, ticker, token] - SubjectSwapsInitiated: "Sent swaps worth %.8f %s on order %s", + TopicSwapsInitiated: { + subject: "Swaps initiated", + template: "Sent swaps worth %.8f %s on order %s", + }, // [qty, ticker, token] - SubjectRedemptionError: "Error encountered sending redemptions worth %.8f %s on order %s", + TopicRedemptionError: { + subject: "Redemption error", + template: "Error encountered sending redemptions worth %.8f %s on order %s", + }, // [qty, ticker, token] - SubjectMatchComplete: "Redeemed %.8f %s on order %s", + TopicMatchComplete: { + subject: "Match complete", + template: "Redeemed %.8f %s on order %s", + }, // [qty, ticker, token] - SubjectRefundFailure: "Refunded %.8f %s on order %s, with some errors", + TopicRefundFailure: { + subject: "Refund Failure", + template: "Refunded %.8f %s on order %s, with some errors", + }, // [qty, ticker, token] - SubjectMatchesRefunded: "Refunded %.8f %s on order %s", + TopicMatchesRefunded: { + subject: "Matches Refunded", + template: "Refunded %.8f %s on order %s", + }, // [match ID token] - SubjectMatchRevoked: "Match %s has been revoked", + TopicMatchRevoked: { + subject: "Match revoked", + template: "Match %s has been revoked", + }, // [token, market name, host] - SubjectOrderRevoked: "Order %s on market %s at %s has been revoked by the server", + TopicOrderRevoked: { + subject: "Order revoked", + template: "Order %s on market %s at %s has been revoked by the server", + }, // [token, market name, host] - SubjectOrderAutoRevoked: "Order %s on market %s at %s revoked due to market suspension", + TopicOrderAutoRevoked: { + subject: "Order auto-revoked", + template: "Order %s on market %s at %s revoked due to market suspension", + }, // [ticker, coin ID, match] - SubjectMatchRecovered: "Found maker's redemption (%s: %v) and validated secret for match %s", + TopicMatchRecovered: { + subject: "Match recovered", + template: "Found maker's redemption (%s: %v) and validated secret for match %s", + }, // [token] - SubjectCancellingOrder: "A cancel order has been submitted for order %s", + TopicCancellingOrder: { + subject: "Cancelling order", + template: "A cancel order has been submitted for order %s", + }, // [token, old status, new status] - SubjectOrderStatusUpdate: "Status of order %v revised from %v to %v", + TopicOrderStatusUpdate: { + subject: "Order status update", + template: "Status of order %v revised from %v to %v", + }, // [count, host, token] - SubjectMatchResolutionError: "%d matches reported by %s were not found for %s.", + TopicMatchResolutionError: { + subject: "Match resolution error", + template: "%d matches reported by %s were not found for %s.", + }, // [token] - SubjectFailedCancel: "Cancel order for order %s stuck in Epoch status for 2 epochs and is now deleted.", + TopicFailedCancel: { + subject: "Failed cancel", + template: "Cancel order for order %s stuck in Epoch status for 2 epochs and is now deleted.", + }, // [coin ID, ticker, match] - SubjectAuditTrouble: "Still searching for counterparty's contract coin %v (%s) for match %s. Are your internet and wallet connections good?", + TopicAuditTrouble: { + subject: "Audit trouble", + template: "Still searching for counterparty's contract coin %v (%s) for match %s. Are your internet and wallet connections good?", + }, // [host, error] - SubjectDexAuthError: "%s: %v", + TopicDexAuthError: { + subject: "DEX auth error", + template: "%s: %v", + }, // [count, host] - SubjectUnknownOrders: "%d active orders reported by DEX %s were not found.", + TopicUnknownOrders: { + subject: "DEX reported unknown orders", + template: "%d active orders reported by DEX %s were not found.", + }, // [count] - SubjectOrdersReconciled: "Statuses updated for %d orders.", + TopicOrdersReconciled: { + subject: "Orders reconciled with DEX", + template: "Statuses updated for %d orders.", + }, // [ticker, address] - SubjectWalletConfigurationUpdated: "Configuration for %s wallet has been updated. Deposit address = %s", + TopicWalletConfigurationUpdated: { + subject: "Wallet configuration updated", + template: "Configuration for %s wallet has been updated. Deposit address = %s", + }, // [ticker] - SubjectWalletPasswordUpdated: "Password for %s wallet has been updated.", + TopicWalletPasswordUpdated: { + subject: "Wallet Password Updated", + template: "Password for %s wallet has been updated.", + }, // [market name, host, time] - SubjectMarketSuspendScheduled: "Market %s at %s is now scheduled for suspension at %v", + TopicMarketSuspendScheduled: { + subject: "Market suspend scheduled", + template: "Market %s at %s is now scheduled for suspension at %v", + }, // [market name, host] - SubjectMarketSuspended: "Trading for market %s at %s is now suspended.", + TopicMarketSuspended: { + subject: "Market suspended", + template: "Trading for market %s at %s is now suspended.", + }, // [market name, host] - SubjectMarketSuspendedWithPurge: "Trading for market %s at %s is now suspended. All booked orders are now PURGED.", + TopicMarketSuspendedWithPurge: { + subject: "Market suspended, orders purged", + template: "Trading for market %s at %s is now suspended. All booked orders are now PURGED.", + }, // [market name, host, time] - SubjectMarketResumeScheduled: "Market %s at %s is now scheduled for resumption at %v", + TopicMarketResumeScheduled: { + subject: "Market resume scheduled", + template: "Market %s at %s is now scheduled for resumption at %v", + }, // [market name, host, epoch] - SubjectMarketResumed: "Market %s at %s has resumed trading at epoch %d", + TopicMarketResumed: { + subject: "Market resumed", + template: "Market %s at %s has resumed trading at epoch %d", + }, // [host] - SubjectUpgradeNeeded: "You may need to update your client to trade at %s.", + TopicUpgradeNeeded: { + subject: "Upgrade needed", + template: "You may need to update your client to trade at %s.", + }, // [host] - SubjectDEXConnected: "%s is connected", + TopicDEXConnected: { + subject: "Server connected", + template: "%s is connected", + }, // [host] - SubjectDEXDisconnected: "%s is disconnected", + TopicDEXDisconnected: { + subject: "Server disconnect", + template: "%s is disconnected", + }, // [host, rule, time, details] - SubjectPenalized: "Penalty from DEX at %s\nlast broken rule: %s\ntime: %v\ndetails:\n\"%s\"\n", + TopicPenalized: { + subject: "Server has penalized you", + template: "Penalty from DEX at %s\nlast broken rule: %s\ntime: %v\ndetails:\n\"%s\"\n", + }, + TopicSeedNeedsSaving: { + subject: "Don't forget to back up your application seed", + template: "A new application seed has been created. Make a back up now in the settings view.", + }, + TopicUpgradedToSeed: { + subject: "Back up your new application seed", + template: "The client has been upgraded to use an application seed. Back up the seed now in the settings view.", + }, + TopicDEXNotification: { + subject: "Message from DEX", + template: "%s: %s", + }, } -// EnLocale is the english translations. We can construct the EnLocale in an -// init function, since the subjects and templates are untranslated from the -// TemplateKeys. Other translators will define each entry in a struct literal. -var EnLocale = map[string]*Translation{} - -var Subjects map[string]map[string]string - -func registerTranslations(lang language.Tag, translations map[string]*Translation) { - for subject, t := range translations { - tmplKey := TemplateKeys[subject] - message.SetString(lang, tmplKey, t.Template) - message.SetString(lang, subject, t.Subject) - } +var locales = map[string]map[Topic]*translation{ + language.AmericanEnglish.String(): enUS, } -func inintializeEnLocale() { - for subject, t := range TemplateKeys { - EnLocale[subject] = &Translation{ - Subject: subject, - Template: t, +func init() { + for lang, translations := range locales { + for topic, translation := range translations { + message.SetString(language.Make(lang), string(topic), translation.template) } } - registerTranslations(language.AmericanEnglish, EnLocale) -} - -func init() { - inintializeEnLocale() } diff --git a/client/core/notification.go b/client/core/notification.go index 37fb5666ae..92c1ab391d 100644 --- a/client/core/notification.go +++ b/client/core/notification.go @@ -24,6 +24,7 @@ const ( NoteTypeServerNotify = "notify" NoteTypeSecurity = "security" NoteTypeUpgrade = "upgrade" + NoteTypeDEXAuth = "dex_auth" ) // notify sends a notification to all subscribers. If the notification is of @@ -80,8 +81,13 @@ func (c *Core) AckNotes(ids []dex.Bytes) { } } -func (c *Core) formatDetails(subject string, args ...interface{}) (translatedSubject, details string) { - return c.localePrinter.Sprintf(subject), c.localePrinter.Sprintf(TemplateKeys[subject], args...) +func (c *Core) formatDetails(topic Topic, args ...interface{}) (translatedSubject, details string) { + trans, found := c.locale[topic] + if !found { + c.log.Errorf("no translation found for topic %q", topic) + return string(topic), "translation error" + } + return trans.subject, c.localePrinter.Sprintf(string(topic), args...) } // Notification is an interface for a user notification. Notification is @@ -89,6 +95,10 @@ func (c *Core) formatDetails(subject string, args ...interface{}) (translatedSub type Notification interface { // Type is a string ID unique to the concrete type. Type() string + // Topic is a string ID unique to the message subject. Since subjects must + // be translated, we cannot rely on the subject to programatically identify + // the message. + Topic() Topic // Subject is a short description of the notification contents. When displayed // to the user, the Subject will typically be given visual prominence. For // notifications with Severity < Poke (not meant for display), the Subject @@ -117,6 +127,9 @@ type Notification interface { String() string } +// Topic is a language-independent unique ID for a Notification. +type Topic = db.Topic + // SecurityNote is a note regarding application security, credentials, or // authentication. type SecurityNote struct { @@ -124,13 +137,13 @@ type SecurityNote struct { } const ( - SubjectSeedNeedsSaving = "Don't forget to back up your application seed" - SubjectUpgradedToSeed = "Back up your new application seed" + TopicSeedNeedsSaving Topic = "SeedNeedsSaving" + TopicUpgradedToSeed Topic = "UpgradedToSeed" ) -func newSecurityNote(subject, details string, severity db.Severity) *SecurityNote { +func newSecurityNote(topic Topic, subject, details string, severity db.Severity) *SecurityNote { return &SecurityNote{ - Notification: db.NewNotification(NoteTypeSecurity, subject, details, severity), + Notification: db.NewNotification(NoteTypeSecurity, topic, subject, details, severity), } } @@ -142,26 +155,26 @@ type FeePaymentNote struct { } const ( - SubjectFeePaymentInProgress = "Fee payment in progress" - SubjectRegUpdate = "regupdate" - SubjectFeePaymentError = "Fee payment error" - SubjectAccountRegistered = "Account registered" - SubjectAccountUnlockError = "Account unlock error" - SubjectFeeCoinError = "Fee coin error" - SubjectWalletConnectionWarning = "Wallet connection warning" - SubjectWalletUnlockError = "Wallet unlock error" + TopicFeePaymentInProgress Topic = "FeePaymentInProgress" + TopicRegUpdate Topic = "RegUpdate" + TopicFeePaymentError Topic = "FeePaymentError" + TopicAccountRegistered Topic = "AccountRegistered" + TopicAccountUnlockError Topic = "AccountUnlockError" + TopicFeeCoinError Topic = "FeeCoinError" + TopicWalletConnectionWarning Topic = "WalletConnectionWarning" + TopicWalletUnlockError Topic = "WalletUnlockError" ) -func newFeePaymentNote(subject, details string, severity db.Severity, dexAddr string) *FeePaymentNote { +func newFeePaymentNote(topic Topic, subject, details string, severity db.Severity, dexAddr string) *FeePaymentNote { host, _ := addrHost(dexAddr) return &FeePaymentNote{ - Notification: db.NewNotification(NoteTypeFeePayment, subject, details, severity), + Notification: db.NewNotification(NoteTypeFeePayment, topic, subject, details, severity), Dex: host, } } -func newFeePaymentNoteWithConfirmations(subject, details string, severity db.Severity, currConfs uint32, dexAddr string) *FeePaymentNote { - feePmtNt := newFeePaymentNote(subject, details, severity, dexAddr) +func newFeePaymentNoteWithConfirmations(topic Topic, subject, details string, severity db.Severity, currConfs uint32, dexAddr string) *FeePaymentNote { + feePmtNt := newFeePaymentNote(topic, subject, details, severity, dexAddr) feePmtNt.Confirmations = &currConfs return feePmtNt } @@ -172,13 +185,13 @@ type WithdrawNote struct { } const ( - SubjectWithdrawError = "Withdraw error" - SubjectWithdrawSend = "Withdraw sent" + TopicWithdrawError Topic = "WithdrawError" + TopicWithdrawSend Topic = "WithdrawSend" ) -func newWithdrawNote(subject, details string, severity db.Severity) *WithdrawNote { +func newWithdrawNote(topic Topic, subject, details string, severity db.Severity) *WithdrawNote { return &WithdrawNote{ - Notification: db.NewNotification(NoteTypeWithdraw, subject, details, severity), + Notification: db.NewNotification(NoteTypeWithdraw, topic, subject, details, severity), } } @@ -189,48 +202,47 @@ type OrderNote struct { } const ( - SubjectOrderLoadFailure = "Order load failure" - SubjectOrderPlaced = "Order placed" - SubjectYoloPlaced = "Market order placed" - SubjectMissingMatches = "Missing matches" - SubjectWalletMissing = "Wallet missing" - SubjectMatchErrorCoin = "Match coin error" - SubjectMatchErrorContract = "Match contract error" - SubjectMatchRecoveryError = "Match recovery error" - SubjectNoFundingCoins = "No funding coins" - SubjectOrderCoinError = "Order coin error" - SubjectOrderCoinFetchError = "Order coin fetch error" - SubjectPreimageSent = "preimage sent" - SubjectCancelPreimageSent = "cancel preimage sent" - SubjectMissedCancel = "Missed cancel" - SubjectOrderBooked = "Order booked" - SubjectNoMatch = "No match" - SubjectOrderCanceled = "Order canceled" - SubjectCancel = "cancel" - SubjectMatchesMade = "Matches made" - SubjectSwapSendError = "Swap send error" - SubjectInitError = "Swap reporting error" - SubjectReportRedeemError = "Redeem reporting error" - SubjectSwapsInitiated = "Swaps initiated" - SubjectRedemptionError = "Redemption error" - SubjectMatchComplete = "Match complete" - SubjectRefundFailure = "Refund Failure" - SubjectMatchesRefunded = "Matches Refunded" - SubjectMatchRevoked = "Match revoked" - SubjectOrderRevoked = "Order revoked" - SubjectOrderAutoRevoked = "Order auto-revoked" - SubjectMatchRecovered = "Match recovered" - SubjectCancellingOrder = "Cancelling order" - SubjectOrderStatusUpdate = "Order status update" - SubjectMatchResolutionError = "Match resolution error" - SubjectFailedCancel = "Failed cancel" - SubjectOrderLoaded = "Order loaded" - SubjectOrderRetired = "Order retired" + TopicOrderLoadFailure Topic = "OrderLoadFailure" + TopicOrderPlaced Topic = "OrderPlaced" + TopicYoloPlaced Topic = "YoloPlaced" + TopicMissingMatches Topic = "MissingMatches" + TopicWalletMissing Topic = "WalletMissing" + TopicMatchErrorCoin Topic = "MatchErrorCoin" + TopicMatchErrorContract Topic = "MatchErrorContract" + TopicMatchRecoveryError Topic = "MatchRecoveryError" + TopicOrderCoinError Topic = "OrderCoinError" + TopicOrderCoinFetchError Topic = "OrderCoinFetchError" + TopicPreimageSent Topic = "PreimageSent" + TopicCancelPreimageSent Topic = "CancelPreimageSent" + TopicMissedCancel Topic = "MissedCancel" + TopicOrderBooked Topic = "OrderBooked" + TopicNoMatch Topic = "NoMatch" + TopicOrderCanceled Topic = "OrderCanceled" + TopicCancel Topic = "Cancel" + TopicMatchesMade Topic = "MatchesMade" + TopicSwapSendError Topic = "SwapSendError" + TopicInitError Topic = "InitError" + TopicReportRedeemError Topic = "ReportRedeemError" + TopicSwapsInitiated Topic = "SwapsInitiated" + TopicRedemptionError Topic = "RedemptionError" + TopicMatchComplete Topic = "MatchComplete" + TopicRefundFailure Topic = "RefundFailure" + TopicMatchesRefunded Topic = "MatchesRefunded" + TopicMatchRevoked Topic = "MatchRevoked" + TopicOrderRevoked Topic = "OrderRevoked" + TopicOrderAutoRevoked Topic = "OrderAutoRevoked" + TopicMatchRecovered Topic = "MatchRecovered" + TopicCancellingOrder Topic = "CancellingOrder" + TopicOrderStatusUpdate Topic = "OrderStatusUpdate" + TopicMatchResolutionError Topic = "MatchResolutionError" + TopicFailedCancel Topic = "FailedCancel" + TopicOrderLoaded Topic = "OrderLoaded" + TopicOrderRetired Topic = "OrderRetired" ) -func newOrderNote(subject, details string, severity db.Severity, corder *Order) *OrderNote { +func newOrderNote(topic Topic, subject, details string, severity db.Severity, corder *Order) *OrderNote { return &OrderNote{ - Notification: db.NewNotification(NoteTypeOrder, subject, details, severity), + Notification: db.NewNotification(NoteTypeOrder, topic, subject, details, severity), Order: corder, } } @@ -245,14 +257,14 @@ type MatchNote struct { } const ( - SubjectAudit = "audit" - SubjectAuditTrouble = "Audit trouble" - SubjectNewMatch = "new_match" - SubjectCounterConfirms = "counterconfirms" - SubjectConfirms = "confirms" + TopicAudit Topic = "Audit" + TopicAuditTrouble Topic = "AuditTrouble" + TopicNewMatch Topic = "NewMatch" + TopicCounterConfirms Topic = "CounterConfirms" + TopicConfirms Topic = "Confirms" ) -func newMatchNote(subject, details string, severity db.Severity, t *trackedTrade, match *matchTracker) *MatchNote { +func newMatchNote(topic Topic, subject, details string, severity db.Severity, t *trackedTrade, match *matchTracker) *MatchNote { var counterConfs int64 if match.counterConfirms > 0 { // This can be -1 before it is actually checked, but for purposes of the @@ -260,7 +272,7 @@ func newMatchNote(subject, details string, severity db.Severity, t *trackedTrade counterConfs = match.counterConfirms } return &MatchNote{ - Notification: db.NewNotification(NoteTypeMatch, subject, details, severity), + Notification: db.NewNotification(NoteTypeMatch, topic, subject, details, severity), OrderID: t.ID().Bytes(), Match: matchFromMetaMatchWithConfs(t.Order, &match.MetaMatch, match.swapConfirms, int64(t.wallets.fromAsset.SwapConf), counterConfs, int64(t.wallets.toAsset.SwapConf)), @@ -287,11 +299,13 @@ type EpochNotification struct { Epoch uint64 `json:"epoch"` } +const TopicEpoch Topic = "Epoch" + func newEpochNotification(host, mktID string, epochIdx uint64) *EpochNotification { return &EpochNotification{ Host: host, MarketID: mktID, - Notification: db.NewNotification(NoteTypeEpoch, "", "", db.Data), + Notification: db.NewNotification(NoteTypeEpoch, TopicEpoch, "", "", db.Data), Epoch: epochIdx, } } @@ -309,13 +323,13 @@ type ConnEventNote struct { } const ( - SubjectDEXConnected = "Server connected" - SubjectDEXDisconnected = "Server disconnect" + TopicDEXConnected Topic = "DEXConnected" + TopicDEXDisconnected Topic = "DEXDisconnected" ) -func newConnEventNote(subject, host string, connected bool, details string, severity db.Severity) *ConnEventNote { +func newConnEventNote(topic Topic, subject, host string, connected bool, details string, severity db.Severity) *ConnEventNote { return &ConnEventNote{ - Notification: db.NewNotification(NoteTypeConnEvent, subject, details, severity), + Notification: db.NewNotification(NoteTypeConnEvent, topic, subject, details, severity), Host: host, Connected: connected, } @@ -328,9 +342,11 @@ type BalanceNote struct { Balance *WalletBalance `json:"balance"` } +const TopicBalanceUpdated Topic = "BalanceUpdated" + func newBalanceNote(assetID uint32, bal *WalletBalance) *BalanceNote { return &BalanceNote{ - Notification: db.NewNotification(NoteTypeBalance, "balance updated", "", db.Data), + Notification: db.NewNotification(NoteTypeBalance, TopicBalanceUpdated, "balance updated", "", db.Data), AssetID: assetID, Balance: bal, // Once created, balance is never modified by Core. } @@ -344,14 +360,14 @@ type DEXAuthNote struct { } const ( - SubjectDexAuthError = "DEX auth error" - SubjectUnknownOrders = "DEX reported unknown orders" - SubjectOrdersReconciled = "Orders reconciled with DEX" + TopicDexAuthError Topic = "DexAuthError" + TopicUnknownOrders Topic = "UnknownOrders" + TopicOrdersReconciled Topic = "OrdersReconciled" ) -func newDEXAuthNote(subject, host string, authenticated bool, details string, severity db.Severity) *DEXAuthNote { +func newDEXAuthNote(topic Topic, subject, host string, authenticated bool, details string, severity db.Severity) *DEXAuthNote { return &DEXAuthNote{ - Notification: db.NewNotification("dex_auth", subject, details, severity), + Notification: db.NewNotification(NoteTypeDEXAuth, topic, subject, details, severity), Host: host, Authenticated: authenticated, } @@ -365,13 +381,13 @@ type WalletConfigNote struct { } const ( - SubjectWalletConfigurationUpdated = "Wallet Configuration Updated" - SubjectWalletPasswordUpdated = "Wallet Password Updated" + TopicWalletConfigurationUpdated Topic = "WalletConfigurationUpdated" + TopicWalletPasswordUpdated Topic = "WalletPasswordUpdated" ) -func newWalletConfigNote(subject, details string, severity db.Severity, walletState *WalletState) *WalletConfigNote { +func newWalletConfigNote(topic Topic, subject, details string, severity db.Severity, walletState *WalletState) *WalletConfigNote { return &WalletConfigNote{ - Notification: db.NewNotification(NoteTypeWalletConfig, subject, details, severity), + Notification: db.NewNotification(NoteTypeWalletConfig, topic, subject, details, severity), Wallet: walletState, } } @@ -381,9 +397,11 @@ func newWalletConfigNote(subject, details string, severity db.Severity, walletSt // a Data Severity notification. type WalletStateNote WalletConfigNote +const TopicWalletState Topic = "WalletState" + func newWalletStateNote(walletState *WalletState) *WalletStateNote { return &WalletStateNote{ - Notification: db.NewNotification(NoteTypeWalletState, "", "", db.Data), + Notification: db.NewNotification(NoteTypeWalletState, TopicWalletState, "", "", db.Data), Wallet: walletState, } } @@ -394,17 +412,18 @@ type ServerNotifyNote struct { } const ( - SubjectMarketSuspendScheduled = "Market suspend scheduled" - SubjectMarketSuspended = "Market suspended" - SubjectMarketSuspendedWithPurge = "Market suspended, orders purged" - SubjectMarketResumeScheduled = "Market resume scheduled" - SubjectMarketResumed = "Market resumed" - SubjectPenalized = "Server has penalized you" + TopicMarketSuspendScheduled Topic = "MarketSuspendScheduled" + TopicMarketSuspended Topic = "MarketSuspended" + TopicMarketSuspendedWithPurge Topic = "MarketSuspendedWithPurge" + TopicMarketResumeScheduled Topic = "MarketResumeScheduled" + TopicMarketResumed Topic = "MarketResumed" + TopicPenalized Topic = "Penalized" + TopicDEXNotification Topic = "DEXNotification" ) -func newServerNotifyNote(subject, details string, severity db.Severity) *ServerNotifyNote { +func newServerNotifyNote(topic Topic, subject, details string, severity db.Severity) *ServerNotifyNote { return &ServerNotifyNote{ - Notification: db.NewNotification(NoteTypeServerNotify, subject, details, severity), + Notification: db.NewNotification(NoteTypeServerNotify, topic, subject, details, severity), } } @@ -414,11 +433,11 @@ type UpgradeNote struct { } const ( - SubjectUpgradeNeeded = "Upgrade needed" + TopicUpgradeNeeded Topic = "UpgradeNeeded" ) -func newUpgradeNote(subject, details string, severity db.Severity) *UpgradeNote { +func newUpgradeNote(topic Topic, subject, details string, severity db.Severity) *UpgradeNote { return &UpgradeNote{ - Notification: db.NewNotification(NoteTypeUpgrade, subject, details, severity), + Notification: db.NewNotification(NoteTypeUpgrade, topic, subject, details, severity), } } diff --git a/client/core/trade.go b/client/core/trade.go index 5b1071514b..c45e2acf17 100644 --- a/client/core/trade.go +++ b/client/core/trade.go @@ -148,7 +148,7 @@ type trackedTrade struct { cancel *trackedCancel matches map[order.MatchID]*matchTracker notify func(Notification) - formatDetails func(string, ...interface{}) (string, string) + formatDetails func(Topic, ...interface{}) (string, string) epochLen uint64 fromAssetID uint32 } @@ -156,7 +156,7 @@ type trackedTrade struct { // newTrackedTrade is a constructor for a trackedTrade. func newTrackedTrade(dbOrder *db.MetaOrder, preImg order.Preimage, dc *dexConnection, epochLen uint64, lockTimeTaker, lockTimeMaker time.Duration, db db.DB, latencyQ *wait.TickerQueue, wallets *walletSet, - coins asset.Coins, notify func(Notification), formatDetails func(string, ...interface{}) (string, string)) *trackedTrade { + coins asset.Coins, notify func(Notification), formatDetails func(Topic, ...interface{}) (string, string)) *trackedTrade { fromID := dbOrder.Order.Quote() if dbOrder.Order.Trade().Sell { @@ -348,8 +348,8 @@ func (t *trackedTrade) nomatch(oid order.OrderID) (assetMap, error) { t.cancel = nil t.metaData.LinkedOrder = order.OrderID{} - subject, details := t.formatDetails(SubjectMissedCancel, t.token()) - t.notify(newOrderNote(subject, details, db.WarningLevel, t.coreOrderInternal())) + subject, details := t.formatDetails(TopicMissedCancel, t.token()) + t.notify(newOrderNote(TopicMissedCancel, subject, details, db.WarningLevel, t.coreOrderInternal())) return assets, t.db.UpdateOrderStatus(oid, order.OrderStatusExecuted) } @@ -361,13 +361,13 @@ func (t *trackedTrade) nomatch(oid order.OrderID) (assetMap, error) { if lo, ok := t.Order.(*order.LimitOrder); ok && lo.Force == order.StandingTiF { t.dc.log.Infof("Standing order %s did not match and is now booked.", t.token()) t.metaData.Status = order.OrderStatusBooked - t.notify(newOrderNote(SubjectOrderBooked, "", db.Data, t.coreOrderInternal())) + t.notify(newOrderNote(TopicOrderBooked, "", "", db.Data, t.coreOrderInternal())) } else { t.returnCoins() assets.count(t.wallets.fromAsset.ID) t.dc.log.Infof("Non-standing order %s did not match.", t.token()) t.metaData.Status = order.OrderStatusExecuted - t.notify(newOrderNote(SubjectNoMatch, "", db.Data, t.coreOrderInternal())) + t.notify(newOrderNote(TopicNoMatch, "", "", db.Data, t.coreOrderInternal())) } return assets, t.db.UpdateOrderStatus(t.ID(), t.metaData.Status) } @@ -534,12 +534,12 @@ func (t *trackedTrade) negotiate(msgMatches []*msgjson.Match) error { // Send notifications. corder := t.coreOrderInternal() if cancelMatch != nil { - subject, details := t.formatDetails(SubjectOrderCanceled, + subject, details := t.formatDetails(TopicOrderCanceled, strings.Title(sellString(trade.Sell)), unbip(t.Base()), unbip(t.Quote()), t.dc.acct.host, t.token()) - t.notify(newOrderNote(subject, details, db.Success, corder)) + t.notify(newOrderNote(TopicOrderCanceled, subject, details, db.Poke, corder)) // Also send out a data notification with the cancel order information. - t.notify(newOrderNote(SubjectCancel, "", db.Data, corder)) + t.notify(newOrderNote(TopicCancel, "", "", db.Data, corder)) } if len(newTrackers) > 0 { fillPct := 100 * float64(filled) / float64(trade.Quantity) @@ -548,13 +548,13 @@ func (t *trackedTrade) negotiate(msgMatches []*msgjson.Match) error { // Match notifications. for _, match := range newTrackers { - t.notify(newMatchNote(SubjectNewMatch, "", db.Data, t, match)) + t.notify(newMatchNote(TopicNewMatch, "", "", db.Data, t, match)) } // A single order notification. - subject, details := t.formatDetails(SubjectMatchesMade, + subject, details := t.formatDetails(TopicMatchesMade, strings.Title(sellString(trade.Sell)), unbip(t.Base()), unbip(t.Quote()), fillPct, t.token()) - t.notify(newOrderNote(subject, details, db.Poke, corder)) + t.notify(newOrderNote(TopicMatchesMade, subject, details, db.Poke, corder)) } err := t.db.UpdateOrder(t.metaOrder()) @@ -677,7 +677,7 @@ func (t *trackedTrade) counterPartyConfirms(ctx context.Context, match *matchTra if match.counterConfirms != int64(have) { match.counterConfirms = int64(have) changed = true - t.notify(newMatchNote(SubjectCounterConfirms, "", db.Data, t, match)) + t.notify(newMatchNote(TopicCounterConfirms, "", "", db.Data, t, match)) } return @@ -746,8 +746,8 @@ func (t *trackedTrade) deleteStaleCancelOrder() { t.dc.log.Errorf("DB error unlinking cancel order %s for trade %s: %v", t.cancel.ID(), t.ID(), err) } - subject, details := t.formatDetails(SubjectFailedCancel, t.token()) - t.notify(newOrderNote(subject, details, db.WarningLevel, t.coreOrderInternal())) + subject, details := t.formatDetails(TopicFailedCancel, t.token()) + t.notify(newOrderNote(TopicFailedCancel, subject, details, db.WarningLevel, t.coreOrderInternal())) } // isActive will be true if the trade is booked or epoch, or if any of the @@ -903,7 +903,7 @@ func (t *trackedTrade) isSwappable(ctx context.Context, match *matchTracker) boo t.dc.log.Errorf("error getting confirmation for our own swap transaction: %v", err) } match.swapConfirms = int64(confs) - t.notify(newMatchNote(SubjectConfirms, "", db.Data, t, match)) + t.notify(newMatchNote(TopicConfirms, "", "", db.Data, t, match)) return false } if match.Side == order.Maker && match.Status == order.NewlyMatched { @@ -955,7 +955,7 @@ func (t *trackedTrade) isRedeemable(ctx context.Context, match *matchTracker) bo t.dc.log.Errorf("error getting confirmation for our own swap transaction: %v", err) } match.swapConfirms = int64(confs) - t.notify(newMatchNote(SubjectConfirms, "", db.Data, t, match)) + t.notify(newMatchNote(TopicConfirms, "", "", db.Data, t, match)) return false } if match.Side == order.Taker && match.Status == order.MakerRedeemed { @@ -1158,13 +1158,13 @@ func (c *Core) tick(t *trackedTrade) (assetMap, error) { corder := t.coreOrderInternal() if err != nil { errs.addErr(err) - subject, details := c.formatDetails(SubjectSwapSendError, + subject, details := c.formatDetails(TopicSwapSendError, float64(qty)/conversionFactor, unbip(fromID), t.token()) - t.notify(newOrderNote(subject, details, db.ErrorLevel, corder)) + t.notify(newOrderNote(TopicSwapSendError, subject, details, db.ErrorLevel, corder)) } else { - subject, details := c.formatDetails(SubjectSwapsInitiated, + subject, details := c.formatDetails(TopicSwapsInitiated, float64(qty)/conversionFactor, unbip(fromID), t.token()) - t.notify(newOrderNote(subject, details, db.Poke, corder)) + t.notify(newOrderNote(TopicSwapsInitiated, subject, details, db.Poke, corder)) } } @@ -1188,13 +1188,13 @@ func (c *Core) tick(t *trackedTrade) (assetMap, error) { corder := t.coreOrderInternal() if err != nil { errs.addErr(err) - subject, details := c.formatDetails(SubjectRedemptionError, + subject, details := c.formatDetails(TopicRedemptionError, float64(qty)/conversionFactor, unbip(toAsset), t.token()) - t.notify(newOrderNote(subject, details, db.ErrorLevel, corder)) + t.notify(newOrderNote(TopicRedemptionError, subject, details, db.ErrorLevel, corder)) } else { - subject, details := c.formatDetails(SubjectMatchComplete, + subject, details := c.formatDetails(TopicMatchComplete, float64(qty)/conversionFactor, unbip(toAsset), t.token()) - t.notify(newOrderNote(subject, details, db.Poke, corder)) + t.notify(newOrderNote(TopicMatchComplete, subject, details, db.Poke, corder)) } } @@ -1211,13 +1211,13 @@ func (c *Core) tick(t *trackedTrade) (assetMap, error) { corder := t.coreOrderInternal() if err != nil { errs.addErr(err) - subject, details := c.formatDetails(SubjectRefundFailure, + subject, details := c.formatDetails(TopicRefundFailure, float64(refunded)/conversionFactor, unbip(fromID), t.token()) - t.notify(newOrderNote(subject, details, db.ErrorLevel, corder)) + t.notify(newOrderNote(TopicRefundFailure, subject, details, db.ErrorLevel, corder)) } else { - subject, details := c.formatDetails(SubjectMatchesRefunded, + subject, details := c.formatDetails(TopicMatchesRefunded, float64(refunded)/conversionFactor, unbip(fromID), t.token()) - t.notify(newOrderNote(subject, details, db.WarningLevel, corder)) + t.notify(newOrderNote(TopicMatchesRefunded, subject, details, db.WarningLevel, corder)) } } @@ -1313,8 +1313,8 @@ func (t *trackedTrade) revokeMatch(matchID order.MatchID, fromServer bool) error // Notify the user of the failed match. corder := t.coreOrderInternal() // no cancel order - subject, details := t.formatDetails(SubjectMatchRevoked, token(matchID[:])) - t.notify(newOrderNote(subject, details, db.WarningLevel, corder)) + subject, details := t.formatDetails(TopicMatchRevoked, token(matchID[:])) + t.notify(newOrderNote(TopicMatchRevoked, subject, details, db.WarningLevel, corder)) // Unlock coins if we're not expecting future matches for this // trade and there are no matches that MAY later require sending @@ -1566,8 +1566,8 @@ func (c *Core) sendInitAsync(t *trackedTrade, match *matchTracker, coinID, contr atomic.StoreUint32(&match.sendingInitAsync, 0) if err != nil { corder := t.coreOrder() - subject, details := c.formatDetails(SubjectInitError, match, err) - t.notify(newOrderNote(subject, details, db.ErrorLevel, corder)) + subject, details := c.formatDetails(TopicInitError, match, err) + t.notify(newOrderNote(TopicInitError, subject, details, db.ErrorLevel, corder)) } }() @@ -1779,8 +1779,8 @@ func (c *Core) sendRedeemAsync(t *trackedTrade, match *matchTracker, coinID, sec atomic.StoreUint32(&match.sendingRedeemAsync, 0) if err != nil { corder := t.coreOrder() - subject, details := c.formatDetails(SubjectReportRedeemError, match, err) - t.notify(newOrderNote(subject, details, db.ErrorLevel, corder)) + subject, details := c.formatDetails(TopicReportRedeemError, match, err) + t.notify(newOrderNote(TopicReportRedeemError, subject, details, db.ErrorLevel, corder)) } }() @@ -1905,9 +1905,9 @@ func (t *trackedTrade) findMakersRedemption(match *matchTracker) { t.dc.log.Errorf("waitForRedemptions: error storing match info in database: %v", err) } - subject, details := t.formatDetails(SubjectMatchRecovered, + subject, details := t.formatDetails(TopicMatchRecovered, fromAsset.Symbol, coinIDString(fromAsset.ID, redemptionCoinID), match) - t.notify(newOrderNote(subject, details, db.Poke, t.coreOrderInternal())) + t.notify(newOrderNote(TopicMatchRecovered, subject, details, db.Poke, t.coreOrderInternal())) }() } @@ -2028,7 +2028,7 @@ func (t *trackedTrade) processAuditMsg(msgID uint64, audit *msgjson.Audit) error t.mtx.Lock() auth := &match.MetaData.Proof.Auth auth.AuditStamp, auth.AuditSig = audit.Time, audit.Sig - t.notify(newMatchNote(SubjectAudit, "", db.Data, t, match)) + t.notify(newMatchNote(TopicAudit, "", "", db.Data, t, match)) err = t.db.UpdateMatch(&match.MetaMatch) t.mtx.Unlock() if err != nil { @@ -2082,8 +2082,8 @@ func (t *trackedTrade) searchAuditInfo(match *matchTracker, coinID []byte, contr return wait.DontTryAgain } if tries > 0 && tries%12 == 0 { - subject, detail := t.formatDetails(SubjectAuditTrouble, contractID, contractSymb, match) - t.notify(newOrderNote(subject, detail, db.WarningLevel, t.coreOrder())) + subject, detail := t.formatDetails(TopicAuditTrouble, contractID, contractSymb, match) + t.notify(newOrderNote(TopicAuditTrouble, subject, detail, db.WarningLevel, t.coreOrder())) } tries++ return wait.TryAgain diff --git a/client/core/trade_simnet_test.go b/client/core/trade_simnet_test.go index 03696f603c..878cf3da06 100644 --- a/client/core/trade_simnet_test.go +++ b/client/core/trade_simnet_test.go @@ -501,7 +501,7 @@ func TestOrderStatusReconciliation(t *testing.T) { client2.log("Waiting %v for preimage reveal, order %s", twoEpochs, tracker.token()) preimageRevealed := notes.find(ctx, twoEpochs, func(n Notification) bool { orderNote, isOrderNote := n.(*OrderNote) - if isOrderNote && n.Subject() == SubjectPreimageSent && orderNote.Order.ID.String() == orderID { + if isOrderNote && n.Topic() == TopicPreimageSent && orderNote.Order.ID.String() == orderID { forgetClient2Order(oid) return true } @@ -536,7 +536,7 @@ func TestOrderStatusReconciliation(t *testing.T) { client2.log("Waiting %v for preimage reveal, order %s", twoEpochs, tracker.token()) preimageRevealed := notes.find(ctx, twoEpochs, func(n Notification) bool { orderNote, isOrderNote := n.(*OrderNote) - return isOrderNote && n.Subject() == SubjectPreimageSent && orderNote.Order.ID.String() == orderID + return isOrderNote && n.Topic() == TopicPreimageSent && orderNote.Order.ID.String() == orderID }) if !preimageRevealed { return fmt.Errorf("preimage not revealed for order %s after %s", tracker.token(), twoEpochs) @@ -551,7 +551,7 @@ func TestOrderStatusReconciliation(t *testing.T) { client2.log("Waiting %v for order %s to be partially matched", maxMatchDuration, tracker.token()) matched := notes.find(ctx, maxMatchDuration, func(n Notification) bool { orderNote, isOrderNote := n.(*OrderNote) - return isOrderNote && n.Subject() == SubjectMatchesMade && orderNote.Order.ID.String() == orderID + return isOrderNote && n.Topic() == TopicMatchesMade && orderNote.Order.ID.String() == orderID }) if !matched { return fmt.Errorf("order %s not matched after %s", tracker.token(), maxMatchDuration) @@ -758,8 +758,8 @@ func TestResendPendingRequests(t *testing.T) { for !foundSwapErrorNote { select { case note := <-notes: - foundSwapErrorNote = note.Severity() == db.ErrorLevel && (note.Subject() == SubjectSwapSendError || - note.Subject() == SubjectInitError || note.Subject() == SubjectReportRedeemError) + foundSwapErrorNote = note.Severity() == db.ErrorLevel && (note.Topic() == TopicSwapSendError || + note.Topic() == TopicInitError || note.Topic() == TopicReportRedeemError) case <-time.After(4 * time.Second): return fmt.Errorf("client %d: no init/redeem error note after 4 seconds", client.id) } @@ -939,7 +939,7 @@ func monitorOrderMatchingAndTradeNeg(ctx context.Context, client *tClient, order client.log("Waiting up to %v for matches on order %s", maxMatchDuration, tracker.token()) matched := client.notes.find(ctx, maxMatchDuration, func(n Notification) bool { orderNote, isOrderNote := n.(*OrderNote) - return isOrderNote && n.Subject() == SubjectMatchesMade && orderNote.Order.ID.String() == orderID + return isOrderNote && n.Topic() == TopicMatchesMade && orderNote.Order.ID.String() == orderID }) if ctx.Err() != nil { // context canceled return nil @@ -1348,7 +1348,7 @@ func (client *tClient) registerDEX(ctx context.Context) error { feeTimeout := time.Millisecond*time.Duration(client.dc().cfg.BroadcastTimeout) + 12*time.Second client.log("Waiting %v for fee confirmation notice", feeTimeout) feePaid := client.notes.find(ctx, feeTimeout, func(n Notification) bool { - return n.Type() == NoteTypeFeePayment && n.Subject() == SubjectAccountRegistered + return n.Type() == NoteTypeFeePayment && n.Topic() == TopicAccountRegistered }) if !feePaid { return fmt.Errorf("fee payment not confirmed after %s", feeTimeout) diff --git a/client/db/types.go b/client/db/types.go index 81a2bc7aae..3103c533f6 100644 --- a/client/db/types.go +++ b/client/db/types.go @@ -678,10 +678,14 @@ func RestoreAccountBackup(path string) (*AccountBackup, error) { return ab, nil } +// Topic is a language-independent unique ID for a Notification. +type Topic string + // Notification is information for the user that is typically meant for display, // and is persisted for recall across sessions. type Notification struct { NoteType string `json:"type"` + TopicID Topic `json:"topic"` SubjectText string `json:"subject"` DetailText string `json:"details"` Severeness Severity `json:"severity"` @@ -691,9 +695,10 @@ type Notification struct { } // NewNotification is a constructor for a Notification. -func NewNotification(noteType, subject, details string, severity Severity) Notification { +func NewNotification(noteType string, topic Topic, subject, details string, severity Severity) Notification { note := Notification{ NoteType: noteType, + TopicID: topic, SubjectText: subject, DetailText: details, Severeness: severity, @@ -712,6 +717,11 @@ func (n *Notification) Type() string { return n.NoteType } +// Topic is a language-independent unique ID for the Notification. +func (n *Notification) Topic() Topic { + return n.TopicID +} + // Subject is a short description of the notification contents. func (n *Notification) Subject() string { return n.SubjectText @@ -781,6 +791,8 @@ func DecodeNotification(b []byte) (*Notification, error) { return nil, err } switch ver { + case 1: + return decodeNotification_v1(pushes) case 0: return decodeNotification_v0(pushes) } @@ -788,7 +800,11 @@ func DecodeNotification(b []byte) (*Notification, error) { } func decodeNotification_v0(pushes [][]byte) (*Notification, error) { - if len(pushes) != 5 { + return decodeNotification_v1(append(pushes, []byte{})) +} + +func decodeNotification_v1(pushes [][]byte) (*Notification, error) { + if len(pushes) != 6 { return nil, fmt.Errorf("decodeNotification_v0: expected 5 pushes, got %d", len(pushes)) } if len(pushes[3]) != 1 { @@ -797,6 +813,7 @@ func decodeNotification_v0(pushes [][]byte) (*Notification, error) { return &Notification{ NoteType: string(pushes[0]), + TopicID: Topic(string(pushes[5])), SubjectText: string(pushes[1]), DetailText: string(pushes[2]), Severeness: Severity(pushes[3][0]), @@ -806,12 +823,13 @@ func decodeNotification_v0(pushes [][]byte) (*Notification, error) { // Encode encodes the Notification to a versioned blob. func (n *Notification) Encode() []byte { - return dbBytes{0}. + return dbBytes{1}. AddData([]byte(n.NoteType)). AddData([]byte(n.SubjectText)). AddData([]byte(n.DetailText)). AddData([]byte{byte(n.Severeness)}). - AddData(uint64Bytes(n.TimeStamp)) + AddData(uint64Bytes(n.TimeStamp)). + AddData([]byte(n.TopicID)) } // OrderFilter is used to limit the results returned by a query to (DB).Orders. diff --git a/client/webserver/live_test.go b/client/webserver/live_test.go index 5f989413eb..abf32f78bd 100644 --- a/client/webserver/live_test.go +++ b/client/webserver/live_test.go @@ -829,7 +829,7 @@ func randomBalance(assetID uint32) *core.WalletBalance { func randomBalanceNote(assetID uint32) *core.BalanceNote { return &core.BalanceNote{ - Notification: db.NewNotification(core.NoteTypeBalance, "", "", db.Data), + Notification: db.NewNotification(core.NoteTypeBalance, core.TopicBalanceUpdated, "", "", db.Data), AssetID: assetID, Balance: randomBalance(assetID), } @@ -913,7 +913,7 @@ func (c *TCore) CreateWallet(appPW, walletPW []byte, form *core.WalletForm) erro w := c.walletState(form.AssetID) c.noteFeed <- &core.WalletStateNote{ - Notification: db.NewNotification(core.NoteTypeWalletState, "", "", db.Data), + Notification: db.NewNotification(core.NoteTypeWalletState, core.TopicWalletState, "", "", db.Data), Wallet: w, } @@ -1090,7 +1090,7 @@ out: c.noteFeed <- &core.EpochNotification{ Host: dexAddr, MarketID: mktID, - Notification: db.NewNotification(core.NoteTypeEpoch, "", "", db.Data), + Notification: db.NewNotification(core.NoteTypeEpoch, core.TopicEpoch, "", "", db.Data), Epoch: getEpoch(), } @@ -1146,7 +1146,7 @@ func (c *TCore) runRandomPokes() { for { select { case <-time.NewTimer(nextWait()).C: - note := db.NewNotification(randStr(5, 30), strings.Title(randStr(5, 30)), randStr(5, 100), db.Poke) + note := db.NewNotification(randStr(5, 30), core.Topic(randStr(5, 30)), strings.Title(randStr(5, 30)), randStr(5, 100), db.Poke) c.noteFeed <- ¬e case <-tCtx.Done(): return @@ -1169,7 +1169,7 @@ func (c *TCore) runRandomNotes() { severity = db.WarningLevel } - note := db.NewNotification(randStr(5, 30), strings.Title(randStr(5, 30)), randStr(5, 100), severity) + note := db.NewNotification(randStr(5, 30), core.Topic(randStr(5, 30)), strings.Title(randStr(5, 30)), randStr(5, 100), severity) c.noteFeed <- ¬e case <-tCtx.Done(): return diff --git a/client/webserver/site/src/js/app.js b/client/webserver/site/src/js/app.js index 51860cc16f..8094334d5f 100644 --- a/client/webserver/site/src/js/app.js +++ b/client/webserver/site/src/js/app.js @@ -430,11 +430,11 @@ export default class Application { * is used to update the dex registration status. */ handleFeePaymentNote (note) { - switch (note.subject) { - case 'regupdate': + switch (note.topic) { + case 'RegUpdate': this.updateExchangeRegistration(note.dex, false, note.confirmations) break - case 'Account registered': + case 'AccountRegistered': this.updateExchangeRegistration(note.dex, true) break default: diff --git a/client/webserver/site/src/js/markets.js b/client/webserver/site/src/js/markets.js index 9be37839ab..950029eb9c 100644 --- a/client/webserver/site/src/js/markets.js +++ b/client/webserver/site/src/js/markets.js @@ -1257,7 +1257,7 @@ export default class MarketsPage extends BasePage { const oldStatus = metaOrder.status metaOrder.order = order const bttn = Doc.tmplElement(metaOrder.row, 'cancelBttn') - if (note.subject === 'Missed cancel') { + if (note.topic === 'MissedCancel') { Doc.show(bttn) } if (order.filled === order.qty) { diff --git a/dex/testing/loadbot/mantle.go b/dex/testing/loadbot/mantle.go index 85d5ef3dcc..8a2acbfac3 100644 --- a/dex/testing/loadbot/mantle.go +++ b/dex/testing/loadbot/mantle.go @@ -70,7 +70,7 @@ out: switch n := note.(type) { case *core.FeePaymentNote: // Once registration is complete, register for a book feed. - if n.Subject() == core.SubjectAccountRegistered { + if n.Topic() == core.TopicAccountRegistered { // Even if we're not going to use it, we need to subscribe // to a book feed and keep the channel empty, so that we // can keep receiving book feed notifications. @@ -99,7 +99,7 @@ out: m.replenishBalances() } case *core.MatchNote: - if n.Subject() == core.SubjectNewMatch { + if n.Topic() == core.TopicNewMatch { atomic.AddUint32(&matchCounter, 1) } } diff --git a/dex/testing/loadbot/pingpong.go b/dex/testing/loadbot/pingpong.go index 812b829d4e..d8e34578d6 100644 --- a/dex/testing/loadbot/pingpong.go +++ b/dex/testing/loadbot/pingpong.go @@ -48,13 +48,13 @@ func (p *pingPonger) SetupWallets(m *Mantle) { func (p *pingPonger) HandleNotification(m *Mantle, note core.Notification) { switch n := note.(type) { case *core.FeePaymentNote: - if n.Subject() == core.SubjectAccountRegistered { + if n.Topic() == core.TopicAccountRegistered { p.buy(m) p.sell(m) } case *core.MatchNote: - switch n.Subject() { - case core.SubjectAudit: + switch n.Topic() { + case core.TopicAudit: ord, err := m.Order(n.OrderID) if err != nil { m.fatalError("Error fetching order for match: %v", err)