Skip to content
Permalink
Browse files

feat(wallet): support custom fee target confs

  • Loading branch information...
mrfelton committed May 15, 2019
1 parent a47f7cf commit ac2bcc9fa0d5f21e712277ad96ad3517752899b9
@@ -95,6 +95,11 @@ class ChannelCreateForm extends React.Component {
fetchTickers: PropTypes.func.isRequired,
intl: intlShape.isRequired,
isQueryingFees: PropTypes.bool,
lndTargetConfirmations: PropTypes.shape({
fast: PropTypes.number.isRequired,
medium: PropTypes.number.isRequired,
slow: PropTypes.number.isRequired,
}).isRequired,
onchainFees: PropTypes.shape({
fast: PropTypes.number,
medium: PropTypes.number,
@@ -244,7 +249,13 @@ class ChannelCreateForm extends React.Component {
}

renderFormFields() {
const { intl, activeWalletSettings, isQueryingFees, searchQuery } = this.props
const {
intl,
activeWalletSettings,
isQueryingFees,
lndTargetConfirmations,
searchQuery,
} = this.props

const formState = this.formApi.getState()
const { speed } = formState.values
@@ -286,6 +297,7 @@ class ChannelCreateForm extends React.Component {
field="speed"
isQueryingFees={isQueryingFees}
label={intl.formatMessage({ ...messages.fee })}
lndTargetConfirmations={lndTargetConfirmations}
required
/>

@@ -337,6 +349,7 @@ class ChannelCreateForm extends React.Component {
walletBalance,
currencyName,
isQueryingFees,
lndTargetConfirmations,
fetchTickers,
onchainFees,
onSubmit,
@@ -21,10 +21,6 @@ export default defineMessages({
fee_unknown: 'unknown',
fee_per_byte: 'per byte',
total: 'Total',
transaction_speed_slow: 'Slow',
transaction_speed_medium: 'Medium',
transaction_speed_fast: 'Fast',
transaction_speed_fast_description: 'Estimated Delivery: less than 1 hour',
open_channel_form_title: 'Open a Channel',
open_channel_form_description:
"To open a channel, enter the desired node's publickey@host, set the amount of BTC you'd like to commit to the channel, and submit.",
@@ -69,53 +69,36 @@ const ShowHideAmount = Keyframes.Spring({
class Pay extends React.Component {
static propTypes = {
chain: PropTypes.string.isRequired,
/** The currently active chain (bitcoin, litecoin etc) */
changeFilter: PropTypes.func.isRequired,
/** The currently active chain (mainnet, testnet) */
channelBalance: PropTypes.number.isRequired,
/** Human readable chain name */
closeModal: PropTypes.func.isRequired,
/** Current channel balance (in satoshis). */
cryptoCurrency: PropTypes.string.isRequired,
/** Currently selected cryptocurrency (key). */
cryptoCurrencyTicker: PropTypes.string.isRequired,
/** Ticker symbol of the currently selected cryptocurrency. */
cryptoName: PropTypes.string.isRequired,
/** Fetch fiat ticker data. */
fetchTickers: PropTypes.func.isRequired,
/** Amount value to populate the amountCrypto field with when the form first loads. */
initialAmountCrypto: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
/** Amount value to populate the amountFiat field with when the form first loads. */
initialAmountFiat: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
/** intel-react */
intl: intlShape.isRequired,
/** Boolean indicating wether the form is being processed. If true, form buttons are disabled. */
isProcessing: PropTypes.bool,
/** Payment request to load into the form. */
isQueryingFees: PropTypes.bool,
/** Routing information */
lndTargetConfirmations: PropTypes.shape({
fast: PropTypes.number.isRequired,
medium: PropTypes.number.isRequired,
slow: PropTypes.number.isRequired,
}).isRequired,
network: PropTypes.string.isRequired,
/** Current wallet balance (in satoshis). */
onchainFees: PropTypes.shape({
fast: PropTypes.number,
medium: PropTypes.number,
slow: PropTypes.number,
}),

/** Payment request to load into the form. */
payInvoice: PropTypes.func.isRequired,
/** Method to close the current modal */
payReq: PropTypes.object,
queryFees: PropTypes.func.isRequired,
/** Method to process offChain invoice payments. Called when the form is submitted. */
queryRoutes: PropTypes.func.isRequired,
/** Set the current payment request. */
routes: PropTypes.array,
/** Method to process onChain transactions. Called when the form is submitted. */
sendCoins: PropTypes.func.isRequired,
/** Method to collect route information for lightning invoices. */
setPayReq: PropTypes.func.isRequired,
/** If true, lnd will attempt to send all coins available to selected address */
walletBalanceConfirmed: PropTypes.number.isRequired,
}

@@ -512,7 +495,13 @@ class Pay extends React.Component {

renderAmountFields = () => {
const { currentStep, isOnchain } = this.state
const { intl, initialAmountCrypto, initialAmountFiat, isQueryingFees } = this.props
const {
intl,
initialAmountCrypto,
initialAmountFiat,
isQueryingFees,
lndTargetConfirmations,
} = this.props
const fee = this.getFee()

const formState = this.formApi.getState()
@@ -555,6 +544,7 @@ class Pay extends React.Component {
field="speed"
isQueryingFees={isQueryingFees}
label={intl.formatMessage({ ...messages.fee })}
lndTargetConfirmations={lndTargetConfirmations}
required
/>
</>
@@ -638,6 +628,7 @@ class Pay extends React.Component {
initialAmountFiat,
isProcessing,
isQueryingFees,
lndTargetConfirmations,
onchainFees,
payInvoice,
sendCoins,
@@ -27,9 +27,6 @@ export default defineMessages({
amount: 'Amount',
total: 'Total',
memo: 'Memo',
transaction_speed_slow_description: 'Estimated Delivery: 1-24 hours',
transaction_speed_medium_description: 'Estimated Delivery: 1-6 hours',
transaction_speed_fast_description: 'Estimated Delivery: less than 1 hour',
sweep_funds: 'Send all of your funds',
description:
'You can send {chain} ({ticker}) through the Lightning Network or make a On-Chain Transaction. Just paste your Lightning Payment Request or the {chain} Address in the field below. Zap will guide you to the process.',
@@ -12,19 +12,20 @@ export default defineMessages({
locale_label: 'Language',
locale_description: 'Your preferred language.',
currency_label: 'Fiat currency',
currency_description: 'Currency to use when display fiat amounts.',
currency_description: 'Your preferred fiat currency.',
theme_label: 'Theme',
theme_description: 'Your preferred display mode.',
theme_option_light: 'Light',
theme_option_dark: 'Dark',
address_label: 'Address format',
address_description: 'Your preferred address format.',
address_option_p2wkh: 'Bech 32',
address_option_np2wkh: 'Wrapped Bech 32',
lndTargetConfirmations_slow_label: 'Target confs (slow).',
lndTargetConfirmations_slow_description: 'Number of blocks to target for "slow" transactions',
lndTargetConfirmations_medium_label: 'Target confs (medium).',
lndTargetConfirmations_medium_description: 'Number of blocks to target for "medium" transactions',
lndTargetConfirmations_fast_label: 'Target confs (fast).',
lndTargetConfirmations_fast_description: 'Number of blocks to target for "fast" transactions',
address_description: 'Your preferred receiving address format.',
address_option_p2wkh: 'Bech32',
address_option_np2wkh: 'Segwit',
lndTargetConfirmations_slow_label: 'Target confirmations (slow)',
lndTargetConfirmations_slow_description: 'Number of blocks to target for "slow" sending speed.',
lndTargetConfirmations_medium_label: 'Target confirmations (medium)',
lndTargetConfirmations_medium_description:
'Number of blocks to target for "medium" sending speed.',
lndTargetConfirmations_fast_label: 'Target confirmations (fast)',
lndTargetConfirmations_fast_description: 'Number of blocks to target for "fast" sending speed.',
})
@@ -18,8 +18,46 @@ import {
} from './constants'

const speeds = [TRANSACTION_SPEED_SLOW, TRANSACTION_SPEED_MEDIUM, TRANSACTION_SPEED_FAST]

function TransactionFeeInput({ isQueryingFees, initialValue, label, field, formApi, fee }) {
const speedMessageMap = [
{
threshold: 2,
messageKey: 'transaction_speed_description_fastest',
},
{
threshold: 4,
messageKey: 'transaction_speed_description_fast',
},
{
threshold: 12,
messageKey: 'transaction_speed_description_medium',
},
{
threshold: 100,
messageKey: 'transaction_speed_description_slow',
},
{
threshold: Infinity,
messageKey: 'transaction_speed_description_slowest',
},
]
function TransactionFeeInput({
lndTargetConfirmations,
isQueryingFees,
initialValue,
label,
field,
formApi,
fee,
}) {
const CONF_MAP = {
[TRANSACTION_SPEED_SLOW]: lndTargetConfirmations.slow,
[TRANSACTION_SPEED_MEDIUM]: lndTargetConfirmations.medium,
[TRANSACTION_SPEED_FAST]: lndTargetConfirmations.fast,
}
const value = formApi.getValue(field)
const targetConf = CONF_MAP[value]
const speedMessageKey =
targetConf && speedMessageMap.find(item => targetConf <= item.threshold).messageKey
return (
<Flex alignItems="center" justifyContent="space-between">
<Box>
@@ -48,20 +86,20 @@ function TransactionFeeInput({ isQueryingFees, initialValue, label, field, formA
</Flex>
)}

{!isQueryingFees && !fee && <FormattedMessage {...messages.fee_unknown} />}
{!isQueryingFees && !Number.isFinite(fee) && <FormattedMessage {...messages.fee_unknown} />}

{!isQueryingFees && fee && formApi.getValue(field) && (
{!isQueryingFees && Number.isFinite(fee) && value && (
<Flex alignItems="flex-end" flexDirection="column">
<Box>
<CryptoValue value={fee} />
<CryptoSelector mx={2} />
<FormattedMessage {...messages.fee_per_byte} />
</Box>
<Text color="gray">
<FormattedMessage
{...messages[formApi.getValue(field).toLowerCase() + '_description']}
/>
</Text>
{speedMessageKey && (
<Text color="gray">
<FormattedMessage {...messages[speedMessageKey]} />
</Text>
)}
</Flex>
)}
</Box>
@@ -76,6 +114,11 @@ TransactionFeeInput.propTypes = {
initialValue: PropTypes.string,
isQueryingFees: PropTypes.bool,
label: PropTypes.string,
lndTargetConfirmations: PropTypes.shape({
fast: PropTypes.number.isRequired,
medium: PropTypes.number.isRequired,
slow: PropTypes.number.isRequired,
}).isRequired,
}

TransactionFeeInput.defaultProps = {
@@ -19,9 +19,11 @@ export default defineMessages({
pubkey_placeholder: 'pubkey@host:port',
lnd_connection_string_placeholder: 'Paste an Lnd Connect URI or BTCPay Server config here',
transaction_speed_slow: 'Slow',
transaction_speed_slow_description: 'Estimated Delivery: 1-24 hours',
transaction_speed_medium: 'Medium',
transaction_speed_medium_description: 'Estimated Delivery: 1-6 hours',
transaction_speed_fast: 'Fast',
transaction_speed_fast_description: 'Estimated Delivery: less than 1 hour',
transaction_speed_description_fastest: 'less than 30 minutes',
transaction_speed_description_fast: 'less than 1 hour',
transaction_speed_description_medium: 'less than 3 hours',
transaction_speed_description_slow: 'up to 24 hours',
transaction_speed_description_slowest: 'more than 24 hours',
})
@@ -6,6 +6,7 @@ import { queryFees } from 'reducers/pay'
import { balanceSelectors } from 'reducers/balance'
import { updateContactFormSearchQuery, contactFormSelectors } from 'reducers/contactsform'
import { walletSelectors } from 'reducers/wallet'
import { settingsSelectors } from 'reducers/settings'

const mapStateToProps = state => ({
activeWalletSettings: walletSelectors.activeWalletSettings(state),
@@ -16,6 +17,7 @@ const mapStateToProps = state => ({
currencyName: tickerSelectors.currencyName(state),
isQueryingFees: state.pay.isQueryingFees,
onchainFees: state.pay.onchainFees,
lndTargetConfirmations: settingsSelectors.currentConfig(state).lndTargetConfirmations,
})

const mapDispatchToProps = {
@@ -7,6 +7,7 @@ import { changeFilter } from 'reducers/activity'
import { sendCoins } from 'reducers/transaction'
import { payInvoice } from 'reducers/payment'
import { closeModal } from 'reducers/modal'
import { settingsSelectors } from 'reducers/settings'

const mapStateToProps = state => ({
chain: state.info.chain,
@@ -16,6 +17,7 @@ const mapStateToProps = state => ({
cryptoCurrency: state.ticker.currency,
cryptoCurrencyTicker: tickerSelectors.currencyName(state),
isQueryingFees: state.pay.isQueryingFees,
lndTargetConfirmations: settingsSelectors.currentConfig(state).lndTargetConfirmations,
payReq: state.pay.payReq,
onchainFees: state.pay.onchainFees,
routes: state.pay.routes,
@@ -1,5 +1,4 @@
import axios from 'axios'
import renameKeys from '@zap/utils/renameKeys'
import { mainLog } from '@zap/utils/log'

// When running in development/hot mode we load the renderer js code via webpack dev server, and it is from there that
@@ -68,17 +67,38 @@ export function requestNodeScores(chain, network) {
)
}

export function requestFees() {
const BASE_URL = 'https://bitcoinfees.earn.com/api/v1/fees/recommended'
export function requestFees(options) {
const BASE_URL = 'https://bitcoinfees.earn.com/api/v1/fees/list'
return axios({
method: 'get',
url: BASE_URL,
}).then(response => {
const keysMap = {
fastestFee: 'fast',
halfHourFee: 'medium',
hourFee: 'slow',
/**
* Get the lowest fee to get in within a given number of target confs
* @param {number} targetConfs The target number of blocks
* @return {number|null} The fee rate in satoshi/byte
*/
const getFee = targetConfs => {
const targetDelay = targetConfs - 1
let feeRange = response.data.fees
// Filter out everything where the max delay is less than our target delay.
.filter(f => f.maxDelay >= targetDelay)
// Only include items with the lowest fee that is closest to our target delay.
.reduce((acc, cur, idx, src) => {
const lowestDelay = src[src.length - 1].maxDelay
if (cur.maxDelay === lowestDelay) {
acc.push(cur)
}
return acc
}, [])
return feeRange.length ? feeRange[0].maxFee : null
}

const { fast, medium, slow } = options
return {
fast: getFee(fast),
medium: getFee(medium),
slow: getFee(slow),
}
return renameKeys(keysMap, response.data)
})
}
@@ -51,20 +51,22 @@ export async function estimateFeeRange({
asRate = true,
fallback = requestFees,
}) {
const { fast, medium, slow } = range

// lnd fee estimator requires this params
if (!address || !amountInSats) {
return fallback()
return fallback({ fast, medium, slow })
}

const { fast, medium, slow } = range
const [fastestFee, mediumFee, slowFee] = await Promise.all([
estimateLndFee(address, amountInSats, fast),
estimateLndFee(address, amountInSats, medium),
estimateLndFee(address, amountInSats, slow),
])

// check if we have at least one estimate and fill the gap with fallback values otherwise
const fallbackFees = !fastestFee || !mediumFee || !slowFee ? await fallback() : {}
const fallbackFees =
!fastestFee || !mediumFee || !slowFee ? await fallback({ fast, medium, slow }) : {}

// extracts fee from a lnd grpc response
const getFee = feeObj => {

0 comments on commit ac2bcc9

Please sign in to comment.
You can’t perform that action at this time.