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

Commit

Permalink
feat(wallet): lnurl-auth support
Browse files Browse the repository at this point in the history
  • Loading branch information
mrfelton committed Jun 9, 2020
1 parent 8c04978 commit d9e281a
Show file tree
Hide file tree
Showing 17 changed files with 455 additions and 17 deletions.
91 changes: 90 additions & 1 deletion electron/lnurl/service.js
Expand Up @@ -2,6 +2,7 @@ import { ipcMain } from 'electron'
import { parse } from 'url'
import {
fetchLnurlParams,
makeAuthRequest,
makeChannelRequest,
makeWithdrawRequest,
LNURL_STATUS_ERROR,
Expand Down Expand Up @@ -35,11 +36,17 @@ export default class LnurlService {
this.channelParams = {}
this.isChannelProcessing = false

this.authParams = {}
this.isAuthProcessing = false

ipcMain.on('lnurlFinishWithdraw', this.onFinishWithdraw)
ipcMain.on('lnurlCancelWithdraw', this.onCancelWithdraw)

ipcMain.on('lnurlFinishChannel', this.onFinishChannel)
ipcMain.on('lnurlCancelChannel', this.onCancelChannel)

ipcMain.on('lnurlFinishAuth', this.onFinishAuth)
ipcMain.on('lnurlCancelAuth', this.onCancelAuth)
}

/**
Expand All @@ -52,6 +59,9 @@ export default class LnurlService {

ipcMain.off('lnurlFinishChannel', this.onFinishChannel)
ipcMain.off('lnurlCancelChannel', this.onCancelChannel)

ipcMain.off('lnurlFinishAuth', this.onFinishAuth)
ipcMain.off('lnurlCancelAuth', this.onCancelAuth)
}

/**
Expand Down Expand Up @@ -97,6 +107,11 @@ export default class LnurlService {
await this.startChannel()
break

case 'authRequest':
this.authParams = res
await this.startAuth()
break

default:
throw new Error('Unable to process lnurl')
}
Expand All @@ -106,6 +121,8 @@ export default class LnurlService {
}
}

/** WITHDRAW -------------------------------------------------------------- */

/**
* startWithdraw - Initiates lnurl withdrawal process by sending query to
* renderer process to generate LN invoice.
Expand Down Expand Up @@ -181,6 +198,8 @@ export default class LnurlService {
}
}

/** CHANNEL --------------------------------------------------------------- */

/**
* startChannel - Initiates lnurl channel process by sending query to renderer
* process to initiate channel connect.
Expand Down Expand Up @@ -209,7 +228,7 @@ export default class LnurlService {
}

/**
* resetChannel - Resets lnurl-withdraw state.
* resetChannel - Resets lnurl-channel state.
*/
resetChannel = () => {
this.isChannelProcessing = false
Expand Down Expand Up @@ -252,4 +271,74 @@ export default class LnurlService {
this.resetChannel()
}
}

/** AUTH ------------------------------------------------------------------ */

/**
* startAuth - Initiates lnurl auth process by sending query to renderer
* process to initiate auth connect.
*/
async startAuth() {
const { lnurl, secret } = this.authParams
const service = getServiceName(lnurl)

if (this.isAuthProcessing) {
mainLog.warn('Error processing lnurl auth request: busy')
this.sendMessage('lnurlAuthError', { service, reason: 'service busy' })
return
}
this.isAuthProcessing = true

const authParams = { service, secret }
mainLog.info('Processing lnurl auth request: %o', authParams)
this.sendMessage('lnurlAuthRequest', authParams)
}

/**
* resetAuth - Resets lnurl-auth state.
*/
resetAuth = () => {
this.isAuthProcessing = false
this.authParams = {}
}

/**
* onCancelAuth - Cancels an lnurl-auth request.
*/
onCancelAuth = () => {
mainLog.info('Cancelling lnurl auth request: %o', this.authParams)
this.resetAuth()
}

/**
* onFinishAuth - Finalizes an lnurl-auth request.
*
* @param {object} event Event
* @param {object} data Data
*/
onFinishAuth = async (event, { sig, key }) => {
mainLog.info('Finishing lnurl auth request: %o', this.authParams)
const { lnurl } = this.authParams
const service = getServiceName(lnurl)

try {
if (sig && key) {
const callback = `${lnurl}&sig=${sig}&key=${key}`
const { data } = await makeAuthRequest({ lnurl, callback })

if (data.status === LNURL_STATUS_ERROR) {
mainLog.warn('Got error from lnurl auth request: %o', data)
throw new Error(data.reason)
}

mainLog.info('Completed auth request: %o', data)
this.sendMessage('lnurlAuthSuccess', { service })
}
} catch (e) {
mainLog.warn('Unable to complete lnurl auth request: %s', e.message)
this.sendMessage('lnurlAuthError', { service, reason: e.message })
} finally {
this.resetAuth()
}
}
}
22 changes: 18 additions & 4 deletions renderer/components/App/App.js
@@ -1,11 +1,12 @@
import React, { useEffect } from 'react'
import PropTypes from 'prop-types'
import { Flex } from 'rebass/styled-components'
import LnurlChannelPrompt from 'containers/Channels/LnurlChannelPrompt'
import LnurlAuthPrompt from 'containers/Lnurl/LnurlAuthPrompt'
import createScheduler from '@zap/utils/scheduler'
import Wallet from 'containers/Wallet'
import Activity from 'containers/Activity'
import LnurlWithdrawPrompt from 'containers/Pay/LnurlWithdrawPrompt'
import LnurlChannelPrompt from 'containers/Channels/LnurlChannelPrompt'

// Bitcoin blocks come on average every 10 mins
// but we poll a lot more frequently to make UI a little bit more responsive
Expand Down Expand Up @@ -40,10 +41,13 @@ const App = ({
initBackupService,
fetchSuggestedNodes,
initTickers,
lnurlAuthParams,
lnurlChannelParams,
lnurlWithdrawParams,
finishLnurlChannel,
finishLnurlAuth,
finishLnurlWithdraw,
finishLnurlChannel,
willShowLnurlAuthPrompt,
willShowLnurlChannelPrompt,
willShowLnurlWithdrawPrompt,
}) => {
Expand Down Expand Up @@ -96,10 +100,13 @@ const App = ({
// initialize backup service in forceUseTokens mode to avoid
// launching it for wallets that don't have backup setup
initBackupService()
if (!willShowLnurlWithdrawPrompt) {
if (lnurlAuthParams && !willShowLnurlAuthPrompt) {
finishLnurlAuth()
}
if (lnurlWithdrawParams && !willShowLnurlWithdrawPrompt) {
finishLnurlWithdraw()
}
if (!willShowLnurlChannelPrompt) {
if (lnurlChannelParams && !willShowLnurlChannelPrompt) {
finishLnurlChannel()
}
}, [
Expand All @@ -111,10 +118,13 @@ const App = ({
initTickers,
setIsWalletOpen,
updateAutopilotNodeScores,
finishLnurlAuth,
finishLnurlChannel,
finishLnurlWithdraw,
lnurlAuthParams,
lnurlChannelParams,
lnurlWithdrawParams,
willShowLnurlAuthPrompt,
willShowLnurlChannelPrompt,
willShowLnurlWithdrawPrompt,
])
Expand All @@ -136,6 +146,7 @@ const App = ({
<Flex as="article" flexDirection="column" width={1}>
<Wallet />
<Activity />
{willShowLnurlAuthPrompt && <LnurlAuthPrompt />}
{willShowLnurlWithdrawPrompt && <LnurlWithdrawPrompt />}
{willShowLnurlChannelPrompt && <LnurlChannelPrompt />}
</Flex>
Expand All @@ -147,19 +158,22 @@ App.propTypes = {
fetchPeers: PropTypes.func.isRequired,
fetchSuggestedNodes: PropTypes.func.isRequired,
fetchTransactions: PropTypes.func.isRequired,
finishLnurlAuth: PropTypes.func.isRequired,
finishLnurlChannel: PropTypes.func.isRequired,
finishLnurlWithdraw: PropTypes.func.isRequired,
initActivityHistory: PropTypes.func.isRequired,
initBackupService: PropTypes.func.isRequired,
initTickers: PropTypes.func.isRequired,
isAppReady: PropTypes.bool.isRequired,
lnurlAuthParams: PropTypes.object,
lnurlChannelParams: PropTypes.object,
lnurlWithdrawParams: PropTypes.object,
modals: PropTypes.array.isRequired,
redirectPayReq: PropTypes.object,
setIsWalletOpen: PropTypes.func.isRequired,
setModals: PropTypes.func.isRequired,
updateAutopilotNodeScores: PropTypes.func.isRequired,
willShowLnurlAuthPrompt: PropTypes.bool,
willShowLnurlChannelPrompt: PropTypes.bool,
willShowLnurlWithdrawPrompt: PropTypes.bool,
}
Expand Down
52 changes: 52 additions & 0 deletions renderer/components/Lnurl/LnurlAuthPrompt.js
@@ -0,0 +1,52 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Flex } from 'rebass/styled-components'
import { FormattedMessage } from 'react-intl'
import { Dialog, Heading, Button, DialogOverlay, Text } from 'components/UI'
import { Form } from 'components/Form'
import messages from './messages'

const LnurlAuthPrompt = ({ params, onOk, onCancel, onClose }) => {
const { service } = params
const buttons = (
<>
<Button type="submit" variant="normal">
<FormattedMessage {...messages.lnurl_auth_prompt_dialog_confirm_text} />
</Button>
<Button onClick={onCancel} type="button" variant="secondary">
<FormattedMessage {...messages.lnurl_auth_prompt_dialog_decline_text} />
</Button>
</>
)

const header = (
<Flex alignItems="center" flexDirection="column" mb={4}>
<Heading.h1>
<FormattedMessage {...messages.lnurl_auth_prompt_dialog_header} />
</Heading.h1>
</Flex>
)

const handleSubmit = values => onOk(values)

return (
<DialogOverlay alignItems="center" justifyContent="center">
<Form onSubmit={handleSubmit}>
<Dialog buttons={buttons} header={header} onClose={onClose} width={640}>
<Text color="gray">
<FormattedMessage values={{ service }} {...messages.lnurl_auth_prompt_dialog_body} />
</Text>
</Dialog>
</Form>
</DialogOverlay>
)
}

LnurlAuthPrompt.propTypes = {
onCancel: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
onOk: PropTypes.func.isRequired,
params: PropTypes.object.isRequired,
}

export default LnurlAuthPrompt
1 change: 1 addition & 0 deletions renderer/components/Lnurl/index.js
@@ -0,0 +1 @@
export LnurlAuthPrompt from './LnurlAuthPrompt'
9 changes: 9 additions & 0 deletions renderer/components/Lnurl/messages.js
@@ -0,0 +1,9 @@
import { defineMessages } from 'react-intl'

/* eslint-disable max-len */
export default defineMessages({
lnurl_auth_prompt_dialog_header: 'Confirm login',
lnurl_auth_prompt_dialog_body: 'Do you confirm authentication request from {service}',
lnurl_auth_prompt_dialog_confirm_text: 'Confirm',
lnurl_auth_prompt_dialog_decline_text: 'Decline',
})
3 changes: 3 additions & 0 deletions renderer/containers/App/App.js
Expand Up @@ -7,6 +7,7 @@ import { updateAutopilotNodeScores } from 'reducers/autopilot'
import { initActivityHistory } from 'reducers/activity'
import { fetchTransactions } from 'reducers/transaction'
import { appSelectors } from 'reducers/app'
import { finishLnurlAuth, lnurlSelectors } from 'reducers/lnurl'
import { finishLnurlWithdraw, paySelectors } from 'reducers/pay'
import { initBackupService } from 'reducers/backup'
import { setModals, modalSelectors } from 'reducers/modal'
Expand All @@ -21,6 +22,7 @@ const mapStateToProps = state => ({
redirectPayReq: state.pay.redirectPayReq,
modals: modalSelectors.getModalState(state),
lnurlWithdrawParams: paySelectors.lnurlWithdrawParams(state),
willShowLnurlAuthPrompt: lnurlSelectors.willShowLnurlAuthPrompt(state),
willShowLnurlWithdrawPrompt: paySelectors.willShowLnurlWithdrawPrompt(state),
willShowLnurlChannelPrompt: channelsSelectors.willShowLnurlChannelPrompt(state),
})
Expand All @@ -36,6 +38,7 @@ const mapDispatchToProps = {
initTickers,
initBackupService,
fetchSuggestedNodes,
finishLnurlAuth,
finishLnurlChannel,
finishLnurlWithdraw,
}
Expand Down
15 changes: 15 additions & 0 deletions renderer/containers/Lnurl/LnurlAuthPrompt.js
@@ -0,0 +1,15 @@
import { connect } from 'react-redux'
import { finishLnurlAuth, lnurlSelectors, clearLnurlAuth, declineLnurlAuth } from 'reducers/lnurl'
import { LnurlAuthPrompt } from 'components/Lnurl'

const mapStateToProps = state => ({
params: lnurlSelectors.lnurlAuthParams(state),
})

const mapDispatchToProps = {
onOk: finishLnurlAuth,
onCancel: declineLnurlAuth,
onClose: clearLnurlAuth,
}

export default connect(mapStateToProps, mapDispatchToProps)(LnurlAuthPrompt)
11 changes: 3 additions & 8 deletions renderer/reducers/channels/selectors.js
Expand Up @@ -377,14 +377,9 @@ channelsSelectors.isCustomFilter = createSelector(filterSelector, filters => {
/**
* willShowLnurlChannelPrompt - Boolean indicating wether lnurl channel prompt should show.
*/
channelsSelectors.willShowLnurlChannelPrompt = createSelector(
lnurlChannelParams,
settingsSelectors.currentConfig,
(params, config) => {
const promptEnabled = config.lnurl.requirePrompt
return Boolean(promptEnabled && params)
}
)
channelsSelectors.willShowLnurlChannelPrompt = createSelector(lnurlChannelParams, params => {
return Boolean(params)
})

/**
* lnurlChannelParams - Current lnurl withdrawal paramaters.
Expand Down
3 changes: 3 additions & 0 deletions renderer/reducers/index.js
Expand Up @@ -29,6 +29,7 @@ import settings from './settings'
import settingsmenu from './settingsmenu'
import wallet from './wallet'
import backup from './backup'
import lnurl from './lnurl'

/**
* @typedef State
Expand All @@ -41,6 +42,7 @@ import backup from './backup'
* @property {import('./balance').State} balance Balance reducer.
* @property {import('./info').State} info Info reducer.
* @property {import('./invoice').State} invoice Invoice reducer.
* @property {import('./lnurl').State} lnurl Lnurl reducer.
* @property {import('./network').State} network Network reducer.
* @property {import('./transaction').State} transaction Transaction reducer.
*/
Expand All @@ -67,6 +69,7 @@ const reducers = {
info,
invoice,
lnd,
lnurl,
modal,
network,
neutrino,
Expand Down

0 comments on commit d9e281a

Please sign in to comment.