Skip to content
Permalink
Browse files

feat(wallet): ability to sweep on-chain balance

resolves #142
  • Loading branch information...
thedon-chris authored and mrfelton committed Mar 17, 2019
1 parent 0d8286b commit a5a3e618f7cb241b5a6167a87c5204d7822520a3
@@ -40,13 +40,18 @@ export function getTransactions(lnd) {

/**
* Executes a request to send coins to a particular address
* @param {[type]} lnd [description]
* @param {[type]} addr [description]
* @param {[type]} amount [description]
* @return {[type]} [description]
* @param {[type]} lnd [description]
* @param {[type]} data [description]
* @return {[type]} [description]
*/
export function sendCoins(lnd, { addr, amount, target_conf, sat_per_byte }) {
return promisifiedCall(lnd, lnd.sendCoins, { addr, amount, target_conf, sat_per_byte })
export function sendCoins(lnd, { addr, amount, target_conf, sat_per_byte, send_all }) {
return promisifiedCall(lnd, lnd.sendCoins, {
addr,
amount,
target_conf,
sat_per_byte,
send_all,
})
}

/**
@@ -1,6 +1,6 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Box } from 'rebass'
import { Box, Flex } from 'rebass'
import { animated, Keyframes, Transition } from 'react-spring/renderprops.cjs'
import { FormattedMessage, injectIntl, intlShape } from 'react-intl'
import { decodePayReq, getMinFee, getMaxFee, isOnchain, isLn } from '@zap/utils/crypto'
@@ -14,6 +14,8 @@ import {
Panel,
Text,
TransactionFeeInput,
Toggle,
Label,
} from 'components/UI'
import { CurrencyFieldGroup, CryptoValue } from 'containers/UI'
import PaySummaryLightning from 'containers/Pay/PaySummaryLightning'
@@ -81,35 +83,36 @@ class Pay extends React.Component {
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]),
/** Boolean indicating wether the form is being processed. If true, form buttons are disabled. */
/** intel-react */
intl: intlShape.isRequired,
/** Current fee information as provided by bitcoinfees.earn.com */
/** Boolean indicating wether the form is being processed. If true, form buttons are disabled. */
isProcessing: PropTypes.bool,
isQueryingFees: PropTypes.bool,
/** Payment request to load into the form. */
network: PropTypes.string.isRequired,
isQueryingFees: PropTypes.bool,
/** Routing information */
network: PropTypes.string.isRequired,
/** Current wallet balance (in satoshis). */
onchainFees: PropTypes.shape({
fastestFee: PropTypes.number,
halfHourFee: PropTypes.number,
hourFee: PropTypes.number,
}),
/** Current wallet balance (in satoshis). */
payInvoice: PropTypes.func.isRequired,

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

static defaultProps = {
@@ -231,6 +234,14 @@ class Pay extends React.Component {
}
}

setCoinSweep = isEnabled => {
if (isEnabled) {
const { cryptoCurrency, walletBalanceConfirmed } = this.props
let onChainBalance = convert('sats', cryptoCurrency, walletBalanceConfirmed)
this.formApi.setValue('amountCrypto', onChainBalance.toString())
}
}

amountInSats = () => {
const { isLn, isOnchain, invoice } = this.state
const { cryptoCurrency } = this.props
@@ -266,6 +277,7 @@ class Pay extends React.Component {
value: values.amountCrypto,
currency: cryptoCurrency,
satPerByte: satPerByte,
isCoinSweep: values.isCoinSweep,
})
// Close the form modal once the transaction has been sent
changeFilter('ALL_ACTIVITY')
@@ -471,6 +483,9 @@ class Pay extends React.Component {
const { intl, initialAmountCrypto, initialAmountFiat, isQueryingFees } = this.props
const fee = this.getFee()

const formState = this.formApi.getState()
const { isCoinSweep } = formState.values

return (
<ShowHideAmount
context={this}
@@ -485,14 +500,23 @@ class Pay extends React.Component {
forwardedRef={this.amountInput}
initialAmountCrypto={initialAmountCrypto}
initialAmountFiat={initialAmountFiat}
isDisabled={currentStep !== 'amount'}
isDisabled={currentStep !== 'amount' || isCoinSweep}
isRequired
/>

{isOnchain && (
<>
<Bar my={3} variant="light" />

<Flex alignItems="center" justifyContent="space-between">
<Label htmlFor="isCoinSweep">
<FormattedMessage {...messages.sweep_funds} />
</Label>
<Toggle field="isCoinSweep" id="isCoinSweep" onValueChange={this.setCoinSweep} />
</Flex>

<Bar my={3} variant="light" />

<TransactionFeeInput
fee={fee}
field="speed"
@@ -513,7 +537,7 @@ class Pay extends React.Component {
const { routes } = this.props

const formState = this.formApi.getState()
const { speed, payReq } = formState.values
const { speed, payReq, isCoinSweep } = formState.values
let minFee, maxFee
if (routes.length) {
minFee = getMinFee(routes)
@@ -529,6 +553,7 @@ class Pay extends React.Component {
address={payReq}
amount={amount}
fee={this.getFee()}
isCoinSweep={isCoinSweep}
mt={-3}
speed={speed}
/>
@@ -586,7 +611,7 @@ class Pay extends React.Component {
queryFees,
queryRoutes,
routes,
walletBalance,
walletBalanceConfirmed,
...rest
} = this.props
return (
@@ -610,21 +635,26 @@ class Pay extends React.Component {
if (isLn && invoice) {
hasEnoughFunds = amountInSats <= channelBalance
} else if (isOnchain) {
hasEnoughFunds = amountInSats <= walletBalance
hasEnoughFunds = amountInSats <= walletBalanceConfirmed
}

// Determine what the text should be for the next button.
let nextButtonText = <FormattedMessage {...messages.next} />
if (currentStep === 'summary') {
nextButtonText = (
<>
<FormattedMessage {...messages.send} />
{` `}
<CryptoValue value={amountInSats} />
{` `}
{cryptoCurrencyTicker}
</>
)
const { isCoinSweep } = formState.values
if (isCoinSweep) {
nextButtonText = <FormattedMessage {...messages.send_all} />
} else {
nextButtonText = (
<>
<FormattedMessage {...messages.send} />
{` `}
<CryptoValue value={amountInSats} />
{` `}
{cryptoCurrencyTicker}
</>
)
}
}
return (
<Panel>
@@ -670,13 +700,13 @@ class Pay extends React.Component {
previousStep={this.previousStep}
/>

{walletBalance !== null && (
{walletBalanceConfirmed !== null && (
<React.Fragment>
<Text fontWeight="normal" mt={3} textAlign="center">
<FormattedMessage {...messages.current_balance} />:
</Text>
<Text fontSize="xs" textAlign="center">
<CryptoValue value={walletBalance} />
<CryptoValue value={walletBalanceConfirmed} />
{` `}
{cryptoCurrencyTicker} (onchain),
</Text>
@@ -18,6 +18,8 @@ class PaySummaryOnChain extends React.Component {
cryptoCurrencyTicker: PropTypes.string.isRequired,
/** Fee in sats per byte */
fee: PropTypes.number,
/** Boolean indicating wether transaction is a coin sweep. */
isCoinSweep: PropTypes.bool,
/** Boolean indicating wether routing information is currently being fetched. */
isQueryingFees: PropTypes.bool,
/** Current fee information as provided by bitcoinfees.earn.com */
@@ -49,6 +51,7 @@ class PaySummaryOnChain extends React.Component {
cryptoCurrencyTicker,
onchainFees,
isQueryingFees,
isCoinSweep,
fee,
speed,
...rest
@@ -88,7 +91,16 @@ class PaySummaryOnChain extends React.Component {
<Bar variant="light" />

<DataRow
left={<FormattedMessage {...messages.fee} />}
left={
<Box>
<Text>
<FormattedMessage {...messages.fee} />
</Text>
<Text color="gray" fontWeight="light">
<FormattedMessage {...messages[isCoinSweep ? 'fee_subtraction' : 'fee_addition']} />
</Text>
</Box>
}
right={
isQueryingFees ? (
<Flex alignItems="center" justifyContent="flex-end" ml="auto">
@@ -112,18 +124,6 @@ class PaySummaryOnChain extends React.Component {
)
}
/>

<Bar variant="light" />

<DataRow
left={<FormattedMessage {...messages.total} />}
right={
<React.Fragment>
<CryptoValue value={amount} /> {cryptoCurrencyTicker}
{!isQueryingFees && fee && <Text fontSize="s">(+ {fee} satoshis per byte)</Text>}
</React.Fragment>
}
/>
</Box>
)
}
@@ -15,18 +15,22 @@ export default defineMessages({
next: 'Next',
back: 'Back',
send: 'Send',
send_all: 'Send all',
fee: 'Fee',
fee_less_than_1: 'less than 1 satoshi',
fee_range: 'between {minFee} and {maxFee} satoshis',
fee_upto: 'up to {maxFee} satoshi',
fee_unknown: 'unknown',
fee_per_byte: 'per byte',
fee_subtraction: 'Deducted from total',
fee_addition: 'Added to total',
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.',
})
@@ -2,6 +2,7 @@ import { connect } from 'react-redux'
import { Pay } from 'components/Pay'
import { tickerSelectors } from 'reducers/ticker'
import { setPayReq, queryFees, queryRoutes } from 'reducers/pay'
import { balanceSelectors } from 'reducers/balance'
import { changeFilter } from 'reducers/activity'
import { sendCoins } from 'reducers/transaction'
import { payInvoice } from 'reducers/payment'
@@ -11,14 +12,14 @@ const mapStateToProps = state => ({
chain: state.info.chain,
network: state.info.network,
cryptoName: tickerSelectors.cryptoName(state),
channelBalance: state.balance.channelBalance,
channelBalance: balanceSelectors.channelBalance(state),
cryptoCurrency: state.ticker.currency,
cryptoCurrencyTicker: tickerSelectors.currencyName(state),
isQueryingFees: state.pay.isQueryingFees,
payReq: state.pay.payReq,
onchainFees: state.pay.onchainFees,
routes: state.pay.routes,
walletBalance: state.balance.walletBalance,
walletBalanceConfirmed: balanceSelectors.walletBalanceConfirmed(state),
})

const mapDispatchToProps = {
@@ -79,12 +79,25 @@ export const receiveTransactions = (event, { transactions }) => (dispatch, getSt
dispatch(fetchBalance())
}

export const sendCoins = ({ value, addr, currency, targetConf, satPerByte }) => dispatch => {
export const sendCoins = ({
value,
addr,
currency,
targetConf,
satPerByte,
isCoinSweep,
}) => dispatch => {
// backend needs amount in satoshis no matter what currency we are using
const amount = convert(currency, 'sats', value)

// submit the transaction to LND
const data = { amount, addr, target_conf: targetConf, sat_per_byte: satPerByte }
const data = {
amount: isCoinSweep ? null : amount,
addr,
target_conf: targetConf,
sat_per_byte: satPerByte,
send_all: isCoinSweep,
}
dispatch(
send('lnd', {
msg: 'sendCoins',

0 comments on commit a5a3e61

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