diff --git a/go.mod b/go.mod index e40d3564f..57d2e7af5 100644 --- a/go.mod +++ b/go.mod @@ -41,6 +41,7 @@ require ( github.com/decred/slog v1.2.0 github.com/decred/vspd/client/v3 v3.0.0 github.com/decred/vspd/types/v2 v2.1.0 + github.com/decred/vspd/types/v3 v3.0.0 github.com/dgraph-io/badger v1.6.2 github.com/gen2brain/beeep v0.0.0-20220402123239-6a3042f4b71a github.com/gomarkdown/markdown v0.0.0-20230922105210-14b16010c2ee @@ -126,7 +127,6 @@ require ( github.com/decred/dcrtime v0.0.0-20191018193024-8d8b4ef0458e // indirect github.com/decred/go-socks v1.1.0 // indirect github.com/decred/vspd/client/v4 v4.0.0 // indirect - github.com/decred/vspd/types/v3 v3.0.0 // indirect github.com/dgraph-io/ristretto v0.0.2 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/ethereum/c-kzg-4844 v1.0.0 // indirect diff --git a/libwallet/assets/dcr/dex_wallet.go b/libwallet/assets/dcr/dex_wallet.go index 7c1a1c733..951f5f281 100644 --- a/libwallet/assets/dcr/dex_wallet.go +++ b/libwallet/assets/dcr/dex_wallet.go @@ -428,8 +428,12 @@ func (dw *DEXWallet) medianTime(ctx context.Context, iBlkHeader *wire.BlockHeade // GetBlock returns the *wire.MsgBlock. // Part of the Wallet interface. func (dw *DEXWallet) GetBlock(ctx context.Context, blockHash *chainhash.Hash) (*wire.MsgBlock, error) { + if dw.syncData == nil || dw.syncData.activeSyncData == nil { + return nil, errors.New("invalid sync state") + } + // TODO: Use a block cache. - blocks, err := dw.syncData.syncer.Blocks(ctx, []*chainhash.Hash{blockHash}) + blocks, err := dw.syncData.activeSyncData.syncer.Blocks(ctx, []*chainhash.Hash{blockHash}) if err != nil { return nil, err } @@ -547,7 +551,7 @@ func (dw *DEXWallet) UnlockAccount(ctx context.Context, pass []byte, _ string) e // Part of the Wallet interface. func (dw *DEXWallet) SyncStatus(_ context.Context) (*dexasset.SyncStatus, error) { ss := new(dexasset.SyncStatus) - if dw.syncData != nil && dw.ctx.Err() == nil { // dex might call this method during wallet shutdown. + if dw.syncData != nil && dw.syncData.activeSyncData != nil { ss.Synced = dw.syncData.isSynced() ss.Blocks = uint64(dw.syncData.syncedTo()) ss.TargetHeight = uint64(dw.syncData.targetHeight()) diff --git a/libwallet/assets/dcr/sync.go b/libwallet/assets/dcr/sync.go index 0d41d2629..214149f72 100644 --- a/libwallet/assets/dcr/sync.go +++ b/libwallet/assets/dcr/sync.go @@ -39,7 +39,7 @@ type SyncData struct { rescanning bool numOfConnectedPeers int32 - *activeSyncData + activeSyncData *activeSyncData } func (s *SyncData) isSynced() bool { @@ -57,7 +57,10 @@ func (s *SyncData) syncedTo() int32 { func (s *SyncData) targetHeight() int32 { s.mu.RLock() defer s.mu.RUnlock() - return s.scanStartHeight + if s.activeSyncData == nil { + return 0 + } + return s.activeSyncData.scanStartHeight } func (s *SyncData) connectedPeers() int32 { @@ -69,7 +72,10 @@ func (s *SyncData) connectedPeers() int32 { func (s *SyncData) generalSyncProgress() *sharedW.GeneralSyncProgress { s.mu.RLock() defer s.mu.RUnlock() - return s.genSyncProgress + if s.activeSyncData == nil { + return nil + } + return s.activeSyncData.genSyncProgress } // reading/writing of properties of this struct are protected by syncData.mu. @@ -167,10 +173,10 @@ func (asset *Asset) SyncInactiveForPeriod(totalInactiveDuration time.Duration) { return } - asset.syncData.totalInactiveDuration += totalInactiveDuration + asset.syncData.activeSyncData.totalInactiveDuration += totalInactiveDuration if asset.syncData.numOfConnectedPeers == 0 { // assume it would take another 60 seconds to reconnect to peers - asset.syncData.totalInactiveDuration += secondsToDuration(60.0) + asset.syncData.activeSyncData.totalInactiveDuration += secondsToDuration(60.0) } } @@ -228,7 +234,7 @@ func (asset *Asset) SpvSync() error { asset.syncData.syncing = true asset.syncData.cancelSync = cancel asset.syncData.syncCanceled = make(chan struct{}) - asset.syncData.syncer = syncer + asset.syncData.activeSyncData.syncer = syncer asset.syncData.mu.Unlock() for _, listener := range asset.syncProgressListeners() { @@ -326,8 +332,8 @@ func (asset *Asset) CurrentSyncStage() utils.SyncStage { asset.syncData.mu.RLock() defer asset.syncData.mu.RUnlock() - if asset.syncData != nil && asset.syncData.syncing { - return asset.syncData.syncStage + if asset.syncData != nil && asset.syncData.syncing && asset.syncData.activeSyncData != nil { + return asset.syncData.activeSyncData.syncStage } return InvalidSyncStage } @@ -336,8 +342,8 @@ func (asset *Asset) IsAddressDiscovering() bool { asset.syncData.mu.RLock() defer asset.syncData.mu.RUnlock() - if asset.syncData != nil && asset.syncData.syncing { - return asset.syncData.isAddressDiscovery + if asset.syncData != nil && asset.syncData.syncing && asset.syncData.activeSyncData != nil { + return asset.syncData.activeSyncData.isAddressDiscovery } return false @@ -347,8 +353,8 @@ func (asset *Asset) IsSycnRescanning() bool { asset.syncData.mu.RLock() defer asset.syncData.mu.RUnlock() - if asset.syncData != nil && asset.syncData.syncing { - return asset.syncData.isRescanning + if asset.syncData != nil && asset.syncData.syncing && asset.syncData.activeSyncData != nil { + return asset.syncData.activeSyncData.isRescanning } return false @@ -370,11 +376,11 @@ func (asset *Asset) SyncData() *SyncData { } func (asset *Asset) PeerInfoRaw() ([]sharedW.PeerInfo, error) { - if !asset.IsConnectedToDecredNetwork() { + if !asset.IsConnectedToDecredNetwork() || asset.syncData.activeSyncData == nil { return nil, errors.New(utils.ErrNotConnected) } - syncer := asset.syncData.syncer + syncer := asset.syncData.activeSyncData.syncer infos := make([]sharedW.PeerInfo, 0, len(syncer.GetRemotePeers())) for _, rp := range syncer.GetRemotePeers() { diff --git a/libwallet/assets/dcr/syncnotification.go b/libwallet/assets/dcr/syncnotification.go index ff10270a0..d040bbef3 100644 --- a/libwallet/assets/dcr/syncnotification.go +++ b/libwallet/assets/dcr/syncnotification.go @@ -54,12 +54,11 @@ func (asset *Asset) handlePeerCountUpdate(peerCount int32) { } // Fetch CFilters Callbacks - func (asset *Asset) fetchCFiltersStarted() { asset.syncData.mu.Lock() - asset.syncData.syncStage = CFiltersFetchSyncStage - asset.syncData.scanStartTime = time.Now() - asset.syncData.scanStartHeight = -1 + asset.syncData.activeSyncData.syncStage = CFiltersFetchSyncStage + asset.syncData.activeSyncData.scanStartTime = time.Now() + asset.syncData.activeSyncData.scanStartHeight = -1 showLogs := asset.syncData.showLogs asset.syncData.mu.Unlock() @@ -72,8 +71,8 @@ func (asset *Asset) fetchCFiltersProgress(startCFiltersHeight, endCFiltersHeight // lock the mutex before reading and writing to asset.syncData.* asset.syncData.mu.Lock() - if asset.syncData.scanStartHeight == -1 { - asset.syncData.scanStartHeight = startCFiltersHeight + if asset.syncData.activeSyncData.scanStartHeight == -1 { + asset.syncData.activeSyncData.scanStartHeight = startCFiltersHeight } var cfiltersFetchData = &sharedW.CFiltersFetchProgressReport{ @@ -81,16 +80,16 @@ func (asset *Asset) fetchCFiltersProgress(startCFiltersHeight, endCFiltersHeight TotalFetchedCFiltersCount: endCFiltersHeight - startCFiltersHeight, } - totalCFiltersToFetch := float64(asset.GetBestBlockHeight() - asset.syncData.scanStartHeight) + totalCFiltersToFetch := float64(asset.GetBestBlockHeight() - asset.syncData.activeSyncData.scanStartHeight) cfiltersFetchProgress := float64(cfiltersFetchData.TotalFetchedCFiltersCount) / totalCFiltersToFetch // If there was some period of inactivity, // assume that this process started at some point in the future, // thereby accounting for the total reported time of inactivity. - asset.syncData.scanStartTime = asset.syncData.scanStartTime.Add(asset.syncData.totalInactiveDuration) - asset.syncData.totalInactiveDuration = 0 + asset.syncData.activeSyncData.scanStartTime = asset.syncData.activeSyncData.scanStartTime.Add(asset.syncData.activeSyncData.totalInactiveDuration) + asset.syncData.activeSyncData.totalInactiveDuration = 0 - timeDurationTaken := time.Since(asset.syncData.scanStartTime) + timeDurationTaken := time.Since(asset.syncData.activeSyncData.scanStartTime) timeTakenSoFar := timeDurationTaken.Seconds() if timeTakenSoFar < 1 { timeTakenSoFar = 1 @@ -142,12 +141,12 @@ func (asset *Asset) fetchCFiltersEnded() { defer asset.syncData.mu.Unlock() // Record the time spent when the filter scan. - asset.syncData.cfiltersScanTimeSpent = time.Since(asset.syncData.scanStartTime) + asset.syncData.activeSyncData.cfiltersScanTimeSpent = time.Since(asset.syncData.activeSyncData.scanStartTime) // Clean up the shared data fields - asset.syncData.scanStartTime = time.Time{} - asset.syncData.scanStartHeight = -1 - asset.syncData.genSyncProgress = nil // clear preset general progress + asset.syncData.activeSyncData.scanStartTime = time.Time{} + asset.syncData.activeSyncData.scanStartHeight = -1 + asset.syncData.activeSyncData.genSyncProgress = nil // clear preset general progress } // Fetch Headers Callbacks @@ -157,7 +156,7 @@ func (asset *Asset) fetchHeadersStarted() { } asset.syncData.mu.RLock() - headersFetchingStarted := asset.syncData.scanStartHeight != -1 + headersFetchingStarted := asset.syncData.activeSyncData.scanStartHeight != -1 asset.syncData.mu.RUnlock() if headersFetchingStarted { @@ -171,14 +170,14 @@ func (asset *Asset) fetchHeadersStarted() { // Returns the best synced block if syncing is done or the best block from // the connected peers if not connected. ctx, _ := asset.ShutdownContextWithCancel() - _, peerInitialHeight := asset.syncData.syncer.Synced(ctx) + _, peerInitialHeight := asset.syncData.activeSyncData.syncer.Synced(ctx) asset.syncData.mu.Lock() - asset.syncData.syncStage = HeadersFetchSyncStage - asset.syncData.scanStartTime = time.Now() - asset.syncData.scanStartHeight = lowestBlockHeight + asset.syncData.activeSyncData.syncStage = HeadersFetchSyncStage + asset.syncData.activeSyncData.scanStartTime = time.Now() + asset.syncData.activeSyncData.scanStartHeight = lowestBlockHeight asset.syncData.bestBlockHeight = peerInitialHeight // Best block synced in the connected peers - asset.syncData.totalInactiveDuration = 0 + asset.syncData.activeSyncData.totalInactiveDuration = 0 showLogs := asset.syncData.showLogs asset.syncData.mu.Unlock() @@ -193,10 +192,10 @@ func (asset *Asset) fetchHeadersProgress(lastFetchedHeaderHeight int32, _ int64) } asset.syncData.mu.RLock() - startHeight := asset.syncData.scanStartHeight - startTime := asset.syncData.scanStartTime + startHeight := asset.syncData.activeSyncData.scanStartHeight + startTime := asset.syncData.activeSyncData.scanStartTime peersBestBlock := asset.syncData.bestBlockHeight - headerSpentTime := asset.syncData.headersScanTimeSpent + headerSpentTime := asset.syncData.activeSyncData.headersScanTimeSpent asset.syncData.mu.RUnlock() if startHeight == -1 { @@ -212,7 +211,7 @@ func (asset *Asset) fetchHeadersProgress(lastFetchedHeaderHeight int32, _ int64) // It returns the best synced block if syncing is done or the best block // from the connected peers if not connected. - _, peersBestBlock = asset.syncData.syncer.Synced(ctx) + _, peersBestBlock = asset.syncData.activeSyncData.syncer.Synced(ctx) if lastFetchedHeaderHeight <= peersBestBlock { asset.syncData.mu.Lock() @@ -285,12 +284,12 @@ func (asset *Asset) fetchHeadersFinished() { } // Record the time spent when the headers scan. - asset.syncData.headersScanTimeSpent = time.Since(asset.syncData.scanStartTime) + asset.syncData.activeSyncData.headersScanTimeSpent = time.Since(asset.syncData.activeSyncData.scanStartTime) // Clean up the shared data fields - asset.syncData.scanStartTime = time.Time{} - asset.syncData.scanStartHeight = -1 - asset.syncData.genSyncProgress = nil // clear preset general progress + asset.syncData.activeSyncData.scanStartTime = time.Time{} + asset.syncData.activeSyncData.scanStartHeight = -1 + asset.syncData.activeSyncData.genSyncProgress = nil // clear preset general progress if asset.syncData.showLogs && asset.syncData.syncing { log.Info("Fetch headers completed.") @@ -298,14 +297,13 @@ func (asset *Asset) fetchHeadersFinished() { } // Address/Account Discovery Callbacks - func (asset *Asset) discoverAddressesStarted() { if !asset.IsSyncing() { return } asset.syncData.mu.RLock() - addressDiscoveryAlreadyStarted := !asset.syncData.scanStartTime.IsZero() + addressDiscoveryAlreadyStarted := !asset.syncData.activeSyncData.scanStartTime.IsZero() asset.syncData.mu.RUnlock() if addressDiscoveryAlreadyStarted { @@ -313,10 +311,10 @@ func (asset *Asset) discoverAddressesStarted() { } asset.syncData.mu.Lock() - asset.syncData.isAddressDiscovery = true - asset.syncData.syncStage = AddressDiscoverySyncStage - asset.syncData.scanStartTime = time.Now() - asset.syncData.addressDiscoveryCompletedOrCanceled = make(chan bool) + asset.syncData.activeSyncData.isAddressDiscovery = true + asset.syncData.activeSyncData.syncStage = AddressDiscoverySyncStage + asset.syncData.activeSyncData.scanStartTime = time.Now() + asset.syncData.activeSyncData.addressDiscoveryCompletedOrCanceled = make(chan bool) asset.syncData.mu.Unlock() go asset.updateAddressDiscoveryProgress() @@ -332,8 +330,8 @@ func (asset *Asset) updateAddressDiscoveryProgress() { everySecondTicker := time.NewTicker(5 * time.Second) asset.syncData.mu.Lock() - totalHeadersFetchTime := asset.syncData.headersScanTimeSpent.Seconds() - totalCfiltersFetchTime := asset.syncData.cfiltersScanTimeSpent.Seconds() + totalHeadersFetchTime := asset.syncData.activeSyncData.headersScanTimeSpent.Seconds() + totalCfiltersFetchTime := asset.syncData.activeSyncData.cfiltersScanTimeSpent.Seconds() asset.syncData.mu.Unlock() // these values will be used every second to calculate the total sync progress @@ -353,14 +351,14 @@ func (asset *Asset) updateAddressDiscoveryProgress() { // assume that this process started at some point in the future, // thereby accounting for the total reported time of inactivity. asset.syncData.mu.Lock() - asset.syncData.scanStartTime = asset.syncData.scanStartTime.Add(asset.syncData.totalInactiveDuration) - asset.syncData.totalInactiveDuration = 0 - addressDiscoveryStartTime := asset.syncData.scanStartTime + asset.syncData.activeSyncData.scanStartTime = asset.syncData.activeSyncData.scanStartTime.Add(asset.syncData.activeSyncData.totalInactiveDuration) + asset.syncData.activeSyncData.totalInactiveDuration = 0 + addressDiscoveryStartTime := asset.syncData.activeSyncData.scanStartTime showLogs := asset.syncData.showLogs asset.syncData.mu.Unlock() select { - case <-asset.syncData.addressDiscoveryCompletedOrCanceled: + case <-asset.syncData.activeSyncData.addressDiscoveryCompletedOrCanceled: // stop calculating and broadcasting address discovery progress everySecondTicker.Stop() if showLogs { @@ -433,16 +431,16 @@ func (asset *Asset) discoverAddressesFinished() { } asset.syncData.mu.Lock() - asset.syncData.isAddressDiscovery = false - asset.syncData.genSyncProgress = nil // clear preset general progress + asset.syncData.activeSyncData.isAddressDiscovery = false + asset.syncData.activeSyncData.genSyncProgress = nil // clear preset general progress // Record the time spent when the headers scan. - asset.syncData.addrDiscoveryTimeSpent = time.Since(asset.syncData.scanStartTime) + asset.syncData.activeSyncData.addrDiscoveryTimeSpent = time.Since(asset.syncData.activeSyncData.scanStartTime) // Clean up the shared data fields - asset.syncData.scanStartTime = time.Time{} - asset.syncData.scanStartHeight = -1 - asset.syncData.genSyncProgress = nil // clear preset general progress + asset.syncData.activeSyncData.scanStartTime = time.Time{} + asset.syncData.activeSyncData.scanStartHeight = -1 + asset.syncData.activeSyncData.genSyncProgress = nil // clear preset general progress asset.syncData.mu.Unlock() err := asset.MarkWalletAsDiscoveredAccounts() // Mark address discovery as completed. @@ -455,10 +453,10 @@ func (asset *Asset) discoverAddressesFinished() { func (asset *Asset) stopUpdatingAddressDiscoveryProgress() { asset.syncData.mu.Lock() - if asset.syncData != nil && asset.syncData.addressDiscoveryCompletedOrCanceled != nil { - close(asset.syncData.addressDiscoveryCompletedOrCanceled) - asset.syncData.addressDiscoveryCompletedOrCanceled = nil - asset.syncData.addrDiscoveryTimeSpent = time.Since(asset.syncData.scanStartTime) + if asset.syncData.activeSyncData != nil && asset.syncData.activeSyncData.addressDiscoveryCompletedOrCanceled != nil { + close(asset.syncData.activeSyncData.addressDiscoveryCompletedOrCanceled) + asset.syncData.activeSyncData.addressDiscoveryCompletedOrCanceled = nil + asset.syncData.activeSyncData.addrDiscoveryTimeSpent = time.Since(asset.syncData.activeSyncData.scanStartTime) } asset.syncData.mu.Unlock() } @@ -467,17 +465,16 @@ func (asset *Asset) stopUpdatingAddressDiscoveryProgress() { func (asset *Asset) rescanStarted() { asset.stopUpdatingAddressDiscoveryProgress() + if !asset.IsSyncing() { + return // ignore if sync is not in progress + } + asset.syncData.mu.Lock() defer asset.syncData.mu.Unlock() - if !asset.syncData.syncing { - // ignore if sync is not in progress - return - } - - asset.syncData.isRescanning = true - asset.syncData.syncStage = HeadersRescanSyncStage - asset.syncData.scanStartTime = time.Now() + asset.syncData.activeSyncData.isRescanning = true + asset.syncData.activeSyncData.syncStage = HeadersRescanSyncStage + asset.syncData.activeSyncData.scanStartTime = time.Now() if asset.syncData.showLogs && asset.syncData.syncing { log.Info("Step 3 of 3 - Scanning block headers.") @@ -486,8 +483,7 @@ func (asset *Asset) rescanStarted() { func (asset *Asset) rescanProgress(rescannedThrough int32) { if !asset.IsSyncing() { - // ignore if sync is not in progress - return + return // ignore if sync is not in progress } totalHeadersToScan := asset.GetBestBlockHeight() @@ -499,14 +495,14 @@ func (asset *Asset) rescanProgress(rescannedThrough int32) { // If there was some period of inactivity, // assume that this process started at some point in the future, // thereby accounting for the total reported time of inactivity. - asset.syncData.scanStartTime = asset.syncData.scanStartTime.Add(asset.syncData.totalInactiveDuration) - asset.syncData.totalInactiveDuration = 0 + asset.syncData.activeSyncData.scanStartTime = asset.syncData.activeSyncData.scanStartTime.Add(asset.syncData.activeSyncData.totalInactiveDuration) + asset.syncData.activeSyncData.totalInactiveDuration = 0 - elapsedRescanTime := time.Since(asset.syncData.scanStartTime) + elapsedRescanTime := time.Since(asset.syncData.activeSyncData.scanStartTime) estimatedTotalRescanTime := elapsedRescanTime.Seconds() / rescanRate totalTimeRemaining := secondsToDuration(estimatedTotalRescanTime) - elapsedRescanTime - totalElapsedTimePreRescans := asset.syncData.cfiltersScanTimeSpent + - asset.syncData.headersScanTimeSpent + asset.syncData.addrDiscoveryTimeSpent + totalElapsedTimePreRescans := asset.syncData.activeSyncData.cfiltersScanTimeSpent + + asset.syncData.activeSyncData.headersScanTimeSpent + asset.syncData.activeSyncData.addrDiscoveryTimeSpent asset.syncData.mu.Unlock() totalElapsedTime := totalElapsedTimePreRescans + elapsedRescanTime @@ -557,20 +553,19 @@ func (asset *Asset) publishHeadersRescanProgress(headersRescanData *sharedW.Head func (asset *Asset) rescanFinished() { if !asset.IsSyncing() { - // ignore if sync is not in progress - return + return // ignore if sync is not in progress } asset.syncData.mu.Lock() - asset.syncData.isRescanning = false + asset.syncData.activeSyncData.isRescanning = false // Record the time spent when the headers scan. - asset.syncData.rescanTimeSpent = time.Since(asset.syncData.scanStartTime) + asset.syncData.activeSyncData.rescanTimeSpent = time.Since(asset.syncData.activeSyncData.scanStartTime) // Clean up the shared data fields - asset.syncData.scanStartTime = time.Time{} - asset.syncData.scanStartHeight = -1 - asset.syncData.genSyncProgress = nil // clear preset general progress + asset.syncData.activeSyncData.scanStartTime = time.Time{} + asset.syncData.activeSyncData.scanStartHeight = -1 + asset.syncData.activeSyncData.genSyncProgress = nil // clear preset general progress asset.syncData.mu.Unlock() } @@ -687,6 +682,6 @@ func (asset *Asset) syncedWallet(synced bool) { // running sync. func (asset *Asset) updateGeneralSyncProgress(progress *sharedW.GeneralSyncProgress) { asset.syncData.mu.Lock() - asset.syncData.genSyncProgress = progress + asset.syncData.activeSyncData.genSyncProgress = progress asset.syncData.mu.Unlock() } diff --git a/libwallet/assets/ltc/wallet.go b/libwallet/assets/ltc/wallet.go index 1809f8dd1..4c3f56885 100644 --- a/libwallet/assets/ltc/wallet.go +++ b/libwallet/assets/ltc/wallet.go @@ -316,7 +316,6 @@ func (asset *Asset) NeutrinoClient() *ChainService { func (asset *Asset) IsSynced() bool { asset.syncData.mu.RLock() defer asset.syncData.mu.RUnlock() - return asset.syncData.synced } diff --git a/libwallet/ext/rate_source.go b/libwallet/ext/rate_source.go index 1c038d106..c94039cca 100644 --- a/libwallet/ext/rate_source.go +++ b/libwallet/ext/rate_source.go @@ -87,14 +87,21 @@ var ( stats: "https://api.kucoin.com/api/v1/market/stats?symbol=%s", // symbol like BTC-USDT } - // supportedMarkets is a map of markets supported by rate sources - // implemented (Binance, Bittrex). - supportedMarkets = map[values.Market]*struct{}{ + // supportedUSDTMarkets is a map of usdt markets supported by rate sources + // implemented (Binance, Bittrex etc). + supportedUSDTMarkets = map[values.Market]*struct{}{ values.BTCUSDTMarket: {}, values.DCRUSDTMarket: {}, values.LTCUSDTMarket: {}, } + // At the time of implementation, only messari support was added for BTC + // markets. + supportedBTCMarkets = map[values.Market]*struct{}{ + values.LTCBTCMarket: {}, + values.DCRBTCMarket: {}, + } + // Rates exceeding rateExpiry are expired and should be removed unless there // was an error fetching a new rate. rateExpiry = 30 * time.Minute @@ -320,7 +327,9 @@ func (cs *CommonRateSource) Refresh(force bool) { tickers = cs.copyRates() } - for market := range supportedMarkets { + // Refresh only supportedUSDTMarkets, supportedBTCMarkets are requested for + // special use-case. + for market := range supportedUSDTMarkets { t, ok := tickers[market] if ok && time.Since(t.lastUpdate) < rateExpiry { continue @@ -479,7 +488,7 @@ func (cs *CommonRateSource) coinpaprikaGetTicker(market values.Market) (*Ticker, cs.mtx.Lock() for _, coinInfo := range res { market := values.NewMarket(coinInfo.Symbol, "USDT") - _, found := supportedMarkets[market] + _, found := supportedUSDTMarkets[market] if !found { continue } @@ -511,6 +520,7 @@ func messariGetTicker(market values.Market) (*Ticker, error) { Data struct { MarketData struct { Price float64 `json:"price_usd"` + BTCPrice float64 `json:"price_btc"` PercentChange float64 `json:"percent_change_usd_last_24_hours"` } `json:"market_data"` } `json:"data"` @@ -523,11 +533,16 @@ func messariGetTicker(market values.Market) (*Ticker, error) { ticker := &Ticker{ Market: market.String(), // Ok: e.g BTC-USDT - LastTradePrice: res.Data.MarketData.Price, lastUpdate: time.Now(), PriceChangePercent: &res.Data.MarketData.PercentChange, } + if market == values.DCRBTCMarket || market == values.LTCBTCMarket { + ticker.LastTradePrice = res.Data.MarketData.BTCPrice + } else { + ticker.LastTradePrice = res.Data.MarketData.Price + } + return ticker, nil } @@ -601,8 +616,9 @@ func isSupportedMarket(market values.Market, rateSource string) (values.Market, } marketName := values.NewMarket(fromCur, toCur) - _, ok := supportedMarkets[marketName] - if !ok { + _, notUSDTMarket := supportedUSDTMarkets[marketName] + _, notBTCMarket := supportedBTCMarkets[marketName] + if !notUSDTMarket && !notBTCMarket { return "", false } diff --git a/libwallet/instantswap.go b/libwallet/instantswap.go index 265b37625..72aecf8cb 100644 --- a/libwallet/instantswap.go +++ b/libwallet/instantswap.go @@ -16,15 +16,21 @@ import ( "github.com/crypto-power/cryptopower/libwallet/utils" "github.com/crypto-power/cryptopower/ui/values" "github.com/crypto-power/instantswap/blockexplorer" + _ "github.com/crypto-power/instantswap/blockexplorer/blockcypher" //nolint:revive _ "github.com/crypto-power/instantswap/blockexplorer/btcexplorer" //nolint:revive _ "github.com/crypto-power/instantswap/blockexplorer/dcrexplorer" ) const ( - // BTCBlockTime is the average time it takes to mine a block on the BTC network. + // BTCBlockTime is the average time it takes to mine a block on the BTC + // network. BTCBlockTime = 10 * time.Minute - // DCRBlockTime is the average time it takes to mine a block on the DCR network. + // DCRBlockTime is the average time it takes to mine a block on the DCR + // network. DCRBlockTime = 5 * time.Minute + // LTCBlockTime is approx. how long it takes to mine a block on the Litecoin + // network. + LTCBlockTime = 3 * time.Minute // DefaultMarketDeviation is the maximum deviation the server rate // can deviate from the market rate. @@ -126,47 +132,57 @@ func (mgr *AssetsManager) StartScheduler(ctx context.Context, params instantswap market := values.NewMarket(fromCur, toCur) source := mgr.RateSource.Name() - ticker := mgr.RateSource.GetTicker(market, false) // TODO: (@ukane-philemon) Can we proceed if there is no rate information from rate source? I think we should. + ticker := mgr.RateSource.GetTicker(market, false) if ticker == nil { - return errors.E(op, fmt.Errorf("unable to get market rate from %s", source)) + log.Errorf("unable to get market(%s) rate from %s.", market, source) + log.Infof("Proceeding without checking market rate deviation...") + } else { + exchangeServerRate := res.ExchangeRate // estimated receivable value for libwallet.DefaultRateRequestAmount (1) + rateSourceRate := ticker.LastTradePrice + + // Current rate source supported Binance and Bittrex always returns + // ticker.LastTradePrice in's the quote asset unit e.g DCR-BTC, LTC-BTC. + // We will also do this when and if USDT is supported. + if strings.EqualFold(fromCur, "btc") { + rateSourceRate = 1 / ticker.LastTradePrice + } + + serverRateStr := values.StringF(values.StrServerRate, params.Order.ExchangeServer.Server, fromCur, exchangeServerRate, toCur) + log.Info(serverRateStr) + binanceRateStr := values.StringF(values.StrCurrencyConverterRate, source, fromCur, rateSourceRate, toCur) + log.Info(binanceRateStr) + + // Check if the server rate deviates from the market rate by ± 5% + // exit if true + percentageDiff := math.Abs((exchangeServerRate-rateSourceRate)/((exchangeServerRate+rateSourceRate)/2)) * 100 + if percentageDiff > params.MaxDeviationRate { + errMsg := fmt.Errorf("exchange rate deviates from the market rate by (%.2f%%) more than %.2f%%", percentageDiff-params.MaxDeviationRate, params.MaxDeviationRate) + log.Error(errMsg) + return errors.E(op, errMsg) + } } - exchangeServerRate := res.EstimatedAmount // estimated receivable value for libwallet.DefaultRateRequestAmount (1) - rateSourceRate := ticker.LastTradePrice - // Current rate source supported Binance and Bittrex always returns - // ticker.LastTradePrice in's the quote asset unit e.g DCR-BTC, LTC-BTC. - // We will also do this when and if USDT is supported. - if strings.EqualFold(fromCur, "btc") { - rateSourceRate = 1 / ticker.LastTradePrice + // set the max send amount to the max limit set by the server + invoicedAmount := res.Max + walletBalance := sourceAccountBalance.Spendable.ToCoin() + + estimatedBalanceAfterExchange := walletBalance - invoicedAmount + // if the max send limit is 0, then the server does not have a max limit + // constraint so we can send the entire source wallet balance + if res.Max == 0 || estimatedBalanceAfterExchange < params.BalanceToMaintain { + invoicedAmount = walletBalance - params.BalanceToMaintain // deduct the balance to maintain from the source wallet balance } - serverRateStr := values.StringF(values.StrServerRate, params.Order.ExchangeServer.Server, fromCur, exchangeServerRate, toCur) - log.Info(serverRateStr) - binanceRateStr := values.StringF(values.StrCurrencyConverterRate, source, fromCur, rateSourceRate, toCur) - log.Info(binanceRateStr) - - // check if the server rate deviates from the market rate by ± 5% - // exit if true - percentageDiff := math.Abs((exchangeServerRate-rateSourceRate)/((exchangeServerRate+rateSourceRate)/2)) * 100 - if percentageDiff > params.MaxDeviationRate { - log.Error("exchange rate deviates from the market rate by more than 5%") - return errors.E(op, "exchange rate deviates from the market rate by more than 5%") + if invoicedAmount <= 0 { + errMsg := fmt.Errorf("balance to maintain is the same or greater than wallet balance(Current Balance: %v, Balance to Maintain: %v)", walletBalance, params.BalanceToMaintain) + log.Error(errMsg) + return errors.E(op, errMsg) } - // set the max send amount to the max limit set by the server - invoicedAmount := res.Min - - // if the max send limit is 0, then the server does not have a max limit constraint - // so we can send the entire source wallet balance - // if res.Max == 0 { - // invoicedAmount = sourceAccountBalance.Spendable.ToCoin() - params.BalanceToMaintain // deduct the balance to maintain from the source wallet balance - // } - - log.Info("Order Scheduler: check balance after exchange") - estimatedBalanceAfterExchange := sourceAccountBalance.Spendable.ToCoin() - invoicedAmount - if estimatedBalanceAfterExchange < params.BalanceToMaintain { - log.Error("source wallet balance after the exchange would be less than the set balance to maintain") - return errors.E(op, "source wallet balance after the exchange would be less than the set balance to maintain") // stop scheduling if the source wallet balance after the exchange would be less than the set balance to maintain + if invoicedAmount == walletBalance { + errMsg := "Specify a little balance to maintain to cover for transaction fees... e.g 0.001 for DCR to BTC or LTC swaps" + log.Error(errMsg) + return errors.E(op, errMsg) } log.Info("Order Scheduler: creating order") @@ -179,8 +195,8 @@ func (mgr *AssetsManager) StartScheduler(ctx context.Context, params instantswap lastOrderTime = time.Now() log.Info("Order Scheduler: creating unsigned transaction") - // construct the transaction to send the invoiced amount to the exchange server + // construct the transaction to send the invoiced amount to the exchange server err = sourceWallet.NewUnsignedTx(params.Order.SourceAccountNumber, nil) if err != nil { return errors.E(op, err) @@ -194,7 +210,12 @@ func (mgr *AssetsManager) StartScheduler(ctx context.Context, params instantswap amount = dcr.AmountAtom(params.Order.InvoicedAmount) } - log.Infof("Order Scheduler: adding send destination, address: %s, amount: %d", order.DepositAddress, amount) + log.Infof("Order Scheduler: adding send destination, address: %s, amount: %.2f", order.DepositAddress, params.Order.InvoicedAmount) + // TODO: Broadcast will fail below if params.Order.InvoicedAmount is the + // same as the current wallet balance. We should be able to consider + // wallet fees for the transaction whilst constructing the transaction. + // As a temporary band aid, a check has been added above to error if + // swap amount does not consider tx fees. err = sourceWallet.AddSendDestination(0, order.DepositAddress, amount, false) if err != nil { log.Error("error adding send destination: ", err.Error()) @@ -225,6 +246,9 @@ func (mgr *AssetsManager) StartScheduler(ctx context.Context, params instantswap case utils.DCRWalletAsset.String(): log.Info("Order Scheduler: waiting for dcr block time (5 minutes)") time.Sleep(DCRBlockTime) + case utils.LTCWalletAsset.String(): + log.Info("Order Scheduler: waiting for ltc block time (~3 minutes)") + time.Sleep(LTCBlockTime) } log.Info("Order Scheduler: get newly created order info") @@ -326,7 +350,8 @@ func (mgr *AssetsManager) IsOrderSchedulerRunning() bool { return mgr.InstantSwap.CancelOrderScheduler != nil } -// GetShedulerRuntime returns the duration the order scheduler has been running. -func (mgr *AssetsManager) GetShedulerRuntime() string { +// GetSchedulerRuntime returns the duration the order scheduler has been +// running. +func (mgr *AssetsManager) GetSchedulerRuntime() string { return time.Since(mgr.InstantSwap.SchedulerStartTime).Round(time.Second).String() } diff --git a/ui/page/components/account_dropdown.go b/ui/page/components/account_dropdown.go index 51508d8f2..17313661e 100644 --- a/ui/page/components/account_dropdown.go +++ b/ui/page/components/account_dropdown.go @@ -179,6 +179,10 @@ func (d *AccountDropdown) Handle(gtx C) { func (d *AccountDropdown) Layout(gtx C, title string) D { return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(func(gtx C) D { + if title == "" { + return D{} + } + lbl := d.Theme.H6(title) lbl.TextSize = values.TextSizeTransform(d.IsMobileView(), values.TextSize16) lbl.Font.Weight = font.SemiBold diff --git a/ui/page/dcrdex/market.go b/ui/page/dcrdex/market.go index d238a6c51..c1ee44935 100644 --- a/ui/page/dcrdex/market.go +++ b/ui/page/dcrdex/market.go @@ -792,7 +792,7 @@ func (pg *DEXMarketPage) orderForm(gtx C) D { } } - balStr = fmt.Sprintf("%f %s", availableAssetBal, baseOrQuoteAssetSym) + balStr = fmt.Sprintf("%s %s", trimmedConventionalAmtString(availableAssetBal), baseOrQuoteAssetSym) totalSubText, lotsSubText := pg.orderFormEditorSubtext() return cryptomaterial.LinearLayout{ Width: cryptomaterial.MatchParent, @@ -1420,6 +1420,11 @@ func (pg *DEXMarketPage) orderFormEditorSubtext() (totalSubText, lotsSubText str } func (pg *DEXMarketPage) handleEditorEvents(gtx C) { + isMktOrder := pg.isMarketOrder() + if pg.priceEditor.Editor.Text() == "" && !isMktOrder && !pg.priceEditor.IsFocused() { + pg.refreshPriceField() + } + if pg.toggleBuyAndSellBtn.Changed() { pg.refreshOrderForm() pg.setMaxBuyAndMaxSell() @@ -1429,11 +1434,10 @@ func (pg *DEXMarketPage) handleEditorEvents(gtx C) { pg.refreshPriceField() } - isMktOrder := pg.isMarketOrder() mkt := pg.selectedMarketInfo() var reEstimateFee bool - for pg.priceEditor.Changed() { + for pg.priceEditor.Changed() && pg.priceEditor.IsFocused() { pg.priceEditor.SetError("") priceStr := pg.priceEditor.Editor.Text() if isMktOrder || priceStr == "" { @@ -1446,7 +1450,13 @@ func (pg *DEXMarketPage) handleEditorEvents(gtx C) { continue } - formattedPrice := price - mkt.MsgRateToConventional(mkt.ConventionalRateToMsg(price)%mkt.RateStep) + mktRate := mkt.ConventionalRateToMsg(price) + if mktRate < mkt.MinimumRate { + pg.priceEditor.SetError(values.StringF(values.StrInvalidRateFmt, trimmedConventionalAmtString(price), trimmedConventionalAmtString(mkt.MsgRateToConventional(mkt.MinimumRate)))) + continue + } + + formattedPrice := price - mkt.MsgRateToConventional(mktRate%mkt.RateStep) if formattedPrice != price { pg.priceEditor.Editor.SetText(trimmedConventionalAmtString(formattedPrice)) } @@ -1484,9 +1494,17 @@ func (pg *DEXMarketPage) refreshPriceField() { pg.priceEditor.Editor.ReadOnly = isMktOrder if isMktOrder { pg.priceEditor.Editor.SetText(values.String(values.StrMarket)) - } else if mkt != nil && mkt.SpotPrice != nil { - price := mkt.MsgRateToConventional(mkt.SpotPrice.Rate) - pg.priceEditor.Editor.SetText(trimmedConventionalAmtString(price)) + } else if mkt != nil { + orderPrice := pg.orderPrice(mkt) + if orderPrice == 0 { + pg.priceEditor.Editor.SetText("") + } else { + pg.priceEditor.Editor.SetText(trimmedConventionalAmtString(orderPrice)) + formattedPrice := orderPrice - mkt.MsgRateToConventional(mkt.ConventionalRateToMsg(orderPrice)%mkt.RateStep) + if formattedPrice != orderPrice { + pg.priceEditor.Editor.SetText(trimmedConventionalAmtString(formattedPrice)) + } + } } else { pg.priceEditor.Editor.SetText("") } @@ -1903,6 +1921,14 @@ func (pg *DEXMarketPage) orderPrice(mkt *core.Market) (price float64) { limitOrdPriceStr := pg.priceEditor.Editor.Text() if !pg.isMarketOrder() && limitOrdPriceStr != "" { price, _ = strconv.ParseFloat(limitOrdPriceStr, 64) + } else if mkt != nil && !pg.isSellOrder() { + var midGap uint64 + if pg.selectedMarketOrderBook.book != nil { + midGap, _ = pg.selectedMarketOrderBook.book.MidGap() + } + if midGap != 0 { + price = mkt.MsgRateToConventional(midGap) + } } else if mkt != nil && mkt.SpotPrice != nil { price = mkt.MsgRateToConventional(mkt.SpotPrice.Rate) } diff --git a/ui/page/exchange/create_order_page.go b/ui/page/exchange/create_order_page.go index bc5b9fb91..a7bba32c2 100644 --- a/ui/page/exchange/create_order_page.go +++ b/ui/page/exchange/create_order_page.go @@ -87,11 +87,11 @@ type orderData struct { exchange api.IDExchange exchangeServer instantswap.ExchangeServer - sourceAccountSelector *components.AccountDropdown sourceWalletSelector *components.WalletDropdown + sourceAccountSelector *components.AccountDropdown - destinationAccountSelector *components.AccountDropdown destinationWalletSelector *components.WalletDropdown + destinationAccountSelector *components.AccountDropdown sourceWalletID int sourceAccountNumber int32 @@ -310,57 +310,6 @@ func (pg *CreateOrderPage) OnNavigatedFrom() { pg.stopNtfnListeners() } -func (pg *CreateOrderPage) handleEditorEvents(gtx C) { - for { - event, ok := pg.fromAmountEditor.Edit.Editor.Update(gtx) - if !ok { - break - } - - if gtx.Source.Focused(pg.fromAmountEditor.Edit.Editor) { - switch event.(type) { - case widget.ChangeEvent: - pg.setToAmount(pg.fromAmountEditor.Edit.Editor.Text()) - } - } - } - - for { - event, ok := pg.toAmountEditor.Edit.Editor.Update(gtx) - if !ok { - break - } - - if gtx.Source.Focused(pg.toAmountEditor.Edit.Editor) { - switch event.(type) { - case widget.ChangeEvent: - if pg.inputsNotEmpty(pg.toAmountEditor.Edit.Editor) { - f, err := strconv.ParseFloat(pg.toAmountEditor.Edit.Editor.Text(), 32) - if err != nil { - // empty usd input - pg.fromAmountEditor.Edit.Editor.SetText("") - pg.amountErrorText = values.String(values.StrInvalidAmount) - pg.fromAmountEditor.Edit.LineColor = pg.Theme.Color.Danger - pg.toAmountEditor.Edit.LineColor = pg.Theme.Color.Danger - return - } - pg.amountErrorText = "" - if pg.exchangeRate != -1 { - value := f * pg.exchangeRate - v := strconv.FormatFloat(value, 'f', 8, 64) - pg.amountErrorText = "" - pg.fromAmountEditor.Edit.LineColor = pg.Theme.Color.Gray2 - pg.toAmountEditor.Edit.LineColor = pg.Theme.Color.Gray2 - pg.fromAmountEditor.Edit.Editor.SetText(v) - } - } else { - pg.fromAmountEditor.Edit.Editor.SetText("") - } - } - } - } -} - func (pg *CreateOrderPage) HandleUserInteractions(gtx C) { pg.createOrderBtn.SetEnabled(pg.canCreateOrder()) @@ -451,10 +400,6 @@ func (pg *CreateOrderPage) HandleUserInteractions(gtx C) { pg.destinationAddress = destinationAddress orderSchedulerModal := newOrderSchedulerModalModal(pg.Load, pg.orderData). - OnOrderSchedulerStarted(func() { - infoModal := modal.NewSuccessModal(pg.Load, values.String(values.StrSchedulerRunning), modal.DefaultClickFunc()) - pg.ParentWindow().ShowModal(infoModal) - }). OnCancel(func() { // needed to satisfy the modal instance pg.scheduler.SetChecked(false) }) @@ -479,6 +424,53 @@ func (pg *CreateOrderPage) HandleUserInteractions(gtx C) { pg.walletCreationSuccessFunc(false, assetToCreate) }, assetToCreate)) } + + if pg.sourceWalletSelector != nil { + pg.sourceWalletSelector.Handle(gtx) + } + + if pg.sourceAccountSelector != nil { + pg.sourceAccountSelector.Handle(gtx) + } + + if pg.destinationWalletSelector != nil { + pg.destinationWalletSelector.Handle(gtx) + } + + if pg.destinationAccountSelector != nil { + pg.destinationAccountSelector.Handle(gtx) + } + + for pg.fromAmountEditor.Edit.Changed() && pg.fromAmountEditor.Edit.IsFocused() { + pg.setToAmount(pg.fromAmountEditor.Edit.Editor.Text()) + } + + for pg.toAmountEditor.Edit.Changed() && pg.toAmountEditor.Edit.IsFocused() { + amountTxt := pg.toAmountEditor.Edit.Editor.Text() + if amountTxt == "" { + pg.fromAmountEditor.Edit.Editor.SetText("") + continue + } + + f, err := strconv.ParseFloat(amountTxt, 32) + if err != nil { + // empty usd input + pg.fromAmountEditor.Edit.Editor.SetText("") + pg.amountErrorText = values.String(values.StrInvalidAmount) + pg.fromAmountEditor.Edit.LineColor = pg.Theme.Color.Danger + pg.toAmountEditor.Edit.LineColor = pg.Theme.Color.Danger + return + } + pg.amountErrorText = "" + if pg.exchangeRate != -1 { + value := f * pg.exchangeRate + v := strconv.FormatFloat(value, 'f', 8, 64) + pg.amountErrorText = "" + pg.fromAmountEditor.Edit.LineColor = pg.Theme.Color.Gray2 + pg.toAmountEditor.Edit.LineColor = pg.Theme.Color.Gray2 + pg.fromAmountEditor.Edit.Editor.SetText(v) + } + } } func (pg *CreateOrderPage) setToAmount(amount string) { @@ -574,6 +566,9 @@ func (pg *CreateOrderPage) updateAssetSelection(selectedFromAsset []libutils.Ass pg.fromCurrency = selectedAsset pg.fromAmountEditor.AssetTypeSelector.SetSelectedAssetType(pg.fromCurrency) + if ok := pg.resetSourceWallet(nil); !ok { + return selectedAsset, false + } // If the to and from asset are the same, select a new to asset. if selectedAsset == pg.toCurrency { @@ -581,11 +576,10 @@ func (pg *CreateOrderPage) updateAssetSelection(selectedFromAsset []libutils.Ass allAssets := pg.AssetsManager.AllAssetTypes() for _, asset := range allAssets { if asset != selectedAsset { - // Select the first available asset as the new to asset. - pg.toCurrency = asset pg.toAmountEditor.AssetTypeSelector.SetSelectedAssetType(pg.toCurrency) + pg.resetDestinationWallet(nil) break } @@ -598,6 +592,9 @@ func (pg *CreateOrderPage) updateAssetSelection(selectedFromAsset []libutils.Ass pg.toCurrency = selectedAsset pg.toAmountEditor.AssetTypeSelector.SetSelectedAssetType(pg.toCurrency) + if ok := pg.resetDestinationWallet(nil); !ok { + return selectedAsset, false + } // If the to and from asset are the same, select a new from asset. if selectedAsset == pg.fromCurrency { @@ -606,11 +603,10 @@ func (pg *CreateOrderPage) updateAssetSelection(selectedFromAsset []libutils.Ass allAssets := pg.AssetsManager.AllAssetTypes() for _, asset := range allAssets { if asset != selectedAsset { - // Select the first available asset as the new from asset. - pg.fromCurrency = asset pg.fromAmountEditor.AssetTypeSelector.SetSelectedAssetType(pg.fromCurrency) + pg.resetSourceWallet(nil) break } @@ -689,7 +685,6 @@ func (pg *CreateOrderPage) isMultipleAssetTypeWalletAvailable() bool { } func (pg *CreateOrderPage) Layout(gtx C) D { - pg.handleEditorEvents(gtx) if pg.isFirstVisit { return pg.Theme.List(pg.splashPageContainer).Layout(gtx, 1, func(gtx C, _ int) D { return pg.splashPage(gtx) @@ -762,7 +757,6 @@ func (pg *CreateOrderPage) Layout(gtx C) D { return layout.Stack{}.Layout(gtx, layout.Expanded(pg.layoutMobile), overlay) } return layout.Stack{}.Layout(gtx, layout.Expanded(pg.layoutDesktop), overlay) - }) }) }) @@ -920,7 +914,7 @@ func (pg *CreateOrderPage) layoutDesktop(gtx C) D { if pg.fetchingRate { gtx.Constraints.Max.X = gtx.Dp(values.MarginPadding16) gtx.Constraints.Min.X = gtx.Constraints.Max.X - return pg.materialLoader.Layout(gtx) + return layout.Inset{Top: values.MarginPadding5}.Layout(gtx, pg.materialLoader.Layout) } txt := pg.Theme.Label(textSize14, pg.exchangeRateInfo) txt.Color = pg.Theme.Color.Gray1 @@ -941,7 +935,7 @@ func (pg *CreateOrderPage) layoutDesktop(gtx C) D { if pg.fetchingRate { gtx.Constraints.Max.X = gtx.Dp(values.MarginPadding16) gtx.Constraints.Min.X = gtx.Constraints.Max.X - return pg.materialLoader.Layout(gtx) + return layout.Inset{Top: values.MarginPadding5}.Layout(gtx, pg.materialLoader.Layout) } fromCur := strings.ToUpper(pg.fromCurrency.String()) @@ -1042,7 +1036,7 @@ func (pg *CreateOrderPage) layoutDesktop(gtx C) D { if pg.AssetsManager.InstantSwap.IsSyncing() { gtx.Constraints.Max.X = gtx.Dp(values.MarginPadding8) gtx.Constraints.Min.X = gtx.Constraints.Max.X - return layout.Inset{Bottom: values.MarginPadding1}.Layout(gtx, pg.materialLoader.Layout) + return layout.Inset{Top: values.MarginPadding2, Bottom: values.MarginPadding1}.Layout(gtx, pg.materialLoader.Layout) } return pg.Theme.NewIcon(pg.Theme.Icons.NavigationRefresh).LayoutTransform(gtx, pg.IsMobileView(), values.MarginPadding18) }) @@ -1113,7 +1107,7 @@ func (pg *CreateOrderPage) orderSchedulerLayout(gtx C) D { }.Layout(gtx, pg.Theme.Icons.TimerIcon.Layout12dp) }), layout.Rigid(func(gtx C) D { - title := pg.Theme.Label(textSize16, pg.AssetsManager.GetShedulerRuntime()) + title := pg.Theme.Label(textSize16, pg.AssetsManager.GetSchedulerRuntime()) title.Color = pg.Theme.Color.GrayText2 return title.Layout(gtx) }), @@ -1203,7 +1197,7 @@ func (pg *CreateOrderPage) showConfirmOrderModal() { confirmOrderModal := newConfirmOrderModal(pg.Load, pg.orderData). OnOrderCompleted(func(order *instantswap.Order) { - pg.scroll.FetchScrollData(false, pg.ParentWindow(), false) + pg.scroll.FetchScrollData(false, pg.ParentWindow(), true) successModal := modal.NewCustomModal(pg.Load). Title(values.String(values.StrOrderSubmitted)). SetCancelable(true). @@ -1320,64 +1314,38 @@ func (pg *CreateOrderPage) loadOrderConfig() { if noSourceWallet { isConfigUpdateRequired = true } - pg.sourceWalletSelector = components. - NewWalletDropdown(pg.Load, pg.fromCurrency). - Setup(sourceWallet) - sourceWallet = pg.sourceWalletSelector.SelectedWallet() - // Source account picker - pg.sourceAccountSelector = components.NewAccountDropdown(pg.Load). - AccountValidator(func(account *sharedW.Account) bool { - return account.Number != load.MaxInt32 && !sourceWallet.IsWatchingOnlyWallet() - }). - Setup(pg.sourceWalletSelector.SelectedWallet()) + pg.resetSourceWallet(sourceWallet) if sourceAccount != -1 { - if _, err := sourceWallet.GetAccount(sourceAccount); err != nil { + if _, err := pg.sourceWalletSelector.SelectedWallet().GetAccount(sourceAccount); err != nil { log.Error(err) } } if pg.sourceAccountSelector.SelectedAccount() == nil { isConfigUpdateRequired = true - _ = pg.sourceAccountSelector.Setup(sourceWallet) + _ = pg.sourceAccountSelector.Setup(pg.sourceWalletSelector.SelectedWallet()) } - pg.sourceWalletSelector.SetChangedCallback(func(selectedWallet sharedW.Asset) { - _ = pg.sourceAccountSelector.Setup(selectedWallet) - }) - // Destination wallet picker if noDestinationWallet { isConfigUpdateRequired = true } - pg.destinationWalletSelector = components. - NewWalletDropdown(pg.Load, pg.toCurrency). - Setup(destinationWallet) - destinationWallet = pg.destinationWalletSelector.SelectedWallet() - // Destination account picker - pg.destinationAccountSelector = components.NewAccountDropdown(pg.Load). - AccountValidator(func(account *sharedW.Account) bool { - return account.Number != load.MaxInt32 - }). - Setup(pg.destinationWalletSelector.SelectedWallet()) + pg.resetDestinationWallet(destinationWallet) if destinationAccount != -1 { - if _, err := destinationWallet.GetAccount(destinationAccount); err != nil { + if _, err := pg.destinationWalletSelector.SelectedWallet().GetAccount(destinationAccount); err != nil { log.Error(err) } } if pg.destinationAccountSelector.SelectedAccount() == nil { isConfigUpdateRequired = true - _ = pg.destinationAccountSelector.Setup(destinationWallet) + _ = pg.destinationAccountSelector.Setup(pg.destinationWalletSelector.SelectedWallet()) } - pg.destinationWalletSelector.SetChangedCallback(func(selectedWallet sharedW.Asset) { - _ = pg.destinationAccountSelector.Setup(selectedWallet) - }) - if isConfigUpdateRequired { pg.updateExchangeConfig() } @@ -1386,6 +1354,62 @@ func (pg *CreateOrderPage) loadOrderConfig() { pg.toAmountEditor.AssetTypeSelector.SetSelectedAssetType(pg.toCurrency) } +func (pg *CreateOrderPage) resetSourceWallet(sourceWallet sharedW.Asset) bool { + if sourceWallet == nil { + // Try to fetch a wallet that match from currency. + wallets := pg.AssetsManager.AssetWallets(pg.fromCurrency) + if len(wallets) == 0 { + return false + } + + sourceWallet = wallets[0] + } + + pg.sourceWalletSelector = components. + NewWalletDropdown(pg.Load, pg.fromCurrency). + SetChangedCallback(func(a sharedW.Asset) { + _ = pg.sourceAccountSelector.Setup(a) + }). + Setup(sourceWallet) + + // Source account picker + pg.sourceAccountSelector = components.NewAccountDropdown(pg.Load). + AccountValidator(func(account *sharedW.Account) bool { + return account.Number != load.MaxInt32 && !sourceWallet.IsWatchingOnlyWallet() + }). + Setup(pg.sourceWalletSelector.SelectedWallet()) + + return true +} + +func (pg *CreateOrderPage) resetDestinationWallet(destinationWallet sharedW.Asset) bool { + if destinationWallet == nil { + // Try to fetch a wallet that match from currency. + wallets := pg.AssetsManager.AssetWallets(pg.toCurrency) + if len(wallets) == 0 { + return false + } + + destinationWallet = wallets[0] + } + + pg.destinationWalletSelector = components. + NewWalletDropdown(pg.Load, pg.toCurrency). + SetChangedCallback(func(selectedWallet sharedW.Asset) { + _ = pg.destinationAccountSelector.Setup(selectedWallet) + }). + Setup(destinationWallet) + + // Destination account picker + pg.destinationAccountSelector = components.NewAccountDropdown(pg.Load). + AccountValidator(func(account *sharedW.Account) bool { + return account.Number != load.MaxInt32 + }). + Setup(pg.destinationWalletSelector.SelectedWallet()) + + return true +} + // updateExchangeConfig Updates the newly created or modified exchange // configuration. func (pg *CreateOrderPage) updateExchangeConfig() { @@ -1408,11 +1432,11 @@ func (pg *CreateOrderPage) updateExchangeConfig() { func (pg *CreateOrderPage) listenForNotifications() { orderNotificationListener := &instantswap.OrderNotificationListener{ OnExchangeOrdersSynced: func() { - pg.scroll.FetchScrollData(false, pg.ParentWindow(), false) + pg.scroll.FetchScrollData(false, pg.ParentWindow(), true) pg.ParentWindow().Reload() }, OnOrderCreated: func(_ *instantswap.Order) { - pg.scroll.FetchScrollData(false, pg.ParentWindow(), false) + pg.scroll.FetchScrollData(false, pg.ParentWindow(), true) pg.ParentWindow().Reload() }, OnOrderSchedulerStarted: func() { diff --git a/ui/page/exchange/order_details_page.go b/ui/page/exchange/order_details_page.go index f2c6ec224..488c71b0c 100644 --- a/ui/page/exchange/order_details_page.go +++ b/ui/page/exchange/order_details_page.go @@ -3,7 +3,6 @@ package exchange import ( "context" "fmt" - "time" "gioui.org/layout" "gioui.org/widget/material" @@ -13,6 +12,7 @@ import ( libutils "github.com/crypto-power/cryptopower/libwallet/utils" "github.com/crypto-power/cryptopower/ui/cryptomaterial" "github.com/crypto-power/cryptopower/ui/load" + "github.com/crypto-power/cryptopower/ui/modal" "github.com/crypto-power/cryptopower/ui/page/components" "github.com/crypto-power/cryptopower/ui/values" api "github.com/crypto-power/instantswap/instantswap" @@ -78,13 +78,15 @@ func NewOrderDetailsPage(l *load.Load, order *instantswap.Order) *OrderDetailsPa go func() { pg.isRefreshing = true - pg.orderInfo, err = pg.getOrderInfo(pg.orderInfo.UUID) + orderInfo, err := pg.getOrderInfo(pg.orderInfo.UUID) if err != nil { - pg.isRefreshing = false log.Error(err) + pg.notifyError(err) + pg.isRefreshing = false + return } - time.Sleep(1 * time.Second) + pg.orderInfo = orderInfo pg.isRefreshing = false }() @@ -109,9 +111,15 @@ func (pg *OrderDetailsPage) HandleUserInteractions(gtx C) { if pg.refreshBtn.Clicked(gtx) { go func() { pg.isRefreshing = true - pg.orderInfo, _ = pg.getOrderInfo(pg.orderInfo.UUID) + orderInfo, err := pg.getOrderInfo(pg.orderInfo.UUID) + if err != nil { + log.Error(err) + pg.notifyError(err) + pg.isRefreshing = false + return + } - time.Sleep(1 * time.Second) + pg.orderInfo = orderInfo pg.isRefreshing = false }() } @@ -121,8 +129,13 @@ func (pg *OrderDetailsPage) HandleUserInteractions(gtx C) { } } +func (pg *OrderDetailsPage) notifyError(err error) { + m := modal.NewErrorModal(pg.Load, values.String(values.StrUnexpectedError), modal.DefaultClickFunc()).Body(values.StringF(values.StrUnexpectedErrorMsgFmt, err)) + pg.ParentWindow().ShowModal(m) +} + func (pg *OrderDetailsPage) Layout(gtx C) D { - container := func(gtx C) D { + return cryptomaterial.UniformPadding(gtx, func(gtx C) D { sp := components.SubPage{ Load: pg.Load, Title: values.String(values.StrOrderDetails), @@ -133,9 +146,7 @@ func (pg *OrderDetailsPage) Layout(gtx C) D { Body: pg.layout, } return sp.Layout(pg.ParentWindow(), gtx) - } - - return cryptomaterial.UniformPadding(gtx, container) + }) } func (pg *OrderDetailsPage) layout(gtx C) D { diff --git a/ui/page/exchange/order_scheduler_modal.go b/ui/page/exchange/order_scheduler_modal.go index 387f9033e..c7574672d 100644 --- a/ui/page/exchange/order_scheduler_modal.go +++ b/ui/page/exchange/order_scheduler_modal.go @@ -17,7 +17,7 @@ import ( libutils "github.com/crypto-power/cryptopower/libwallet/utils" "github.com/crypto-power/cryptopower/ui/cryptomaterial" "github.com/crypto-power/cryptopower/ui/load" - "github.com/crypto-power/cryptopower/ui/page/components" + "github.com/crypto-power/cryptopower/ui/modal" "github.com/crypto-power/cryptopower/ui/values" api "github.com/crypto-power/instantswap/instantswap" ) @@ -31,8 +31,7 @@ type orderSchedulerModal struct { pageContainer *widget.List - orderSchedulerStarted func() - onCancel func() + onCancel func() cancelBtn cryptomaterial.Button startBtn cryptomaterial.Button @@ -123,11 +122,6 @@ func newOrderSchedulerModalModal(l *load.Load, data *orderData) *orderSchedulerM return osm } -func (osm *orderSchedulerModal) OnOrderSchedulerStarted(orderSchedulerStarted func()) *orderSchedulerModal { - osm.orderSchedulerStarted = orderSchedulerStarted - return osm -} - func (osm *orderSchedulerModal) OnCancel(cancel func()) *orderSchedulerModal { osm.onCancel = cancel return osm @@ -171,32 +165,24 @@ func (osm *orderSchedulerModal) Handle(gtx C) { }() } - for { - event, ok := osm.balanceToMaintain.Editor.Update(gtx) - if !ok { - break + for osm.balanceToMaintain.Changed() && osm.balanceToMaintain.IsFocused() { + balanceToMaintain := osm.balanceToMaintain.Editor.Text() + if balanceToMaintain == "" { + continue } - if gtx.Source.Focused(osm.balanceToMaintain.Editor) { - switch event.(type) { - case widget.ChangeEvent: - if components.InputsNotEmpty(osm.balanceToMaintain.Editor) { - f, err := strconv.ParseFloat(osm.balanceToMaintain.Editor.Text(), 32) - if err != nil { - osm.balanceToMaintainErrorText = values.String(values.StrInvalidAmount) - osm.balanceToMaintain.LineColor = osm.Theme.Color.Danger - return - } - - if f >= osm.sourceAccountSelector.SelectedAccount().Balance.Spendable.ToCoin() || f < 0 { - osm.balanceToMaintainErrorText = values.String(values.StrInvalidAmount) - osm.balanceToMaintain.LineColor = osm.Theme.Color.Danger - return - } - osm.balanceToMaintainErrorText = "" - - } - } + osm.balanceToMaintainErrorText = "" + f, err := strconv.ParseFloat(osm.balanceToMaintain.Editor.Text(), 32) + if err != nil { + osm.balanceToMaintainErrorText = values.String(values.StrInvalidAmount) + osm.balanceToMaintain.LineColor = osm.Theme.Color.Danger + return + } + + if f >= osm.sourceAccountSelector.SelectedAccount().Balance.Spendable.ToCoin() || f < 0 { + osm.balanceToMaintainErrorText = values.String(values.StrInvalidAmount) + osm.balanceToMaintain.LineColor = osm.Theme.Color.Danger + return } } } @@ -279,7 +265,7 @@ func (osm *orderSchedulerModal) Layout(gtx layout.Context) D { if osm.fetchingRate { gtx.Constraints.Max.X = gtx.Dp(values.MarginPadding16) gtx.Constraints.Min.X = gtx.Constraints.Max.X - return osm.materialLoader.Layout(gtx) + return layout.Inset{Top: values.MarginPadding5}.Layout(gtx, osm.materialLoader.Layout) } fromCur := osm.fromCurrency.String() @@ -499,12 +485,22 @@ func (osm *orderSchedulerModal) startOrderScheduler() { SpendingPassphrase: osm.passwordEditor.Editor.Text(), } + successModal := modal.NewSuccessModal(osm.Load, values.String(values.StrSchedulerRunning), modal.DefaultClickFunc()) go func() { - _ = osm.AssetsManager.StartScheduler(context.Background(), params) + err = osm.AssetsManager.StartScheduler(context.Background(), params) + if err != nil { + // Dismiss the success modal if still displayed before showing + // the error modal. + successModal.Dismiss() + + errModal := modal.NewErrorModal(osm.Load, values.String(values.StrOrderScheduler), modal.DefaultClickFunc()). + Body(values.StringF(values.StrUnexpectedErrorMsgFmt, err.Error())) + osm.ParentWindow().ShowModal(errModal) + } }() osm.Dismiss() - osm.orderSchedulerStarted() + osm.ParentWindow().ShowModal(successModal) }() } diff --git a/ui/page/exchange/order_settings_modal.go b/ui/page/exchange/order_settings_modal.go index 1c6c63e43..cc7839649 100644 --- a/ui/page/exchange/order_settings_modal.go +++ b/ui/page/exchange/order_settings_modal.go @@ -201,6 +201,22 @@ func (osm *orderSettingsModal) Handle(gtx C) { if osm.feeRateSelector.SaveRate.Clicked(gtx) { osm.feeRateSelector.OnEditRateClicked(osm.sourceWalletSelector.SelectedWallet()) } + + if osm.sourceWalletSelector != nil { + osm.sourceWalletSelector.Handle(gtx) + } + + if osm.sourceAccountSelector != nil { + osm.sourceAccountSelector.Handle(gtx) + } + + if osm.destinationWalletSelector != nil { + osm.destinationWalletSelector.Handle(gtx) + } + + if osm.destinationAccountSelector != nil { + osm.destinationAccountSelector.Handle(gtx) + } } func (osm *orderSettingsModal) handleCopyEvent(gtx C) { @@ -300,11 +316,11 @@ func (osm *orderSettingsModal) Layout(gtx layout.Context) D { return layout.Inset{ Bottom: values.MarginPadding16, }.Layout(gtx, func(gtx C) D { - return osm.sourceWalletSelector.Layout(gtx, values.StrSource) + return osm.sourceWalletSelector.Layout(gtx, "") }) }), layout.Rigid(func(gtx C) D { - return osm.sourceAccountSelector.Layout(gtx, values.StrSource) + return osm.sourceAccountSelector.Layout(gtx, "") }), layout.Rigid(func(gtx C) D { if !osm.sourceWalletSelector.SelectedWallet().IsSynced() { @@ -347,14 +363,14 @@ func (osm *orderSettingsModal) Layout(gtx layout.Context) D { return layout.Inset{ Bottom: values.MarginPadding16, }.Layout(gtx, func(gtx C) D { - return osm.destinationWalletSelector.Layout(gtx, values.StrSource) + return osm.destinationWalletSelector.Layout(gtx, "") }) }), layout.Rigid(func(gtx C) D { return layout.Inset{ Bottom: values.MarginPadding16, }.Layout(gtx, func(gtx C) D { - return osm.destinationAccountSelector.Layout(gtx, values.StrSource) + return osm.destinationAccountSelector.Layout(gtx, "") }) }), layout.Rigid(func(gtx C) D { diff --git a/ui/values/localizable/en.go b/ui/values/localizable/en.go index 00617b1fc..77b43e7fa 100644 --- a/ui/values/localizable/en.go +++ b/ui/values/localizable/en.go @@ -297,6 +297,7 @@ const EN = ` "insufficientFund" = "Insufficient funds" "invalidAddress" = "Invalid address" "invalidAmount" = "Invalid amount" +"invalidRateFmt" = "rate is lower than the market's minimum rate. %v < %v" "invalidHex" = "Invalid hex" "invalidPassphrase" = "Password entered was not valid." "invalidSeedPhrase" = "Invalid seed phrase" @@ -956,4 +957,6 @@ const EN = ` "privacy" = "Privacy" "removeRecipient" = "Remove recipient" "removeRecipientWarning" = "Are you sure you want to proceed with removing the recipient?" +"unexpectedErrorMsgFmt" = "Something unexpected happened: %s" +"unexpectedError" = "Unexpected Error" ` diff --git a/ui/values/strings.go b/ui/values/strings.go index 2a520e3ae..ccea4fed6 100644 --- a/ui/values/strings.go +++ b/ui/values/strings.go @@ -406,6 +406,7 @@ const ( StrInsufficientFund = "insufficientFund" StrInvalidAddress = "invalidAddress" StrInvalidAmount = "invalidAmount" + StrInvalidRateFmt = "invalidRateFmt" StrInvalidHex = "invalidHex" StrInvalidPassphrase = "invalidPassphrase" StrInvalidSeedPhrase = "invalidSeedPhrase" @@ -1065,4 +1066,6 @@ const ( StrPrivacy = "privacy" StrRemoveRecipient = "removeRecipient" StrRemoveRecipientWarning = "removeRecipientWarning" + StrUnexpectedErrorMsgFmt = "unexpectedErrorMsgFmt" + StrUnexpectedError = "unexpectedError" )