diff --git a/lib/plugins/wallet/bitcoind/bitcoind.js b/lib/plugins/wallet/bitcoind/bitcoind.js index a64e99c3..7166f769 100644 --- a/lib/plugins/wallet/bitcoind/bitcoind.js +++ b/lib/plugins/wallet/bitcoind/bitcoind.js @@ -51,11 +51,32 @@ function balance (account, cryptoCode, settings, operatorId) { return accountBalance(cryptoCode) } -function sendCoins (account, tx, settings, operatorId) { +function estimateFee () { + return fetch('estimatesmartfee', [6, 'unset']) + .then(result => BN(result.feerate)) + .catch(() => {}) +} + +function calculateFeeDiscount (feeMultiplier) { + // 0 makes bitcoind do automatic fee selection + const AUTOMATIC_FEE = 0 + if (!feeMultiplier || feeMultiplier.eq(1)) return AUTOMATIC_FEE + return estimateFee() + .then(estimatedFee => { + if (!estimatedFee) return AUTOMATIC_FEE + const newFee = estimatedFee.times(feeMultiplier) + if (newFee.lt(0.00001) || newFee.gt(0.1)) return AUTOMATIC_FEE + return newFee + }) +} + +function sendCoins (account, tx, settings, operatorId, feeMultiplier) { const { toAddress, cryptoAtoms, cryptoCode } = tx const coins = cryptoAtoms.shiftedBy(-unitScale).toFixed(8) return checkCryptoCode(cryptoCode) + .then(() => calculateFeeDiscount(feeMultiplier)) + .then(newFee => fetch('settxfee', [newFee])) .then(() => fetch('sendtoaddress', [toAddress, coins])) .then((txId) => fetch('gettransaction', [txId])) .then((res) => _.pick(['fee', 'txid'], res)) @@ -148,5 +169,6 @@ module.exports = { getStatus, newFunding, cryptoNetwork, - fetchRBF + fetchRBF, + estimateFee } diff --git a/lib/wallet.js b/lib/wallet.js index f5078e12..9391addb 100644 --- a/lib/wallet.js +++ b/lib/wallet.js @@ -3,6 +3,7 @@ const mem = require('mem') const hkdf = require('futoin-hkdf') const configManager = require('./new-config-manager') +const { loadLatestConfig } = require('./new-settings-loader') const pify = require('pify') const fs = pify(require('fs')) @@ -59,7 +60,8 @@ function _balance (settings, cryptoCode) { function sendCoins (settings, tx) { return fetchWallet(settings, tx.cryptoCode) .then(r => { - return r.wallet.sendCoins(r.account, tx, settings, r.operatorId) + const feeMultiplier = settings[`wallets_${tx.cryptoCode}_feeMultiplier`] + return r.wallet.sendCoins(r.account, tx, settings, r.operatorId, feeMultiplier) .then(res => { mem.clear(module.exports.balance) return res @@ -69,7 +71,6 @@ function sendCoins (settings, tx) { if (err.name === INSUFFICIENT_FUNDS_NAME) { throw httpError(INSUFFICIENT_FUNDS_NAME, INSUFFICIENT_FUNDS_CODE) } - throw err }) } diff --git a/migrations/1620954224627-add-fee-priority.js b/migrations/1620954224627-add-fee-priority.js new file mode 100644 index 00000000..e1a46496 --- /dev/null +++ b/migrations/1620954224627-add-fee-priority.js @@ -0,0 +1,21 @@ +const _ = require('lodash/fp') +const { saveConfig, loadLatest } = require('../lib/new-settings-loader') +const { getCryptosFromWalletNamespace } = require('../lib/new-config-manager') + +exports.up = function (next) { + const newConfig = {} + return loadLatest() + .then(config => { + const coins = getCryptosFromWalletNamespace(config) + _.map(coin => { newConfig[`wallets_${coin}_feeMultiplier`] = '1' }, coins) + return saveConfig(newConfig) + }) + .then(next) + .catch(err => { + return next(err) + }) +} + +module.exports.down = function (next) { + next() +} diff --git a/new-lamassu-admin/src/components/editableTable/Table.js b/new-lamassu-admin/src/components/editableTable/Table.js index a6e7b77c..701f4b20 100644 --- a/new-lamassu-admin/src/components/editableTable/Table.js +++ b/new-lamassu-admin/src/components/editableTable/Table.js @@ -127,6 +127,7 @@ const ETable = ({ ((enableToggle && toggleWidth) ?? 0) const width = getWidth(elements) + actionColSize + const classes = useStyles({ width }) const showButtonOnEmpty = !data.length && enableCreate && !adding diff --git a/new-lamassu-admin/src/pages/Wallet/Wallet.js b/new-lamassu-admin/src/pages/Wallet/Wallet.js index 386cc1a6..b80ea805 100644 --- a/new-lamassu-admin/src/pages/Wallet/Wallet.js +++ b/new-lamassu-admin/src/pages/Wallet/Wallet.js @@ -1,18 +1,24 @@ import { useQuery, useMutation } from '@apollo/react-hooks' +import { DialogActions, makeStyles, Box } from '@material-ui/core' import gql from 'graphql-tag' import * as R from 'ramda' import React, { useState } from 'react' import Modal from 'src/components/Modal' +import { IconButton, Button } from 'src/components/buttons' import { NamespacedTable as EditableTable } from 'src/components/editableTable' +import { RadioGroup } from 'src/components/inputs' import TitleSection from 'src/components/layout/TitleSection' +import { P, Label1 } from 'src/components/typography' import FormRenderer from 'src/pages/Services/FormRenderer' import schemas from 'src/pages/Services/schemas' +import { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/enabled.svg' import { ReactComponent as ReverseSettingsIcon } from 'src/styling/icons/circle buttons/settings/white.svg' import { ReactComponent as SettingsIcon } from 'src/styling/icons/circle buttons/settings/zodiac.svg' import { fromNamespace, toNamespace } from 'src/utils/config' import AdvancedWallet from './AdvancedWallet' +import styles from './Wallet.styles.js' import Wizard from './Wizard' import { WalletSchema, getElements } from './helper' @@ -46,9 +52,15 @@ const GET_INFO = gql` } } ` + const LOCALE = 'locale' +const useStyles = makeStyles(styles) + const Wallet = ({ name: SCREEN_KEY }) => { + const classes = useStyles() + const [editingFeeDiscount, setEditingFeeDiscount] = useState(null) + const [selectedDiscount, setSelectedDiscount] = useState(null) const [editingSchema, setEditingSchema] = useState(null) const [onChangeFunction, setOnChangeFunction] = useState(null) const [wizard, setWizard] = useState(false) @@ -104,17 +116,51 @@ const Wallet = ({ name: SCREEN_KEY }) => { return it }) + const saveFeeDiscount = rawConfig => { + const config = toNamespace(SCREEN_KEY)(rawConfig) + setEditingFeeDiscount(false) + return saveConfig({ variables: { config } }) + } + + const handleRadioButtons = evt => { + const selectedDiscount = R.path(['target', 'value'])(evt) + setSelectedDiscount(selectedDiscount) + } + + const radioButtonOptions = [ + { display: '+20%', code: '1.2' }, + { display: 'Default', code: '1' }, + { display: '-20%', code: '0.8' }, + { display: '-40%', code: '0.6' }, + { display: '-60%', code: '0.4' } + ] + return ( <> - +
+ + + Fee discount + +

{selectedDiscount}

+ setEditingFeeDiscount(true)}> + + +
+
+
{!advancedSettings && ( <> { )} {advancedSettings && } + {editingFeeDiscount && ( + setEditingFeeDiscount(null)} + open={true}> +

+ Set a priority level for your outgoing BTC transactions, selecting a + percentage off of the fee estimate your wallet uses. +

+ + + + +
+ )} ) } diff --git a/new-lamassu-admin/src/pages/Wallet/Wallet.styles.js b/new-lamassu-admin/src/pages/Wallet/Wallet.styles.js new file mode 100644 index 00000000..f9a6bb52 --- /dev/null +++ b/new-lamassu-admin/src/pages/Wallet/Wallet.styles.js @@ -0,0 +1,16 @@ +import { offColor } from 'src/styling/variables' + +export default { + header: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between' + }, + feeDiscountLabel: { + color: offColor, + margin: [[13, 0, -5, 20]] + }, + selection: { + marginRight: 12 + } +} diff --git a/new-lamassu-admin/src/pages/Wallet/helper.js b/new-lamassu-admin/src/pages/Wallet/helper.js index 67491ac5..226dbd66 100644 --- a/new-lamassu-admin/src/pages/Wallet/helper.js +++ b/new-lamassu-admin/src/pages/Wallet/helper.js @@ -95,7 +95,7 @@ const getElements = (cryptoCurrencies, accounts, onChange, wizard = false) => { { name: 'id', header: 'Cryptocurrency', - width: 180 - widthAdjust, + width: 150 - widthAdjust, view: viewCryptoCurrency, size: 'sm', editable: false @@ -105,7 +105,7 @@ const getElements = (cryptoCurrencies, accounts, onChange, wizard = false) => { size: 'sm', stripe: true, view: getDisplayName('ticker'), - width: 190 - widthAdjust, + width: 175 - widthAdjust, input: Autocomplete, inputProps: { options: getOptions('ticker'), @@ -119,7 +119,7 @@ const getElements = (cryptoCurrencies, accounts, onChange, wizard = false) => { size: 'sm', stripe: true, view: getDisplayName('wallet'), - width: 190 - widthAdjust, + width: 175 - widthAdjust, input: Autocomplete, inputProps: { options: getOptions('wallet'), @@ -134,7 +134,7 @@ const getElements = (cryptoCurrencies, accounts, onChange, wizard = false) => { size: 'sm', stripe: true, view: getDisplayName('exchange'), - width: 190 - widthAdjust, + width: 175 - widthAdjust, input: Autocomplete, inputProps: { options: getOptions('exchange'), @@ -151,7 +151,7 @@ const getElements = (cryptoCurrencies, accounts, onChange, wizard = false) => { stripe: true, view: getDisplayName('zeroConf'), input: Autocomplete, - width: 220 - widthAdjust, + width: 210 - widthAdjust, inputProps: { options: getOptions('zeroConf'), valueProp: 'code', @@ -168,7 +168,7 @@ const getElements = (cryptoCurrencies, accounts, onChange, wizard = false) => { view: (it, row) => row.id === 'ETH' ? {it} : it, input: NumberInput, - width: 190 - widthAdjust, + width: 145 - widthAdjust, inputProps: { decimalPlaces: 0 },