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 support for lnurl-channel
Browse files Browse the repository at this point in the history
  • Loading branch information
mrfelton committed May 25, 2020
1 parent e2903f4 commit bad6762
Show file tree
Hide file tree
Showing 26 changed files with 612 additions and 72 deletions.
184 changes: 150 additions & 34 deletions electron/lnurl/service.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { ipcMain } from 'electron'
import { parse } from 'url'
import { fetchWithdrawParams, makeWithdrawRequest, LNURL_STATUS_ERROR } from '@zap/utils/lnurl'
import {
fetchLnurlParams,
makeChannelRequest,
makeWithdrawRequest,
LNURL_STATUS_ERROR,
} from '@zap/utils/lnurl'
import { mainLog } from '@zap/utils/log'

/**
Expand All @@ -23,69 +28,180 @@ const getServiceName = url => {
export default class LnurlService {
constructor(mainWindow) {
this.mainWindow = mainWindow
this.isWithdrawalProcessing = false

this.withdrawParams = {}
ipcMain.on('lnurlCreateInvoice', this.onCreateInvoice)
this.isWithdrawalProcessing = false

this.channelParams = {}
this.isChannelProcessing = false

ipcMain.on('lnurlFinalizeWithdraw', this.onFinishWithdrawal)
ipcMain.on('lnurlFinalizeChannel', this.onFinishChannel)
}

/**
* terminate - De-initializer routine. Must be called when
* particular `LnurlService` instance is of no use anymore.
*
* @memberof LnurlService
* terminate - De-initializer routine. Must be called when particular
* `LnurlService` instance is of no use anymore.
*/
terminate() {
ipcMain.off('lnurlCreateInvoice', this.onCreateInvoice)
ipcMain.off('lnurlFinalizeWithdraw', this.onFinishWithdrawal)
ipcMain.off('lnurlFinalizeChannel', this.onFinishChannel)
}

/**
* send - Send a message to the renderer.
*
* @param {string} msg Message
* @param {object} data Data
*/
send(msg, data) {
this.mainWindow.webContents.send(msg, data)
}

onCreateInvoice = async (event, { paymentRequest }) => {
/**
* process - Process an lnurl.
*
* @param {string} lnurl decoded lnurl
*/
async process(lnurl) {
mainLog.info('Attempting to process lnurl: %s', lnurl)
try {
const { callback, secret } = this.withdrawParams
if (callback && secret && paymentRequest) {
const res = await makeWithdrawRequest({ callback, secret, invoice: paymentRequest })
mainLog.info('Completed withdraw request: %o', res.data)
const res = await fetchLnurlParams(lnurl)

if (res.status === LNURL_STATUS_ERROR) {
throw new Error(res.reason)
}
} catch (e) {
mainLog.warn('Unable to process lnurl uri: %s', e)
} finally {
this.isWithdrawalProcessing = false
this.withdrawParams = {}
}
}

send(msg, params) {
this.mainWindow.webContents.send(msg, params)
switch (res.tag) {
case 'withdrawRequest':
this.withdrawParams = res
await this.startWithdrawal(lnurl)
break

case 'channelRequest':
this.channelParams = res
await this.startChannel()
break

default:
throw new Error('Unable to process lnurl')
}
} catch (error) {
this.send('lnurlError', { message: error.message })
throw error
}
}

/**
* startWithdrawal - Initiates lnurl withdrawal process by fetching params and sending query to renderer
* process to generate LN invoice.
* startWithdrawal - Initiates lnurl withdrawal process by sending query to
* renderer process to generate LN invoice.
*
* @param {string} lnurl decoded lnurl
* @memberof LnurlService
*/
async startWithdrawal(lnurl) {
mainLog.info('Attempting to process lnurl withdraw request: %s', lnurl)

if (this.isWithdrawalProcessing) {
mainLog.warn('Error processing lnurl withdraw request: busy')
this.send('lnurlWithdrawalBusy')
return
}
this.isWithdrawalProcessing = true

this.withdrawParams = await fetchWithdrawParams(lnurl)
const { status, reason, maxWithdrawable, defaultDescription } = this.withdrawParams
const service = getServiceName(lnurl)

if (status === LNURL_STATUS_ERROR) {
const params = { status, reason, service }
mainLog.error('Unable to process lnurl withdraw request: %o', params)
this.send('lnurlError', params)
const withdrawParams = { status, reason, service }
this.isWithdrawalProcessing = false
mainLog.error('Unable to process lnurl withdraw request: %o', withdrawParams)
this.send('lnurlWithdrawError', withdrawParams)
return
}

const withdrawParams = { amount: maxWithdrawable, memo: defaultDescription, service }
mainLog.info('Processing lnurl withdraw request: %o', withdrawParams)
this.send('lnurlWithdrawRequest', withdrawParams)
}

/**
* onFinishChannel - Finalizes an lnurl-withdraw request.
*
* @param {object} event Event
* @param {object} data Data
*/
onFinishWithdrawal = async (event, { paymentRequest }) => {
try {
const { callback, secret } = this.withdrawParams
if (callback && secret && paymentRequest) {
const { data } = await makeWithdrawRequest({ callback, secret, invoice: paymentRequest })

if (data.status === LNURL_STATUS_ERROR) {
mainLog.warn('Unable to complete lnurl withdraw request: %o', data)
this.send('lnurlWithdrawError', data)
return
}

mainLog.info('Completed withdraw request: %o', data)
}
} catch (e) {
mainLog.warn('Unable to complete lnurl withdrawal request: %s', e)
} finally {
this.isWithdrawalProcessing = false
this.withdrawParams = {}
}
}

/**
* startChannel - Initiates lnurl channel process by sending query to renderer
* process to initiate channel connect.
*/
async startChannel() {
if (this.isChannelProcessing) {
mainLog.warn('Error processing lnurl channel request: busy')
this.send('lnurlChannelBusy')
return
}
this.isChannelProcessing = true

const { status, uri } = this.channelParams

if (status === LNURL_STATUS_ERROR) {
const channelParams = { status, uri }
this.isChannelProcessing = false
mainLog.error('Unable to process lnurl channel request: %o', channelParams)
this.send('lnurlChannelError', channelParams)
return
}

const params = { amount: maxWithdrawable, memo: defaultDescription, service }
mainLog.info('Processing lnurl withdraw request: %o', params)
this.send('lnurlRequest', params)
const channelParams = { uri }
mainLog.info('Processing lnurl channel request: %o', channelParams)
this.send('lnurlChannelRequest', channelParams)
}

/**
* onFinishChannel - Finalizes an lnurl-channel request.
*
* @param {object} event Event
* @param {object} data Data
*/
onFinishChannel = async (event, { pubkey }) => {
try {
const { callback, secret, uri } = this.channelParams
if (callback && secret && pubkey) {
const { data } = await makeChannelRequest({ callback, secret, pubkey, isPrivate: true })

if (data.status === LNURL_STATUS_ERROR) {
mainLog.warn('Unable to complete lnurl channel request: %o', data)
this.send('lnurlChannelError', { ...data, uri })
return
}

mainLog.info('Completed channel request: %o', data)
}
} catch (e) {
mainLog.warn('Unable to complete lnurl channel request: %s', e)
} finally {
this.isChannelProcessing = false
this.channelParams = {}
}
}
}
24 changes: 14 additions & 10 deletions electron/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,10 @@ const handleBitcoinLink = input => {
try {
const decoded = bip21.decode(input)
zap.sendMessage('bitcoinPaymentUri', decoded)
mainWindow.show()
} catch (e) {
mainLog.warn('Unable to process bitcoin uri: %s', e)
} finally {
mainWindow.show()
}
}

Expand All @@ -77,13 +78,14 @@ const handleBitcoinLink = input => {
*
* @param {string} input lnurl string
*/
const handleLnurlLink = input => {
const handleLnurlLink = async input => {
mainLog.info('Processing lightning uri as lnurl: %s', input)
try {
lnurlService.startWithdrawal(input)
mainWindow.show()
await lnurlService.process(input)
} catch (e) {
mainLog.warn('Unable to process lnurl uri: %s', e)
mainLog.warn('Unable to process lnurl uri: %s', e.message)
} finally {
mainWindow.show()
}
}

Expand All @@ -96,9 +98,10 @@ const handleLninvoiceLink = address => {
mainLog.info('Processing lightning uri as lninvoice: %s', address)
try {
zap.sendMessage('lightningPaymentUri', { address })
mainWindow.show()
} catch (e) {
mainLog.warn('Unable to process lightning uri: %s', e)
mainLog.warn('Unable to process lightning uri: %s', e.message)
} finally {
mainWindow.show()
}
}

Expand All @@ -118,7 +121,7 @@ const handleLightningLink = fullUrl => {
handleLninvoiceLink(url)
}
} catch (e) {
mainLog.warn('Unable to process lightning uri: %s', e)
mainLog.warn('Unable to process lightning uri: %s', e.message)
}
}

Expand All @@ -130,9 +133,10 @@ const handleLightningLink = fullUrl => {
const handleLndconnectLink = input => {
try {
zap.sendMessage('lndconnectUri', input)
mainWindow.show()
} catch (e) {
mainLog.warn('Unable to process lndconnect uri: %s', e)
mainLog.warn('Unable to process lndconnect uri: %s', e.message)
} finally {
mainWindow.show()
}
}

Expand Down
14 changes: 14 additions & 0 deletions renderer/components/App/App.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useEffect } from 'react'
import PropTypes from 'prop-types'
import { Flex } from 'rebass/styled-components'
import LnurlChannelPrompt from 'containers/Channels/LnurlChannelPrompt'
import createScheduler from '@zap/utils/scheduler'
import Wallet from 'containers/Wallet'
import Activity from 'containers/Activity'
Expand Down Expand Up @@ -39,8 +40,11 @@ const App = ({
initBackupService,
fetchSuggestedNodes,
initTickers,
lnurlChannelParams,
lnurlWithdrawParams,
finishLnurlChannel,
finishLnurlWithdrawal,
willShowLnurlChannelPrompt,
willShowLnurlWithdrawalPrompt,
}) => {
/**
Expand Down Expand Up @@ -95,6 +99,9 @@ const App = ({
if (!willShowLnurlWithdrawalPrompt) {
finishLnurlWithdrawal()
}
if (!willShowLnurlChannelPrompt) {
finishLnurlChannel()
}
}, [
initActivityHistory,
fetchDescribeNetwork,
Expand All @@ -104,8 +111,11 @@ const App = ({
initTickers,
setIsWalletOpen,
updateAutopilotNodeScores,
finishLnurlChannel,
finishLnurlWithdrawal,
lnurlChannelParams,
lnurlWithdrawParams,
willShowLnurlChannelPrompt,
willShowLnurlWithdrawalPrompt,
])

Expand All @@ -127,6 +137,7 @@ const App = ({
<Wallet />
<Activity />
{willShowLnurlWithdrawalPrompt && <LnurlWithdrawalPrompt />}
{willShowLnurlChannelPrompt && <LnurlChannelPrompt />}
</Flex>
)
}
Expand All @@ -136,17 +147,20 @@ App.propTypes = {
fetchPeers: PropTypes.func.isRequired,
fetchSuggestedNodes: PropTypes.func.isRequired,
fetchTransactions: PropTypes.func.isRequired,
finishLnurlChannel: PropTypes.func.isRequired,
finishLnurlWithdrawal: PropTypes.func.isRequired,
initActivityHistory: PropTypes.func.isRequired,
initBackupService: PropTypes.func.isRequired,
initTickers: PropTypes.func.isRequired,
isAppReady: PropTypes.bool.isRequired,
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,
willShowLnurlChannelPrompt: PropTypes.bool,
willShowLnurlWithdrawalPrompt: PropTypes.bool,
}

Expand Down
Loading

0 comments on commit bad6762

Please sign in to comment.