From 1d129f77f3235ee0e2885f6c15f3dc7b114ad444 Mon Sep 17 00:00:00 2001 From: Brian Stafford Date: Mon, 23 Oct 2023 13:43:51 -0500 Subject: [PATCH 1/5] 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/core/bond.go | 25 +- client/core/core.go | 4 +- 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 | 33 +- 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 | 2 +- .../webserver/site/src/css/market_dark.scss | 2 - client/webserver/site/src/css/settings.scss | 13 +- .../webserver/site/src/html/dexsettings.tmpl | 165 +++++---- 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 | 6 +- 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 +- 34 files changed, 1278 insertions(+), 563 deletions(-) create mode 100644 client/webserver/site/src/js/account.ts 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 89c95219d0..19d753276c 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, 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 b943452768..68c800ea49 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 49a6c6d9f2..906b611c80 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", @@ -433,15 +445,8 @@ var EnUS = map[string]string{ "fiat_rates": "Fiat Rates", "market_making_running": "Market making is running", "cannot_manually_trade": "You cannot manually place orders while market making is running", - "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.", + "back": "Back", + "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 0c29b24f44..3927117fa9 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 { @@ -275,11 +268,6 @@ div[data-handler=register] { width: 25px; } - .borderleft { - padding-left: 25px; - border-left: solid 1px #777; - } - .logo { width: 40px; height: 40px; @@ -339,8 +327,7 @@ div[data-handler=register] { #vSendForm, #exportSeedAuth, #cancelForm, -#quickConfigForm, -#bondDetailsForm { +#quickConfigForm { width: 375px; } @@ -422,6 +409,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 2adc2b6f77..dc11f48a80 100644 --- a/client/webserver/site/src/css/forms_dark.scss +++ b/client/webserver/site/src/css/forms_dark.scss @@ -31,6 +31,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 c64525a626..844a69e208 100644 --- a/client/webserver/site/src/css/main.scss +++ b/client/webserver/site/src/css/main.scss @@ -445,7 +445,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/market_dark.scss b/client/webserver/site/src/css/market_dark.scss index 765bb373f2..d911093361 100644 --- a/client/webserver/site/src/css/market_dark.scss +++ b/client/webserver/site/src/css/market_dark.scss @@ -87,8 +87,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 fded08256a..58e0002651 100644 --- a/client/webserver/site/src/html/dexsettings.tmpl +++ b/client/webserver/site/src/html/dexsettings.tmpl @@ -1,35 +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]]] +
+
+ +
@@ -49,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 9721355021..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 a82e86f99d..24d98c0ffd 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 +
+ +
@@ -414,6 +423,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 02cf568a4e..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 7b192bf915..e66b202717 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, @@ -528,6 +529,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 @@ -622,6 +624,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 1a82359056..9a40867a70 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,62 +29,179 @@ 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.goBackToSettings, 'click', () => app().loadPage('settings')) + 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 @@ -94,6 +216,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 @@ -161,64 +305,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) @@ -289,40 +375,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 d2315379de..a6d82101d0 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.top = `${(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 e6985f17b7..12431a471b 100644 --- a/client/webserver/site/src/js/locales.ts +++ b/client/webserver/site/src/js/locales.ts @@ -154,6 +154,8 @@ export const ID_ORDER_BUTTON_QTY_ERROR = 'ID_ORDER_BUTTON_QTY_ERROR' export const ID_ORDER_BUTTON_QTY_RATE_ERROR = 'ID_ORDER_BUTTON_QTY_RATE_ERROR' export const ID_CREATE_ASSET_WALLET_MSG = 'CREATE_ASSET_WALLET_MSG' export const ID_NO_WALLET_MSG = 'ID_NO_WALLET_MSG' +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', @@ -309,7 +311,9 @@ export const enUS: Locale = { [ID_BROWSER_NTFN_BONDS]: 'Bonds', [ID_BROWSER_NTFN_CONNECTIONS]: 'Server connections', [ID_CREATE_ASSET_WALLET_MSG]: 'Create a {{ asset }} wallet to trade', - [ID_NO_WALLET_MSG]: 'Create {{ asset1 }} and {{ asset2 }} wallet to trade' + [ID_NO_WALLET_MSG]: 'Create {{ asset1 }} and {{ asset2 }} wallet to trade', + [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 c809464f52..a248b6c95a 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() }) @@ -504,7 +508,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 = {} @@ -1197,6 +1203,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() } @@ -1344,6 +1353,7 @@ export default class MarketsPage extends BasePage { }] } this.drawChartLines() + this.updateTradeLimits(order) if (!show || !adjusted || !order.qty) { page.orderPreview.textContent = '' this.drawChartLines() @@ -1358,6 +1368,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. */ @@ -1538,7 +1566,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() @@ -1551,7 +1579,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 @@ -2485,9 +2515,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. @@ -2504,6 +2582,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 @@ -2693,7 +2772,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) this.setOrderBttnEnabled(false, intl.prep(intl.ID_ORDER_BUTTON_QTY_ERROR)) @@ -2751,6 +2830,7 @@ export default class MarketsPage extends BasePage { } else { this.setOrderBttnEnabled(false, intl.prep(intl.ID_ORDER_BUTTON_QTY_ERROR)) } + 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 72b7852005..dd17180c6a 100644 --- a/client/webserver/site/src/js/registry.ts +++ b/client/webserver/site/src/js/registry.ts @@ -56,6 +56,8 @@ export interface Exchange { viewOnly: boolean bondAssets: Record candleDurs: string[] + maxScore: number + penaltyThreshold: number } export interface Candle { @@ -82,6 +84,7 @@ export interface Market { quoteid: number quotesymbol: string lotsize: number + parcelsize: number ratestep: number epochlen: number startepoch: number @@ -353,6 +356,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 9cd511dddc..2fa1e56a8c 100644 --- a/client/webserver/site/src/js/settings.ts +++ b/client/webserver/site/src/js/settings.ts @@ -76,14 +76,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 @@ -93,6 +92,7 @@ export default class SettingsPage extends BasePage { return } + this.confirmRegisterForm.setAsset(assetID, tier, 0) this.newWalletForm.setAsset(assetID) this.slideSwap(page.newWalletForm) }) @@ -264,6 +264,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 96e6e90ed0..351e200743 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' @@ -751,25 +752,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() }) } From f952b70cdc6f549e26c1c854aff673c54e14b447 Mon Sep 17 00:00:00 2001 From: Brian Stafford Date: Tue, 31 Oct 2023 20:09:14 -0500 Subject: [PATCH 2/5] initial review fixes --- client/webserver/locales/en-us.go | 19 +- .../webserver/site/src/html/dexsettings.tmpl | 3 + client/webserver/site/src/html/forms.tmpl | 248 ++++++++++-------- client/webserver/site/src/html/markets.tmpl | 26 +- client/webserver/site/src/js/dexsettings.ts | 8 +- client/webserver/site/src/js/forms.ts | 56 ++-- client/webserver/site/src/js/markets.ts | 63 ++--- client/webserver/site/src/js/register.ts | 8 +- client/webserver/site/src/js/settings.ts | 8 +- 9 files changed, 242 insertions(+), 197 deletions(-) diff --git a/client/webserver/locales/en-us.go b/client/webserver/locales/en-us.go index 906b611c80..c8f3267434 100644 --- a/client/webserver/locales/en-us.go +++ b/client/webserver/locales/en-us.go @@ -28,7 +28,7 @@ var EnUS = map[string]string{ "Skip Registration": "No account (view-only mode)", "Confirm Registration": "Confirm Registration and Bonding", "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.`, + "reg_confirm_submit": `When you submit this form, funds from your wallet will be temporarily locked into a fidelity bond contract, which is redeemable by you in the future.`, "bond_strength": "Bond Strength", "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).", @@ -205,13 +205,25 @@ var EnUS = map[string]string{ "Progress": "Progress", "remaining": "remaining", "2 Fund your Wallet": "2: Fund your Wallet", - "whatsabond": "Fidelity bonds are time-locked funds redeemable only by you, but in the future. This is meant to combat disruptive behavior like backing out on swaps.", + "bond_definition": "A fidelity bond is funds temporarily locked in an on-chain contract. After the contract expires, your wallet will reclaim the funds.", + "bonds_can_be_revoked": "Bonds can be revoked if an account engages in continued disruptive behavior, such as backing out on a swap. Revoked bonds can be re-activated with continued normal trading activity.", + "bigger_bonds_higher_limit": "You can create larger bonds to increase your trading tier, enabling trading of larger quantities at a time. Larger bonds also increase your capacity for violations before trading privileges are suspended.", + "limits_reputation": "Trading limits are also increased as you establish reputation by engaging in normal trading activity and successfully completing matches.", + "wallet_bond_reserves": "Your wallet will automatically reserve the funds necessary to keep you bonded at your selected trading tier, and will broadcast new bonds to replace expiring ones. You can lower or raise your trading tier in the exchange's settings panel. Set your trading tier to zero to disable your account (once your existing bonds expire).", + "Got it": "Got it", + "Trading Limits": "Trading Limits", + "What is a fidelity bond": "What is a fidelity bond?", + "order_form_remaining_limit": ` lots remaining in tier trading limit`, + "Parcel Size": "Parcel Size", + "Trading Limit": "Trading Limit", + "Current Usage": "Current Usage", + "score_factors": "Increase your score by successfully completing trades. Failure to act on a trade will decrease your score.", "Bond amount": "Bond amount", "Reserves for tx fees": "Funds to reserve for transaction fees to maintain your bonds", "Tx Fee Balance": "Transaction Fee Balance:", "Your Deposit Address": "Your Wallet's Deposit Address", "Send enough for bonds": `Make sure you send enough to also cover network fees. You may deposit as much as you like to your wallet, since only the bond amount will be used in the next step. The deposit must confirm to proceed.`, - "Send enough with estimate": `Deposit at least XYZ to cover network fees and overlap periods when a bond is expired (but waiting for refund) and another must be posted. You may deposit as much as you like to your wallet, since only the required amount will be used in the next step. The deposit must confirm to proceed.`, + "Send enough with estimate": `Deposit at least XYZ to cover your bond and fees. You may deposit as much as you like to your wallet, since only the required amount will be used in the next step. The wallet may require a confirmation on the new funds before proceeding.`, "Send funds for token": `Deposit at least XYZ and XYZ to also cover fees. You may deposit as much as you like to your wallet, since only the required amount will be used in the next step. The deposit must confirm to proceed.`, "add a different server": "add a different server", "Add a custom server": "Add a custom server", @@ -281,7 +293,6 @@ var EnUS = map[string]string{ "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`, diff --git a/client/webserver/site/src/html/dexsettings.tmpl b/client/webserver/site/src/html/dexsettings.tmpl index 58e0002651..360523a042 100644 --- a/client/webserver/site/src/html/dexsettings.tmpl +++ b/client/webserver/site/src/html/dexsettings.tmpl @@ -3,6 +3,9 @@ {{$passwordIsCached := .UserInfo.PasswordIsCached}}
+
+ +
{{.Exchange.Host}}
diff --git a/client/webserver/site/src/html/forms.tmpl b/client/webserver/site/src/html/forms.tmpl index 1c834913c4..9446a92281 100644 --- a/client/webserver/site/src/html/forms.tmpl +++ b/client/webserver/site/src/html/forms.tmpl @@ -227,117 +227,139 @@ {{end}} {{define "regAssetForm"}} -
-
- [[[Choose your trading tier]]] - [[[trading_tier_message]]] -
-
- - -
-
-
-
-
-
[[[current_bonding_asset]]]
-
[[[choose a different asset]]]
-
-
-
[[[Select your bond asset]]]
- - - - - - - - - - - - - + + + +
[[[Asset]]][[[Bond Lock]]]Trading Limits
-
-
- - -
-
-
-
-
-
- - -
-
- ~ - - [[[USD]]] -
-
-
-
-
- - - - +
+
[[[What is a fidelity bond]]]
+
    +
  • + [[[bond_definition]]] +
  • +
  • + [[[bonds_can_be_revoked]]] +
  • +
  • + [[[bigger_bonds_higher_limit]]] +
  • +
  • + [[[limits_reputation]]] +
  • +
  • + [[[wallet_bond_reserves]]] +
  • +
+
+
[[[Got it]]]
+
+
+
+
+
+ [[[Choose your trading tier]]] + [[[trading_tier_message]]] +
+
+ + +
+
+
+
+
+
[[[current_bonding_asset]]]
+
[[[choose a different asset]]]
+
+
+
[[[Select your bond asset]]]
+
What's a bond?
+ + + + + + + + + + + + - - -
[[[Asset]]][[[Bond Lock]]][[[Trading Limits]]]
+
+
+ + +
+
-
- +
+
+
+ + +
+
~ - + [[[USD]]] - -
-
-
[[[Other Trading Limits]]]
- - - -
-
-
-
[[[All markets at]]]
- -
-
- - - - - - - - - - - - - -
[[[Market]]][[[:title:lot_size]]]
-
- - -
-
- - - -
-
+
+
+
+
+ + + + +
+
+ + ~ + + [[[USD]]] + +
+
+
[[[Other Trading Limits]]]
+ + + +
+
+
+
[[[All markets at]]]
+ +
+
+ + + + + + + + + + + + + +
[[[Market]]][[[:title:lot_size]]]
+
+ + - +
+
+ + + +
+
+
-
-
-
- - [[[whatsabond]]] +
{{end}} @@ -405,7 +427,7 @@

-
+
[[[reg_confirm_submit]]]
@@ -612,19 +634,18 @@
- [[[Balance]]]: + [[[Available Balance]]]: XYZ
-
- - [[[whatsabond]]] -
- [[[Bond amount]]]: + Bond Lock: XYZ
+
+ Includes for your bond and for transaction fees. +
[[[Send enough with estimate]]] {{- /* NOTE: includes totalForBond */}} [[[Send funds for token]]]
@@ -746,5 +767,8 @@
+
+ [[[score_factors]]] +
{{end}} diff --git a/client/webserver/site/src/html/markets.tmpl b/client/webserver/site/src/html/markets.tmpl index 24d98c0ffd..674929945c 100644 --- a/client/webserver/site/src/html/markets.tmpl +++ b/client/webserver/site/src/html/markets.tmpl @@ -380,10 +380,8 @@
{{- /* ORDER LIMITS */ -}} -
- 0 parcels - of the 0 unused parcels in your - tier 0 trading limit +
+ [[[order_form_remaining_limit]]]
@@ -424,25 +422,25 @@
{{- /* REPUTATION */ -}} -
+
- Parcel Size + [[[Parcel Size]]] - lots + [[[lots]]]
- +
- + ~ @@ -452,23 +450,23 @@
- Trading Tier + [[[Trading Tier]]]
- Trading Limit - parcels + [[[Trading Limit]]] + lots
- Current usage + [[[Current Usage]]] %
-
+
{{template "reputationMeter"}}
diff --git a/client/webserver/site/src/js/dexsettings.ts b/client/webserver/site/src/js/dexsettings.ts index 9a40867a70..0158818207 100644 --- a/client/webserver/site/src/js/dexsettings.ts +++ b/client/webserver/site/src/js/dexsettings.ts @@ -63,7 +63,7 @@ export default class DexSettingsPage extends BasePage { this.newWalletForm = new forms.NewWalletForm( page.newWalletForm, - assetID => this.newWalletCreated(assetID), + assetID => this.newWalletCreated(assetID, this.confirmRegisterForm.tier), this.pwCache, () => this.runAnimation(this.regAssetForm, page.regAssetForm) ) @@ -168,7 +168,7 @@ export default class DexSettingsPage extends BasePage { this.progressTierFormWithSyncedFundedWallet(assetID) return } - this.walletWaitForm.setWallet(wallet, fees) + this.walletWaitForm.setWallet(assetID, fees, this.confirmRegisterForm.tier) this.showForm(page.walletWait) } @@ -389,7 +389,7 @@ export default class DexSettingsPage extends BasePage { return '' } - async newWalletCreated (assetID: number) { + async newWalletCreated (assetID: number, tier: number) { this.regAssetForm.refresh() const user = await app().fetchUser() if (!user) return @@ -407,7 +407,7 @@ export default class DexSettingsPage extends BasePage { return } - this.walletWaitForm.setWallet(wallet, bondsFeeBuffer) + this.walletWaitForm.setWallet(assetID, bondsFeeBuffer, tier) await this.showForm(page.walletWait) } } diff --git a/client/webserver/site/src/js/forms.ts b/client/webserver/site/src/js/forms.ts index a6d82101d0..0541a875e6 100644 --- a/client/webserver/site/src/js/forms.ts +++ b/client/webserver/site/src/js/forms.ts @@ -927,6 +927,16 @@ export class FeeAssetSelectionForm { Doc.show(page.assetSelection) }) + Doc.bind(page.whatsABond, 'click', () => { + Doc.hide(page.mainBondingForm) + Doc.show(page.whatsABondPanel) + }) + + Doc.bind(page.bondGotIt, 'click', () => { + Doc.show(page.mainBondingForm) + Doc.hide(page.whatsABondPanel) + }) + app().registerNoteFeeder({ createwallet: (note: WalletCreationNote) => { if (note.topic === 'QueuedCreationSuccess') this.walletCreated(note.assetID) @@ -984,7 +994,7 @@ export class FeeAssetSelectionForm { const r = cFactor(qui) / cFactor(bui) const quoteLot = mkt.lotsize * mkt.spot.rate / OrderUtil.RateEncodingFactor * r const s = Doc.formatCoinValue(quoteLot, qui) - marketTmpl.quoteLotSize.textContent = `(~${s} ${qui})` + marketTmpl.quoteLotSize.textContent = `(~${s} ${qui.conventional.unit})` } return n } @@ -1205,17 +1215,18 @@ export class WalletWaitForm { } /* setWallet must be called before showing the WalletWaitForm. */ - setWallet (wallet: WalletState, bondFeeBuffer: number) { - this.assetID = wallet.assetID + setWallet (assetID: number, bondFeeBuffer: number, tier: number) { + this.assetID = assetID this.progressCache = [] this.progressed = false this.funded = false this.bondFeeBuffer = bondFeeBuffer // in case we're a token, parent's balance must cover this.parentAssetSynced = false const page = this.page - const asset = app().assets[wallet.assetID] - this.parentID = asset.token?.parentID - const bondAsset = this.bondAsset = this.xc.bondAssets[asset.symbol] + const asset = app().assets[assetID] + const { symbol, unitInfo: ui, wallet: { balance: bal, address, synced, syncProgress }, token } = asset + this.parentID = token?.parentID + const bondAsset = this.bondAsset = this.xc.bondAssets[symbol] const symbolize = (el: PageElement, asset: SupportedAsset) => { Doc.empty(el) @@ -1223,26 +1234,30 @@ export class WalletWaitForm { } for (const span of Doc.applySelector(this.form, '.unit')) symbolize(span, asset) - page.logo.src = Doc.logoPath(asset.symbol) - page.depoAddr.textContent = wallet.address - page.fee.textContent = Doc.formatCoinValue(bondAsset.amount, asset.unitInfo) + page.logo.src = Doc.logoPath(symbol) + page.depoAddr.textContent = address - Doc.hide(page.syncUncheck, page.syncCheck, page.balUncheck, page.balCheck, page.syncRemainBox) + Doc.hide(page.syncUncheck, page.syncCheck, page.balUncheck, page.balCheck, page.syncRemainBox, page.bondCostBreakdown) Doc.show(page.balanceBox) + let bondLock = 2 * bondAsset.amount * tier if (bondFeeBuffer > 0) { - // overlap * increment + buffer - page.totalForBond.textContent = Doc.formatCoinValue(2 * bondAsset.amount + bondFeeBuffer, asset.unitInfo) + Doc.show(page.bondCostBreakdown) + page.bondLockNoFees.textContent = Doc.formatCoinValue(bondLock, ui) + page.bondLockFees.textContent = Doc.formatCoinValue(bondFeeBuffer, ui) + bondLock += bondFeeBuffer + const need = Math.max(bondLock - bal.available + bal.reservesDeficit, 0) + page.totalForBond.textContent = Doc.formatCoinValue(need, ui) Doc.hide(page.sendEnough) // generic msg when no fee info available when Doc.hide(page.txFeeBox, page.sendEnoughForToken, page.txFeeBalanceBox) // for tokens Doc.hide(page.sendEnoughWithEst) // non-tokens - if (asset.token) { + if (token) { Doc.show(page.txFeeBox, page.sendEnoughForToken, page.txFeeBalanceBox) - const parentAsset = app().assets[asset.token.parentID] + const parentAsset = app().assets[token.parentID] page.txFee.textContent = Doc.formatCoinValue(bondFeeBuffer, parentAsset.unitInfo) page.parentFees.textContent = Doc.formatCoinValue(bondFeeBuffer, parentAsset.unitInfo) - page.tokenFees.textContent = Doc.formatCoinValue(bondAsset.amount, asset.unitInfo) + page.tokenFees.textContent = Doc.formatCoinValue(need, ui) symbolize(page.txFeeUnit, parentAsset) symbolize(page.parentUnit, parentAsset) symbolize(page.parentBalUnit, parentAsset) @@ -1250,19 +1265,20 @@ export class WalletWaitForm { } else { Doc.show(page.sendEnoughWithEst) } + page.fee.textContent = Doc.formatCoinValue(bondLock, ui) } else { // show some generic message with no amounts, this shouldn't happen... show wallet error? Doc.show(page.sendEnough) } - Doc.show(wallet.synced ? page.syncCheck : wallet.syncProgress >= 1 ? page.syncSpinner : page.syncUncheck) - Doc.show(wallet.balance.available >= 2 * bondAsset.amount + bondFeeBuffer ? page.balCheck : page.balUncheck) + Doc.show(synced ? page.syncCheck : syncProgress >= 1 ? page.syncSpinner : page.syncUncheck) + Doc.show(bal.available >= 2 * bondAsset.amount + bondFeeBuffer ? page.balCheck : page.balUncheck) - page.progress.textContent = (wallet.syncProgress * 100).toFixed(1) + page.progress.textContent = (syncProgress * 100).toFixed(1) - if (wallet.synced) { + if (synced) { this.progressed = true } - this.reportBalance(wallet.assetID) + this.reportBalance(assetID) } /* diff --git a/client/webserver/site/src/js/markets.ts b/client/webserver/site/src/js/markets.ts index a248b6c95a..4149c93d5e 100644 --- a/client/webserver/site/src/js/markets.ts +++ b/client/webserver/site/src/js/markets.ts @@ -760,6 +760,11 @@ export default class MarketsPage extends BasePage { Doc.setVis(await showOrderForm(), page.orderForm, page.orderTypeBttns) + if (this.market) { + const { auth: { effectiveTier, pendingStrength } } = this.market.dex + Doc.setVis(effectiveTier > 0 || pendingStrength > 0, page.tradingLimits, page.reputationMeter) + } + if (app().user.experimental && this.mmRunning === undefined) { const marketMakingStatus = await app().getMarketMakingStatus() this.mmRunning = marketMakingStatus.running @@ -1205,7 +1210,6 @@ export default class MarketsPage extends BasePage { this.updateTitle() this.reputationMeter.setHost(dex.host) this.updateReputation() - this.updateTradeLimits(null) this.loadUserOrders() } @@ -1353,7 +1357,6 @@ export default class MarketsPage extends BasePage { }] } this.drawChartLines() - this.updateTradeLimits(order) if (!show || !adjusted || !order.qty) { page.orderPreview.textContent = '' this.drawChartLines() @@ -1368,24 +1371,6 @@ 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. */ @@ -2535,9 +2520,10 @@ export default class MarketsPage extends BasePage { 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.tradingLimit.textContent = String(parcelLimit * mkt.parcelsize) + page.limitUsage.textContent = parcelLimit > 0 ? (usedParcels / parcelLimit * 100).toFixed(1) : '0' + + page.orderLimitRemain.textContent = ((parcelLimit - usedParcels) * mkt.parcelsize).toFixed(1) page.orderTradingTier.textContent = String(tier) this.reputationMeter.update() @@ -2565,14 +2551,18 @@ export default class MarketsPage extends BasePage { } handleMatchNote (note: MatchNote) { - this.updateReputation() const mord = this.metaOrders[note.orderID] + const match = note.match if (!mord) return this.refreshActiveOrders() - else if (mord.ord.type === OrderUtil.Market && note.match.status === OrderUtil.NewlyMatched) { // Update the average market rate display. + else if (mord.ord.type === OrderUtil.Market && match.status === OrderUtil.NewlyMatched) { // Update the average market rate display. // Fetch and use the updated order. const ord = app().order(note.orderID) if (ord) mord.details.rate.textContent = mord.header.rate.textContent = this.marketOrderRateString(ord, this.market) } + if ( + (match.side === OrderUtil.MatchSideMaker && match.status === OrderUtil.MakerRedeemed) || + (match.side === OrderUtil.MatchSideTaker && match.status === OrderUtil.MatchComplete) + ) this.updateReputation() if (app().canAccelerateOrder(mord.ord)) Doc.show(mord.details.accelerateBttn) else Doc.hide(mord.details.accelerateBttn) } @@ -2582,9 +2572,8 @@ 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] + const ord = note.order + const mord = this.metaOrders[ord.id] // - If metaOrder doesn't exist for the given order it means it was created // via dexcctl and the GUI isn't aware of it or it was an inflight order. // refreshActiveOrders must be called to grab this order. @@ -2593,19 +2582,24 @@ export default class MarketsPage extends BasePage { // and unlocked) has now become ready to tick. The active orders section // needs to be refreshed. const wasInflight = note.topic === 'AsyncOrderFailure' || note.topic === 'AsyncOrderSubmitted' - if (!mord || wasInflight || (note.topic === 'OrderLoaded' && order.readyToTick)) { + if (!mord || wasInflight || (note.topic === 'OrderLoaded' && ord.readyToTick)) { return this.refreshActiveOrders() } const oldStatus = mord.ord.status - mord.ord = order + mord.ord = ord if (note.topic === 'MissedCancel') Doc.show(mord.details.cancelBttn) - if (order.filled === order.qty) Doc.hide(mord.details.cancelBttn) - if (app().canAccelerateOrder(order)) Doc.show(mord.details.accelerateBttn) + if (ord.filled === ord.qty) Doc.hide(mord.details.cancelBttn) + if (app().canAccelerateOrder(ord)) Doc.show(mord.details.accelerateBttn) else Doc.hide(mord.details.accelerateBttn) this.updateMetaOrder(mord) // Only reset markers if there is a change, since the chart is redrawn. - if ((oldStatus === OrderUtil.StatusEpoch && order.status === OrderUtil.StatusBooked) || - (oldStatus === OrderUtil.StatusBooked && order.status > OrderUtil.StatusBooked)) this.setDepthMarkers() + if ( + (oldStatus === OrderUtil.StatusEpoch && ord.status === OrderUtil.StatusBooked) || + (oldStatus === OrderUtil.StatusBooked && ord.status > OrderUtil.StatusBooked) + ) { + this.setDepthMarkers() + this.updateReputation() + } } /* @@ -2830,7 +2824,6 @@ export default class MarketsPage extends BasePage { } else { this.setOrderBttnEnabled(false, intl.prep(intl.ID_ORDER_BUTTON_QTY_ERROR)) } - this.updateTradeLimits(this.parseOrder()) if (!gap || !qty) { page.mktBuyLots.textContent = '0' page.mktBuyScore.textContent = '0' diff --git a/client/webserver/site/src/js/register.ts b/client/webserver/site/src/js/register.ts index b90cd42100..b8df3f17d3 100644 --- a/client/webserver/site/src/js/register.ts +++ b/client/webserver/site/src/js/register.ts @@ -77,7 +77,7 @@ export default class RegistrationPage extends BasePage { this.newWalletForm = new NewWalletForm( page.newWalletForm, - assetID => this.newWalletCreated(assetID), + assetID => this.newWalletCreated(assetID, this.confirmRegisterForm.tier), this.pwCache, () => this.animateRegAsset(page.newWalletForm) ) @@ -106,7 +106,7 @@ export default class RegistrationPage extends BasePage { this.animateConfirmForm(page.regAssetForm) return } - this.walletWaitForm.setWallet(wallet, bondsFeeBuffer) + this.walletWaitForm.setWallet(assetID, bondsFeeBuffer, tier) slideSwap(page.regAssetForm, page.walletWait) return } @@ -207,7 +207,7 @@ export default class RegistrationPage extends BasePage { await app().loadPage('markets') } - async newWalletCreated (assetID: number) { + async newWalletCreated (assetID: number, tier: number) { this.regAssetForm.refresh() const user = await app().fetchUser() if (!user) return @@ -222,7 +222,7 @@ export default class RegistrationPage extends BasePage { return } - this.walletWaitForm.setWallet(wallet, bondsFeeBuffer) + this.walletWaitForm.setWallet(assetID, bondsFeeBuffer, tier) await slideSwap(page.newWalletForm, page.walletWait) } } diff --git a/client/webserver/site/src/js/settings.ts b/client/webserver/site/src/js/settings.ts index 2fa1e56a8c..2334e03d5b 100644 --- a/client/webserver/site/src/js/settings.ts +++ b/client/webserver/site/src/js/settings.ts @@ -87,7 +87,7 @@ export default class SettingsPage extends BasePage { this.animateConfirmForm(page.regAssetForm) return } - this.walletWaitForm.setWallet(wallet, bondsFeeBuffer) + this.walletWaitForm.setWallet(assetID, bondsFeeBuffer, tier) this.slideSwap(page.walletWait) return } @@ -107,7 +107,7 @@ export default class SettingsPage extends BasePage { // Create a new wallet this.newWalletForm = new forms.NewWalletForm( page.newWalletForm, - assetID => this.newWalletCreated(assetID), + assetID => this.newWalletCreated(assetID, this.confirmRegisterForm.tier), this.pwCache, () => this.animateRegAsset(page.newWalletForm) ) @@ -255,7 +255,7 @@ export default class SettingsPage extends BasePage { return res.feeBuffer } - async newWalletCreated (assetID: number) { + async newWalletCreated (assetID: number, tier: number) { const user = await app().fetchUser() if (!user) return const page = this.page @@ -270,7 +270,7 @@ export default class SettingsPage extends BasePage { return } - this.walletWaitForm.setWallet(wallet, bondsFeeBuffer) + this.walletWaitForm.setWallet(assetID, bondsFeeBuffer, tier) this.slideSwap(page.walletWait) } From bc456cf3a648bc2189dc1f0afc7cdd39e8b1e6b0 Mon Sep 17 00:00:00 2001 From: Brian Stafford Date: Wed, 8 Nov 2023 13:27:14 -0600 Subject: [PATCH 3/5] add auto-renew toggle and penalty comps input --- client/core/bond.go | 9 +- client/core/core_test.go | 12 +- client/core/types.go | 4 +- client/rpcserver/types.go | 7 +- client/webserver/locales/en-us.go | 3 +- .../webserver/site/src/css/dex_settings.scss | 4 + client/webserver/site/src/css/forms.scss | 27 ++++ .../webserver/site/src/html/dexsettings.tmpl | 26 ++- client/webserver/site/src/html/forms.tmpl | 3 + client/webserver/site/src/js/app.ts | 4 +- client/webserver/site/src/js/dexsettings.ts | 152 +++++++++++++++--- client/webserver/site/src/js/forms.ts | 8 +- client/webserver/site/src/js/locales.ts | 5 +- client/webserver/site/src/js/registry.ts | 2 +- 14 files changed, 224 insertions(+), 42 deletions(-) diff --git a/client/core/bond.go b/client/core/bond.go index a2f719d73b..eab1e09539 100644 --- a/client/core/bond.go +++ b/client/core/bond.go @@ -986,12 +986,12 @@ func (c *Core) nextBondKey(assetID uint32) (*secp256k1.PrivateKey, uint32, error // the target trading tier, the preferred asset to use for bonds, and the // maximum amount allowable to be locked in bonds. func (c *Core) UpdateBondOptions(form *BondOptionsForm) error { - dc, _, err := c.dex(form.Addr) + dc, _, err := c.dex(form.Host) if err != nil { return err } // TODO: exclude unregistered and/or watch-only - dbAcct, err := c.db.Account(form.Addr) + dbAcct, err := c.db.Account(form.Host) if err != nil { return err } @@ -1063,7 +1063,10 @@ func (c *Core) UpdateBondOptions(form *BondOptionsForm) error { dbAcct.TargetTier = targetTier } - penaltyComps := form.PenaltyComps + var penaltyComps = penaltyComps0 + if form.PenaltyComps != nil { + penaltyComps = *form.PenaltyComps + } dc.acct.penaltyComps = penaltyComps dbAcct.PenaltyComps = penaltyComps diff --git a/client/core/core_test.go b/client/core/core_test.go index 6b7c006218..a030f6531c 100644 --- a/client/core/core_test.go +++ b/client/core/core_test.go @@ -10576,7 +10576,7 @@ func TestUpdateBondOptions(t *testing.T) { name: "set target tier to 1", bal: singlyBondedReserves, form: BondOptionsForm{ - Addr: acct.host, + Host: acct.host, TargetTier: &targetTier, BondAssetID: &bondAsset.ID, }, @@ -10590,7 +10590,7 @@ func TestUpdateBondOptions(t *testing.T) { name: "low balance", bal: singlyBondedReserves - 1, form: BondOptionsForm{ - Addr: acct.host, + Host: acct.host, TargetTier: &targetTier, BondAssetID: &bondAsset.ID, }, @@ -10600,7 +10600,7 @@ func TestUpdateBondOptions(t *testing.T) { name: "max-bonded too low", bal: singlyBondedReserves, form: BondOptionsForm{ - Addr: acct.host, + Host: acct.host, TargetTier: &targetTier, BondAssetID: &bondAsset.ID, MaxBondedAmt: &tooLowMaxBonded, @@ -10610,7 +10610,7 @@ func TestUpdateBondOptions(t *testing.T) { { name: "unsupported bond asset", form: BondOptionsForm{ - Addr: acct.host, + Host: acct.host, TargetTier: &targetTier, BondAssetID: &wrongBondAssetID, }, @@ -10620,7 +10620,7 @@ func TestUpdateBondOptions(t *testing.T) { name: "lower target tier with zero balance OK", bal: 0, form: BondOptionsForm{ - Addr: acct.host, + Host: acct.host, TargetTier: &targetTierZero, BondAssetID: &bondAsset.ID, }, @@ -10635,7 +10635,7 @@ func TestUpdateBondOptions(t *testing.T) { name: "lower target tier to zero with other exchanges still keeps reserves", bal: 0, form: BondOptionsForm{ - Addr: acct.host, + Host: acct.host, TargetTier: &targetTierZero, BondAssetID: &bondAsset.ID, }, diff --git a/client/core/types.go b/client/core/types.go index 34eb71d927..ea21e8bc80 100644 --- a/client/core/types.go +++ b/client/core/types.go @@ -188,10 +188,10 @@ type SupportedAsset struct { // BondOptionsForm is used from the settings page to change the auto-bond // maintenance setting for a DEX. type BondOptionsForm struct { - Addr string `json:"host"` + Host string `json:"host"` TargetTier *uint64 `json:"targetTier,omitempty"` MaxBondedAmt *uint64 `json:"maxBondedAmt,omitempty"` - PenaltyComps uint16 `json:"penaltyComps"` + PenaltyComps *uint16 `json:"penaltyComps,omitempty"` BondAssetID *uint32 `json:"bondAssetID,omitempty"` } diff --git a/client/rpcserver/types.go b/client/rpcserver/types.go index 4dab0a2432..fb7a7ef109 100644 --- a/client/rpcserver/types.go +++ b/client/rpcserver/types.go @@ -507,7 +507,7 @@ func parseBondOptsArgs(params *RawParams) (*core.BondOptionsForm, error) { } } - var penaltyComps uint16 + var penaltyComps *uint16 if len(params.Args) > 4 { pc, err := checkIntArg(params.Args[4], "penaltyComps", 16) if err != nil { @@ -517,12 +517,13 @@ func parseBondOptsArgs(params *RawParams) (*core.BondOptionsForm, error) { return nil, fmt.Errorf("penaltyComps out of range (0, %d)", math.MaxUint16) } if pc > 0 { - penaltyComps = uint16(pc) + pc16 := uint16(pc) + penaltyComps = &pc16 } } req := &core.BondOptionsForm{ - Addr: params.Args[0], + Host: params.Args[0], TargetTier: targetTierP, MaxBondedAmt: maxBondedP, BondAssetID: bondAssetP, diff --git a/client/webserver/locales/en-us.go b/client/webserver/locales/en-us.go index c8f3267434..6ee4d04d12 100644 --- a/client/webserver/locales/en-us.go +++ b/client/webserver/locales/en-us.go @@ -32,6 +32,7 @@ var EnUS = map[string]string{ "bond_strength": "Bond Strength", "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).", + "compensation_tooltip": "Enable posting additional bonds to offset penalized tiers.", "Actual Tier": "Actual Tier", "Penalties": "Penalties", "Change Tier": "Change Tier", @@ -205,7 +206,7 @@ var EnUS = map[string]string{ "Progress": "Progress", "remaining": "remaining", "2 Fund your Wallet": "2: Fund your Wallet", - "bond_definition": "A fidelity bond is funds temporarily locked in an on-chain contract. After the contract expires, your wallet will reclaim the funds.", + "bond_definition": "A fidelity bond is funds temporarily locked in an on-chain contract. After the contract expires, your wallet will reclaim the funds. On mainnet, funds are locked for up to 2 months.", "bonds_can_be_revoked": "Bonds can be revoked if an account engages in continued disruptive behavior, such as backing out on a swap. Revoked bonds can be re-activated with continued normal trading activity.", "bigger_bonds_higher_limit": "You can create larger bonds to increase your trading tier, enabling trading of larger quantities at a time. Larger bonds also increase your capacity for violations before trading privileges are suspended.", "limits_reputation": "Trading limits are also increased as you establish reputation by engaging in normal trading activity and successfully completing matches.", diff --git a/client/webserver/site/src/css/dex_settings.scss b/client/webserver/site/src/css/dex_settings.scss index e114c4a09a..e930ebd150 100644 --- a/client/webserver/site/src/css/dex_settings.scss +++ b/client/webserver/site/src/css/dex_settings.scss @@ -6,3 +6,7 @@ color: green; } +#penaltyCompInput { + width: 2rem; +} + diff --git a/client/webserver/site/src/css/forms.scss b/client/webserver/site/src/css/forms.scss index 3927117fa9..f66eb820fe 100644 --- a/client/webserver/site/src/css/forms.scss +++ b/client/webserver/site/src/css/forms.scss @@ -457,3 +457,30 @@ div[data-handler=init] { height: 25px; } } + +.anitoggle { + width: 1.5rem; + height: 0.9rem; + border-radius: 0.45rem; + background-color: #777a; + cursor: pointer; + + &.on { + background-color: $buycolor_dark; + } + + & > div { + position: relative; + top: 0.1rem; + left: 0.1rem; + width: 0.7rem; + height: 0.7rem; + border-radius: 0.35rem; + transition: left 0.5s; + background-color: $dark_body_bg; + } + + &.on > div { + left: 0.7rem; + } +} \ No newline at end of file diff --git a/client/webserver/site/src/html/dexsettings.tmpl b/client/webserver/site/src/html/dexsettings.tmpl index 360523a042..1bc2c9d83a 100644 --- a/client/webserver/site/src/html/dexsettings.tmpl +++ b/client/webserver/site/src/html/dexsettings.tmpl @@ -37,7 +37,31 @@

-
+
+
+
+ Auto Renew +
+
+
+
+
+
+
+
+
+ Penalty Comps + +
+
+ + +
+
+
+
+
+
{{template "reputationMeter"}}
diff --git a/client/webserver/site/src/html/forms.tmpl b/client/webserver/site/src/html/forms.tmpl index 9446a92281..2499f67dbe 100644 --- a/client/webserver/site/src/html/forms.tmpl +++ b/client/webserver/site/src/html/forms.tmpl @@ -228,6 +228,9 @@ {{define "regAssetForm"}}
+
+ +
[[[What is a fidelity bond]]]
  • diff --git a/client/webserver/site/src/js/app.ts b/client/webserver/site/src/js/app.ts index e66b202717..cbfdccb562 100644 --- a/client/webserver/site/src/js/app.ts +++ b/client/webserver/site/src/js/app.ts @@ -164,9 +164,7 @@ export default class Application { } if (process.env.NODE_ENV === 'development') { - window.user = () => { - console.log(this.user) - } + window.user = () => this.user } // use user current locale set by backend diff --git a/client/webserver/site/src/js/dexsettings.ts b/client/webserver/site/src/js/dexsettings.ts index 0158818207..c524028f28 100644 --- a/client/webserver/site/src/js/dexsettings.ts +++ b/client/webserver/site/src/js/dexsettings.ts @@ -18,6 +18,13 @@ interface Animator { animate: (() => Promise) } +interface BondOptionsForm { + host?: string // Required, but set by updateBondOptions + bondAssetID?: number + targetTier?: number + penaltyComps?: number +} + const animationLength = 300 export default class DexSettingsPage extends BasePage { @@ -37,6 +44,7 @@ export default class DexSettingsPage extends BasePage { reputationMeter: ReputationMeter animation: Animation pwCache: PasswordCache + renewToggle: AniToggle constructor (body: HTMLElement) { super() @@ -49,6 +57,7 @@ export default class DexSettingsPage extends BasePage { this.confirmRegisterForm = new forms.ConfirmRegistrationForm(page.confirmRegForm, () => { this.showSuccess(intl.prep(intl.ID_TRADING_TIER_UPDATED)) + this.renewToggle.setState(this.confirmRegisterForm.tier > 0) }, () => { this.runAnimation(this.regAssetForm, page.regAssetForm) }, this.pwCache) @@ -98,9 +107,66 @@ export default class DexSettingsPage extends BasePage { Doc.bind(page.updateHostBtn, 'click', () => this.prepareUpdateHost()) Doc.bind(page.certFileInput, 'change', () => this.onCertFileChange()) Doc.bind(page.goBackToSettings, 'click', () => app().loadPage('settings')) - Doc.bind(page.changeTier, 'click', () => { + + const showTierForm = () => { this.regAssetForm.setExchange(app().exchanges[host]) // reset form this.showForm(page.regAssetForm) + } + Doc.bind(page.changeTier, 'click', () => { showTierForm() }) + const willAutoRenew = xc.auth.targetTier > 0 + this.renewToggle = new AniToggle(page.toggleAutoRenew, page.renewErr, willAutoRenew, async (newState: boolean) => { + if (newState) showTierForm() + else return this.disableAutoRenew() + }) + Doc.bind(page.autoRenewBox, 'click', (e: MouseEvent) => { + e.stopPropagation() + page.toggleAutoRenew.click() + }) + + page.penaltyComps.textContent = String(xc.auth.penaltyComps) + const hideCompInput = () => { + Doc.hide(page.penaltyCompInput) + Doc.show(page.penaltyComps) + } + Doc.bind(page.penaltyCompBox, 'click', (e: MouseEvent) => { + e.stopPropagation() + const xc = app().exchanges[this.host] + page.penaltyCompInput.value = String(xc.auth.penaltyComps) + Doc.hide(page.penaltyComps) + Doc.show(page.penaltyCompInput) + page.penaltyCompInput.focus() + const checkClick = (e: MouseEvent) => { + if (Doc.mouseInElement(e, page.penaltyCompBox)) return + hideCompInput() + Doc.unbind(document, 'click', checkClick) + } + Doc.bind(document, 'click', checkClick) + }) + + Doc.bind(page.penaltyCompInput, 'keyup', async (e: KeyboardEvent) => { + Doc.hide(page.penaltyCompsErr) + if (e.key === 'Escape') { + hideCompInput() + return + } + if (!(e.key === 'Enter')) return + const penaltyComps = parseInt(page.penaltyCompInput.value || '') + if (isNaN(penaltyComps)) { + Doc.show(page.penaltyCompsErr) + page.penaltyCompsErr.textContent = intl.prep(intl.ID_INVALID_COMPS_VALUE) + return + } + const loaded = app().loading(page.otherBondSettings) + try { + await this.updateBondOptions({ penaltyComps }) + loaded() + page.penaltyComps.textContent = String(penaltyComps) + } catch (e) { + loaded() + Doc.show(page.penaltyCompsErr) + page.penaltyCompsErr.textContent = intl.prep(intl.ID_API_ERROR, { msg: e.msg }) + } + hideCompInput() }) this.dexAddrForm = new forms.DEXAddressForm(page.dexAddrForm, async (xc: Exchange) => { @@ -172,19 +238,23 @@ export default class DexSettingsPage extends BasePage { this.showForm(page.walletWait) } - async progressTierFormWithSyncedFundedWallet (assetID: number) { + async progressTierFormWithSyncedFundedWallet (bondAssetID: number) { const xc = app().exchanges[this.host] - const tier = this.confirmRegisterForm.tier + const targetTier = 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) { + if (targetTier > xc.auth.targetTier && targetTier > strongTier) { this.runAnimation(this.confirmRegisterForm, page.confirmRegForm) return } // Lowering tier - const updateErr = await this.updateBondOptions(assetID, tier) - if (updateErr) { - this.regAssetForm.setError(updateErr) + const loaded = app().loading(this.body) + try { + await this.updateBondOptions({ bondAssetID, targetTier }) + loaded() + } catch (e) { + loaded() + this.regAssetForm.setError(e.msg) return } // this.animateConfirmForm(page.regAssetForm) @@ -371,22 +441,26 @@ export default class DexSettingsPage extends BasePage { } } + async disableAutoRenew () { + const loaded = app().loading(this.page.otherBondSettings) + try { + this.updateBondOptions({ targetTier: 0 }) + loaded() + } catch (e) { + loaded() + throw e + } + } + /* * updateBondOptions is called when the form to update bond options is * submitted. */ - async updateBondOptions (bondAssetID: number, targetTier: number): Promise { - const bondOptions: Record = { - host: this.host, - targetTier: targetTier, - bondAssetID: bondAssetID - } - - const loaded = app().loading(this.body) - const res = await postJSON('/api/updatebondoptions', bondOptions) - loaded() - if (!app().checkResponse(res)) return res.msg - return '' + async updateBondOptions (conf: BondOptionsForm): Promise { + conf.host = this.host + await postJSON('/api/updatebondoptions', conf) + const targetTier = conf.targetTier ?? app().exchanges[this.host].auth.targetTier + this.renewToggle.setState(targetTier > 0) } async newWalletCreated (assetID: number, tier: number) { @@ -411,3 +485,43 @@ export default class DexSettingsPage extends BasePage { await this.showForm(page.walletWait) } } + +/* + * AniToggle is a small toggle switch, defined in HTML with the element + *
    . The animations are defined in the anitoggle + * CSS class. AniToggle triggers the callback on click events, but does not + * update toggle appearance, so the caller must call the setState method from + * the callback or elsewhere if the newState + * is accepted. + */ +class AniToggle { + toggle: PageElement + toggling: boolean + + constructor (toggle: PageElement, errorEl: PageElement, initialState: boolean, callback: (newState: boolean) => Promise) { + this.toggle = toggle + if (toggle.children.length === 0) toggle.appendChild(document.createElement('div')) + + Doc.bind(toggle, 'click', async (e: MouseEvent) => { + e.stopPropagation() + Doc.hide(errorEl) + const newState = !toggle.classList.contains('on') + this.toggling = true + try { + await callback(newState) + } catch (e) { + this.toggling = false + Doc.show(errorEl) + errorEl.textContent = intl.prep(intl.ID_API_ERROR, { msg: e.msg }) + return + } + this.toggling = false + }) + this.setState(initialState) + } + + setState (state: boolean) { + if (state) this.toggle.classList.add('on') + else this.toggle.classList.remove('on') + } +} diff --git a/client/webserver/site/src/js/forms.ts b/client/webserver/site/src/js/forms.ts index 0541a875e6..ca5c2ef7a9 100644 --- a/client/webserver/site/src/js/forms.ts +++ b/client/webserver/site/src/js/forms.ts @@ -932,10 +932,14 @@ export class FeeAssetSelectionForm { Doc.show(page.whatsABondPanel) }) - Doc.bind(page.bondGotIt, 'click', () => { + const hideWhatsABond = () => { Doc.show(page.mainBondingForm) Doc.hide(page.whatsABondPanel) - }) + } + + Doc.bind(page.bondGotIt, 'click', () => { hideWhatsABond() }) + + Doc.bind(page.whatsABondBack, 'click', () => { hideWhatsABond() }) app().registerNoteFeeder({ createwallet: (note: WalletCreationNote) => { diff --git a/client/webserver/site/src/js/locales.ts b/client/webserver/site/src/js/locales.ts index 12431a471b..386dbedb8c 100644 --- a/client/webserver/site/src/js/locales.ts +++ b/client/webserver/site/src/js/locales.ts @@ -156,6 +156,7 @@ export const ID_CREATE_ASSET_WALLET_MSG = 'CREATE_ASSET_WALLET_MSG' export const ID_NO_WALLET_MSG = 'ID_NO_WALLET_MSG' export const ID_TRADING_TIER_UPDATED = 'TRADING_TIER_UPDATED' export const ID_INVALID_TIER_VALUE = 'INVALID_TIER_VALUE' +export const ID_INVALID_COMPS_VALUE = 'INVALID_COMPS_VALUE' export const enUS: Locale = { [ID_NO_PASS_ERROR_MSG]: 'password cannot be empty', @@ -313,7 +314,9 @@ export const enUS: Locale = { [ID_CREATE_ASSET_WALLET_MSG]: 'Create a {{ asset }} wallet to trade', [ID_NO_WALLET_MSG]: 'Create {{ asset1 }} and {{ asset2 }} wallet to trade', [ID_TRADING_TIER_UPDATED]: 'Trading Tier Updated', - [ID_INVALID_TIER_VALUE]: 'Invalid tier value' + [ID_INVALID_TIER_VALUE]: 'Invalid tier value', + [ID_INVALID_COMPS_VALUE]: 'Invalid comps value', + [ID_API_ERROR]: 'api error: {{ msg }}' } export const ptBR: Locale = { diff --git a/client/webserver/site/src/js/registry.ts b/client/webserver/site/src/js/registry.ts index dd17180c6a..81962a0d96 100644 --- a/client/webserver/site/src/js/registry.ts +++ b/client/webserver/site/src/js/registry.ts @@ -7,7 +7,7 @@ declare global { localeDiscrepancies: () => void testFormatFourSigFigs: () => void testFormatRateFullPrecision: () => void - user: () => void + user: () => User isWebview?: () => boolean openUrl: (url: string) => void } From 01adc976b6f099a0dd7e8d87adaa28b9f0381a1e Mon Sep 17 00:00:00 2001 From: Brian Stafford Date: Wed, 15 Nov 2023 17:07:25 -0600 Subject: [PATCH 4/5] solidify locktime messaging --- client/webserver/locales/en-us.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/webserver/locales/en-us.go b/client/webserver/locales/en-us.go index 6ee4d04d12..a35e5b03cc 100644 --- a/client/webserver/locales/en-us.go +++ b/client/webserver/locales/en-us.go @@ -206,7 +206,7 @@ var EnUS = map[string]string{ "Progress": "Progress", "remaining": "remaining", "2 Fund your Wallet": "2: Fund your Wallet", - "bond_definition": "A fidelity bond is funds temporarily locked in an on-chain contract. After the contract expires, your wallet will reclaim the funds. On mainnet, funds are locked for up to 2 months.", + "bond_definition": "A fidelity bond is funds temporarily locked in an on-chain contract. After the contract expires, your wallet will reclaim the funds. On mainnet, funds are locked for 2 months.", "bonds_can_be_revoked": "Bonds can be revoked if an account engages in continued disruptive behavior, such as backing out on a swap. Revoked bonds can be re-activated with continued normal trading activity.", "bigger_bonds_higher_limit": "You can create larger bonds to increase your trading tier, enabling trading of larger quantities at a time. Larger bonds also increase your capacity for violations before trading privileges are suspended.", "limits_reputation": "Trading limits are also increased as you establish reputation by engaging in normal trading activity and successfully completing matches.", From bcd91f66c1040d3095d83808e2c1480219e82991 Mon Sep 17 00:00:00 2001 From: Brian Stafford Date: Thu, 16 Nov 2023 19:53:51 -0600 Subject: [PATCH 5/5] adapt to new ui colors --- client/webserver/site/src/css/market.scss | 2 +- client/webserver/site/src/css/market_dark.scss | 2 +- client/webserver/site/src/html/dexsettings.tmpl | 2 +- client/webserver/site/src/html/settings.tmpl | 2 +- client/webserver/site/src/js/charts.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/client/webserver/site/src/css/market.scss b/client/webserver/site/src/css/market.scss index b34c804d06..6b823b01de 100644 --- a/client/webserver/site/src/css/market.scss +++ b/client/webserver/site/src/css/market.scss @@ -585,7 +585,7 @@ div[data-handler=markets] { &.selected { opacity: 1; - background-color: #e8ebed; + background-color: #0001; div.opt-check { background-color: #2cce9c; diff --git a/client/webserver/site/src/css/market_dark.scss b/client/webserver/site/src/css/market_dark.scss index d911093361..81d3d57336 100644 --- a/client/webserver/site/src/css/market_dark.scss +++ b/client/webserver/site/src/css/market_dark.scss @@ -135,7 +135,7 @@ body.dark { } .order-opt.selected { - background-color: #222e38; + background-color: #fff1; } } diff --git a/client/webserver/site/src/html/dexsettings.tmpl b/client/webserver/site/src/html/dexsettings.tmpl index 1bc2c9d83a..f02214247f 100644 --- a/client/webserver/site/src/html/dexsettings.tmpl +++ b/client/webserver/site/src/html/dexsettings.tmpl @@ -101,7 +101,7 @@ {{- /* SUCCESS ANIMATION */ -}} -
    +
    diff --git a/client/webserver/site/src/html/settings.tmpl b/client/webserver/site/src/html/settings.tmpl index ff95d983cc..d3886603ba 100644 --- a/client/webserver/site/src/html/settings.tmpl +++ b/client/webserver/site/src/html/settings.tmpl @@ -7,7 +7,7 @@
    - + diff --git a/client/webserver/site/src/js/charts.ts b/client/webserver/site/src/js/charts.ts index 908dba3464..63932e45d8 100644 --- a/client/webserver/site/src/js/charts.ts +++ b/client/webserver/site/src/js/charts.ts @@ -1108,7 +1108,7 @@ export class Wave extends Chart { const { region, msgRegion, canvas: { width: w, height: h }, opts: { backgroundColor: bg, message: msg }, colorShift, ctx } = this if (bg) { - if (bg === true) ctx.fillStyle = window.getComputedStyle(document.body, null).getPropertyValue('background-color') + if (bg === true) ctx.fillStyle = State.isDark() ? '#122739' : '#f0f0f0' // $dark_panel or $light_panel else ctx.fillStyle = bg ctx.fillRect(0, 0, w, h) }