Skip to content
This repository has been archived by the owner on Nov 17, 2023. It is now read-only.

Commit

Permalink
feat(wallet): add ability to create hold invoices
Browse files Browse the repository at this point in the history
  • Loading branch information
mrfelton committed Jun 11, 2020
1 parent 89ebb7f commit 248d86c
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 25 deletions.
1 change: 1 addition & 0 deletions config/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ module.exports = {
lnurlAuth: false,
lnurlChannel: true,
lnurlWithdraw: true,
holdInvoice: true,
},

// number of onchain confirmations for the specified periods
Expand Down
58 changes: 50 additions & 8 deletions renderer/components/Request/Request.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Flex } from 'rebass/styled-components'
import { Box, Flex } from 'rebass/styled-components'
import { FormattedMessage, injectIntl } from 'react-intl'
import { intlShape } from '@zap/i18n'
import { convert } from '@zap/utils/btc'
import { CoinBig } from '@zap/utils/coin'
import { Bar, Button, Header, Panel, Span, Text, Tooltip, Message } from 'components/UI'
import { Form, Label, TextArea, Toggle } from 'components/Form'
import { Form, Input, Label, TextArea, Toggle } from 'components/Form'
import { CurrencyFieldGroup } from 'containers/Form'
import Lightning from 'components/Icon/Lightning'
import Padlock from 'components/Icon/Padlock'
Expand All @@ -33,6 +33,7 @@ class Request extends React.Component {
intl: intlShape.isRequired,
invoice: PropTypes.object,
isAnimating: PropTypes.bool,
isHoldInvoiceEnabled: PropTypes.bool,
isProcessing: PropTypes.bool,
maxOneTimeReceive: PropTypes.string.isRequired,
payReq: PropTypes.string,
Expand Down Expand Up @@ -108,11 +109,14 @@ class Request extends React.Component {
showError,
} = this.props
try {
const { amountCrypto, memo, hasRoutingHints, isHoldInvoice, preimage } = values
const invoiceArgs = {
amount: values.amountCrypto,
amount: amountCrypto,
cryptoUnit,
memo: values.memo,
isPrivate: values.routingHints,
memo,
isPrivate: !hasRoutingHints,
isHoldInvoice,
preimage,
}

if (willUseFallback) {
Expand Down Expand Up @@ -199,24 +203,60 @@ class Request extends React.Component {
}

renderRoutingHints = () => (
<Flex alignItems="center" justifyContent="space-between">
<Flex alignItems="center" justifyContent="space-between" mb={2}>
<Flex>
<Span color="gray" fontSize="s" mr={2}>
<Padlock />
</Span>
<Flex>
<Label htmlFor="routingHints">
<Label htmlFor="hasRoutingHints">
<FormattedMessage {...messages.routing_hints_label} />
</Label>
<Tooltip ml={1}>
<FormattedMessage {...messages.routing_hints_tooltip} />
</Tooltip>
</Flex>
</Flex>
<Toggle field="routingHints" />
<Toggle field="hasRoutingHints" />
</Flex>
)

renderHoldInvoice = () => {
const { intl } = this.props
const { values } = this.formApi.getState()
return (
<Box>
<Flex alignItems="center" justifyContent="space-between">
<Flex>
<Span color="gray" fontSize="s" mr={2}>
<Padlock />
</Span>
<Flex>
<Label htmlFor="isHoldInvoice">
<FormattedMessage {...messages.hold_invoice_label} />
</Label>
<Tooltip ml={1}>
<FormattedMessage {...messages.hold_invoice_tooltip} />
</Tooltip>
</Flex>
</Flex>
<Toggle field="isHoldInvoice" />
</Flex>
{values.isHoldInvoice && (
<Input
field="preimage"
isRequired
mt={2}
name="preimage"
placeholder={intl.formatMessage({ ...messages.preimage_placeholder })}
validateOnBlur
validateOnChange
/>
)}
</Box>
)
}

render() {
const {
activeWalletSettings,
Expand All @@ -227,6 +267,7 @@ class Request extends React.Component {
fetchTickers,
intl,
isProcessing,
isHoldInvoiceEnabled,
isAnimating,
invoice,
payReq,
Expand Down Expand Up @@ -268,6 +309,7 @@ class Request extends React.Component {
{this.renderAmountFields()}
{this.renderMemoField()}
{activeWalletSettings.type !== 'local' && this.renderRoutingHints()}
{isHoldInvoiceEnabled && this.renderHoldInvoice()}
</>
) : (
<RequestSummary
Expand Down
3 changes: 3 additions & 0 deletions renderer/components/Request/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ export default defineMessages({
add_error: 'An error has occurred',
routing_hints_label: 'Include routing hints',
routing_hints_tooltip: 'Whether this invoice should include routing hints for private channels.',
hold_invoice_label: 'Hold invoice',
hold_invoice_tooltip: 'Enables manual control the invoice settlement process.',
preimage_placeholder: 'Preimage (required to settle invoice)',
memo_tooltip:
'Add some describer text to your payment request for the recipient to see when paying.',
not_paid: 'not paid',
Expand Down
2 changes: 2 additions & 0 deletions renderer/containers/Request.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { connect } from 'react-redux'
import { isHoldInvoiceEnabled } from '@zap/utils/featureFlag'
import { Request } from 'components/Request'
import { fetchTickers, tickerSelectors } from 'reducers/ticker'
import { createNewAddress } from 'reducers/address'
Expand All @@ -15,6 +16,7 @@ const mapStateToProps = state => ({
cryptoUnit: tickerSelectors.cryptoUnit(state),
cryptoUnitName: tickerSelectors.cryptoUnitName(state),
isProcessing: state.invoice.isInvoicesLoading,
isHoldInvoiceEnabled: isHoldInvoiceEnabled(),
payReq: state.invoice.invoice,
invoice: invoiceSelectors.invoice(state),
maxOneTimeReceive: channelsSelectors.maxOneTimeReceive(state),
Expand Down
51 changes: 34 additions & 17 deletions renderer/reducers/invoice/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import createReducer from '@zap/utils/createReducer'
import { showSystemNotification } from '@zap/utils/notifications'
import { convert } from '@zap/utils/btc'
import { getIntl } from '@zap/i18n'
import { generatePreimage } from '@zap/utils/crypto'
import { sha256digest } from '@zap/utils/sha256'
import { grpc } from 'workers'
import { fetchBalance } from 'reducers/balance'
import { fetchChannels } from 'reducers/channels'
Expand Down Expand Up @@ -127,11 +129,11 @@ export const receiveInvoices = invoices => dispatch => {
* @returns {(dispatch:Function) => void} Thunk
*/
export const createInvoiceSuccess = invoice => dispatch => {
// Add new invoice to invoices list
dispatch({ type: INVOICE_SUCCESSFUL, invoice })

// Set current invoice to newly created invoice.
dispatch(setInvoice(invoice.paymentRequest))

// Add new invoice to invoices list
dispatch({ type: INVOICE_SUCCESSFUL, invoice })
}

/**
Expand All @@ -151,15 +153,22 @@ export const createInvoiceFailure = error => dispatch => {
* @param {object} options request options
* @param {number} options.amount Amount
* @param {string} options.cryptoUnit Crypto unit (sats, bits, btc)
* @param {string} options.memo Memo
* @param {boolean} options.isPrivate Set to true to include routing hints
* @param {string} options.fallbackAddr on-chain address fallback
* @param {string} [options.memo] Memo
* @param {boolean} [options.isPrivate] Set to true to include routing hints
* @param {boolean} [options.isHoldInvoice=false] Set to true to make this a hold invoice
* @param {string} [options.fallbackAddr] on-chain address fallback
* @param {string} [options.preimage] on-chain address fallback
* @returns {(dispatch:Function, getState:Function) => Promise<void>} Thunk
*/
export const createInvoice = ({ amount, cryptoUnit, memo, isPrivate, fallbackAddr }) => async (
dispatch,
getState
) => {
export const createInvoice = ({
amount,
cryptoUnit,
memo,
isPrivate,
isHoldInvoice,
fallbackAddr,
preimage,
}) => async (dispatch, getState) => {
const state = getState()

// backend needs value in satoshis no matter what currency we are using
Expand All @@ -174,14 +183,22 @@ export const createInvoice = ({ amount, cryptoUnit, memo, isPrivate, fallbackAdd
const activeWalletSettings = walletSelectors.activeWalletSettings(state)
const currentConfig = settingsSelectors.currentConfig(state)

const payload = {
value,
memo,
private: isPrivate || activeWalletSettings.type === 'local',
expiry: currentConfig.invoices.expire,
fallbackAddr,
}
let invoice
try {
const invoice = await grpc.services.Lightning.createInvoice({
value,
memo,
private: isPrivate || activeWalletSettings.type === 'local',
expiry: currentConfig.invoices.expire,
fallbackAddr,
})
if (isHoldInvoice) {
const preimageBytes = preimage ? new TextEncoder().encode(preimage) : generatePreimage()
payload.hash = sha256digest(preimageBytes)
invoice = await grpc.services.Invoices.addHoldInvoice(payload)
} else {
invoice = await grpc.services.Lightning.createInvoice(payload)
}
dispatch(createInvoiceSuccess(invoice))
return invoice
} catch (error) {
Expand Down
9 changes: 9 additions & 0 deletions utils/featureFlag.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,12 @@ export function isNetworkSelectionEnabled() {
export function isSCBRestoreEnabled() {
return config.features.scbRestore
}

/**
* isHoldInvoiceEnabled - Check if Hold Invoice feature is enabled.
*
* @returns {boolean} Boolean indicating whether Hold Invoice is enabled
*/
export function isHoldInvoiceEnabled() {
return config.features.holdInvoice
}

0 comments on commit 248d86c

Please sign in to comment.