From d1658a6c4393af549ec7573ab1599902b87e76d8 Mon Sep 17 00:00:00 2001 From: Brian Stafford Date: Mon, 23 Oct 2023 13:43:51 -0500 Subject: [PATCH] reputation and limits ui Adapt registration forms to bonding. Show trading limits for the tier input value. Add bond data to bond options registration form. Reuse registration forms for updating bond options on dexsettings. Show tier and reputation on dexsettings. Show parcel limits and reputation on markets view. A couple of notes. Nowhere are we going to show the cost of an individual bond, because that value is misleading. Instead, we'll show the "bond lock", which is the amount that will actually be locked for the given tier. I have removed the simnetfiatrates settings. fiat rates are all on by default. I have also changed coinpaprika so that it pulls all coin data, instead of just the ones with wallets. This enables us to e.g. show USD conversion of the bond lock value for assets that we don't yet have a wallet for. --- client/app/config.go | 2 - client/core/bond.go | 25 +- client/core/core.go | 35 +- client/core/core_test.go | 3 +- client/core/exchangeratefetcher.go | 64 ++-- client/core/notification.go | 44 ++- client/core/types.go | 4 +- client/webserver/locales/ar.go | 1 - client/webserver/locales/de-de.go | 2 - client/webserver/locales/en-us.go | 32 +- client/webserver/locales/pl-pl.go | 1 - client/webserver/locales/pt-br.go | 1 - client/webserver/locales/zh-cn.go | 1 - client/webserver/site/src/css/forms.scss | 105 +++--- client/webserver/site/src/css/forms_dark.scss | 6 + client/webserver/site/src/css/main.scss | 6 +- client/webserver/site/src/css/main_dark.scss | 4 + .../webserver/site/src/css/market_dark.scss | 2 - client/webserver/site/src/css/settings.scss | 13 +- .../webserver/site/src/html/dexsettings.tmpl | 162 ++++----- client/webserver/site/src/html/forms.tmpl | 226 +++++++++---- client/webserver/site/src/html/markets.tmpl | 58 ++++ client/webserver/site/src/html/register.tmpl | 4 +- client/webserver/site/src/html/settings.tmpl | 142 ++++---- client/webserver/site/src/js/account.ts | 159 +++++++++ client/webserver/site/src/js/app.ts | 7 + client/webserver/site/src/js/dexsettings.ts | 285 ++++++++++------ client/webserver/site/src/js/doc.ts | 7 +- client/webserver/site/src/js/forms.ts | 317 +++++++++++++----- client/webserver/site/src/js/locales.ts | 16 +- client/webserver/site/src/js/markets.ts | 88 ++++- client/webserver/site/src/js/orderutil.ts | 17 +- client/webserver/site/src/js/register.ts | 7 +- client/webserver/site/src/js/registry.ts | 9 + client/webserver/site/src/js/settings.ts | 7 +- client/webserver/site/src/js/wallets.ts | 26 +- 36 files changed, 1302 insertions(+), 586 deletions(-) create mode 100644 client/webserver/site/src/js/account.ts diff --git a/client/app/config.go b/client/app/config.go index d0ed8f4410..6f04e0b095 100644 --- a/client/app/config.go +++ b/client/app/config.go @@ -99,7 +99,6 @@ type CoreConfig struct { NoAutoWalletLock bool `long:"no-wallet-lock" description:"Disable locking of wallets on shutdown or logout. Use this if you want your external wallets to stay unlocked after closing the DEX app."` NoAutoDBBackup bool `long:"no-db-backup" description:"Disable creation of a database backup on shutdown."` UnlockCoinsOnLogin bool `long:"release-wallet-coins" description:"On login or wallet creation, instruct the wallet to release any coins that it may have locked."` - SimnetFiatRates bool `long:"simnet-fiat-rates" description:"Fetch fiat rates when running in simnet mode."` ExtensionModeFile string `long:"extension-mode-file" description:"path to a file that specifies options for running core as an extension."` } @@ -203,7 +202,6 @@ func (cfg *Config) Core(log dex.Logger) *core.Config { UnlockCoinsOnLogin: cfg.UnlockCoinsOnLogin, NoAutoWalletLock: cfg.NoAutoWalletLock, NoAutoDBBackup: cfg.NoAutoDBBackup, - SimnetFiatRates: cfg.SimnetFiatRates, ExtensionModeFile: cfg.ExtensionModeFile, } } diff --git a/client/core/bond.go b/client/core/bond.go index 1640ca61e9..a2f719d73b 100644 --- a/client/core/bond.go +++ b/client/core/bond.go @@ -458,6 +458,10 @@ func (c *Core) bondStateOfDEX(dc *dexConnection, bondCfg *dexBondCfg) *dexAcctBo return state } +func (c *Core) exchangeAuth(dc *dexConnection) *ExchangeAuth { + return &c.bondStateOfDEX(dc, c.dexBondConfig(dc, time.Now().Unix())).ExchangeAuth +} + type bondID struct { assetID uint32 coinID []byte @@ -908,7 +912,7 @@ func (c *Core) monitorBondConfs(dc *dexConnection, bond *asset.Bond, reqConfs ui if confs < reqConfs { details := fmt.Sprintf("Bond confirmations %v/%v", confs, reqConfs) c.notify(newBondPostNoteWithConfirmations(TopicRegUpdate, string(TopicRegUpdate), - details, db.Data, assetID, coinIDStr, int32(confs), host)) + details, db.Data, assetID, coinIDStr, int32(confs), host, c.exchangeAuth(dc))) } return confs >= reqConfs, nil @@ -1015,8 +1019,14 @@ func (c *Core) UpdateBondOptions(form *BondOptionsForm) error { } }() + var success bool dc.acct.authMtx.Lock() - defer dc.acct.authMtx.Unlock() + defer func() { + dc.acct.authMtx.Unlock() + if success { + c.notify(newBondAuthUpdate(dc.acct.host, c.exchangeAuth(dc))) + } + }() if !dc.acct.isAuthed { return errors.New("login or register first") @@ -1025,7 +1035,6 @@ func (c *Core) UpdateBondOptions(form *BondOptionsForm) error { // Revert to initial values if we encounter any error below. bondAssetID0 = dc.acct.bondAsset targetTier0, maxBondedAmt0, penaltyComps0 = dc.acct.targetTier, dc.acct.maxBondedAmt, dc.acct.penaltyComps - var success bool defer func() { // still under authMtx lock on defer stack if !success { dc.acct.bondAsset = bondAssetID0 @@ -1169,6 +1178,7 @@ func (c *Core) UpdateBondOptions(form *BondOptionsForm) error { success = true } // else we might have already done ReserveBondFunds... return err + } // BondsFeeBuffer suggests how much extra may be required for the transaction @@ -1530,7 +1540,7 @@ func (c *Core) makeAndPostBond(dc *dexConnection, acctExists bool, wallet *xcWal details := fmt.Sprintf("Waiting for %d confirmations to post bond %v (%s) to %s", reqConfs, bondCoinStr, unbip(bond.AssetID), dc.acct.host) // TODO: subject, detail := c.formatDetails(...) c.notify(newBondPostNoteWithConfirmations(TopicBondConfirming, string(TopicBondConfirming), - details, db.Success, bond.AssetID, bondCoinStr, 0, dc.acct.host)) + details, db.Success, bond.AssetID, bondCoinStr, 0, dc.acct.host, c.exchangeAuth(dc))) // Set up the coin waiter, which watches confirmations so the user knows // when to expect their account to be marked paid by the server. c.monitorBondConfs(dc, bond, reqConfs) @@ -1591,7 +1601,8 @@ func (c *Core) bondConfirmed(dc *dexConnection, assetID uint32, coinID []byte, p } c.log.Infof("Bond %s (%s) confirmed.", bondIDStr, unbip(assetID)) details := fmt.Sprintf("New tier = %d (target = %d).", effectiveTier, targetTier) // TODO: format to subject,details - c.notify(newBondPostNoteWithTier(TopicBondConfirmed, string(TopicBondConfirmed), details, db.Success, dc.acct.host, bondedTier)) + + c.notify(newBondPostNoteWithTier(TopicBondConfirmed, string(TopicBondConfirmed), details, db.Success, dc.acct.host, bondedTier, c.exchangeAuth(dc))) } else if !foundConfirmed { c.log.Errorf("bondConfirmed: Bond %s (%s) not found", bondIDStr, unbip(assetID)) // just try to authenticate... @@ -1617,7 +1628,7 @@ func (c *Core) bondConfirmed(dc *dexConnection, assetID uint32, coinID []byte, p details := fmt.Sprintf("New tier = %d", effectiveTier) // TODO: format to subject,details c.notify(newBondPostNoteWithTier(TopicAccountRegistered, string(TopicAccountRegistered), - details, db.Success, dc.acct.host, bondedTier)) // possibly redundant with SubjectBondConfirmed + details, db.Success, dc.acct.host, bondedTier, c.exchangeAuth(dc))) // possibly redundant with SubjectBondConfirmed return nil } @@ -1669,7 +1680,7 @@ func (c *Core) bondExpired(dc *dexConnection, assetID uint32, coinID []byte, not if int64(targetTier) > effectiveTier { details := fmt.Sprintf("New tier = %d (target = %d).", effectiveTier, targetTier) c.notify(newBondPostNoteWithTier(TopicBondExpired, string(TopicBondExpired), - details, db.WarningLevel, dc.acct.host, bondedTier)) + details, db.WarningLevel, dc.acct.host, bondedTier, c.exchangeAuth(dc))) } return nil diff --git a/client/core/core.go b/client/core/core.go index 67b1433c9a..79de0bea7f 100644 --- a/client/core/core.go +++ b/client/core/core.go @@ -339,6 +339,7 @@ func coreMarketFromMsgMarket(dc *dexConnection, msgMkt *msgjson.Market) *Market QuoteID: quote.ID, QuoteSymbol: quote.Symbol, LotSize: msgMkt.LotSize, + ParcelSize: msgMkt.ParcelSize, RateStep: msgMkt.RateStep, EpochLen: msgMkt.EpochLen, StartEpoch: msgMkt.StartEpoch, @@ -519,7 +520,8 @@ func (c *Core) exchangeInfo(dc *dexConnection) *Exchange { CandleDurs: cfg.BinSizes, ViewOnly: dc.acct.isViewOnly(), Auth: acctBondState.ExchangeAuth, - // TODO: Bonds + MaxScore: cfg.MaxScore, + PenaltyThreshold: cfg.PenaltyThreshold, // Legacy reg fee (V0PURGE) RegFees: feeAssets, @@ -1378,9 +1380,6 @@ type Config struct { TorIsolation bool // Language. A BCP 47 language tag. Default is en-US. Language string - // SimnetFiatRates specifies whether to fetch fiat rates when running on - // simnet. - SimnetFiatRates bool // NoAutoWalletLock instructs Core to skip locking the wallet on shutdown or // logout. This can be helpful if the user wants the wallet to remain @@ -1612,25 +1611,21 @@ func (c *Core) Run(ctx context.Context) { c.latencyQ.Run(ctx) }() - // Skip rate fetch setup if on simnet. Rate fetching maybe enabled if - // desired. - if c.cfg.Net != dex.Simnet || c.cfg.SimnetFiatRates { - // Retrieve disabled fiat rate sources from database. - disabledSources, err := c.db.DisabledRateSources() - if err != nil { - c.log.Errorf("Unable to retrieve disabled fiat rate source: %v", err) - } + // Retrieve disabled fiat rate sources from database. + disabledSources, err := c.db.DisabledRateSources() + if err != nil { + c.log.Errorf("Unable to retrieve disabled fiat rate source: %v", err) + } - // Construct enabled fiat rate sources. - fetchers: - for token, rateFetcher := range fiatRateFetchers { - for _, v := range disabledSources { - if token == v { - continue fetchers - } + // Construct enabled fiat rate sources. +fetchers: + for token, rateFetcher := range fiatRateFetchers { + for _, v := range disabledSources { + if token == v { + continue fetchers } - c.fiatRateSources[token] = newCommonRateSource(rateFetcher) } + c.fiatRateSources[token] = newCommonRateSource(rateFetcher) } c.fetchFiatExchangeRates(ctx) diff --git a/client/core/core_test.go b/client/core/core_test.go index 1c48872911..6b7c006218 100644 --- a/client/core/core_test.go +++ b/client/core/core_test.go @@ -250,6 +250,7 @@ func testDexConnection(ctx context.Context, crypter *tCrypter) (*dexConnection, Base: tUTXOAssetA.ID, Quote: tUTXOAssetB.ID, LotSize: dcrBtcLotSize, + ParcelSize: 100, RateStep: dcrBtcRateStep, EpochLen: 60000, MarketBuyBuffer: 1.1, @@ -10775,7 +10776,7 @@ func TestRotateBonds(t *testing.T) { // if the locktime is not too soon. acct.bonds = append(acct.bonds, acct.pendingBonds[0]) acct.pendingBonds = nil - acct.bonds[0].LockTime = mergeableLocktimeThresh + 1 + acct.bonds[0].LockTime = mergeableLocktimeThresh + 5 rig.queuePrevalidateBond() run(1, 0, 2*bondAsset.Amt+bondFeeBuffer) mergingBond := acct.pendingBonds[0] diff --git a/client/core/exchangeratefetcher.go b/client/core/exchangeratefetcher.go index 704629250d..13b2e4f9c7 100644 --- a/client/core/exchangeratefetcher.go +++ b/client/core/exchangeratefetcher.go @@ -35,7 +35,7 @@ const ( var ( dcrDataURL = "https://explorer.dcrdata.org/api/exchangerate" // coinpaprika has two options. /tickers is for the top 2500 assets all in - // one request. /ticker/[slug] is for a single ticker. From testing + // one request. /tickers/[slug] is for a single ticker. From testing // Single ticker request took 274.626125ms // Size of single ticker response: 0.733 kB // All tickers request took 47.651851ms @@ -48,7 +48,7 @@ var ( // So any more than 25000 / 3600 = 6.9 assets, and we can expect to run into // rate limits. But the bandwidth of the full tickers request is kinda // ridiculous too. Solution needed. - coinpaprikaURL = "https://api.coinpaprika.com/v1/tickers/%s" + coinpaprikaURL = "https://api.coinpaprika.com/v1/tickers" // The best info I can find on Messari says // Without an API key requests are rate limited to 20 requests per minute // and 1000 requests per day. @@ -142,26 +142,12 @@ func newCommonRateSource(fetcher rateFetcher) *commonRateSource { // for sample request and response information. func fetchCoinpaprikaRates(ctx context.Context, log dex.Logger, assets map[uint32]*SupportedAsset) map[uint32]float64 { fiatRates := make(map[uint32]float64) - fetchRate := func(sa *SupportedAsset) { - assetID := sa.ID - if sa.Wallet == nil { - // we don't want to fetch rates for assets with no wallet. - return - } - - res := new(struct { - Quotes struct { - Currency struct { - Price float64 `json:"price"` - } `json:"USD"` - } `json:"quotes"` - }) - + slugAssets := make(map[string]uint32) + for _, sa := range assets { symbol := dex.TokenSymbol(sa.Symbol) if symbol == "dextt" { - return + continue } - name := sa.Name // TODO: Store these within the *SupportedAsset. switch symbol { @@ -171,21 +157,37 @@ func fetchCoinpaprikaRates(ctx context.Context, log dex.Logger, assets map[uint3 symbol = "matic" name = "polygon" } + slug := coinpapSlug(symbol, name) + slugAssets[slug] = sa.ID + } - reqStr := fmt.Sprintf(coinpaprikaURL, coinpapSlug(symbol, name)) - - ctx, cancel := context.WithTimeout(ctx, fiatRequestTimeout) - defer cancel() + ctx, cancel := context.WithTimeout(ctx, fiatRequestTimeout) + defer cancel() - if err := getRates(ctx, reqStr, res); err != nil { - log.Errorf("Error getting fiat exchange rates from coinpaprika: %v", err) - return - } + var res []*struct { + ID string `json:"id"` + Quotes struct { + USD struct { + Price float64 `json:"price"` + } `json:"USD"` + } `json:"quotes"` + } - fiatRates[assetID] = res.Quotes.Currency.Price + if err := getRates(ctx, coinpaprikaURL, &res); err != nil { + log.Errorf("Error getting fiat exchange rates from coinpaprika: %v", err) + return fiatRates } - for _, sa := range assets { - fetchRate(sa) + for _, coinInfo := range res { + assetID, found := slugAssets[coinInfo.ID] + if !found { + continue + } + price := coinInfo.Quotes.USD.Price + if price == 0 { + log.Errorf("zero-price returned from coinpaprika for slug %s", coinInfo.ID) + continue + } + fiatRates[assetID] = price } return fiatRates } @@ -288,6 +290,6 @@ func getRates(ctx context.Context, url string, thing any) error { return fmt.Errorf("error %d fetching %q", resp.StatusCode, url) } - reader := io.LimitReader(resp.Body, 1<<20) + reader := io.LimitReader(resp.Body, 1<<22) return json.NewDecoder(reader).Decode(thing) } diff --git a/client/core/notification.go b/client/core/notification.go index cb5bbf3721..83b6b9111a 100644 --- a/client/core/notification.go +++ b/client/core/notification.go @@ -265,14 +265,19 @@ func newBondRefundNote(topic Topic, subject, details string, severity db.Severit } } +const ( + TopicBondAuthUpdate Topic = "BondAuthUpdate" +) + // BondPostNote is a notification regarding bond posting. type BondPostNote struct { db.Notification - Asset *uint32 `json:"asset,omitempty"` - Confirmations *int32 `json:"confirmations,omitempty"` - BondedTier *int64 `json:"bondedTier,omitempty"` - CoinID *string `json:"coinID,omitempty"` - Dex string `json:"dex,omitempty"` + Asset *uint32 `json:"asset,omitempty"` + Confirmations *int32 `json:"confirmations,omitempty"` + BondedTier *int64 `json:"bondedTier,omitempty"` + CoinID *string `json:"coinID,omitempty"` + Dex string `json:"dex,omitempty"` + Auth *ExchangeAuth `json:"auth,omitempty"` } func newBondPostNote(topic Topic, subject, details string, severity db.Severity, dexAddr string) *BondPostNote { @@ -283,20 +288,39 @@ func newBondPostNote(topic Topic, subject, details string, severity db.Severity, } } -func newBondPostNoteWithConfirmations(topic Topic, subject, details string, severity db.Severity, asset uint32, coinID string, currConfs int32, dexAddr string) *BondPostNote { - bondPmtNt := newBondPostNote(topic, subject, details, severity, dexAddr) +func newBondPostNoteWithConfirmations( + topic Topic, + subject string, + details string, + severity db.Severity, + asset uint32, + coinID string, + currConfs int32, + host string, + auth *ExchangeAuth, +) *BondPostNote { + + bondPmtNt := newBondPostNote(topic, subject, details, severity, host) bondPmtNt.Asset = &asset bondPmtNt.CoinID = &coinID bondPmtNt.Confirmations = &currConfs + bondPmtNt.Auth = auth return bondPmtNt } -func newBondPostNoteWithTier(topic Topic, subject, details string, severity db.Severity, dexAddr string, bondedTier int64) *BondPostNote { +func newBondPostNoteWithTier(topic Topic, subject, details string, severity db.Severity, dexAddr string, bondedTier int64, auth *ExchangeAuth) *BondPostNote { bondPmtNt := newBondPostNote(topic, subject, details, severity, dexAddr) bondPmtNt.BondedTier = &bondedTier + bondPmtNt.Auth = auth return bondPmtNt } +func newBondAuthUpdate(host string, auth *ExchangeAuth) *BondPostNote { + n := newBondPostNote(TopicBondAuthUpdate, "", "", db.Data, host) + n.Auth = auth + return n +} + // SendNote is a notification regarding a requested send or withdraw. type SendNote struct { db.Notification @@ -673,8 +697,8 @@ func newWalletNote(n asset.WalletNotification) *WalletNote { type ReputationNote struct { db.Notification - Host string - Reputation account.Reputation + Host string `json:"host"` + Reputation account.Reputation `json:"rep"` } const TopicReputationUpdate = "ReputationUpdate" diff --git a/client/core/types.go b/client/core/types.go index 7b6155fc18..34eb71d927 100644 --- a/client/core/types.go +++ b/client/core/types.go @@ -525,6 +525,7 @@ type Market struct { QuoteID uint32 `json:"quoteid"` QuoteSymbol string `json:"quotesymbol"` LotSize uint64 `json:"lotsize"` + ParcelSize uint32 `json:"parcelsize"` RateStep uint64 `json:"ratestep"` EpochLen uint64 `json:"epochlen"` StartEpoch uint64 `json:"startepoch"` @@ -693,7 +694,8 @@ type Exchange struct { CandleDurs []string `json:"candleDurs"` ViewOnly bool `json:"viewOnly"` Auth ExchangeAuth `json:"auth"` - // TODO: Bonds slice(s) - and a LockedInBonds(assetID) method + PenaltyThreshold uint32 `json:"penaltyThreshold"` + MaxScore uint32 `json:"maxScore"` // OLD fields for the legacy registration fee (V0PURGE): RegFees map[string]*FeeAsset `json:"regFees"` diff --git a/client/webserver/locales/ar.go b/client/webserver/locales/ar.go index b7517ce9e8..1e7a182da4 100644 --- a/client/webserver/locales/ar.go +++ b/client/webserver/locales/ar.go @@ -178,7 +178,6 @@ var Ar = map[string]string{ "All markets at": "جميع الأسواق في", "pick a different asset": "اختر أصلًا مختلفًا", "Create": "انشاء", - "Register_loudly": "التسجيل!", "1 Sync the Blockchain": "1: مزامنة سلسلة الكتل", "Progress": "قيد التنفيذ", "remaining": "الوقت المتبقي", diff --git a/client/webserver/locales/de-de.go b/client/webserver/locales/de-de.go index a4e669b8a9..2807134eca 100644 --- a/client/webserver/locales/de-de.go +++ b/client/webserver/locales/de-de.go @@ -169,11 +169,9 @@ var DeDE = map[string]string{ "Export Trades": "Exportiere Trades", "change the wallet type": "den Wallet-Typ ändern", "confirmations": "Bestätigungen", - "how_reg": "Wie soll die Anmeldegebühr bezahlt werden?", "All markets at": "Alle Märkte bei", "pick a different asset": "ein anderes Asset wählen", "Create": "Erstellen", - "Register_loudly": "Registrieren!", "1 Sync the Blockchain": "1: Blockchain synchronisieren", "Progress": "Fortschritt", "remaining": "verbleibend", diff --git a/client/webserver/locales/en-us.go b/client/webserver/locales/en-us.go index fa84be8469..c1bd8b2f5f 100644 --- a/client/webserver/locales/en-us.go +++ b/client/webserver/locales/en-us.go @@ -30,11 +30,14 @@ var EnUS = map[string]string{ "app_pw_reg": "Enter your app password to confirm DEX registration and bond creation.", "reg_confirm_submit": `When you submit this form, funds will be spent from your wallet to post a fidelity bond, which is redeemable by you in the future.`, "bond_strength": "Bond Strength", - "update_bond_options": "Update Bond Options", - "bond_options": "Bond Options", - "bond_options_update_success": "Bond Options have been updated successfully", "target_tier": "Target Tier", "target_tier_tooltip": "This is the target account tier you wish to maintain. Set to zero if you wish to disable tier maintenance (do not post new bonds).", + "Actual Tier": "Actual Tier", + "Penalties": "Penalties", + "Change Tier": "Change Tier", + "Limit Bonus": "Limit Bonus", + "Score": "Score", + "Confirm Bond Options": "Confirm Bond Options", "provided_markets": "This DEX provides the following markets:", "accepted_fee_assets": "This DEX recognizes the bond assets:", "base_header": "Base", @@ -195,11 +198,9 @@ var EnUS = map[string]string{ "Export Trades": "Export Trades", "change the wallet type": "change the wallet type", "confirmations": "confirmations", - "how_reg": "How will you create your bond?", "All markets at": "All markets at", "pick a different asset": "pick a different asset", "Create": "Create", - "Register_loudly": "Register!", "1 Sync the Blockchain": "1: Sync the Blockchain", "Progress": "Progress", "remaining": "remaining", @@ -276,6 +277,17 @@ var EnUS = map[string]string{ "Settings": "Settings", "asset_name Markets": " Markets", "Host": "Host", + "Trading Tier": "Trading Tier", + "Bond Lock": "Bond Lock", + "USD": "USD", + "Fee Reserves": "Fee Reserves", + "Trading Limits": "Trading Limits", + "Select your bond asset": "Select your bond asset", + "choose a different asset": "choose a different asset", + "current_bonding_asset": `Using for bonding`, + "Choose your trading tier": "Choose your trading tier", + "trading_tier_message": "Increase your trading tier to enable trading of larger amounts. Trading limits also grow with reputation.", + "Other Trading Limits": "Other Trading Limits", "No Recent Activity": "No Recent Activity", "Recent asset_name Activity": "Recent Activity", "other_actions": "Other Actions", @@ -425,15 +437,7 @@ var EnUS = map[string]string{ "market_making_running": "Market making is running", "cannot_manually_trade": "You cannot manually place orders while market making is running", "back": "Back", - "bond_details": "Bond Details", - "current_tier": "Current Tier", - "current_tier_tooltip": "Number of active bonds that have not yet reached the expiry threshold as reported by the DEX server. Increase your target tier to raise your account tier, boost your trading limits, and offset penalties, if any.", - "current_target_tier_tooltip": "This is the target account tier you wish to maintain. If zero, bond maintenance will be disabled and new bonds will not be posted.", - "current_target_tier": "Current Target Tier", - "bond_cost": "Bond Cost", - "bond_cost_tooltip": "Cost of a single bond without fees and bond maintenance fund reservation.", - "bond_reservations": "Bond Reservation", - "bond_reservations_tooltip": "Total funds that will be locked when you post a bond to cover fees and bond maintenance costs.", + "current_tier_tooltip": "Tier represented by active bonds. Increase your target tier to raise your target tier, boost your trading limits, and offset penalties, if any.", "Reset App Password": "Reset App Password", "reset_app_pw_msg": "Reset your app password using your app seed. If you provide the correct app seed, you can login again with the new password.", "Forgot Password": "Forgot Password?", diff --git a/client/webserver/locales/pl-pl.go b/client/webserver/locales/pl-pl.go index cb20991119..781a32dc95 100644 --- a/client/webserver/locales/pl-pl.go +++ b/client/webserver/locales/pl-pl.go @@ -168,7 +168,6 @@ var PlPL = map[string]string{ "All markets at": "Wszystkie rynki na", "pick a different asset": "wybierz inne aktywo", "Create": "Utwórz", - "Register_loudly": "Zarejestruj!", "1 Sync the Blockchain": "1: Zsynchronizuj blockchain", "Progress": "Postęp", "remaining": "pozostało", diff --git a/client/webserver/locales/pt-br.go b/client/webserver/locales/pt-br.go index 7a7aba9715..beebf5e96c 100644 --- a/client/webserver/locales/pt-br.go +++ b/client/webserver/locales/pt-br.go @@ -169,7 +169,6 @@ var PtBr = map[string]string{ "All markets at": "Todos mercados", "pick a different asset": "Escolher ativo diferente", "Create": "Criar", - "Register_loudly": "Registre!", "1 Sync the Blockchain": "1: Sincronizar a Blockchain", "Progress": "Progresso", "remaining": "Faltando", diff --git a/client/webserver/locales/zh-cn.go b/client/webserver/locales/zh-cn.go index 71f8411a99..ac9894fee2 100644 --- a/client/webserver/locales/zh-cn.go +++ b/client/webserver/locales/zh-cn.go @@ -171,7 +171,6 @@ var ZhCN = map[string]string{ "All markets at": "所有市场", "pick a different asset": "选择其它的资产", "Create": "创建", - "Register_loudly": "注册!", "1 Sync the Blockchain": "1: 同步区块链", "Progress": "进度", "remaining": "剩余", diff --git a/client/webserver/site/src/css/forms.scss b/client/webserver/site/src/css/forms.scss index 1415309ddb..d83f468db8 100644 --- a/client/webserver/site/src/css/forms.scss +++ b/client/webserver/site/src/css/forms.scss @@ -1,18 +1,5 @@ #regAssetForm { - #whatsabond { - max-width: 425px; - } - - div.reg-asset-allmkts { - min-width: 210px; - max-width: 320px; - } - div.reg-asset { - min-width: 425px; - padding: 20px 10px; - border-bottom: dashed 2px #7777; - .fader { position: absolute; bottom: 0; @@ -44,11 +31,6 @@ } } - img.reg-asset-logo { - width: 50px; - height: 50px; - } - img.reg-market-logo { width: 14px; height: 14px; @@ -59,7 +41,7 @@ @extend .stylish-overflow; display: block; - max-height: 65px; + max-height: 120px; line-height: 1.15; overflow-y: hidden; margin-right: 8px; @@ -83,13 +65,6 @@ } } - div.reg-asset-details { - box-sizing: content-box; - line-height: 1.25; - width: 100px; - padding: 0 50px 0 25px; - } - div.reg-assets-markets-wrap { position: relative; @@ -102,6 +77,25 @@ color: #c7c3cc; } + .reg-asset-table { + thead > tr > th { + border-top: 1px solid; + } + + th, + td { + border-color: $light_border_color; + border-width: 1px; + border-style: none solid solid; + padding: 5px; + text-align: left; + } + } + + input[data-tmpl=regAssetTier] { + width: 3em; + } + .readygreen { color: #009931; } @@ -125,11 +119,6 @@ } } - img.logo { - width: 30px; - height: 30px; - } - input.app-pass { // margin: 0 auto; display: inline-block; @@ -143,6 +132,10 @@ div.borderright { border-right: 1px solid #777; } + + .mw50 { + max-width: 50%; + } } #newWalletForm { @@ -267,11 +260,6 @@ button.form-button { width: 25px; } - .borderleft { - padding-left: 25px; - border-left: solid 1px #777; - } - .logo { width: 40px; height: 40px; @@ -331,8 +319,7 @@ button.form-button { #vSendForm, #exportSeedAuth, #cancelForm, -#quickConfigForm, -#bondDetailsForm { +#quickConfigForm { width: 375px; } @@ -414,6 +401,48 @@ a[data-tmpl=walletCfgGuide] { } } +div[data-tmpl=scoreTray] { + background-color: $buycolor_dark; + height: 12px; + border-radius: 100px; + overflow: hidden; + + div[data-tmpl=scoreWarn] { + background-color: $sellcolor_dark; + position: absolute; + top: 0; + bottom: 0; + left: 0; + } +} + +span[data-tmpl=scorePointer] { + transform: translateX(-50%); + + div[data-tmpl=scoreData] { + top: 0; + bottom: 0; + + &.positive { + right: 150%; + } + + &.negative { + left: 150%; + } + } +} + +.penalty-marker { + position: absolute; + top: 0; + bottom: 0; + left: 10%; + width: 2px; + z-index: 2; + background-color: black; +} + div[data-handler=init] { .quickconfig-asset-logo { width: 25px; diff --git a/client/webserver/site/src/css/forms_dark.scss b/client/webserver/site/src/css/forms_dark.scss index 5de4fa84ac..78c3ebd3d9 100644 --- a/client/webserver/site/src/css/forms_dark.scss +++ b/client/webserver/site/src/css/forms_dark.scss @@ -33,6 +33,12 @@ body.dark { } } } + + .reg-asset-table { + th, td { + border-color: $dark_border_color; + } + } } ::-webkit-calendar-picker-indicator { diff --git a/client/webserver/site/src/css/main.scss b/client/webserver/site/src/css/main.scss index a65bb696cf..eb1dfbfc0e 100644 --- a/client/webserver/site/src/css/main.scss +++ b/client/webserver/site/src/css/main.scss @@ -422,6 +422,10 @@ div.popup-notes { border-radius: 3px; } +.brdr { + border: 1px solid $light_border_color; +} + .brdrleft { border-left: 1px solid $light_border_color; } @@ -439,7 +443,7 @@ div.popup-notes { } hr.dashed { - border-top: 1px dashed #777; + border-top: dashed 2px #777; } .vscroll { diff --git a/client/webserver/site/src/css/main_dark.scss b/client/webserver/site/src/css/main_dark.scss index 9b82015d46..136dcebbb3 100644 --- a/client/webserver/site/src/css/main_dark.scss +++ b/client/webserver/site/src/css/main_dark.scss @@ -34,6 +34,10 @@ body.dark { border-color: #333; } + .brdr { + border: 1px solid $dark_border_color; + } + .brdrleft { border-left: 1px solid $dark_border_color; } diff --git a/client/webserver/site/src/css/market_dark.scss b/client/webserver/site/src/css/market_dark.scss index f5d3bc4f45..32b3a31263 100644 --- a/client/webserver/site/src/css/market_dark.scss +++ b/client/webserver/site/src/css/market_dark.scss @@ -91,8 +91,6 @@ body.dark { } #orderForm { - color: #a1a1a1; - button { color: #aaa; } diff --git a/client/webserver/site/src/css/settings.scss b/client/webserver/site/src/css/settings.scss index 360977ddc1..8c27894c6f 100644 --- a/client/webserver/site/src/css/settings.scss +++ b/client/webserver/site/src/css/settings.scss @@ -1,11 +1,10 @@ div.settings { - display: inline-block; - width: 500px; - text-align: left; + min-width: 375px; & > div { - position: relative; - padding: 10px; + width: 100%; + text-align: left; + padding: 10px 0; border-bottom: 1px solid #7777; } @@ -13,8 +12,8 @@ div.settings { border-top: 1px solid #7777; } - & > div.form-check { - padding-left: 35px; + div.form-check { + padding-left: 25px; } button { diff --git a/client/webserver/site/src/html/dexsettings.tmpl b/client/webserver/site/src/html/dexsettings.tmpl index f3cc526853..58e0002651 100644 --- a/client/webserver/site/src/html/dexsettings.tmpl +++ b/client/webserver/site/src/html/dexsettings.tmpl @@ -1,32 +1,59 @@ {{define "dexsettings"}} {{template "top" .}} {{$passwordIsCached := .UserInfo.PasswordIsCached}} -
- -
{{.Exchange.Host}}
-
- - - -
-
-
- - -
-
- -
-
- +
+
+
+
{{.Exchange.Host}}
+
+ + +
-
- - - [[[successful_cert_update]]] +
+
+
+
+
+
+ [[[target_tier]]] + +
+
+ [[[Actual Tier]]] + +
+
+ [[[Penalties]]] + +
+
+
+
+ +
+
+
+
+ {{template "reputationMeter"}} +
-
- +
+
+ + +
+
+ +
+
+ + + [[[successful_cert_update]]] +
+
+ +
@@ -46,64 +73,37 @@ {{template "dexAddrForm" .}} - {{- /* BOND DETAILS */ -}} -
-
-
[[[bond_details]]]
-
- - [[[current_tier]]] - -
- -
- - [[[current_target_tier]]] - -
- -
-
-
- - [[[bond_cost]]] - -
- - -
- - ~USD - -
- - [[[bond_reservations]]] - -
- - -
- - ~USD - -
-
[[[bond_options]]]:
-
- - -
-
- - + {{- /* SUCCESS ANIMATION */ -}} + +
+
-
- -
-
-
[[[bond_options_update_success]]]
+
+ + + {{- /* REG ASSET SELECTOR */ -}} +
+ {{template "regAssetForm"}} +
+ + {{- /* CONFIRM POST BOND */ -}} +
+ {{template "confirmRegistrationForm"}} +
+ + {{- /* SYNC AND BALANCE FORM */ -}} +
+ {{template "waitingForWalletForm"}} +
+ + {{- /* Form to set up a wallet. Shown on demand when a user clicks a setupWallet button. */ -}} +
+ {{template "newWalletForm" }} +
+ + {{- /* UNLOCK WALLET */ -}} +
+ {{template "unlockWalletForm"}}
diff --git a/client/webserver/site/src/html/forms.tmpl b/client/webserver/site/src/html/forms.tmpl index 326bda8b93..1c834913c4 100644 --- a/client/webserver/site/src/html/forms.tmpl +++ b/client/webserver/site/src/html/forms.tmpl @@ -215,7 +215,7 @@ {{define "discoverAcctForm"}}
[[[Create Account]]]
-
on
+
@@ -227,22 +227,86 @@ {{end}} {{define "regAssetForm"}} -
[[[how_reg]]]
-
- - [[[whatsabond]]] -
-
-
-
- +
+
+ [[[Choose your trading tier]]] + [[[trading_tier_message]]] +
+
+ + +
+
+
+
+
+
[[[current_bonding_asset]]]
+
[[[choose a different asset]]]
+
+
+
[[[Select your bond asset]]]
+ + + + + + + + + + + + + + + +
[[[Asset]]][[[Bond Lock]]]Trading Limits
+
+
+ + +
+
+
+
+
+
+ + +
+
+ ~ + + [[[USD]]] +
+
+
+
+
+ + + + +
+
+ + ~ + + [[[USD]]] + +
+
+
[[[Other Trading Limits]]]
+ + + +
+
+
+
[[[All markets at]]]
+
-
-
-
[[[confirmations]]]
-
-
-
+
@@ -250,7 +314,7 @@ - +
[[[:title:lot_size]]]
@@ -270,23 +334,10 @@
-
-
-
[[[All markets at]]]
- -
-
- - - - - - - - -
[[[Market]]][[[:title:lot_size]]]
-
-
+
+
+ + [[[whatsabond]]]
{{end}} @@ -312,44 +363,52 @@ {{end}} {{define "confirmRegistrationForm"}} - [[[pick a different asset]]] -
- [[[reg_confirm_submit]]] -
- -
- -
- -
-
- -
-
- -
- -
- -
- - - - - - [[[plus tx fees]]] +
+ [[[pick a different asset]]] +
[[[Confirm Bond Options]]]
+
+
+
+ [[[Host]]] + +
+
+ [[[Trading Tier]]] + +
+
+ [[[Bond Lock]]] + + + + + +
+
+ + ~ [[[USD]]] +
+
+ [[[Fee Reserves]]] + +
+
+
+
+ [[[app_pw_reg]]] + +
+
+ +
+
+
+
+ [[[reg_confirm_submit]]] +
- -
- [[[app_pw_reg]]] - -
-
- -
-
{{end}} {{define "authorizeAccountExportForm"}} @@ -523,7 +582,7 @@ {{define "waitingForWalletForm"}}
-
+
@@ -543,7 +602,7 @@
-
+
@@ -664,3 +723,28 @@
{{end}} + +{{define "reputationMeter"}} +
+
+ + + [[[Limit Bonus]]] + x + + +
+
+
+
+
+
+ + + [[[Score]]]: + + + +
+
+{{end}} diff --git a/client/webserver/site/src/html/markets.tmpl b/client/webserver/site/src/html/markets.tmpl index 99635b3456..48d752a730 100644 --- a/client/webserver/site/src/html/markets.tmpl +++ b/client/webserver/site/src/html/markets.tmpl @@ -378,6 +378,15 @@
{{/* textContent set by script */}}
+ + {{- /* ORDER LIMITS */ -}} +
+ 0 parcels + of the 0 unused parcels in your + tier 0 trading limit +
+ +
@@ -416,6 +425,55 @@
+ {{- /* REPUTATION */ -}} +
+
+ +
+ Parcel Size + + lots + +
+ +
+ + + + +
+ +
+ + + ~ + +
+ +
+
+ +
+ Trading Tier + +
+ +
+ Trading Limit + parcels +
+ +
+ Current usage + % +
+ +
+
+
+ {{template "reputationMeter"}} +
+ {{- /* USER ORDERS */ -}}
[[[Your Orders]]] diff --git a/client/webserver/site/src/html/register.tmpl b/client/webserver/site/src/html/register.tmpl index 72e0b53061..cf10d040d7 100644 --- a/client/webserver/site/src/html/register.tmpl +++ b/client/webserver/site/src/html/register.tmpl @@ -29,12 +29,12 @@ {{- /* REG ASSET SELECTOR */ -}} -
+ {{template "regAssetForm"}}
{{- /* CONFIRM FEE PAYMENT */ -}} -
+ {{template "confirmRegistrationForm"}}
diff --git a/client/webserver/site/src/html/settings.tmpl b/client/webserver/site/src/html/settings.tmpl index c79a95d341..ff95d983cc 100644 --- a/client/webserver/site/src/html/settings.tmpl +++ b/client/webserver/site/src/html/settings.tmpl @@ -3,80 +3,82 @@ {{$passwordIsCached := .UserInfo.PasswordIsCached}} {{$authed := .UserInfo.Authed}}
-
-
-
- - -
-
- Fiat Currency: {{.FiatCurrency}} -
-
- - [[[fiat_exchange_rate_sources]]]: - - - {{range $source, $enabled := .FiatRateSources}} -
- - -
- {{end}} -
-
-
-
[[[registered dexes]]]
- {{range $host, $xc := .Exchanges}} - - {{end}} +
+
+
+
+ +
-
-
-

- [[[simultaneous_servers_msg]]] -

- - +
+ Fiat Currency: {{.FiatCurrency}}
-
-
- - -
-
- - -
-
-
-
- +
+ + [[[fiat_exchange_rate_sources]]]: + + + {{range $source, $enabled := .FiatRateSources}} +
+ + +
+ {{end}} +
+
+
+
[[[registered dexes]]]
+ {{range $host, $xc := .Exchanges}} + + {{end}}
-
[[[browser_ntfn_blocked]]]
-
-
-
+
+ + +
+
+ + +
+
+ +
+
-
- -
- - [[[seed_implore_msg]]]
- +
[[[browser_ntfn_blocked]]]
+
+
+ +
+
+ +
+ + [[[seed_implore_msg]]]
+ +
+
[[[Build ID]]]:
-
[[[Build ID]]]:
{{- /* POP-UP FORMS */ -}} @@ -88,11 +90,11 @@ {{- /* REG ASSET SELECTOR */ -}} -
+ {{template "regAssetForm"}}
{{- /* CONFIRM REGISTRATION */ -}} -
+ {{template "confirmRegistrationForm"}}
diff --git a/client/webserver/site/src/js/account.ts b/client/webserver/site/src/js/account.ts new file mode 100644 index 0000000000..3e6478116a --- /dev/null +++ b/client/webserver/site/src/js/account.ts @@ -0,0 +1,159 @@ +import Doc from './doc' +import { + OrderTypeLimit, + OrderTypeMarket, + OrderTypeCancel, + StatusEpoch, + StatusBooked, + RateEncodingFactor, + MatchSideMaker, + MakerRedeemed, + TakerSwapCast, + ImmediateTiF +} from './orderutil' +import { + app, + PageElement, + ExchangeAuth, + Order, + Market +} from './registry' + +export const bondReserveMultiplier = 2 // Reserves for next bond +export const perTierBaseParcelLimit = 2 +export const parcelLimitScoreMultiplier = 3 + +export class ReputationMeter { + page: Record + host: string + + constructor (div: PageElement) { + this.page = Doc.parseTemplate(div) + Doc.cleanTemplates(this.page.penaltyMarkerTmpl) + } + + setHost (host: string) { + this.host = host + } + + update () { + const { page, host } = this + const { auth, maxScore, penaltyThreshold } = app().exchanges[host] + const { rep: { score } } = auth + + const displayTier = strongTier(auth) + + const minScore = displayTier ? displayTier * penaltyThreshold * -1 : penaltyThreshold * -1 // Just for looks + const warnPct = 25 + const scorePct = 100 - warnPct + page.scoreWarn.style.width = `${warnPct}%` + const pos = score >= 0 ? warnPct + (score / maxScore) * scorePct : warnPct - (Math.min(warnPct, score / minScore * warnPct)) + page.scorePointer.style.left = `${pos}%` + page.scoreMin.textContent = String(minScore) + page.scoreMax.textContent = String(maxScore) + const bonus = limitBonus(score, maxScore) + page.limitBonus.textContent = bonus.toFixed(1) + for (const m of Doc.applySelector(page.scoreTray, '.penalty-marker')) m.remove() + if (displayTier > 1) { + const markerPct = warnPct / displayTier + for (let i = 1; i < displayTier; i++) { + const div = page.penaltyMarkerTmpl.cloneNode(true) as PageElement + page.scoreTray.appendChild(div) + div.style.left = `${markerPct * i}%` + } + } + page.score.textContent = String(score) + page.scoreData.classList.remove('negative', 'positive') + if (score > 0) page.scoreData.classList.add('positive') + else page.scoreData.classList.add('negative') + } +} + +/* + * strongTier is the effective tier, with some respect for bond overlap, such + * that we don't count weak bonds that have already had their replacements + * confirmed. + */ +export function strongTier (auth: ExchangeAuth): number { + const { weakStrength, targetTier, effectiveTier } = auth + if (effectiveTier > targetTier) { + const diff = effectiveTier - targetTier + if (weakStrength >= diff) return targetTier + return targetTier + (diff - weakStrength) + } + return effectiveTier +} + +export function likelyTaker (ord: Order, rate: number): boolean { + if (ord.type === OrderTypeMarket || ord.tif === ImmediateTiF) return true + // Must cross the spread to be a taker (not so conservative). + if (rate === 0) return false + if (ord.sell) return ord.rate < rate + return ord.rate > rate +} + +const preparcelQuantity = (ord: Order, mkt?: Market, midGap?: number) => { + const qty = ord.qty - ord.filled + if (ord.type === OrderTypeLimit) return qty + if (ord.sell) return qty * ord.rate / RateEncodingFactor + const rate = midGap || mkt?.spot?.rate || 0 + // Caller should not call this for market orders without a mkt arg. + if (!mkt) return 0 + // This is tricky. The server will use the mid-gap rate to convert the + // order qty. We don't have a mid-gap rate, only a spot rate. + if (rate && (mkt?.spot?.bookVolume || 0) > 0) return qty * RateEncodingFactor / rate + return mkt.lotsize // server uses same fallback if book is empty +} + +export function epochWeight (ord: Order, mkt: Market, midGap?: number) { + if (ord.status !== StatusEpoch) return 0 + const qty = preparcelQuantity(ord, mkt, midGap) + const rate = midGap || mkt.spot?.rate || 0 + if (likelyTaker(ord, rate)) return qty * 2 + return qty +} + +function bookWeight (ord: Order) { + if (ord.status !== StatusBooked) return 0 + return preparcelQuantity(ord) +} + +function settlingWeight (ord: Order) { + let sum = 0 + for (const m of (ord.matches || [])) { + if (m.side === MatchSideMaker) { + if (m.status > MakerRedeemed) continue + } else if (m.status > TakerSwapCast) continue + sum += m.qty + } + return sum +} + +function parcelWeight (ord: Order, mkt: Market, midGap?: number) { + if (ord.type === OrderTypeCancel) return 0 + return epochWeight(ord, mkt, midGap) + bookWeight(ord) + settlingWeight(ord) +} + +// function roundParcels (p: number): number { +// return Math.floor(Math.round((p * 1e8)) / 1e8) +// } + +function limitBonus (score: number, maxScore: number): number { + return score > 0 ? 1 + score / maxScore * (parcelLimitScoreMultiplier - 1) : 1 +} + +export function tradingLimits (host: string): [number, number] { // [usedParcels, parcelLimit] + const { auth, maxScore, markets } = app().exchanges[host] + const { rep: { score } } = auth + const tier = strongTier(auth) + + let usedParcels = 0 + for (const mkt of Object.values(markets)) { + let mktWeight = 0 + for (const ord of (mkt.inflight || [])) mktWeight += parcelWeight(ord, mkt) + for (const ord of (mkt.orders || [])) mktWeight += parcelWeight(ord, mkt) + usedParcels += (mktWeight / (mkt.parcelsize * mkt.lotsize)) + } + const parcelLimit = perTierBaseParcelLimit * limitBonus(score, maxScore) * tier + return [usedParcels, parcelLimit] +} diff --git a/client/webserver/site/src/js/app.ts b/client/webserver/site/src/js/app.ts index eb2c41eb38..fdac6e3e91 100644 --- a/client/webserver/site/src/js/app.ts +++ b/client/webserver/site/src/js/app.ts @@ -22,6 +22,7 @@ import { Exchange, WalletState, BondNote, + ReputationNote, CoreNote, OrderNote, Market, @@ -508,6 +509,7 @@ export default class Application { * is used to update the dex tier and registration status. */ handleBondNote (note: BondNote) { + if (note.auth) this.exchanges[note.dex].auth = note.auth switch (note.topic) { case 'RegUpdate': if (note.coinID !== null) { // should never be null for RegUpdate @@ -602,6 +604,11 @@ export default class Application { case 'bondpost': this.handleBondNote(note as BondNote) break + case 'reputation': { + const n = note as ReputationNote + this.exchanges[n.host].auth.rep = n.rep + break + } case 'walletstate': case 'walletconfig': { // assets can be null if failed to connect to dex server. diff --git a/client/webserver/site/src/js/dexsettings.ts b/client/webserver/site/src/js/dexsettings.ts index 4920361497..5826042a86 100644 --- a/client/webserver/site/src/js/dexsettings.ts +++ b/client/webserver/site/src/js/dexsettings.ts @@ -1,19 +1,24 @@ -import Doc from './doc' +import Doc, { Animation } from './doc' import BasePage from './basepage' import State from './state' import { postJSON } from './http' import * as forms from './forms' import * as intl from './locales' - +import { ReputationMeter, strongTier } from './account' import { app, PageElement, ConnectionStatus, - Exchange + Exchange, + PasswordCache, + WalletState } from './registry' +interface Animator { + animate: (() => Promise) +} + const animationLength = 300 -const bondOverlap = 2 // See client/core/bond.go#L28 export default class DexSettingsPage extends BasePage { body: HTMLElement @@ -24,61 +29,178 @@ export default class DexSettingsPage extends BasePage { keyup: (e: KeyboardEvent) => void dexAddrForm: forms.DEXAddressForm bondFeeBufferCache: Record + newWalletForm: forms.NewWalletForm + unlockWalletForm: forms.UnlockWalletForm + regAssetForm: forms.FeeAssetSelectionForm + walletWaitForm: forms.WalletWaitForm + confirmRegisterForm: forms.ConfirmRegistrationForm + reputationMeter: ReputationMeter + animation: Animation + pwCache: PasswordCache constructor (body: HTMLElement) { super() this.body = body - this.host = body.dataset.host ? body.dataset.host : '' + this.pwCache = { pw: '' } + const host = this.host = body.dataset.host ? body.dataset.host : '' + const xc = app().exchanges[host] const page = this.page = Doc.idDescendants(body) this.forms = Doc.applySelector(page.forms, ':scope > form') + this.confirmRegisterForm = new forms.ConfirmRegistrationForm(page.confirmRegForm, () => { + this.showSuccess(intl.prep(intl.ID_TRADING_TIER_UPDATED)) + }, () => { + this.runAnimation(this.regAssetForm, page.regAssetForm) + }, this.pwCache) + this.confirmRegisterForm.setExchange(xc, '') + + this.walletWaitForm = new forms.WalletWaitForm(page.walletWait, () => { + this.runAnimation(this.confirmRegisterForm, page.confirmRegForm) + }, () => { + this.runAnimation(this.regAssetForm, page.regAssetForm) + }) + this.walletWaitForm.setExchange(xc) + + this.newWalletForm = new forms.NewWalletForm( + page.newWalletForm, + assetID => this.newWalletCreated(assetID), + this.pwCache, + () => this.runAnimation(this.regAssetForm, page.regAssetForm) + ) + + this.unlockWalletForm = new forms.UnlockWalletForm(page.unlockWalletForm, (assetID: number) => { + this.progressTierFormsWithWallet(assetID, app().walletMap[assetID]) + }, this.pwCache) + + this.regAssetForm = new forms.FeeAssetSelectionForm(page.regAssetForm, async (assetID: number, tier: number) => { + const asset = app().assets[assetID] + const wallet = asset.wallet + if (wallet) { + const loaded = app().loading(page.regAssetForm) + const bondsFeeBuffer = await this.getBondsFeeBuffer(assetID, page.regAssetForm) + this.confirmRegisterForm.setAsset(assetID, tier, bondsFeeBuffer) + loaded() + this.progressTierFormsWithWallet(assetID, wallet) + return + } + this.confirmRegisterForm.setAsset(assetID, tier, 0) + this.newWalletForm.setAsset(assetID) + this.showForm(page.newWalletForm) + }) + this.regAssetForm.setExchange(xc) + + this.reputationMeter = new ReputationMeter(page.repMeter) + this.reputationMeter.setHost(host) + Doc.bind(page.exportDexBtn, 'click', () => this.prepareAccountExport(page.authorizeAccountExportForm)) Doc.bind(page.disableAcctBtn, 'click', () => this.prepareAccountDisable(page.disableAccountForm)) - Doc.bind(page.bondDetailsBtn, 'click', () => this.prepareBondDetailsForm()) Doc.bind(page.updateCertBtn, 'click', () => page.certFileInput.click()) Doc.bind(page.updateHostBtn, 'click', () => this.prepareUpdateHost()) Doc.bind(page.certFileInput, 'change', () => this.onCertFileChange()) - Doc.bind(page.bondAssetSelect, 'change', () => this.updateBondAssetCosts()) - Doc.bind(page.bondTargetTier, 'input', () => this.updateBondAssetCosts()) + Doc.bind(page.changeTier, 'click', () => { + this.regAssetForm.setExchange(app().exchanges[host]) // reset form + this.showForm(page.regAssetForm) + }) this.dexAddrForm = new forms.DEXAddressForm(page.dexAddrForm, async (xc: Exchange) => { window.location.assign(`/dexsettings/${xc.host}`) }, undefined, this.host) - forms.bind(page.bondDetailsForm, page.updateBondOptionsConfirm, () => this.updateBondOptions()) + // forms.bind(page.bondDetailsForm, page.updateBondOptionsConfirm, () => this.updateBondOptions()) forms.bind(page.authorizeAccountExportForm, page.authorizeExportAccountConfirm, () => this.exportAccount()) forms.bind(page.disableAccountForm, page.disableAccountConfirm, () => this.disableAccount()) - const closePopups = () => { - Doc.hide(page.forms) - } - Doc.bind(page.forms, 'mousedown', (e: MouseEvent) => { - if (!Doc.mouseInElement(e, this.currentForm)) { closePopups() } + if (!Doc.mouseInElement(e, this.currentForm)) { this.closePopups() } }) this.keyup = (e: KeyboardEvent) => { if (e.key === 'Escape') { - closePopups() + this.closePopups() } } Doc.bind(document, 'keyup', this.keyup) page.forms.querySelectorAll('.form-closer').forEach(el => { - Doc.bind(el, 'click', () => { closePopups() }) + Doc.bind(el, 'click', () => { this.closePopups() }) }) app().registerNoteFeeder({ - conn: () => { this.setConnectionStatus() } + conn: () => { this.setConnectionStatus() }, + reputation: () => { this.updateReputation() }, + feepayment: () => { this.updateReputation() }, + bondpost: () => { this.updateReputation() } }) this.setConnectionStatus() + this.updateReputation() } unload () { Doc.unbind(document, 'keyup', this.keyup) } + async progressTierFormsWithWallet (assetID: number, wallet: WalletState) { + const { page, host, confirmRegisterForm: { fees } } = this + const asset = app().assets[assetID] + const xc = app().exchanges[host] + const bondAsset = xc.bondAssets[asset.symbol] + if (!wallet.open) { + if (State.passwordIsCached()) { + const loaded = app().loading(page.forms) + const res = await postJSON('/api/openwallet', { assetID: assetID }) + loaded() + if (!app().checkResponse(res)) { + this.regAssetForm.setError(`error unlocking wallet: ${res.msg}`) + this.runAnimation(this.regAssetForm, page.regAssetForm) + } + return + } else { + // Password not cached. Show the unlock wallet form. + this.unlockWalletForm.refresh(asset) + this.showForm(page.unlockWalletForm) + return + } + } + if (wallet.synced && wallet.balance.available >= 2 * bondAsset.amount + fees) { + // If we are raising our tier, we'll show a confirmation form + this.progressTierFormWithSyncedFundedWallet(assetID) + return + } + this.walletWaitForm.setWallet(wallet, fees) + this.showForm(page.walletWait) + } + + async progressTierFormWithSyncedFundedWallet (assetID: number) { + const xc = app().exchanges[this.host] + const tier = this.confirmRegisterForm.tier + const page = this.page + const strongTier = xc.auth.liveStrength + xc.auth.pendingStrength - xc.auth.weakStrength + if (tier > xc.auth.targetTier && tier > strongTier) { + this.runAnimation(this.confirmRegisterForm, page.confirmRegForm) + return + } + // Lowering tier + const updateErr = await this.updateBondOptions(assetID, tier) + if (updateErr) { + this.regAssetForm.setError(updateErr) + return + } + // this.animateConfirmForm(page.regAssetForm) + this.showSuccess(intl.prep(intl.ID_TRADING_TIER_UPDATED)) + } + + updateReputation () { + const page = this.page + const auth = app().exchanges[this.host].auth + const { rep: { penalties }, targetTier } = auth + const displayTier = strongTier(auth) + page.targetTier.textContent = String(targetTier) + page.effectiveTier.textContent = String(displayTier) + page.penalties.textContent = String(penalties) + this.reputationMeter.update() + } + /* showForm shows a modal form with a little animation. */ async showForm (form: HTMLElement) { const page = this.page @@ -93,6 +215,28 @@ export default class DexSettingsPage extends BasePage { form.style.right = '0' } + async runAnimation (ani: Animator, form: PageElement) { + Doc.hide(this.currentForm) + await ani.animate() + this.currentForm = form + Doc.show(form) + } + + closePopups () { + Doc.hide(this.page.forms) + if (this.animation) this.animation.stop() + } + + async showSuccess (msg: string) { + this.forms.forEach(form => Doc.hide(form)) + this.currentForm = this.page.checkmarkForm + this.animation = forms.showSuccess(this.page, msg) + await this.animation.wait() + this.animation = new Animation(1500, () => { /* pass */ }, '', () => { + if (this.currentForm === this.page.checkmarkForm) this.closePopups() + }) + } + // exportAccount exports and downloads the account info. async exportAccount () { const page = this.page @@ -160,64 +304,6 @@ export default class DexSettingsPage extends BasePage { this.showForm(disableAccountForm) } - // prepareBondDetailsForm resets and prepares the Bond Details form. - async prepareBondDetailsForm () { - const page = this.page - const xc = app().user.exchanges[this.host] - // Update bond details on this form - const targetTier = xc.auth.targetTier.toString() - page.currentTargetTier.textContent = `${targetTier}` - page.currentTier.textContent = `${xc.auth.effectiveTier}` - page.bondTargetTier.setAttribute('placeholder', targetTier) - page.bondTargetTier.value = '' - this.bondFeeBufferCache = {} - Doc.empty(page.bondAssetSelect) - for (const [assetSymbol, bondAsset] of Object.entries(xc.bondAssets)) { - const option = document.createElement('option') as HTMLOptionElement - option.value = bondAsset.id.toString() - option.textContent = assetSymbol.toUpperCase() - if (bondAsset.id === xc.auth.bondAssetID) option.selected = true - page.bondAssetSelect.appendChild(option) - } - page.bondOptionsErr.textContent = '' - Doc.hide(page.bondOptionsErr) - await this.updateBondAssetCosts() - this.showForm(page.bondDetailsForm) - } - - async updateBondAssetCosts () { - const xc = app().user.exchanges[this.host] - const page = this.page - const bondAssetID = parseInt(page.bondAssetSelect.value ?? '') - Doc.hide(page.bondCostFiat.parentElement as Element) - Doc.hide(page.bondReservationAmtFiat.parentElement as Element) - const assetInfo = xc.assets[bondAssetID] - const bondAsset = xc.bondAssets[assetInfo.symbol] - - const bondCost = bondAsset.amount - const ui = assetInfo.unitInfo - const assetID = bondAsset.id - Doc.applySelector(page.bondDetailsForm, '.bondAssetSym').forEach((el) => { el.textContent = assetInfo.symbol.toLocaleUpperCase() }) - page.bondCost.textContent = Doc.formatFullPrecision(bondCost, ui) - const xcRate = app().fiatRatesMap[assetID] - Doc.showFiatValue(page.bondCostFiat, bondCost, xcRate, ui) - - let feeBuffer = this.bondFeeBufferCache[assetInfo.symbol] - if (!feeBuffer) { - feeBuffer = await this.getBondsFeeBuffer(assetID, page.bondDetailsForm) - if (feeBuffer > 0) this.bondFeeBufferCache[assetInfo.symbol] = feeBuffer - } - if (feeBuffer === 0) { - page.bondReservationAmt.textContent = intl.prep(intl.ID_UNAVAILABLE) - return - } - const targetTier = parseInt(page.bondTargetTier.value ?? '') - let reservation = 0 - if (targetTier > 0) reservation = bondCost * targetTier * bondOverlap + feeBuffer - page.bondReservationAmt.textContent = Doc.formatFullPrecision(reservation, ui) - Doc.showFiatValue(page.bondReservationAmtFiat, reservation, xcRate, ui) - } - // Retrieve an estimate for the tx fee needed to create new bond reserves. async getBondsFeeBuffer (assetID: number, form: HTMLElement) { const loaded = app().loading(form) @@ -288,40 +374,39 @@ export default class DexSettingsPage extends BasePage { * updateBondOptions is called when the form to update bond options is * submitted. */ - async updateBondOptions () { - const page = this.page - const targetTier = parseInt(page.bondTargetTier.value ?? '') - const bondAssetID = parseInt(page.bondAssetSelect.value ?? '') - + async updateBondOptions (bondAssetID: number, targetTier: number): Promise { const bondOptions: Record = { host: this.host, targetTier: targetTier, bondAssetID: bondAssetID } - const assetInfo = app().assets[bondAssetID] - if (assetInfo) { - const feeBuffer = this.bondFeeBufferCache[assetInfo.symbol] - if (feeBuffer > 0) bondOptions.feeBuffer = feeBuffer - } - const loaded = app().loading(this.body) const res = await postJSON('/api/updatebondoptions', bondOptions) loaded() - if (!app().checkResponse(res)) { - page.bondOptionsErr.textContent = res.msg - Doc.show(page.bondOptionsErr) - } else { - Doc.hide(page.bondOptionsErr) - Doc.show(page.bondOptionsMsg) - setTimeout(() => { - Doc.hide(page.bondOptionsMsg) - }, 5000) - // update the in-memory values. - const xc = app().user.exchanges[this.host] - xc.auth.bondAssetID = bondAssetID - xc.auth.targetTier = targetTier - page.currentTargetTier.textContent = `${targetTier}` + if (!app().checkResponse(res)) return res.msg + return '' + } + + async newWalletCreated (assetID: number) { + this.regAssetForm.refresh() + const user = await app().fetchUser() + if (!user) return + const page = this.page + const asset = user.assets[assetID] + const wallet = asset.wallet + const xc = app().exchanges[this.host] + const bondAmt = xc.bondAssets[asset.symbol].amount + + const bondsFeeBuffer = await this.getBondsFeeBuffer(assetID, page.newWalletForm) + this.confirmRegisterForm.setFees(assetID, bondsFeeBuffer) + + if (wallet.synced && wallet.balance.available >= 2 * bondAmt + bondsFeeBuffer) { + this.progressTierFormWithSyncedFundedWallet(assetID) + return } + + this.walletWaitForm.setWallet(wallet, bondsFeeBuffer) + await this.showForm(page.walletWait) } } diff --git a/client/webserver/site/src/js/doc.ts b/client/webserver/site/src/js/doc.ts index 277a1bee2d..4f26267370 100644 --- a/client/webserver/site/src/js/doc.ts +++ b/client/webserver/site/src/js/doc.ts @@ -59,11 +59,6 @@ const fourSigFigs = new Intl.NumberFormat((navigator.languages as string[]), { maximumSignificantDigits: 4 }) -const oneFractionalDigit = new Intl.NumberFormat((navigator.languages as string[]), { - minimumFractionDigits: 1, - maximumFractionDigits: 1 -}) - /* A cache for formatters used for Doc.formatCoinValue. */ const decimalFormatters: Record = {} @@ -302,7 +297,7 @@ export default class Doc { } static formatFourSigFigs (n: number): string { - return formatSigFigsWithFormatters(oneFractionalDigit, fourSigFigs, n) + return formatSigFigsWithFormatters(intFormatter, fourSigFigs, n) } /* diff --git a/client/webserver/site/src/js/forms.ts b/client/webserver/site/src/js/forms.ts index 0163bf49c7..2743e0ed5c 100644 --- a/client/webserver/site/src/js/forms.ts +++ b/client/webserver/site/src/js/forms.ts @@ -1,9 +1,15 @@ -import Doc from './doc' +import Doc, { Animation } from './doc' import { postJSON } from './http' import State from './state' import * as intl from './locales' import * as OrderUtil from './orderutil' import { Wave } from './charts' +import { + bondReserveMultiplier, + perTierBaseParcelLimit, + parcelLimitScoreMultiplier, + strongTier +} from './account' import { app, PasswordCache, @@ -753,6 +759,8 @@ export class ConfirmRegistrationForm { xc: Exchange certFile: string bondAssetID: number + tier: number + fees: number pwCache: PasswordCache constructor (form: HTMLElement, success: () => void, goBack: () => void, pwCache: PasswordCache) { @@ -763,17 +771,6 @@ export class ConfirmRegistrationForm { this.pwCache = pwCache Doc.bind(this.page.goBack, 'click', () => goBack()) - // bondStrengthField is presently hidden since there is no scaling of user - // limits yet, and there needs to be considerable explanation of why - // anything other than 1 would be used. Unhide bondStrengthInput to show it - // when we are ready. (TODO) - Doc.bind(this.page.bondStrengthField, 'input', () => { - const asset = app().assets[this.bondAssetID] - if (!asset) return - const ui = asset.unitInfo - const bondAsset = this.xc.bondAssets[asset.symbol] - this.page.bondAmt.textContent = Doc.formatCoinValue(this.totalBondAmount(bondAsset.amount), ui) - }) bind(form, this.page.submit, () => this.submitForm()) } @@ -786,20 +783,33 @@ export class ConfirmRegistrationForm { page.host.textContent = xc.host } - setAsset (assetID: number) { + setAsset (assetID: number, tier: number, fees: number) { const asset = app().assets[assetID] - const ui = asset.unitInfo + const { conversionFactor, unit } = asset.unitInfo.conventional this.bondAssetID = asset.id + this.tier = tier + this.fees = fees const page = this.page - const bondAsset = this.xc.bondAssets[asset.symbol] - page.bondAmt.textContent = Doc.formatCoinValue(this.totalBondAmount(bondAsset.amount), ui) - page.bondUnit.textContent = ui.conventional.unit.toUpperCase() + const xc = this.xc + const bondAsset = xc.bondAssets[asset.symbol] + const bondLock = bondAsset.amount * tier * bondReserveMultiplier + const bondLockConventional = bondLock / conversionFactor + page.tradingTier.textContent = String(tier) page.logo.src = Doc.logoPath(asset.symbol) + page.bondLock.textContent = Doc.formatFourSigFigs(bondLockConventional) + page.bondUnit.textContent = unit + const r = app().fiatRatesMap[assetID] + Doc.show(page.bondLockUSDBox) + if (r) page.bondLockUSD.textContent = Doc.formatFourSigFigs(bondLockConventional * r) + else Doc.hide(page.bondLockUSDBox) + if (fees) page.feeReserves.textContent = Doc.formatFourSigFigs(fees / conversionFactor) + page.reservesUnit.textContent = unit } - totalBondAmount (singleBondAmount: number): number { - const bondStrength = +(this.page.bondStrengthField.value ?? 1) - return bondStrength * singleBondAmount + setFees (assetID: number, fees: number) { + this.fees = fees + const conversionFactor = app().assets[assetID].unitInfo.conventional.conversionFactor + this.page.feeReserves.textContent = Doc.formatFourSigFigs(fees / conversionFactor) } /* Form expands into its space quickly from the lower-right as it fades in. */ @@ -830,20 +840,34 @@ export class ConfirmRegistrationForm { return } Doc.hide(page.regErr) - const bondAsset = this.xc.bondAssets[asset.wallet.symbol] + const xc = this.xc + const bondAsset = xc.bondAssets[asset.wallet.symbol] const cert = await this.certFile - const dexAddr = this.xc.host + const dexAddr = xc.host const pw = page.appPass.value || (this.pwCache ? this.pwCache.pw : '') - const postBondForm = { - addr: dexAddr, - cert: cert, - pass: pw, - bond: this.totalBondAmount(bondAsset.amount), - asset: bondAsset.id + let form: any + let url: string + + if (xc.viewOnly || !app().exchanges[xc.host]) { + form = { + addr: dexAddr, + cert: cert, + pass: pw, + bond: bondAsset.amount * this.tier, + asset: bondAsset.id + } + url = '/api/postbond' + } else { + form = { + host: dexAddr, + targetTier: this.tier, + bondAssetID: this.bondAssetID + } + url = '/api/updatebondoptions' } page.appPass.value = '' const loaded = app().loading(this.form) - const res = await postJSON('/api/postbond', postBondForm) + const res = await postJSON(url, form) loaded() if (!app().checkResponse(res)) { page.regErr.textContent = res.msg @@ -854,21 +878,54 @@ export class ConfirmRegistrationForm { } } +interface RegAssetTemplate { + tmpl: Record + asset: SupportedAsset + setTier: ((tier: number) => void) +} + /* * FeeAssetSelectionForm should be used with the "regAssetForm" template. */ export class FeeAssetSelectionForm { form: HTMLElement - success: (assetID: number) => void + success: (assetID: number, tier: number) => Promise xc: Exchange page: Record - assetTmpls: Record> + assetTmpls: Record - constructor (form: HTMLElement, success: (assetID: number) => Promise) { + constructor (form: HTMLElement, success: (assetID: number, tier: number) => Promise) { this.form = form this.success = success - this.page = Doc.parseTemplate(form) - Doc.cleanTemplates(this.page.marketTmpl, this.page.assetTmpl) + const page = this.page = Doc.parseTemplate(form) + Doc.cleanTemplates(page.assetTmpl, page.marketTmpl) + + Doc.bind(page.regAssetTier, 'input', () => { + this.clearError() + const raw = page.regAssetTier.value ?? '' + const tier = parseInt(raw) + if (isNaN(tier)) { + if (raw) this.setError(intl.prep(intl.ID_INVALID_TIER_VALUE)) + return + } + for (const a of Object.values(this.assetTmpls)) a.setTier(tier) + }) + + Doc.bind(page.regAssetTier, 'keyup', (e: KeyboardEvent) => { + if (!(e.key === 'Enter')) return + const { auth: { targetTier, bondAssetID }, viewOnly } = this.xc + if (viewOnly || targetTier === 0) { // They need to select an asset. + Doc.hide(page.assetSelected) // Probably already showing, but do it anyway. + Doc.show(page.assetSelection) + return + } + this.submit(bondAssetID) + }) + + Doc.bind(page.chooseDifferentAsset, 'click', () => { + Doc.hide(page.assetSelected) + Doc.show(page.assetSelection) + }) app().registerNoteFeeder({ createwallet: (note: WalletCreationNote) => { @@ -877,11 +934,21 @@ export class FeeAssetSelectionForm { }) } + setError (errMsg: string) { + this.page.regAssetErr.textContent = errMsg + Doc.show(this.page.regAssetErr) + } + + clearError () { + Doc.hide(this.page.regAssetErr) + } + setExchange (xc: Exchange) { this.xc = xc this.assetTmpls = {} const page = this.page - Doc.empty(page.assets, page.allMarkets) + Doc.empty(page.assets, page.otherAssets, page.allMarkets) + this.clearError() const cFactor = (ui: UnitInfo) => ui.conventional.conversionFactor @@ -889,10 +956,10 @@ export class FeeAssetSelectionForm { const n = page.marketTmpl.cloneNode(true) as HTMLElement const marketTmpl = Doc.parseTemplate(n) - const baseAsset = app().assets[mkt.baseid] - const quoteAsset = app().assets[mkt.quoteid] + const { symbol: baseSymbol, unitInfo: bui } = xc.assets[mkt.baseid] + const { symbol: quoteSymbol, unitInfo: qui } = xc.assets[mkt.quoteid] - if (cFactor(baseAsset.unitInfo) === 0 || cFactor(quoteAsset.unitInfo) === 0) return null + if (cFactor(bui) === 0 || cFactor(qui) === 0) return null if (typeof excludeIcon !== 'undefined') { const excludeBase = excludeIcon === mkt.baseid @@ -900,55 +967,131 @@ export class FeeAssetSelectionForm { marketTmpl.logo.src = Doc.logoPath(otherSymbol) } else { const otherLogo = marketTmpl.logo.cloneNode(true) as PageElement - marketTmpl.logo.src = Doc.logoPath(baseAsset.symbol) - otherLogo.src = Doc.logoPath(quoteAsset.symbol) + marketTmpl.logo.src = Doc.logoPath(baseSymbol) + otherLogo.src = Doc.logoPath(quoteSymbol) const parent = marketTmpl.logo.parentNode if (parent) parent.insertBefore(otherLogo, marketTmpl.logo.nextSibling) } - marketTmpl.baseName.replaceWith(Doc.symbolize(baseAsset)) - marketTmpl.quoteName.replaceWith(Doc.symbolize(quoteAsset)) + marketTmpl.baseName.textContent = bui.conventional.unit + marketTmpl.quoteName.textContent = qui.conventional.unit - marketTmpl.lotSize.textContent = Doc.formatCoinValue(mkt.lotsize, baseAsset.unitInfo) - marketTmpl.lotSizeSymbol.replaceWith(Doc.symbolize(baseAsset)) + marketTmpl.lotSize.textContent = Doc.formatCoinValue(mkt.lotsize, bui) + marketTmpl.lotSizeSymbol.replaceWith(bui.conventional.unit) if (mkt.spot) { Doc.show(marketTmpl.quoteLotSize) - const r = cFactor(quoteAsset.unitInfo) / cFactor(baseAsset.unitInfo) + const r = cFactor(qui) / cFactor(bui) const quoteLot = mkt.lotsize * mkt.spot.rate / OrderUtil.RateEncodingFactor * r - const s = Doc.formatCoinValue(quoteLot, quoteAsset.unitInfo) - marketTmpl.quoteLotSize.textContent = `(~${s} ${quoteAsset.symbol})` + const s = Doc.formatCoinValue(quoteLot, qui) + marketTmpl.quoteLotSize.textContent = `(~${s} ${qui})` } return n } - for (const [symbol, bondAsset] of Object.entries(xc.bondAssets)) { - const asset = app().assets[bondAsset.id] - if (!asset) continue + const addAssetRow = (table: PageElement, assetID: number, bondAsset?: BondAsset) => { + const asset = app().assets[assetID] + if (!asset) return const unitInfo = asset.unitInfo - const assetNode = page.assetTmpl.cloneNode(true) as HTMLElement - Doc.bind(assetNode, 'click', () => { this.success(bondAsset.id) }) - const assetTmpl = this.assetTmpls[bondAsset.id] = Doc.parseTemplate(assetNode) - page.assets.appendChild(assetNode) - assetTmpl.logo.src = Doc.logoPath(symbol) - const fee = Doc.formatCoinValue(bondAsset.amount, unitInfo) - assetTmpl.feeAmt.textContent = String(fee) - assetTmpl.feeSymbol.replaceWith(Doc.symbolize(asset)) - assetTmpl.confs.textContent = String(bondAsset.confs) - setReadyMessage(assetTmpl.ready, asset) - - let count = 0 - for (const mkt of Object.values(xc.markets)) { - if (mkt.baseid !== bondAsset.id && mkt.quoteid !== bondAsset.id) continue - const node = marketNode(mkt, bondAsset.id) - if (!node) continue - count++ - assetTmpl.markets.appendChild(node) + const tr = page.assetTmpl.cloneNode(true) as HTMLElement + table.appendChild(tr) + const tmpl = Doc.parseTemplate(tr) + + tmpl.logo.src = Doc.logoPath(asset.symbol) + + tmpl.tradeLimitSymbol.textContent = unitInfo.conventional.unit + tmpl.name.textContent = asset.name + // assetTmpl.confs.textContent = String(bondAsset.confs) + const setTier = (tier: number) => { + let low = 0 + let high = 0 + const cFactor = app().unitInfo(asset.id, xc).conventional.conversionFactor + for (const { baseid: baseID, quoteid: quoteID, parcelsize: parcelSize, lotsize: lotSize, spot } of Object.values(xc.markets)) { + const conventionalLotSize = lotSize / cFactor + let resolvedLotSize = 0 + if (baseID === asset.id) { + resolvedLotSize = conventionalLotSize + } else if (quoteID === asset.id) { + const baseRate = app().fiatRatesMap[baseID] + const quoteRate = app().fiatRatesMap[quoteID] + if (baseRate && quoteRate) { + resolvedLotSize = conventionalLotSize * baseRate / quoteRate + } else if (spot) { + const bui = xc.assets[baseID].unitInfo + const qui = xc.assets[quoteID].unitInfo + const rateConversionFactor = OrderUtil.RateEncodingFactor / bui.conventional.conversionFactor * qui.conventional.conversionFactor + const conventionalRate = spot.rate / rateConversionFactor + resolvedLotSize = conventionalLotSize * conventionalRate + } + } + if (resolvedLotSize) { + const startingLimit = resolvedLotSize * parcelSize * perTierBaseParcelLimit * tier + const privilegedLimit = resolvedLotSize * parcelSize * perTierBaseParcelLimit * parcelLimitScoreMultiplier * tier + if (low === 0 || startingLimit < low) low = startingLimit + if (privilegedLimit > high) high = privilegedLimit + } + } + + const r = app().fiatRatesMap[assetID] + + if (low && high) { + tmpl.tradeLimitLow.textContent = Doc.formatFourSigFigs(low) + tmpl.tradeLimitHigh.textContent = Doc.formatFourSigFigs(high) + if (r) { + tmpl.fiatTradeLimitLow.textContent = Doc.formatFourSigFigs(low * r) + tmpl.fiatTradeLimitHigh.textContent = Doc.formatFourSigFigs(high * r) + } + } + + if (!bondAsset) { + Doc.hide(tmpl.bondData, tmpl.ready) + tr.classList.remove('hoverbg', 'pointer') + return + } + setReadyMessage(tmpl.ready, asset) + const bondLock = bondAsset.amount * bondReserveMultiplier * tier + const fee = Doc.formatCoinValue(bondLock, unitInfo) + tmpl.feeAmt.textContent = String(fee) + if (r) tmpl.fiatBondAmount.textContent = Doc.formatFiatConversion(bondLock, r, asset.unitInfo) + } + + if (bondAsset) { + Doc.bind(tr, 'click', () => { this.submit(assetID) }) + tmpl.feeSymbol.textContent = unitInfo.conventional.unit } - if (count < 3) Doc.hide(assetTmpl.fader) + + setTier(strongTier(xc.auth) || 1) + this.assetTmpls[asset.symbol] = { tmpl, asset, setTier } + } + + const nonBondAssets: number[] = [] + for (const { symbol, id: assetID } of Object.values(xc.assets)) { + if (!app().assets[assetID]) continue + const bondAsset = xc.bondAssets[symbol] + if (bondAsset) { + addAssetRow(page.assets, assetID, bondAsset) + continue + } + nonBondAssets.push(assetID) + } + + for (const assetID of nonBondAssets) { + addAssetRow(page.otherAssets, assetID) } page.host.textContent = xc.host + page.regAssetTier.value = xc.auth.targetTier ? String(xc.auth.targetTier) : '1' + + Doc.show(page.assetSelection) + Doc.hide(page.assetSelected) + if (!xc.viewOnly && xc.auth.targetTier > 0) { + const currentBondAsset = app().assets[xc.auth.bondAssetID] + page.currentAssetLogo.src = Doc.logoPath(currentBondAsset.symbol) + page.currentAssetName.textContent = currentBondAsset.name + Doc.hide(page.assetSelection) + Doc.show(page.assetSelected) + } + for (const mkt of Object.values(xc.markets)) { const node = marketNode(mkt) if (!node) continue @@ -961,25 +1104,34 @@ export class FeeAssetSelectionForm { * completes successfully. */ walletCreated (assetID: number) { - const tmpl = this.assetTmpls[assetID] + const a = this.assetTmpls[assetID] const asset = app().assets[assetID] - setReadyMessage(tmpl.ready, asset) + setReadyMessage(a.tmpl.ready, asset) } refresh () { this.setExchange(this.xc) } + submit (assetID: number) { + this.clearError() + const raw = this.page.regAssetTier.value ?? '' + const tier = parseInt(raw) + if (isNaN(tier)) { + if (raw) this.setError(intl.prep(intl.ID_INVALID_TIER_VALUE)) + return + } + this.success(assetID, tier) + } + /* * Animation to make the elements sort of expand into their space from the * bottom as they fade in. */ async animate () { const { page, form } = this - const how = page.how const extraMargin = 75 const extraTop = 50 - const fontSize = 24 const regAssetElements = Array.from(page.assets.children) as PageElement[] regAssetElements.push(page.allmkts) form.style.opacity = '0' @@ -992,7 +1144,6 @@ export class FeeAssetSelectionForm { } form.style.opacity = Math.pow(prog, 4).toFixed(1) form.style.paddingTop = `${(1 - prog) * extraTop}px` - how.style.fontSize = `${fontSize * prog}px` }, 'easeOut') } } @@ -1607,8 +1758,8 @@ export class DEXAddressForm { } return } + await app().fetchUser() if (!this.dexToUpdate && (skipRegistration || res.paid || Object.keys(res.xc.auth.pendingBonds).length > 0)) { - await app().fetchUser() await app().loadPage('markets') return } @@ -1933,6 +2084,22 @@ export async function slideSwap (form1: HTMLElement, form2: HTMLElement) { form2.style.right = '0' } +export function showSuccess (page: Record, msg: string) { + page.successMessage.textContent = msg + Doc.show(page.forms, page.checkmarkForm) + page.checkmarkForm.style.right = '0' + page.checkmark.style.fontSize = '0px' + + const [startR, startG, startB] = State.isDark() ? [223, 226, 225] : [51, 51, 51] + const [endR, endG, endB] = [16, 163, 16] + const [diffR, diffG, diffB] = [endR - startR, endG - startG, endB - startB] + + return new Animation(1200, (prog: number) => { + page.checkmark.style.fontSize = `${prog * 80}px` + page.checkmark.style.color = `rgb(${startR + prog * diffR}, ${startG + prog * diffG}, ${startB + prog * diffB})` + }, 'easeOutElastic') +} + /* * bind binds the click and submit events and prevents page reloading on * submission. diff --git a/client/webserver/site/src/js/locales.ts b/client/webserver/site/src/js/locales.ts index e0d1906cb3..5ba429d7bd 100644 --- a/client/webserver/site/src/js/locales.ts +++ b/client/webserver/site/src/js/locales.ts @@ -143,11 +143,13 @@ export const ID_TICKET_STATUS_UNSPENT = 'TICKET_STATUS_UNSPENT' export const ID_TICKET_STATUS_REVOKED = 'TICKET_STATUS_REVOKED' export const ID_INVALID_SEED = 'INVALID_SEED' export const ID_PASSWORD_RESET_SUCCESS_MSG = 'PASSWORD_RESET_SUCCESS_MSG ' -export const ID_BROWSER_NTFN_ENABLED = 'ID_BROWSER_NTFN_ENABLED' -export const ID_BROWSER_NTFN_ORDERS = 'ID_BROWSER_NTFN_ORDERS' -export const ID_BROWSER_NTFN_MATCHES = 'ID_BROWSER_NTFN_MATCHES' -export const ID_BROWSER_NTFN_BONDS = 'ID_BROWSER_NTFN_BONDS' -export const ID_BROWSER_NTFN_CONNECTIONS = 'ID_BROWSER_NTFN_CONNECTIONS' +export const ID_BROWSER_NTFN_ENABLED = 'BROWSER_NTFN_ENABLED' +export const ID_BROWSER_NTFN_ORDERS = 'BROWSER_NTFN_ORDERS' +export const ID_BROWSER_NTFN_MATCHES = 'BROWSER_NTFN_MATCHES' +export const ID_BROWSER_NTFN_BONDS = 'BROWSER_NTFN_BONDS' +export const ID_BROWSER_NTFN_CONNECTIONS = 'BROWSER_NTFN_CONNECTIONS' +export const ID_TRADING_TIER_UPDATED = 'TRADING_TIER_UPDATED' +export const ID_INVALID_TIER_VALUE = 'INVALID_TIER_VALUE' export const enUS: Locale = { [ID_NO_PASS_ERROR_MSG]: 'password cannot be empty', @@ -297,7 +299,9 @@ export const enUS: Locale = { [ID_BROWSER_NTFN_ORDERS]: 'Orders', [ID_BROWSER_NTFN_MATCHES]: 'Matches', [ID_BROWSER_NTFN_BONDS]: 'Bonds', - [ID_BROWSER_NTFN_CONNECTIONS]: 'Server connections' + [ID_BROWSER_NTFN_CONNECTIONS]: 'Server connections', + [ID_TRADING_TIER_UPDATED]: 'Trading Tier Updated', + [ID_INVALID_TIER_VALUE]: 'Invalid tier value' } export const ptBR: Locale = { diff --git a/client/webserver/site/src/js/markets.ts b/client/webserver/site/src/js/markets.ts index bc7b789ca0..cb76348ba4 100644 --- a/client/webserver/site/src/js/markets.ts +++ b/client/webserver/site/src/js/markets.ts @@ -2,6 +2,7 @@ import Doc, { WalletIcons } from './doc' import State from './state' import BasePage from './basepage' import OrderBook from './orderbook' +import { ReputationMeter, tradingLimits, strongTier } from './account' import { CandleChart, DepthChart, @@ -176,6 +177,7 @@ export default class MarketsPage extends BasePage { unlockForm: UnlockWalletForm newWalletForm: NewWalletForm depositAddrForm: DepositAddress + reputationMeter: ReputationMeter keyup: (e: KeyboardEvent) => void secondTicker: number candlesLoading: LoadTracker | null @@ -265,6 +267,8 @@ export default class MarketsPage extends BasePage { this.depositAddrForm = new DepositAddress(page.deposit) } + this.reputationMeter = new ReputationMeter(page.reputationMeter) + // Bind toggle wallet status form. bindForm(page.toggleWalletStatusConfirm, page.toggleWalletStatusSubmit, async () => { this.toggleWalletStatus() }) @@ -500,7 +504,9 @@ export default class MarketsPage extends BasePage { balance: (note: BalanceNote) => { this.handleBalanceNote(note) }, bondpost: (note: BondNote) => { this.handleBondUpdate(note) }, spots: (note: SpotPriceNote) => { this.handlePriceUpdate(note) }, - walletstate: (note: WalletStateNote) => { this.handleWalletState(note) } + walletstate: (note: WalletStateNote) => { this.handleWalletState(note) }, + reputation: () => { this.updateReputation() }, + feepayment: () => { this.updateReputation() } }) this.loadingAnimations = {} @@ -1121,6 +1127,9 @@ export default class MarketsPage extends BasePage { this.setCandleDurBttns() this.previewQuoteAmt(false) this.updateTitle() + this.reputationMeter.setHost(dex.host) + this.updateReputation() + this.updateTradeLimits(null) this.loadUserOrders() } @@ -1248,6 +1257,7 @@ export default class MarketsPage extends BasePage { }] } this.drawChartLines() + this.updateTradeLimits(order) if (!show || !adjusted || !order.qty) { page.orderPreview.textContent = '' this.drawChartLines() @@ -1262,6 +1272,24 @@ export default class MarketsPage extends BasePage { else this.preBuy() } + updateTradeLimits (ord: TradeForm | null) { + const { page, market: { cfg: mkt } } = this + if (!ord) { + page.orderFormParcels.textContent = '0' + return + } + const [msgRate, conversionRate] = this.anyRate() + + let parcelWeight = ord.qty / mkt.lotsize + if (!ord.isLimit && !ord.sell) { + if (conversionRate) parcelWeight = ord.qty / conversionRate / mkt.lotsize + else parcelWeight = 1 / mkt.parcelsize + } + + if (!ord.isLimit || ord.tifnow || ((ord.sell && ord.rate < msgRate) || (!ord.sell && ord.rate > msgRate))) parcelWeight *= 2 + page.orderFormParcels.textContent = (parcelWeight / mkt.parcelsize).toFixed(1) + } + /** * preSell populates the max order message for the largest available sell. */ @@ -1435,7 +1463,7 @@ export default class MarketsPage extends BasePage { /* * midGapConventional is the same as midGap, but returns the mid-gap rate as * the conventional ratio. This is used to convert from a conventional - * quantity from base to quote or vice-versa. + * quantity from base to quote or vice-versa, or for display purposes. */ midGapConventional () { const gap = this.midGap() @@ -1448,7 +1476,9 @@ export default class MarketsPage extends BasePage { * midGap returns the value in the middle of the best buy and best sell. If * either one of the buy or sell sides are empty, midGap returns the best rate * from the other side. If both sides are empty, midGap returns the value - * null. The rate returned is the atomic ratio. + * null. The rate returned is the atomic ratio, used for conversion. For a + * conventional rate for display or to convert conventional units, use + * midGapConventional */ midGap () { const book = this.book @@ -2382,9 +2412,57 @@ export default class MarketsPage extends BasePage { // Update local copy of Exchange. this.market.dex = app().exchanges[dexAddr] this.setRegistrationStatusVisibility() + this.updateReputation() + } + + updateReputation () { + const { page, market: { dex: { host }, cfg: mkt, baseCfg: { unitInfo: bui }, quoteCfg: { unitInfo: qui } } } = this + const { auth } = app().exchanges[host] + + page.parcelSizeLots.textContent = String(mkt.parcelsize) + page.marketLimitBase.textContent = Doc.formatFourSigFigs(mkt.parcelsize * mkt.lotsize / bui.conventional.conversionFactor) + page.marketLimitBaseUnit.textContent = bui.conventional.unit + page.marketLimitQuoteUnit.textContent = qui.conventional.unit + const conversionRate = this.anyRate()[1] + if (conversionRate) { + const quoteLot = mkt.lotsize * conversionRate + page.marketLimitQuote.textContent = Doc.formatFourSigFigs(quoteLot / qui.conventional.conversionFactor) + } else page.marketLimitQuote.textContent = '-' + + const tier = strongTier(auth) + page.tradingTier.textContent = String(tier) + const [usedParcels, parcelLimit] = tradingLimits(host) + page.tradingLimit.textContent = parcelLimit.toFixed(1) + page.limitUsage.textContent = (usedParcels / parcelLimit * 100).toFixed(1) + page.orderParcelsRemain.textContent = (parcelLimit - usedParcels).toFixed(1) + page.orderTradingTier.textContent = String(tier) + + this.reputationMeter.update() + } + + /* + * anyRate finds the best rate from any of, in order of priority, the order + * book, the server's reported spot rate, or the fiat exchange rates. A + * 3-tuple of message-rate encoding, a conversion rate, and a conventional + * rate is generated. + */ + anyRate (): [number, number, number] { + const { cfg: { spot }, base: { id: baseID }, quote: { id: quoteID }, rateConversionFactor } = this.market + const midGap = this.midGap() + if (midGap) return [midGap * OrderUtil.RateEncodingFactor, midGap, this.midGapConventional() || 0] + if (spot && spot.rate) return [spot.rate, spot.rate / OrderUtil.RateEncodingFactor, spot.rate / rateConversionFactor] + const [baseUSD, quoteUSD] = [app().fiatRatesMap[baseID], app().fiatRatesMap[quoteID]] + if (baseUSD && quoteUSD) { + const conventionalRate = baseUSD / quoteUSD + const msgRate = conventionalRate * rateConversionFactor + const conversionRate = msgRate / OrderUtil.RateEncodingFactor + return [msgRate, conversionRate, conventionalRate] + } + return [0, 0, 0] } handleMatchNote (note: MatchNote) { + this.updateReputation() const mord = this.metaOrders[note.orderID] if (!mord) return this.refreshActiveOrders() else if (mord.ord.type === OrderUtil.Market && note.match.status === OrderUtil.NewlyMatched) { // Update the average market rate display. @@ -2401,6 +2479,7 @@ export default class MarketsPage extends BasePage { * used to update a user's order's status. */ handleOrderNote (note: OrderNote) { + this.updateReputation() const order = note.order const mord = this.metaOrders[order.id] // - If metaOrder doesn't exist for the given order it means it was created @@ -2587,7 +2666,7 @@ export default class MarketsPage extends BasePage { const page = this.page const lots = parseInt(page.lotField.value || '0') if (lots <= 0) { - page.lotField.value = '0' + page.lotField.value = page.lotField.value === '' ? '' : '0' page.qtyField.value = '' this.previewQuoteAmt(false) return @@ -2630,6 +2709,7 @@ export default class MarketsPage extends BasePage { const page = this.page const qty = convertToAtoms(page.mktBuyField.value || '', this.market.quoteUnitInfo.conventional.conversionFactor) const gap = this.midGap() + this.updateTradeLimits(this.parseOrder()) if (!gap || !qty) { page.mktBuyLots.textContent = '0' page.mktBuyScore.textContent = '0' diff --git a/client/webserver/site/src/js/orderutil.ts b/client/webserver/site/src/js/orderutil.ts index 3ea64c441c..eb5d99c016 100644 --- a/client/webserver/site/src/js/orderutil.ts +++ b/client/webserver/site/src/js/orderutil.ts @@ -9,9 +9,13 @@ import { import { BooleanOption, XYRangeOption } from './opts' import Doc from './doc' -export const Limit = 1 -export const Market = 2 -export const Cancel = 3 +export const Limit = 1 // TODO: Delete for the versions below +export const Market = 2 // TODO: Delete for the versions below +export const Cancel = 3 // TODO: Delete for the versions below + +export const OrderTypeLimit = 1 +export const OrderTypeMarket = 2 +export const OrderTypeCancel = 3 /* The time-in-force specifiers are a mirror of dex/order.TimeInForce. */ export const ImmediateTiF = 0 @@ -34,8 +38,11 @@ export const MatchComplete = 4 export const MatchConfirmed = 5 /* The match sides are a mirror of dex/order.MatchSide. */ -export const Maker = 0 -export const Taker = 1 +export const Maker = 0 // TODO: Delete for the versions below +export const Taker = 1 // TODO: Delete for the versions below + +export const MatchSideMaker = 0 +export const MatchSideTaker = 1 /* * RateEncodingFactor is used when encoding an atomic exchange rate as an diff --git a/client/webserver/site/src/js/register.ts b/client/webserver/site/src/js/register.ts index e8ec059851..b90cd42100 100644 --- a/client/webserver/site/src/js/register.ts +++ b/client/webserver/site/src/js/register.ts @@ -23,7 +23,7 @@ import State from './state' export default class RegistrationPage extends BasePage { body: HTMLElement pwCache: PasswordCache - currentDEX: Exchange + currentDEX: Exchange // TODO: Just use host and pull xc from app() as needed. page: Record loginForm: LoginForm appPassResetForm: AppPassResetForm @@ -95,14 +95,13 @@ export default class RegistrationPage extends BasePage { } // SELECT REG ASSET - this.regAssetForm = new FeeAssetSelectionForm(page.regAssetForm, async assetID => { - this.confirmRegisterForm.setAsset(assetID) - + this.regAssetForm = new FeeAssetSelectionForm(page.regAssetForm, async (assetID: number, tier: number) => { const asset = app().assets[assetID] const wallet = asset.wallet if (wallet) { const bondAsset = this.currentDEX.bondAssets[asset.symbol] const bondsFeeBuffer = await this.getBondsFeeBuffer(assetID, page.regAssetForm) + this.confirmRegisterForm.setAsset(assetID, tier, bondsFeeBuffer) if (wallet.synced && wallet.balance.available >= 2 * bondAsset.amount + bondsFeeBuffer) { this.animateConfirmForm(page.regAssetForm) return diff --git a/client/webserver/site/src/js/registry.ts b/client/webserver/site/src/js/registry.ts index b5d6548a06..6f8af7a5fe 100644 --- a/client/webserver/site/src/js/registry.ts +++ b/client/webserver/site/src/js/registry.ts @@ -54,6 +54,8 @@ export interface Exchange { viewOnly: boolean bondAssets: Record candleDurs: string[] + maxScore: number + penaltyThreshold: number } export interface Candle { @@ -80,6 +82,7 @@ export interface Market { quoteid: number quotesymbol: string lotsize: number + parcelsize: number ratestep: number epochlen: number startepoch: number @@ -351,6 +354,12 @@ export interface BondNote extends CoreNote { dex: string coinID: string | null tier: number | null + auth: ExchangeAuth | null +} + +export interface ReputationNote extends CoreNote { + host: string + rep: Reputation } export interface BalanceNote extends CoreNote { diff --git a/client/webserver/site/src/js/settings.ts b/client/webserver/site/src/js/settings.ts index 0fa66f377b..ddc5707644 100644 --- a/client/webserver/site/src/js/settings.ts +++ b/client/webserver/site/src/js/settings.ts @@ -75,14 +75,13 @@ export default class SettingsPage extends BasePage { }) // Asset selection - this.regAssetForm = new forms.FeeAssetSelectionForm(page.regAssetForm, async (assetID: number) => { - this.confirmRegisterForm.setAsset(assetID) - + this.regAssetForm = new forms.FeeAssetSelectionForm(page.regAssetForm, async (assetID: number, tier: number) => { const asset = app().assets[assetID] const wallet = asset.wallet if (wallet) { const bondAsset = this.currentDEX.bondAssets[asset.symbol] const bondsFeeBuffer = await this.getBondsFeeBuffer(assetID, page.regAssetForm) + this.confirmRegisterForm.setAsset(assetID, tier, bondsFeeBuffer) if (wallet.synced && wallet.balance.available >= 2 * bondAsset.amount + bondsFeeBuffer) { this.animateConfirmForm(page.regAssetForm) return @@ -92,6 +91,7 @@ export default class SettingsPage extends BasePage { return } + this.confirmRegisterForm.setAsset(assetID, tier, 0) this.newWalletForm.setAsset(assetID) this.slideSwap(page.newWalletForm) }) @@ -263,6 +263,7 @@ export default class SettingsPage extends BasePage { const bondAmt = this.currentDEX.bondAssets[asset.symbol].amount const bondsFeeBuffer = await this.getBondsFeeBuffer(assetID, page.newWalletForm) + this.confirmRegisterForm.setFees(assetID, bondsFeeBuffer) if (wallet.synced && wallet.balance.available >= 2 * bondAmt + bondsFeeBuffer) { await this.animateConfirmForm(page.newWalletForm) return diff --git a/client/webserver/site/src/js/wallets.ts b/client/webserver/site/src/js/wallets.ts index 2185a7974c..b584ace75f 100644 --- a/client/webserver/site/src/js/wallets.ts +++ b/client/webserver/site/src/js/wallets.ts @@ -7,7 +7,8 @@ import { UnlockWalletForm, DepositAddress, bind as bindForm, - baseChainSymbol + baseChainSymbol, + showSuccess } from './forms' import State from './state' import * as intl from './locales' @@ -750,25 +751,12 @@ export default class WalletsPage extends BasePage { } async showSuccess (msg: string) { - const page = this.page - page.successMessage.textContent = msg - this.currentForm = page.checkmarkForm this.forms.forEach(form => Doc.hide(form)) - Doc.show(page.forms, page.checkmarkForm) - page.checkmarkForm.style.right = '0' - page.checkmark.style.fontSize = '0px' - - const [startR, startG, startB] = State.isDark() ? [223, 226, 225] : [51, 51, 51] - const [endR, endG, endB] = [16, 163, 16] - const [diffR, diffG, diffB] = [endR - startR, endG - startG, endB - startB] - - this.animation = new Animation(1200, (prog: number) => { - page.checkmark.style.fontSize = `${prog * 80}px` - page.checkmark.style.color = `rgb(${startR + prog * diffR}, ${startG + prog * diffG}, ${startB + prog * diffB})` - }, 'easeOutElastic', () => { - this.animation = new Animation(1500, () => { /* pass */ }, '', () => { - if (this.currentForm === page.checkmarkForm) this.closePopups() - }) + this.currentForm = this.page.checkmarkForm + this.animation = showSuccess(this.page, msg) + await this.animation.wait() + this.animation = new Animation(1500, () => { /* pass */ }, '', () => { + if (this.currentForm === this.page.checkmarkForm) this.closePopups() }) }