diff --git a/client/webserver/api.go b/client/webserver/api.go index 4e04159fba..f807228889 100644 --- a/client/webserver/api.go +++ b/client/webserver/api.go @@ -425,24 +425,23 @@ func (s *WebServer) apiPostBond(w http.ResponseWriter, r *http.Request) { } defer zero(pass) - feeBuffer, err := s.bondsFeeBuffer(assetID) // could also put it in postBondForm, with some work on the frontend - if err != nil { - s.writeAPIError(w, err) - return - } - - _, err = s.core.PostBond(&core.PostBondForm{ - Addr: post.Addr, - Cert: []byte(post.Cert), - AppPass: pass, - Bond: post.Bond, - Asset: &assetID, - LockTime: post.LockTime, - FeeBuffer: feeBuffer, + bondForm := &core.PostBondForm{ + Addr: post.Addr, + Cert: []byte(post.Cert), + AppPass: pass, + Bond: post.Bond, + Asset: &assetID, + LockTime: post.LockTime, // Options valid only when creating an account with bond: MaintainTier: post.Maintain, MaxBondedAmt: post.MaxBondedAmt, - }) + } + + if post.FeeBuffer != nil { + bondForm.FeeBuffer = *post.FeeBuffer + } + + _, err = s.core.PostBond(bondForm) if err != nil { s.writeAPIError(w, fmt.Errorf("add bond error: %w", err)) return diff --git a/client/webserver/locales/en-us.go b/client/webserver/locales/en-us.go index 6081996e8b..d5bb493377 100644 --- a/client/webserver/locales/en-us.go +++ b/client/webserver/locales/en-us.go @@ -31,6 +31,7 @@ var EnUS = map[string]string{ "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).", @@ -419,4 +420,13 @@ var EnUS = map[string]string{ "market_making_running": "Market making is running", "cannot_manually_trade": "You cannot manually place orders while market making is running", "back": "Back", + "bond_details": "Bond Details", + "current_tier": "Current Tier", + "current_tier_tooltip": "Number of active bonds that have not yet reached the expiry threshold as reported by the DEX server. Increase your target tier to raise your account tier, boost your trading limits, and offset penalties, if any.", + "current_target_tier_tooltip": "This is the target account tier you wish to maintain. If zero, bond maintenance will be disabled and new bonds will not be posted.", + "current_target_tier": "Current Target Tier", + "bond_cost": "Bond Cost", + "bond_cost_tooltip": "Cost of a single bond without fees and bond maintenance fund reservation.", + "bond_reservations": "Bond Reservation", + "bond_reservations_tooltip": "Total funds that will be locked when you post a bond to cover fees and bond maintenance costs.", } diff --git a/client/webserver/site/src/css/forms.scss b/client/webserver/site/src/css/forms.scss index ad4322fe02..8f8411b14c 100644 --- a/client/webserver/site/src/css/forms.scss +++ b/client/webserver/site/src/css/forms.scss @@ -323,8 +323,7 @@ button.form-button { #dexAddrForm, #verifyForm, #appPWForm, -#deleteArchivedRecordsForm, -#updateBondOptionsForm { +#deleteArchivedRecordsForm { width: 325px; } @@ -332,7 +331,8 @@ button.form-button { #vSendForm, #exportSeedAuth, #cancelForm, -#quickConfigForm { +#quickConfigForm, +#bondDetailsForm { width: 375px; } diff --git a/client/webserver/site/src/html/dexsettings.tmpl b/client/webserver/site/src/html/dexsettings.tmpl index 37d329b46d..f3cc526853 100644 --- a/client/webserver/site/src/html/dexsettings.tmpl +++ b/client/webserver/site/src/html/dexsettings.tmpl @@ -17,8 +17,8 @@
- - +
+
@@ -46,15 +46,52 @@ {{template "dexAddrForm" .}} - {{- /* UPDATE BOND OPTIONS */ -}} -
+ {{- /* BOND DETAILS */ -}} +
-
[[[update_bond_options]]]
-
+
[[[bond_details]]]
+
+ + [[[current_tier]]] + +
+ +
+ + [[[current_target_tier]]] + +
+ +
+
+
+ + [[[bond_cost]]] + +
+ + +
+ + ~USD + +
+ + [[[bond_reservations]]] + +
+ + +
+ + ~USD + +
+
[[[bond_options]]]:
+
-
-
- +
[[[bond_options_update_success]]]
diff --git a/client/webserver/site/src/js/dexsettings.ts b/client/webserver/site/src/js/dexsettings.ts index 333076acfa..4920361497 100644 --- a/client/webserver/site/src/js/dexsettings.ts +++ b/client/webserver/site/src/js/dexsettings.ts @@ -13,6 +13,7 @@ import { } from './registry' const animationLength = 300 +const bondOverlap = 2 // See client/core/bond.go#L28 export default class DexSettingsPage extends BasePage { body: HTMLElement @@ -22,6 +23,7 @@ export default class DexSettingsPage extends BasePage { host: string keyup: (e: KeyboardEvent) => void dexAddrForm: forms.DEXAddressForm + bondFeeBufferCache: Record constructor (body: HTMLElement) { super() @@ -32,16 +34,18 @@ export default class DexSettingsPage extends BasePage { Doc.bind(page.exportDexBtn, 'click', () => this.prepareAccountExport(page.authorizeAccountExportForm)) Doc.bind(page.disableAcctBtn, 'click', () => this.prepareAccountDisable(page.disableAccountForm)) - Doc.bind(page.updateBondOptionsBtn, 'click', () => this.prepareUpdateBondOptions()) + 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()) this.dexAddrForm = new forms.DEXAddressForm(page.dexAddrForm, async (xc: Exchange) => { window.location.assign(`/dexsettings/${xc.host}`) }, undefined, this.host) - forms.bind(page.updateBondOptionsForm, 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()) @@ -156,11 +160,17 @@ export default class DexSettingsPage extends BasePage { this.showForm(disableAccountForm) } - // prepareUpdateBondOptions resets and prepares the Update Bond Options form. - async prepareUpdateBondOptions () { + // prepareBondDetailsForm resets and prepares the Bond Details form. + async prepareBondDetailsForm () { const page = this.page const xc = app().user.exchanges[this.host] - page.bondTargetTier.setAttribute('placeholder', xc.auth.targetTier.toString()) + // 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 @@ -171,7 +181,52 @@ export default class DexSettingsPage extends BasePage { } page.bondOptionsErr.textContent = '' Doc.hide(page.bondOptionsErr) - this.showForm(page.updateBondOptionsForm) + 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) + const res = await postJSON('/api/bondsfeebuffer', { assetID }) + loaded() + if (!app().checkResponse(res)) { + return 0 + } + return res.feeBuffer } async prepareUpdateHost () { @@ -238,12 +293,18 @@ export default class DexSettingsPage extends BasePage { const targetTier = parseInt(page.bondTargetTier.value ?? '') const bondAssetID = parseInt(page.bondAssetSelect.value ?? '') - const bondOptions = { + 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() @@ -255,12 +316,12 @@ export default class DexSettingsPage extends BasePage { Doc.show(page.bondOptionsMsg) setTimeout(() => { Doc.hide(page.bondOptionsMsg) - Doc.hide(page.forms) }, 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}` } } } diff --git a/client/webserver/site/src/js/doc.ts b/client/webserver/site/src/js/doc.ts index 50fa30ee08..ed47b199c7 100644 --- a/client/webserver/site/src/js/doc.ts +++ b/client/webserver/site/src/js/doc.ts @@ -466,6 +466,14 @@ export default class Doc { el.textContent = msg Doc.show(el) } + + // showFiatValue displays the fiat equivalent for the provided amount. + static showFiatValue (display: PageElement, amount: number, rate: number, ui: UnitInfo): void { + if (rate) { + display.textContent = Doc.formatFiatConversion(amount, rate, ui) + Doc.show(display.parentElement as Element) + } else Doc.hide(display.parentElement as Element) + } } /* diff --git a/client/webserver/site/src/js/wallets.ts b/client/webserver/site/src/js/wallets.ts index 878c6082a7..6a56975cfc 100644 --- a/client/webserver/site/src/js/wallets.ts +++ b/client/webserver/site/src/js/wallets.ts @@ -240,7 +240,7 @@ export default class WalletsPage extends BasePage { const { unitInfo: ui } = app().assets[this.selectedAssetID] const amt = parseFloat(page.sendAmt.value || '0') const conversionFactor = ui.conventional.conversionFactor - this.showFiatValue(this.selectedAssetID, amt * conversionFactor, page.sendValue) + Doc.showFiatValue(page.sendValue, amt * conversionFactor, app().fiatRatesMap[this.selectedAssetID], ui) }) // Clicking on maxSend on the send form should populate the amount field. @@ -358,14 +358,15 @@ export default class WalletsPage extends BasePage { } else { page.vSendFee.textContent = Doc.formatFullPrecision(txfee, ui) } - this.showFiatValue(assetID, txfee, page.vSendFeeFiat) + const xcRate = app().fiatRatesMap[assetID] + Doc.showFiatValue(page.vSendFeeFiat, txfee, xcRate, ui) page.vSendDestinationAmt.textContent = Doc.formatFullPrecision(value - txfee, ui) page.vTotalSend.textContent = Doc.formatFullPrecision(value, ui) - this.showFiatValue(assetID, value, page.vTotalSendFiat) + Doc.showFiatValue(page.vTotalSendFiat, value, xcRate, ui) page.vSendAddr.textContent = page.sendAddr.value || '' const bal = wallet.balance.available - value page.balanceAfterSend.textContent = Doc.formatFullPrecision(bal, ui) - this.showFiatValue(assetID, bal, page.balanceAfterSendFiat) + Doc.showFiatValue(page.balanceAfterSendFiat, bal, xcRate, ui) Doc.show(page.approxSign) // NOTE: All tokens take this route because they cannot pay the fee. if (!subtract) { @@ -374,17 +375,17 @@ export default class WalletsPage extends BasePage { let totalSend = value if (!token) totalSend += txfee page.vTotalSend.textContent = Doc.formatFullPrecision(totalSend, ui) - this.showFiatValue(assetID, totalSend, page.vTotalSendFiat) + Doc.showFiatValue(page.vTotalSendFiat, totalSend, xcRate, ui) let bal = wallet.balance.available - value if (!token) bal -= txfee // handle edge cases where bal is not enough to cover totalSend. // we don't want a minus display of user bal. if (bal <= 0) { page.balanceAfterSend.textContent = Doc.formatFullPrecision(0, ui) - this.showFiatValue(assetID, 0, page.balanceAfterSendFiat) + Doc.showFiatValue(page.balanceAfterSendFiat, 0, xcRate, ui) } else { page.balanceAfterSend.textContent = Doc.formatFullPrecision(bal, ui) - this.showFiatValue(assetID, bal, page.balanceAfterSendFiat) + Doc.showFiatValue(page.balanceAfterSendFiat, bal, xcRate, ui) } } Doc.hide(page.sendForm) @@ -1578,7 +1579,8 @@ export default class WalletsPage extends BasePage { page.sendAddr.classList.remove('invalid') page.sendAddr.value = '' page.sendAmt.value = '' - this.showFiatValue(assetID, 0, page.sendValue) + const xcRate = app().fiatRatesMap[assetID] + Doc.showFiatValue(page.sendValue, 0, xcRate, ui) page.walletBal.textContent = Doc.formatFullPrecision(wallet.balance.available, ui) page.sendLogo.src = Doc.logoPath(symbol) page.sendName.textContent = ui.conventional.unit @@ -1609,20 +1611,20 @@ export default class WalletsPage extends BasePage { } this.maxSend = canSend page.maxSend.textContent = Doc.formatFullPrecision(canSend, ui) - this.showFiatValue(assetID, canSend, page.maxSendFiat) + Doc.showFiatValue(page.maxSendFiat, canSend, xcRate, ui) if (token) { const { unitInfo: feeUI, symbol: feeSymbol } = app().assets[token.parentID] page.maxSendFee.textContent = Doc.formatFullPrecision(res.txfee, feeUI) + ' ' + feeSymbol - this.showFiatValue(token.parentID, res.txfee, page.maxSendFeeFiat) + Doc.showFiatValue(page.maxSendFeeFiat, res.txfee, app().fiatRatesMap[token.parentID], ui) } else { page.maxSendFee.textContent = Doc.formatFullPrecision(res.txfee, ui) - this.showFiatValue(assetID, res.txfee, page.maxSendFeeFiat) + Doc.showFiatValue(page.maxSendFeeFiat, res.txfee, xcRate, ui) } Doc.show(page.maxSendDisplay) } } - this.showFiatValue(assetID, 0, page.sendValue) + Doc.showFiatValue(page.sendValue, 0, xcRate, ui) page.walletBal.textContent = Doc.formatFullPrecision(wallet.balance.available, ui) box.dataset.assetID = String(assetID) this.showForm(box) @@ -1662,18 +1664,18 @@ export default class WalletsPage extends BasePage { */ async populateMaxSend () { const page = this.page - const asset = app().assets[this.selectedAssetID] - if (!asset) return + const { id: assetID, unitInfo: ui, wallet } = app().assets[this.selectedAssetID] // Populate send amount with max send value and ensure we don't check // subtract checkbox for assets that don't have a withdraw method. - if ((asset.wallet.traits & traitWithdrawer) === 0) { - page.sendAmt.value = String(this.maxSend / asset.unitInfo.conventional.conversionFactor) - this.showFiatValue(asset.id, this.maxSend, page.sendValue) + const xcRate = app().fiatRatesMap[assetID] + if ((wallet.traits & traitWithdrawer) === 0) { + page.sendAmt.value = String(this.maxSend / ui.conventional.conversionFactor) + Doc.showFiatValue(page.sendValue, this.maxSend, xcRate, ui) page.subtractCheckBox.checked = false } else { - const amt = asset.wallet.balance.available - page.sendAmt.value = String(amt / asset.unitInfo.conventional.conversionFactor) - this.showFiatValue(asset.id, amt, page.sendValue) + const amt = wallet.balance.available + page.sendAmt.value = String(amt / ui.conventional.conversionFactor) + Doc.showFiatValue(page.sendValue, amt, xcRate, ui) page.subtractCheckBox.checked = true } } @@ -1866,15 +1868,6 @@ export default class WalletsPage extends BasePage { this.updateDisplayedAssetBalance() } - // showFiatValue displays the fiat equivalent for the provided amount. - showFiatValue (assetID: number, amount: number, display: PageElement): void { - const rate = app().fiatRatesMap[assetID] - if (rate) { - display.textContent = Doc.formatFiatConversion(amount, rate, app().unitInfo(assetID)) - Doc.show(display.parentElement as Element) - } else Doc.hide(display.parentElement as Element) - } - /* * handleWalletStateNote is a handler for both the 'walletstate' and * 'walletconfig' notifications. diff --git a/client/webserver/types.go b/client/webserver/types.go index 53fd31bfd1..d2f95d2661 100644 --- a/client/webserver/types.go +++ b/client/webserver/types.go @@ -66,6 +66,7 @@ type postBondForm struct { LockTime uint64 `json:"lockTime"` Maintain *bool `json:"maintain,omitempty"` MaxBondedAmt *uint64 `json:"maxBondedAmt,omitempty"` + FeeBuffer *uint64 `json:"feeBuffer,omitempty"` } type registrationTxFeeForm struct {