Browse Source

Merge pull request #562 from mrfelton/feat/btc-pay-server

feat(btcpayserver): add support for BTCPayServer
renovate/lint-staged-8.x
Ben Woosley 7 years ago
committed by GitHub
parent
commit
15d0f4f2d9
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 58
      app/components/Onboarding/BtcPayServer.js
  2. 58
      app/components/Onboarding/BtcPayServer.scss
  3. 21
      app/components/Onboarding/ConnectionConfirm.js
  4. 17
      app/components/Onboarding/ConnectionConfirm.scss
  5. 6
      app/components/Onboarding/ConnectionType.js
  6. 2
      app/components/Onboarding/ConnectionType.scss
  7. 64
      app/components/Onboarding/Onboarding.js
  8. 15
      app/containers/Root.js
  9. 22
      app/lnd/config/index.js
  10. 7
      app/lnd/lib/lightning.js
  11. 34
      app/lnd/lib/util.js
  12. 2
      app/package.json
  13. 103
      app/reducers/onboarding.js
  14. 6
      app/yarn.lock
  15. 12
      app/zap.js

58
app/components/Onboarding/BtcPayServer.js

@ -0,0 +1,58 @@
import React from 'react'
import PropTypes from 'prop-types'
import styles from './BtcPayServer.scss'
const BtcPayServer = ({
connectionString,
connectionStringIsValid,
setConnectionString,
startLndHostError
}) => (
<div className={styles.container}>
<section className={styles.input}>
<label htmlFor="connectionString">Connection String:</label>
<textarea
type="text"
id="connectionString"
rows="10"
placeholder="BTCPay Server Connection String"
className={
connectionString && (startLndHostError || !connectionStringIsValid)
? styles.error
: undefined
}
ref={input => input}
value={connectionString}
onChange={event => setConnectionString(event.target.value)}
/>
<p className={styles.description}>
Paste the full content of your BTCPay Server connection config file. This can be found by
clicking the link entitled &quot;Click here to open the configuration file.&quot; in your
BTCPay Server gRPC settings.
</p>
<p
className={`${styles.errorMessage} ${
connectionString && !connectionStringIsValid ? styles.visible : undefined
}`}
>
Invalid connection string.
</p>
<p
className={`${styles.errorMessage} ${
connectionString && startLndHostError ? styles.visible : undefined
}`}
>
{startLndHostError}
</p>
</section>
</div>
)
BtcPayServer.propTypes = {
connectionString: PropTypes.string.isRequired,
connectionStringIsValid: PropTypes.bool.isRequired,
setConnectionString: PropTypes.func.isRequired,
startLndHostError: PropTypes.string
}
export default BtcPayServer

58
app/components/Onboarding/BtcPayServer.scss

@ -0,0 +1,58 @@
@import '../../styles/variables.scss';
.container {
color: $white;
.input {
margin-bottom: 15px;
}
label {
display: block;
font-size: 12px;
line-height: 14px;
padding-bottom: 5px;
min-height: 14px;
}
textarea {
background: transparent;
outline: none;
border: 1px solid #404040;
border-radius: 4px;
padding: 10px;
color: $gold;
-webkit-text-fill-color: $white;
font-size: 14px;
width: 95%;
transition: all 0.25s;
&.error {
border: 1px solid $red;
}
}
textarea::-webkit-input-placeholder {
text-shadow: none;
-webkit-text-fill-color: initial;
}
.description {
margin-top: 8px;
font-size: 12px;
line-height: 18px;
opacity: 0.5;
}
.errorMessage {
margin-top: 8px;
font-size: 12px;
line-height: 18px;
color: $red;
display: none;
&.visible {
display: block;
}
}
}

21
app/components/Onboarding/ConnectionConfirm.js

@ -0,0 +1,21 @@
import React from 'react'
import PropTypes from 'prop-types'
import styles from './ConnectionConfirm.scss'
const ConnectionConfirm = ({ connectionHost }) => (
<div className={styles.container}>
<p>
Are you sure you want to connect to{' '}
<span className={styles.host}>{connectionHost.split(':')[0]}</span>?{' '}
</p>
<p>
<strong>Please check the hostname carefully.</strong>
</p>
</div>
)
ConnectionConfirm.propTypes = {
connectionHost: PropTypes.string.isRequired
}
export default ConnectionConfirm

17
app/components/Onboarding/ConnectionConfirm.scss

@ -0,0 +1,17 @@
@import '../../styles/variables.scss';
.container {
color: $white;
p {
margin-bottom: 20px;
}
strong {
font-weight: bold;
}
.host {
color: $green;
}
}

6
app/components/Onboarding/ConnectionType.js

@ -18,6 +18,12 @@ const ConnectionType = ({ connectionType, setConnectionType }) => (
<span className={styles.label}>Custom</span>
</div>
</section>
<section className={`${styles.option} ${connectionType === 'btcpayserver' && styles.active}`}>
<div className={`${styles.button}`} onClick={() => setConnectionType('btcpayserver')}>
{connectionType === 'btcpayserver' ? <FaCircle /> : <FaCircleThin />}
<span className={styles.label}>BTCPay Server</span>
</div>
</section>
</div>
)

2
app/components/Onboarding/ConnectionType.scss

@ -10,7 +10,7 @@
.button {
width: 150px;
text-align: center;
display: inline-block;
display: flex;
padding: 20px;
border: 1px solid $white;
border-radius: 5px;

64
app/components/Onboarding/Onboarding.js

@ -6,6 +6,8 @@ import LoadingBolt from 'components/LoadingBolt'
import FormContainer from './FormContainer'
import ConnectionType from './ConnectionType'
import ConnectionDetails from './ConnectionDetails'
import ConnectionConfirm from './ConnectionConfirm'
import BtcPayServer from './BtcPayServer'
import Alias from './Alias'
import Autopilot from './Autopilot'
import Login from './Login'
@ -19,7 +21,9 @@ import styles from './Onboarding.scss'
const Onboarding = ({
onboarding: {
step,
previousStep,
connectionType,
connectionString,
connectionHost,
connectionCert,
connectionMacaroon,
@ -32,6 +36,7 @@ const Onboarding = ({
},
connectionTypeProps,
connectionDetailProps,
connectionConfirmProps,
changeStep,
startLnd,
submitNewWallet,
@ -55,7 +60,18 @@ const Onboarding = ({
use Zap to control a remote node if you desire (for advanced users).
"
back={null}
next={() => changeStep(connectionType === 'local' ? 1 : 0.2)}
next={() => {
switch (connectionType) {
case 'custom':
changeStep(0.2)
break
case 'btcpayserver':
changeStep(0.3)
break
default:
changeStep(1)
}
}}
>
<ConnectionType {...connectionTypeProps} />
</FormContainer>
@ -67,16 +83,55 @@ const Onboarding = ({
title="Connection details"
description="Enter the connection details for your Lightning node."
back={() => changeStep(0.1)}
next={() =>
next={() => {
// dont allow the user to move on if we don't at least have a hostname.
if (!connectionDetailProps.connectionHostIsValid) {
return
}
changeStep(0.4)
}}
>
<ConnectionDetails {...connectionDetailProps} />
</FormContainer>
)
case 0.3:
return (
<FormContainer
title="BTCPay Server"
description="Enter the connection details for your BTCPay Server node."
back={() => changeStep(0.1)}
next={() => {
// dont allow the user to move on if the connection string is invalid.
if (!connectionDetailProps.connectionStringIsValid) {
return
}
changeStep(0.4)
}}
>
<BtcPayServer {...connectionDetailProps} />
</FormContainer>
)
case 0.4:
return (
<FormContainer
title="Confirm connection"
description="Confirm the connection details for your Lightning node."
back={() => changeStep(previousStep)}
next={() => {
startLnd({
type: connectionType,
string: connectionString,
host: connectionHost,
cert: connectionCert,
macaroon: connectionMacaroon
})
}
}}
>
<ConnectionDetails {...connectionDetailProps} />
<ConnectionConfirm {...connectionConfirmProps} />
</FormContainer>
)
@ -224,6 +279,7 @@ Onboarding.propTypes = {
onboarding: PropTypes.object.isRequired,
connectionTypeProps: PropTypes.object.isRequired,
connectionDetailProps: PropTypes.object.isRequired,
connectionConfirmProps: PropTypes.object.isRequired,
aliasProps: PropTypes.object.isRequired,
autopilotProps: PropTypes.object.isRequired,
initWalletProps: PropTypes.object.isRequired,

15
app/containers/Root.js

@ -9,6 +9,7 @@ import Onboarding from '../components/Onboarding'
import Syncing from '../components/Onboarding/Syncing'
import {
setConnectionType,
setConnectionString,
setConnectionHost,
setConnectionCert,
setConnectionMacaroon,
@ -35,6 +36,7 @@ import Routes from '../routes'
const mapDispatchToProps = {
setConnectionType,
setConnectionString,
setConnectionHost,
setConnectionCert,
setConnectionMacaroon,
@ -68,7 +70,9 @@ const mapStateToProps = state => ({
showCreateWalletPasswordConfirmationError: onboardingSelectors.showCreateWalletPasswordConfirmationError(
state
),
reEnterSeedChecker: onboardingSelectors.reEnterSeedChecker(state)
reEnterSeedChecker: onboardingSelectors.reEnterSeedChecker(state),
connectionStringIsValid: onboardingSelectors.connectionStringIsValid(state),
connectionHostIsValid: onboardingSelectors.connectionHostIsValid(state)
})
const mergeProps = (stateProps, dispatchProps, ownProps) => {
@ -87,9 +91,13 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
}
const connectionDetailProps = {
connectionHostIsValid: stateProps.connectionHostIsValid,
connectionStringIsValid: stateProps.connectionStringIsValid,
connectionString: stateProps.onboarding.connectionString,
connectionHost: stateProps.onboarding.connectionHost,
connectionCert: stateProps.onboarding.connectionCert,
connectionMacaroon: stateProps.onboarding.connectionMacaroon,
setConnectionString: dispatchProps.setConnectionString,
setConnectionHost: dispatchProps.setConnectionHost,
setConnectionCert: dispatchProps.setConnectionCert,
setConnectionMacaroon: dispatchProps.setConnectionMacaroon,
@ -98,6 +106,10 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
startLndMacaroonError: stateProps.onboarding.startLndMacaroonError
}
const connectionConfirmProps = {
connectionHost: stateProps.onboarding.connectionHost
}
const aliasProps = {
updateAlias: dispatchProps.updateAlias,
alias: stateProps.onboarding.alias
@ -165,6 +177,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
submitNewWallet: dispatchProps.submitNewWallet,
connectionTypeProps,
connectionDetailProps,
connectionConfirmProps,
aliasProps,
autopilotProps,
initWalletProps,

22
app/lnd/config/index.js

@ -58,12 +58,18 @@ if (process.env.NODE_ENV === 'development') {
}
export default {
lnd: () => ({
lndPath,
configPath: join(appRootPath, 'resources', 'lnd.conf'),
rpcProtoPath: join(appRootPath, 'resources', 'rpc.proto'),
host: store.get('host') || 'localhost:10009',
cert: store.get('cert') || join(userInfo().homedir, loc),
macaroon: store.get('macaroon') || join(userInfo().homedir, macaroonPath)
})
lnd: () => {
const cert = store.get('cert')
const host = store.get('host')
const macaroon = store.get('macaroon')
return {
lndPath,
configPath: join(appRootPath, 'resources', 'lnd.conf'),
rpcProtoPath: join(appRootPath, 'resources', 'rpc.proto'),
host: typeof host === 'undefined' ? 'localhost:10009' : host,
cert: typeof cert === 'undefined' ? join(userInfo().homedir, loc) : cert,
macaroon: typeof macaroon === 'undefined' ? join(userInfo().homedir, macaroonPath) : macaroon
}
}
}

7
app/lnd/lib/lightning.js

@ -16,7 +16,12 @@ process.env.GRPC_SSL_CIPHER_SUITES =
'ECDHE-ECDSA-AES128-GCM-SHA256',
'ECDHE-ECDSA-AES256-GCM-SHA384',
'ECDHE-ECDSA-AES128-CBC-SHA256',
'ECDHE-ECDSA-CHACHA20-POLY1305'
'ECDHE-ECDSA-CHACHA20-POLY1305',
// BTCPay Server serves lnd behind an nginx proxy with a trusted SSL cert from Lets Encrypt.
// These certs use an RSA TLS cipher suite.
'ECDHE-RSA-AES256-GCM-SHA384',
'ECDHE-RSA-AES128-GCM-SHA256'
].join(':')
/**

34
app/lnd/lib/util.js

@ -3,6 +3,7 @@ import fs from 'fs'
import axios from 'axios'
import { promisify } from 'util'
import { lookup } from 'ps-node'
import path from 'path'
import grpc from 'grpc'
import isIP from 'validator/lib/isIP'
import isPort from 'validator/lib/isPort'
@ -102,12 +103,14 @@ export const validateHost = async host => {
* @returns {grpc.ChanelCredentials}
*/
export const createSslCreds = async certPath => {
const lndCert = await fsReadFile(certPath).catch(e => {
const error = new Error(`SSL cert path could not be accessed: ${e.message}`)
error.code = 'LND_GRPC_CERT_ERROR'
throw error
})
let lndCert
if (certPath) {
lndCert = await fsReadFile(certPath).catch(e => {
const error = new Error(`SSL cert path could not be accessed: ${e.message}`)
error.code = 'LND_GRPC_CERT_ERROR'
throw error
})
}
return grpc.credentials.createSsl(lndCert)
}
@ -117,14 +120,19 @@ export const createSslCreds = async certPath => {
* @returns {grpc.CallCredentials}
*/
export const createMacaroonCreds = async macaroonPath => {
const macaroon = await fsReadFile(macaroonPath).catch(e => {
const error = new Error(`Macaroon path could not be accessed: ${e.message}`)
error.code = 'LND_GRPC_MACAROON_ERROR'
throw error
})
const metadata = new grpc.Metadata()
metadata.add('macaroon', macaroon.toString('hex'))
// If it's not a filepath, then assume it is a hex encoded string.
if (macaroonPath === path.basename(macaroonPath)) {
metadata.add('macaroon', macaroonPath)
} else {
const macaroon = await fsReadFile(macaroonPath).catch(e => {
const error = new Error(`Macaroon path could not be accessed: ${e.message}`)
error.code = 'LND_GRPC_MACAROON_ERROR'
throw error
})
metadata.add('macaroon', macaroon.toString('hex'))
}
return grpc.credentials.createFromMetadataGenerator((params, callback) =>
callback(null, metadata)

2
app/package.json

@ -14,7 +14,7 @@
},
"license": "MIT",
"dependencies": {
"grpc": "^1.12.4",
"grpc": "^1.13.0",
"ps-node": "^0.1.6",
"react-icons": "^2.2.5"
}

103
app/reducers/onboarding.js

@ -1,6 +1,7 @@
import { createSelector } from 'reselect'
import { ipcRenderer } from 'electron'
import Store from 'electron-store'
import get from 'lodash.get'
const store = new Store({ name: 'connection' })
@ -8,6 +9,7 @@ const store = new Store({ name: 'connection' })
// Constants
// ------------------------------------
export const SET_CONNECTION_TYPE = 'SET_CONNECTION_TYPE'
export const SET_CONNECTION_STRING = 'SET_CONNECTION_STRING'
export const SET_CONNECTION_HOST = 'SET_CONNECTION_HOST'
export const SET_CONNECTION_CERT = 'SET_CONNECTION_CERT'
export const SET_CONNECTION_MACAROON = 'SET_CONNECTION_MACAROON'
@ -45,14 +47,47 @@ export const SET_UNLOCK_WALLET_ERROR = 'SET_UNLOCK_WALLET_ERROR'
export const SET_SIGNUP_CREATE = 'SET_SIGNUP_CREATE'
export const SET_SIGNUP_IMPORT = 'SET_SIGNUP_IMPORT'
// ------------------------------------
// Helpers
// ------------------------------------
function prettyPrint(json) {
try {
return JSON.stringify(JSON.parse(json), undefined, 4)
} catch (e) {
return json
}
}
// ------------------------------------
// Actions
// ------------------------------------
export function setConnectionType(connectionType) {
return {
export const setConnectionType = connectionType => (dispatch, getState) => {
const previousType = connectionTypeSelector(getState())
// When changing the connection type clear out existing config.
if (previousType !== connectionType) {
dispatch(setConnectionString(''))
dispatch(setConnectionHost(''))
dispatch(setConnectionCert(''))
dispatch(setConnectionMacaroon(''))
dispatch(setStartLndError({}))
}
dispatch({
type: SET_CONNECTION_TYPE,
connectionType
}
})
}
export const setConnectionString = connectionString => (dispatch, getState) => {
dispatch({
type: SET_CONNECTION_STRING,
connectionString: prettyPrint(connectionString)
})
const { host, port, macaroon } = onboardingSelectors.connectionStringParamsSelector(getState())
dispatch(setConnectionHost([host, port].join(':')))
dispatch(setConnectionMacaroon(macaroon))
dispatch(setConnectionCert(''))
}
export function setConnectionHost(connectionHost) {
@ -142,6 +177,13 @@ export function changeStep(step) {
}
}
export function setStartLndError(errors) {
return {
type: SET_START_LND_ERROR,
errors
}
}
export function startLnd(options) {
// once the user submits the data needed to start LND we will alert the app that it should start LND
ipcRenderer.send('startLnd', options)
@ -191,8 +233,18 @@ export const startOnboarding = () => dispatch => {
}
// Listener for errors connecting to LND gRPC
export const startLndError = (event, errors) => dispatch => {
dispatch({ type: SET_START_LND_ERROR, errors })
export const startLndError = (event, errors) => (dispatch, getState) => {
dispatch(setStartLndError(errors))
const connectionType = connectionTypeSelector(getState())
switch (connectionType) {
case 'custom':
dispatch({ type: CHANGE_STEP, step: 0.2 })
break
case 'btcpayserver':
dispatch({ type: CHANGE_STEP, step: 0.3 })
break
}
}
// Listener from after the LND walletUnlocker has started
@ -242,6 +294,7 @@ export const unlockWalletError = () => dispatch => {
// ------------------------------------
const ACTION_HANDLERS = {
[SET_CONNECTION_TYPE]: (state, { connectionType }) => ({ ...state, connectionType }),
[SET_CONNECTION_STRING]: (state, { connectionString }) => ({ ...state, connectionString }),
[SET_CONNECTION_HOST]: (state, { connectionHost }) => ({ ...state, connectionHost }),
[SET_CONNECTION_CERT]: (state, { connectionCert }) => ({ ...state, connectionCert }),
[SET_CONNECTION_MACAROON]: (state, { connectionMacaroon }) => ({ ...state, connectionMacaroon }),
@ -272,7 +325,7 @@ const ACTION_HANDLERS = {
[SET_SEED]: (state, { seed }) => ({ ...state, seed, fetchingSeed: false }),
[SET_RE_ENTER_SEED_INDEXES]: (state, { seedIndexesArr }) => ({ ...state, seedIndexesArr }),
[CHANGE_STEP]: (state, { step }) => ({ ...state, step }),
[CHANGE_STEP]: (state, { step }) => ({ ...state, step, previousStep: state.step }),
[ONBOARDING_STARTED]: state => ({ ...state, onboarded: false }),
[ONBOARDING_FINISHED]: state => ({ ...state, onboarded: true }),
@ -305,6 +358,9 @@ const ACTION_HANDLERS = {
[SET_SIGNUP_IMPORT]: state => ({ ...state, signupForm: { create: false, import: true } })
}
// ------------------------------------
// Selector
// ------------------------------------
const onboardingSelectors = {}
const passwordSelector = state => state.onboarding.password
@ -316,6 +372,10 @@ const seedSelector = state => state.onboarding.seed
const seedIndexesArrSelector = state => state.onboarding.seedIndexesArr
const reEnterSeedInputSelector = state => state.onboarding.reEnterSeedInput
const connectionStringSelector = state => state.onboarding.connectionString
const connectionTypeSelector = state => state.onboarding.connectionType
const connectionHostSelector = state => state.onboarding.connectionHost
onboardingSelectors.passwordIsValid = createSelector(
passwordSelector,
password => password.length >= 8
@ -344,6 +404,36 @@ onboardingSelectors.reEnterSeedChecker = createSelector(
)
)
onboardingSelectors.connectionHostIsValid = createSelector(
connectionHostSelector,
connectionHost => {
return connectionHost.length > 0
}
)
onboardingSelectors.connectionStringParamsSelector = createSelector(
connectionStringSelector,
connectionString => {
let config = {}
try {
config = JSON.parse(connectionString)
} catch (e) {
return {}
}
const configurations = get(config, 'configurations', [])
return configurations.find(c => c.type === 'grpc' && c.cryptoCode === 'BTC') || {}
}
)
onboardingSelectors.connectionStringIsValid = createSelector(
onboardingSelectors.connectionStringParamsSelector,
connectionStringParams => {
const { host, port, macaroon } = connectionStringParams
return Boolean(host && port && macaroon)
}
)
export { onboardingSelectors }
// ------------------------------------
@ -353,6 +443,7 @@ const initialState = {
onboarded: false,
step: 0.1,
connectionType: store.get('type', 'local'),
connectionString: store.get('string', ''),
connectionHost: store.get('host', ''),
connectionCert: store.get('cert', ''),
connectionMacaroon: store.get('macaroon', ''),

6
app/yarn.lock

@ -171,9 +171,9 @@ glob@^7.0.5:
once "^1.3.0"
path-is-absolute "^1.0.0"
grpc@^1.12.4:
version "1.12.4"
resolved "https://registry.yarnpkg.com/grpc/-/grpc-1.12.4.tgz#85a3bc26dbf61fb8555d182aec42c1ab6b303ecd"
grpc@^1.13.0:
version "1.13.0"
resolved "https://registry.yarnpkg.com/grpc/-/grpc-1.13.0.tgz#cbf884fa5e072edecb15ff019483db74b361e2c6"
dependencies:
lodash "^4.17.5"
nan "^2.0.0"

12
app/zap.js

@ -201,21 +201,21 @@ class ZapController {
// Save the options.
const store = new Store({ name: 'connection' })
store.store = cleanOptions
mainLog.info('Saved lnd config to:', store.path)
mainLog.info('Saved lnd config to %s: %o', store.path, store.store)
// If the requested connection type is a local one then start up a new lnd instance.
if (cleanOptions.type === 'local') {
mainLog.info('Starting new lnd instance')
mainLog.debug(' > alias:', cleanOptions.alias)
mainLog.debug(' > autopilot:', cleanOptions.autopilot)
mainLog.info(' > alias:', cleanOptions.alias)
mainLog.info(' > autopilot:', cleanOptions.autopilot)
this.startLnd(cleanOptions.alias, cleanOptions.autopilot)
}
// Otherwise attempt to connect to an lnd instance using user supplied connection details.
else {
mainLog.info('Connecting to custom lnd instance')
mainLog.debug(' > host:', cleanOptions.host)
mainLog.debug(' > cert:', cleanOptions.cert)
mainLog.debug(' > macaroon:', cleanOptions.macaroon)
mainLog.info(' > host:', cleanOptions.host)
mainLog.info(' > cert:', cleanOptions.cert)
mainLog.info(' > macaroon:', cleanOptions.macaroon)
this.startGrpc()
.then(() => this.sendMessage('successfullyCreatedWallet'))
.catch(e => {

Loading…
Cancel
Save