Browse Source

Merge pull request #53 from LN-Zap/fix/refactor-pay-request-form

Fix/refactor pay+request form
renovate/lint-staged-8.x
jackmallers 7 years ago
committed by GitHub
parent
commit
d061feaec5
  1. 42
      app/components/Form/Form.js
  2. 59
      app/components/Form/Form.scss
  3. 123
      app/components/Form/PayForm.js
  4. 8
      app/components/Form/PayForm.scss
  5. 59
      app/components/Form/RequestForm.js
  6. 2
      app/components/Form/RequestForm.scss
  7. 0
      app/components/Form/index.js
  8. 28
      app/lnd/config/rpc.proto
  9. 2
      app/lnd/methods/index.js
  10. 13
      app/lnd/methods/invoicesController.js
  11. 4
      app/lnd/utils/index.js
  12. 100
      app/reducers/form.js
  13. 8
      app/reducers/index.js
  14. 25
      app/reducers/invoice.js
  15. 161
      app/reducers/payform.js
  16. 9
      app/reducers/payment.js
  17. 55
      app/reducers/requestform.js
  18. 7
      app/reducers/transaction.js
  19. 84
      app/routes/app/components/App.js
  20. 93
      app/routes/app/components/components/Form/Form.js
  21. 159
      app/routes/app/components/components/Form/Form.scss
  22. 149
      app/routes/app/components/components/Form/components/Pay/Pay.js
  23. 3
      app/routes/app/components/components/Form/components/Pay/index.js
  24. 68
      app/routes/app/components/components/Form/components/Request/Request.js
  25. 3
      app/routes/app/components/components/Form/components/Request/index.js
  26. 11
      app/routes/app/components/components/Nav.js
  27. 142
      app/routes/app/containers/AppContainer.js
  28. 150
      app/utils/bech32.js
  29. 4
      app/utils/index.js
  30. 2
      test/components/Channels.spec.js
  31. 60
      test/components/Form.spec.js
  32. 78
      test/reducers/__snapshots__/form.spec.js.snap
  33. 4
      test/reducers/__snapshots__/invoice.spec.js.snap
  34. 62
      test/reducers/form.spec.js

42
app/components/Form/Form.js

@ -0,0 +1,42 @@
import React from 'react'
import PropTypes from 'prop-types'
import { MdClose } from 'react-icons/lib/md'
import PayForm from './PayForm'
import RequestForm from './RequestForm'
import styles from './Form.scss'
const FORM_TYPES = {
PAY_FORM: PayForm,
REQUEST_FORM: RequestForm
}
const Form = ({ formType, formProps, closeForm }) => {
if (!formType) { return null }
const FormComponent = FORM_TYPES[formType]
return (
<div className={`${styles.outtercontainer} ${formType && styles.open}`}>
<div className={styles.innercontainer}>
<div className={styles.esc} onClick={closeForm}>
<MdClose />
</div>
<div className={styles.content}>
<FormComponent {...formProps} />
</div>
</div>
</div>
)
}
Form.propTypes = {
formType: PropTypes.string,
formProps: PropTypes.object.isRequired,
closeForm: PropTypes.func.isRequired
}
export default Form

59
app/components/Form/Form.scss

@ -0,0 +1,59 @@
@import '../../variables.scss';
.outtercontainer {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
height: 100vh;
background: $white;
z-index: 0;
opacity: 0;
transition: all 0.5s;
&.open {
opacity: 1;
z-index: 10;
}
}
.innercontainer {
position: relative;
height: 100vh;
margin: 5%;
}
.esc {
position: absolute;
top: 0;
right: 0;
color: $darkestgrey;
cursor: pointer;
padding: 20px;
border-radius: 50%;
&:hover {
color: $bluegrey;
background: $darkgrey;
}
&:active {
color: $white;
background: $main;
}
svg {
width: 32px;
height: 32px;
}
}
.content {
width: 50%;
margin: 0 auto;
display: flex;
flex-direction: column;
height: 75vh;
justify-content: center;
align-items: center;
}

123
app/components/Form/PayForm.js

@ -0,0 +1,123 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { FaBolt, FaChain } from 'react-icons/lib/fa'
import LoadingBolt from 'components/LoadingBolt'
import CurrencyIcon from 'components/CurrencyIcon'
import styles from './PayForm.scss'
class PayForm extends Component {
componentDidUpdate(prevProps) {
const { isOnchain, isLn, payform: { payInput }, fetchInvoice } = this.props
// If on-chain, focus on amount to let user know it's editable
if (isOnchain) { this.amountInput.focus() }
// If LN go retrieve invoice details
if ((prevProps.payform.payInput !== payInput) && isLn) {
fetchInvoice(payInput)
}
}
render() {
const {
payform: { amount, payInput },
currency,
crypto,
isOnchain,
isLn,
currentAmount,
inputCaption,
showPayLoadingScreen,
setPayAmount,
setPayInput,
onPaySubmit
} = this.props
return (
<div className={styles.container}>
{showPayLoadingScreen && <LoadingBolt />}
<section className={`${styles.amountContainer} ${isLn ? styles.ln : ''}`}>
<label htmlFor='amount'>
<CurrencyIcon currency={currency} crypto={crypto} />
</label>
<input
type='number'
min='0'
ref={input => this.amountInput = input} // eslint-disable-line
size=''
style={
isLn ?
{ width: '75%', fontSize: '85px' }
:
{ width: `${amount.length > 1 ? (amount.length * 15) - 5 : 25}%`, fontSize: `${190 - (amount.length ** 2)}px` }
}
value={currentAmount}
onChange={event => setPayAmount(event.target.value)}
id='amount'
readOnly={isLn}
/>
</section>
<div className={styles.inputContainer}>
<div className={styles.info}>
<span>{inputCaption}</span>
</div>
<aside className={styles.paymentIcon}>
{isOnchain &&
<i>
<span>on-chain</span>
<FaChain />
</i>
}
{isLn &&
<i>
<span>lightning network</span>
<FaBolt />
</i>
}
</aside>
<section className={styles.input}>
<input
type='text'
placeholder='Payment request or bitcoin address'
value={payInput}
onChange={event => setPayInput(event.target.value)}
id='paymentRequest'
/>
</section>
</div>
<section className={styles.buttonGroup}>
<div className={styles.button} onClick={onPaySubmit}>Pay</div>
</section>
</div>
)
}
}
PayForm.propTypes = {
payform: PropTypes.object.isRequired,
currency: PropTypes.string.isRequired,
crypto: PropTypes.string.isRequired,
isOnchain: PropTypes.bool.isRequired,
isLn: PropTypes.bool.isRequired,
currentAmount: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
]).isRequired,
inputCaption: PropTypes.string.isRequired,
showPayLoadingScreen: PropTypes.bool.isRequired,
setPayAmount: PropTypes.func.isRequired,
setPayInput: PropTypes.func.isRequired,
fetchInvoice: PropTypes.func.isRequired,
onPaySubmit: PropTypes.func.isRequired
}
export default PayForm

8
app/routes/app/components/components/Form/components/Pay/Pay.scss → app/components/Form/PayForm.scss

@ -1,4 +1,4 @@
@import '../../../../../../../variables.scss';
@import '../../variables.scss';
.container {
margin: 0 auto;
@ -21,7 +21,7 @@
opacity: 0.75;
}
label, input[type=text] {
label, input[type=number], input[type=text] {
color: inherit;
display: inline-block;
vertical-align: top;
@ -43,7 +43,7 @@
}
}
input[type=text] {
input[type=number],, input[type=text] {
width: 100px;
font-size: 180px;
border: none;
@ -92,7 +92,7 @@
position: relative;
padding: 0 20px;
label, input[type=text] {
label, input[type=number], input[type=text] {
font-size: inherit;
}

59
app/components/Form/RequestForm.js

@ -0,0 +1,59 @@
import React from 'react'
import PropTypes from 'prop-types'
import CurrencyIcon from 'components/CurrencyIcon'
import styles from './RequestForm.scss'
const RequestForm = ({
requestform: { amount, memo },
currency,
crypto,
setRequestAmount,
setRequestMemo,
onRequestSubmit
}) => (
<div className={styles.container}>
<section className={styles.amountContainer}>
<label htmlFor='amount'>
<CurrencyIcon currency={currency} crypto={crypto} />
</label>
<input
type='text'
size=''
style={{ width: `${amount.length > 1 ? (amount.length * 15) - 5 : 25}%`, fontSize: `${190 - (amount.length ** 2)}px` }}
value={amount}
onChange={event => setRequestAmount(event.target.value)}
id='amount'
/>
</section>
<section className={styles.inputContainer}>
<label htmlFor='memo'>Request:</label>
<input
type='text'
placeholder='Dinner, Rent, etc'
value={memo}
onChange={event => setRequestMemo(event.target.value)}
id='memo'
/>
</section>
<section className={styles.buttonGroup}>
<div className={styles.button} onClick={onRequestSubmit}>
Request
</div>
</section>
</div>
)
RequestForm.propTypes = {
requestform: PropTypes.object.isRequired,
currency: PropTypes.string.isRequired,
crypto: PropTypes.string.isRequired,
setRequestAmount: PropTypes.func.isRequired,
setRequestMemo: PropTypes.func.isRequired,
onRequestSubmit: PropTypes.func.isRequired
}
export default RequestForm

2
app/routes/app/components/components/Form/components/Request/Request.scss → app/components/Form/RequestForm.scss

@ -1,4 +1,4 @@
@import '../../../../../../../variables.scss';
@import '../../variables.scss';
.container {
margin: 0 auto;

0
app/routes/app/components/components/Form/index.js → app/components/Form/index.js

28
app/lnd/config/rpc.proto

@ -1,6 +1,6 @@
syntax = "proto3";
//import "google/api/annotations.proto";
// import "google/api/annotations.proto";
package lnrpc;
/**
@ -585,7 +585,6 @@ message HTLC {
int64 amount = 2 [json_name = "amount"];
bytes hash_lock = 3 [json_name = "hash_lock"];
uint32 expiration_height = 4 [json_name = "expiration_height"];
uint32 revocation_delay = 5 [json_name = "revocation_delay"];
}
message ActiveChannel {
@ -1129,7 +1128,12 @@ message SetAliasResponse {
}
message Invoice {
/// An optional memo to attach along with the invoice
/**
An optional memo to attach along with the invoice. Used for record keeping
purposes for the invoice's creator, and will also be set in the description
field of the encoded payment request if the description_hash field is not
being used.
*/
string memo = 1 [json_name = "memo"];
/// An optional cryptographic receipt of payment
@ -1162,6 +1166,19 @@ message Invoice {
payment to the recipient.
*/
string payment_request = 9 [json_name = "payment_request"];
/**
Hash (SHA-256) of a description of the payment. Used if the description of
payment (memo) is too long to naturally fit within the description field
of an encoded payment request.
*/
bytes description_hash = 10 [json_name = "description_hash"];
/// Payment request expiry time in seconds. Default is 3600 (1 hour).
int64 expiry = 11 [json_name = "expiry"];
/// Fallback on-chain address.
string fallback_addr = 12 [json_name = "fallback_addr"];
}
message AddInvoiceResponse {
bytes r_hash = 1 [json_name = "r_hash"];
@ -1241,6 +1258,11 @@ message PayReq {
string destination = 1 [json_name = "destination"];
string payment_hash = 2 [json_name = "payment_hash"];
int64 num_satoshis = 3 [json_name = "num_satoshis"];
int64 timestamp = 4 [json_name = "timestamp"];
int64 expiry = 5 [json_name = "expiry"];
string description = 6 [json_name = "description"];
string description_hash = 7 [json_name = "description_hash"];
string fallback_addr = 8 [json_name = "fallback_addr"];
}
message FeeReportRequest {}

2
app/lnd/methods/index.js

@ -75,7 +75,7 @@ export default function (lnd, event, msg, data) {
break
case 'invoice':
// Data looks like { invoices: [] }
invoicesController.getInvoice(data.payreq)
invoicesController.getInvoice(lnd, { pay_req: data.payreq })
.then(invoiceData => event.sender.send('receiveInvoice', invoiceData))
.catch(error => console.log('invoice error: ', error))
break

13
app/lnd/methods/invoicesController.js

@ -1,4 +1,3 @@
import { decodeInvoice } from '../utils'
import pushinvoices from '../push/subscribeinvoice'
/**
@ -37,13 +36,13 @@ export function listInvoices(lnd) {
* @param {[type]} payreq [description]
* @return {[type]} [description]
*/
export function getInvoice(payreq) {
export function getInvoice(lnd, { pay_req }) {
return new Promise((resolve, reject) => {
try {
resolve(decodeInvoice(payreq))
} catch (error) {
reject(error)
}
lnd.decodePayReq({ pay_req }, (err, data) => {
if (err) { reject(err) }
resolve(data)
})
})
}

4
app/lnd/utils/index.js

@ -19,8 +19,8 @@ export function decodeInvoice(payreq) {
+ bufferHexRotated.substr(0, bufferHexRotated.length - 1)
const buffer = Buffer.from(bufferHex, 'hex')
var pubkeyBuffer = buffer.slice(0, 33);
var pubkey = pubkeyBuffer.toString('hex');
const pubkeyBuffer = buffer.slice(0, 33);
const pubkey = pubkeyBuffer.toString('hex');
const paymentHashBuffer = buffer.slice(33, 65)
const paymentHashHex = paymentHashBuffer.toString('hex')

100
app/reducers/form.js

@ -1,119 +1,29 @@
import { createSelector } from 'reselect'
import bitcoin from 'bitcoinjs-lib'
// Initial State
const initialState = {
modalOpen: false,
formType: 'pay',
amount: '0',
onchainAmount: '0',
message: '',
pubkey: '',
payment_request: ''
formType: null
}
// Constants
// ------------------------------------
export const SET_FORM = 'SET_FORM'
export const SET_AMOUNT = 'SET_AMOUNT'
export const SET_ONCHAIN_AMOUNT = 'SET_ONCHAIN_AMOUNT'
export const SET_MESSAGE = 'SET_MESSAGE'
export const SET_PUBKEY = 'SET_PUBKEY'
export const SET_PAYMENT_REQUEST = 'SET_PAYMENT_REQUEST'
export const RESET_FORM = 'RESET_FORM'
export const SET_FORM_TYPE = 'SET_FORM_TYPE'
// ------------------------------------
// Actions
// ------------------------------------
export function setForm({ modalOpen, formType }) {
export function setFormType(formType) {
return {
type: SET_FORM,
modalOpen,
type: SET_FORM_TYPE,
formType
}
}
export function setAmount(amount) {
return {
type: SET_AMOUNT,
amount
}
}
export function setOnchainAmount(onchainAmount) {
return {
type: SET_ONCHAIN_AMOUNT,
onchainAmount
}
}
export function setMessage(message) {
return {
type: SET_MESSAGE,
message
}
}
export function setPubkey(pubkey) {
return {
type: SET_PUBKEY,
pubkey
}
}
export function setPaymentRequest(payment_request) {
return {
type: SET_PAYMENT_REQUEST,
payment_request
}
}
export function resetForm() {
return {
type: RESET_FORM
}
}
// ------------------------------------
// Action Handlers
// ------------------------------------
const ACTION_HANDLERS = {
[SET_FORM]: (state, { modalOpen, formType }) => ({ ...state, modalOpen, formType }),
[SET_AMOUNT]: (state, { amount }) => ({ ...state, amount }),
[SET_ONCHAIN_AMOUNT]: (state, { onchainAmount }) => ({ ...state, onchainAmount }),
[SET_MESSAGE]: (state, { message }) => ({ ...state, message }),
[SET_PUBKEY]: (state, { pubkey }) => ({ ...state, pubkey }),
[SET_PAYMENT_REQUEST]: (state, { payment_request }) => ({ ...state, payment_request }),
[RESET_FORM]: () => (initialState)
[SET_FORM_TYPE]: (state, { formType }) => ({ ...state, formType })
}
// ------------------------------------
// Selector
// ------------------------------------
const formSelectors = {}
const paymentRequestSelector = state => state.form.payment_request
formSelectors.isOnchain = createSelector(
paymentRequestSelector,
(paymentRequest) => {
// TODO: work with bitcoin-js to fix p2wkh error and make testnet/mainnet dynamic
try {
bitcoin.address.toOutputScript(paymentRequest, bitcoin.networks.testnet)
return true
} catch (e) {
return false
}
}
)
// TODO: Add more robust logic to detect a LN payment request
formSelectors.isLn = createSelector(
paymentRequestSelector,
paymentRequest => paymentRequest.length === 124
)
export { formSelectors }
// ------------------------------------
// Reducer
// ------------------------------------

8
app/reducers/index.js

@ -7,7 +7,11 @@ import balance from './balance'
import payment from './payment'
import peers from './peers'
import channels from './channels'
import form from './form'
import payform from './payform'
import requestform from './requestform'
import invoice from './invoice'
import modal from './modal'
import address from './address'
@ -22,7 +26,11 @@ const rootReducer = combineReducers({
payment,
peers,
channels,
form,
payform,
requestform,
invoice,
modal,
address,

25
app/reducers/invoice.js

@ -1,5 +1,10 @@
import { createSelector } from 'reselect'
import { ipcRenderer } from 'electron'
import { setFormType } from './form'
import { setPayInvoice } from './payform'
import { resetRequestForm } from './requestform'
import { showNotification } from '../notifications'
import { btc, usd } from '../utils'
// ------------------------------------
@ -78,7 +83,10 @@ export const fetchInvoice = payreq => (dispatch) => {
}
// Receive IPC event for form invoice
export const receiveFormInvoice = (event, formInvoice) => dispatch => dispatch({ type: RECEIVE_FORM_INVOICE, formInvoice })
export const receiveFormInvoice = (event, invoice) => (dispatch) => {
dispatch(setPayInvoice(invoice))
dispatch({ type: RECEIVE_FORM_INVOICE })
}
// Send IPC event for invoices
export const fetchInvoices = () => (dispatch) => {
@ -97,7 +105,16 @@ export const createInvoice = (amount, memo, currency, rate) => (dispatch) => {
}
// Receive IPC event for newly created invoice
export const createdInvoice = (event, invoice) => dispatch => dispatch({ type: INVOICE_SUCCESSFUL, invoice })
export const createdInvoice = (event, invoice) => (dispatch) => {
// Close the form modal once the payment was succesful
dispatch(setFormType(null))
// Add new invoice to invoices list
dispatch({ type: INVOICE_SUCCESSFUL, invoice })
// Reset the payment form
dispatch(resetRequestForm())
}
// Listen for invoice updates pushed from backend from subscribeToInvoices
export const invoiceUpdate = (event, { invoice }) => (dispatch) => {
@ -119,9 +136,7 @@ const ACTION_HANDLERS = {
[GET_INVOICE]: state => ({ ...state, invoiceLoading: true }),
[RECEIVE_INVOICE]: (state, { invoice }) => ({ ...state, invoiceLoading: false, invoice }),
[RECEIVE_FORM_INVOICE]: (state, { formInvoice }) => (
{ ...state, invoiceLoading: false, formInvoice }
),
[RECEIVE_FORM_INVOICE]: state => ({ ...state, invoiceLoading: false }),
[GET_INVOICES]: state => ({ ...state, invoiceLoading: true }),
[RECEIVE_INVOICES]: (state, { invoices }) => ({ ...state, invoiceLoading: false, invoices }),

161
app/reducers/payform.js

@ -0,0 +1,161 @@
import { createSelector } from 'reselect'
import bitcoin from 'bitcoinjs-lib'
import { tickerSelectors } from './ticker'
import { btc, bech32 } from '../utils'
// Initial State
const initialState = {
amount: '0',
payInput: '',
invoice: {
payreq: '',
r_hash: '',
amount: '0'
}
}
// Constants
// ------------------------------------
export const SET_PAY_AMOUNT = 'SET_PAY_AMOUNT'
export const SET_PAY_INPUT = 'SET_PAY_INPUT'
export const SET_PAY_INVOICE = 'SET_PAY_INVOICE'
export const RESET_FORM = 'RESET_FORM'
// ------------------------------------
// Actions
// ------------------------------------
export function setPayAmount(amount) {
return {
type: SET_PAY_AMOUNT,
amount
}
}
export function setPayInput(payInput) {
return {
type: SET_PAY_INPUT,
payInput
}
}
export function setPayInvoice(invoice) {
return {
type: SET_PAY_INVOICE,
invoice
}
}
export function resetPayForm() {
return {
type: RESET_FORM
}
}
// ------------------------------------
// Action Handlers
// ------------------------------------
const ACTION_HANDLERS = {
[SET_PAY_AMOUNT]: (state, { amount }) => ({ ...state, amount }),
[SET_PAY_INPUT]: (state, { payInput }) => ({ ...state, payInput }),
[SET_PAY_INVOICE]: (state, { invoice }) => ({ ...state, invoice }),
[RESET_FORM]: () => (initialState)
}
// ------------------------------------
// Selector
// ------------------------------------
const payFormSelectors = {}
const payAmountSelector = state => state.payform.amount
const payInputSelector = state => state.payform.payInput
const payInvoiceSelector = state => state.payform.invoice
// transaction
const sendingTransactionSelector = state => state.transaction.sendingTransaction
// transaction
const sendingPaymentSelector = state => state.payment.sendingPayment
// ticker
const currencySelector = state => state.ticker.currency
payFormSelectors.isOnchain = createSelector(
payInputSelector,
(input) => {
// TODO: work with bitcoin-js to fix p2wkh error and make testnet/mainnet dynamic
try {
bitcoin.address.toOutputScript(input, bitcoin.networks.testnet)
return true
} catch (e) {
return false
}
}
)
payFormSelectors.isLn = createSelector(
payInputSelector,
(input) => {
if (!input.startsWith('ln')) { return false }
try {
bech32.decode(input)
return true
} catch (e) {
return false
}
}
)
payFormSelectors.currentAmount = createSelector(
payFormSelectors.isLn,
payAmountSelector,
payInvoiceSelector,
currencySelector,
tickerSelectors.currentTicker,
(isLn, amount, invoice, currency, ticker) => {
if (isLn) {
return currency === 'usd' ? btc.satoshisToUsd((invoice.num_satoshis || 0), ticker.price_usd) : btc.satoshisToBtc((invoice.num_satoshis || 0))
}
return amount
}
)
payFormSelectors.inputCaption = createSelector(
payFormSelectors.isOnchain,
payFormSelectors.isLn,
payFormSelectors.currentAmount,
currencySelector,
(isOnchain, isLn, amount, currency) => {
if (!isOnchain && !isLn) { return '' }
if (isOnchain) {
return `You're about to send ${amount} ${currency.toUpperCase()} on-chain which should take around 10 minutes`
}
if (isLn) {
return `You're about to send ${amount} ${currency.toUpperCase()} over the Lightning Network which will be instant`
}
return ''
}
)
payFormSelectors.showPayLoadingScreen = createSelector(
sendingTransactionSelector,
sendingPaymentSelector,
(sendingTransaction, sendingPayment) => sendingTransaction || sendingPayment
)
export { payFormSelectors }
// ------------------------------------
// Reducer
// ------------------------------------
export default function payFormReducer(state = initialState, action) {
const handler = ACTION_HANDLERS[action.type]
return handler ? handler(state, action) : state
}

9
app/reducers/payment.js

@ -1,6 +1,7 @@
import { createSelector } from 'reselect'
import { ipcRenderer } from 'electron'
import { setForm, resetForm } from './form'
import { setFormType } from './form'
import { resetPayForm } from './payform'
// ------------------------------------
// Constants
@ -67,14 +68,16 @@ export const payInvoice = paymentRequest => (dispatch) => {
// Receive IPC event for successful payment
// TODO: Add payment to state, not a total re-fetch
export const paymentSuccessful = () => (dispatch) => {
// Dispatch successful payment to stop loading screen
dispatch(paymentSuccessfull())
// Close the form modal once the payment was succesful
dispatch(setForm({ modalOpen: false }))
dispatch(setFormType(null))
// Refetch payments (TODO: dont do a full refetch, rather append new tx to list)
dispatch(fetchPayments())
// Reset the payment form
dispatch(resetForm())
dispatch(resetPayForm())
}

55
app/reducers/requestform.js

@ -0,0 +1,55 @@
// Initial State
const initialState = {
amount: '0',
memo: ''
}
// Constants
// ------------------------------------
export const SET_REQUEST_AMOUNT = 'SET_REQUEST_AMOUNT'
export const SET_REQUEST_MEMO = 'SET_REQUEST_MEMO'
export const SET_PAY_INVOICE = 'SET_PAY_INVOICE'
export const RESET_FORM = 'RESET_FORM'
// ------------------------------------
// Actions
// ------------------------------------
export function setRequestAmount(amount) {
return {
type: SET_REQUEST_AMOUNT,
amount
}
}
export function setRequestMemo(memo) {
return {
type: SET_REQUEST_MEMO,
memo
}
}
export function resetRequestForm() {
return {
type: RESET_FORM
}
}
// ------------------------------------
// Action Handlers
// ------------------------------------
const ACTION_HANDLERS = {
[SET_REQUEST_AMOUNT]: (state, { amount }) => ({ ...state, amount }),
[SET_REQUEST_MEMO]: (state, { memo }) => ({ ...state, memo }),
[RESET_FORM]: () => (initialState)
}
// ------------------------------------
// Reducer
// ------------------------------------
export default function payFormReducer(state = initialState, action) {
const handler = ACTION_HANDLERS[action.type]
return handler ? handler(state, action) : state
}

7
app/reducers/transaction.js

@ -1,7 +1,8 @@
import { ipcRenderer } from 'electron'
import { showNotification } from '../notifications'
import { btc, usd } from '../utils'
import { setForm, resetForm } from './form'
import { setFormType } from './form'
import { resetPayForm } from './payform'
import { showModal } from './modal'
// ------------------------------------
@ -53,14 +54,14 @@ export const transactionSuccessful = (event, { amount, addr, txid }) => (dispatc
// Get the new list of transactions (TODO dont do an entire new fetch)
dispatch(fetchTransactions())
// Close the form modal once the payment was succesful
dispatch(setForm({ modalOpen: false }))
dispatch(setFormType(null))
// Show successful payment state
dispatch(showModal('SUCCESSFUL_SEND_COINS', { txid, amount, addr }))
// TODO: Add successful on-chain payment to payments list once payments list supports on-chain and LN
// dispatch({ type: PAYMENT_SUCCESSFULL, payment: { amount, addr, txid, pending: true } })
dispatch({ type: TRANSACTION_SUCCESSFULL })
// Reset the payment form
dispatch(resetForm())
dispatch(resetPayForm())
}
export const transactionError = () => (dispatch) => {

84
app/routes/app/components/App.js

@ -1,7 +1,8 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import LoadingBolt from 'components/LoadingBolt'
import Form from 'components/Form'
import ModalRoot from './components/ModalRoot'
import Form from './components/Form'
import Nav from './components/Nav'
import styles from './App.scss'
@ -20,28 +21,19 @@ class App extends Component {
hideModal,
ticker,
balance,
invoice: { formInvoice },
form,
setAmount,
setOnchainAmount,
setMessage,
setPubkey,
setPaymentRequest,
transaction: { sendingTransaction },
peers,
setCurrency,
setForm,
createInvoice,
payInvoice,
sendCoins,
fetchInvoice,
currentTicker,
isOnchain,
isLn,
openPayForm,
openRequestForm,
formProps,
closeForm,
children
} = this.props
if (!currentTicker) { return <div>Loading...</div> }
if (!currentTicker) { return <LoadingBolt /> }
return (
<div>
@ -52,34 +44,18 @@ class App extends Component {
currentTicker={currentTicker}
currency={ticker.currency}
/>
<Form
isOpen={form.modalOpen}
close={() => setForm({ modalOpen: false })}
setAmount={setAmount}
setOnchainAmount={setOnchainAmount}
setMessage={setMessage}
setPubkey={setPubkey}
setPaymentRequest={setPaymentRequest}
peers={peers}
ticker={ticker}
form={form}
sendingTransaction={sendingTransaction}
createInvoice={createInvoice}
payInvoice={payInvoice}
sendCoins={sendCoins}
fetchInvoice={fetchInvoice}
formInvoice={formInvoice}
currentTicker={currentTicker}
isOnchain={isOnchain}
isLn={isLn}
/>
<Form formType={form.formType} formProps={formProps} closeForm={closeForm} />
<Nav
ticker={ticker}
balance={balance}
setCurrency={setCurrency}
formClicked={formType => setForm({ modalOpen: true, formType })}
currentTicker={currentTicker}
openPayForm={openPayForm}
openRequestForm={openRequestForm}
/>
<div className={styles.content}>
{children}
</div>
@ -90,30 +66,22 @@ class App extends Component {
App.propTypes = {
modal: PropTypes.object.isRequired,
hideModal: PropTypes.func.isRequired,
fetchTicker: PropTypes.func.isRequired,
fetchBalance: PropTypes.func.isRequired,
ticker: PropTypes.object.isRequired,
balance: PropTypes.object.isRequired,
invoice: PropTypes.object.isRequired,
form: PropTypes.object.isRequired,
setAmount: PropTypes.func.isRequired,
setOnchainAmount: PropTypes.func.isRequired,
setMessage: PropTypes.func.isRequired,
setPubkey: PropTypes.func.isRequired,
setPaymentRequest: PropTypes.func.isRequired,
transaction: PropTypes.object.isRequired,
peers: PropTypes.array,
setCurrency: PropTypes.func.isRequired,
setForm: PropTypes.func.isRequired,
createInvoice: PropTypes.func.isRequired,
payInvoice: PropTypes.func.isRequired,
sendCoins: PropTypes.func.isRequired,
fetchInvoice: PropTypes.func.isRequired,
formProps: PropTypes.object.isRequired,
closeForm: PropTypes.func.isRequired,
fetchInfo: PropTypes.func.isRequired,
hideModal: PropTypes.func.isRequired,
fetchTicker: PropTypes.func.isRequired,
fetchBalance: PropTypes.func.isRequired,
setCurrency: PropTypes.func.isRequired,
openPayForm: PropTypes.func.isRequired,
openRequestForm: PropTypes.func.isRequired,
currentTicker: PropTypes.object,
isOnchain: PropTypes.bool.isRequired,
isLn: PropTypes.bool.isRequired,
children: PropTypes.object.isRequired
}

93
app/routes/app/components/components/Form/Form.js

@ -1,93 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import { MdClose } from 'react-icons/lib/md'
import Pay from './components/Pay'
import Request from './components/Request'
import styles from './Form.scss'
const Form = ({
form: { formType, amount, onchainAmount, message, payment_request },
setAmount,
setOnchainAmount,
setMessage,
setPaymentRequest,
ticker: { currency, crypto },
isOpen,
close,
createInvoice,
payInvoice,
sendCoins,
fetchInvoice,
formInvoice,
currentTicker,
isOnchain,
isLn,
sendingTransaction
}) => (
<div className={`${styles.formContainer} ${isOpen ? styles.open : ''}`}>
<div className={styles.container}>
<div className={styles.esc} onClick={close}>
<MdClose />
</div>
<div className={styles.content}>
{
formType === 'pay' ?
<Pay
sendingTransaction={sendingTransaction}
invoiceAmount={formInvoice.amount}
onchainAmount={onchainAmount}
setOnchainAmount={setOnchainAmount}
amount={formInvoice.amount}
payment_request={payment_request}
setPaymentRequest={setPaymentRequest}
fetchInvoice={fetchInvoice}
payInvoice={payInvoice}
sendCoins={sendCoins}
currentTicker={currentTicker}
currency={currency}
crypto={crypto}
close={close}
isOnchain={isOnchain}
isLn={isLn}
/>
:
<Request
amount={amount}
setAmount={setAmount}
payment_request={payment_request}
setMessage={setMessage}
createInvoice={createInvoice}
message={message}
currentTicker={currentTicker}
currency={currency}
crypto={crypto}
close={close}
/>
}
</div>
</div>
</div>
)
Form.propTypes = {
form: PropTypes.object.isRequired,
ticker: PropTypes.object.isRequired,
setAmount: PropTypes.func.isRequired,
setOnchainAmount: PropTypes.func.isRequired,
setMessage: PropTypes.func.isRequired,
setPaymentRequest: PropTypes.func.isRequired,
isOpen: PropTypes.bool.isRequired,
close: PropTypes.func.isRequired,
createInvoice: PropTypes.func.isRequired,
payInvoice: PropTypes.func.isRequired,
sendCoins: PropTypes.func.isRequired,
fetchInvoice: PropTypes.func.isRequired,
formInvoice: PropTypes.object.isRequired,
currentTicker: PropTypes.object.isRequired,
isOnchain: PropTypes.bool.isRequired,
isLn: PropTypes.bool.isRequired,
sendingTransaction: PropTypes.bool.isRequired
}
export default Form

159
app/routes/app/components/components/Form/Form.scss

@ -1,159 +0,0 @@
@import '../../../../../variables.scss';
.formContainer {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
height: 100vh;
background: $white;
z-index: 0;
opacity: 0;
transition: all 0.5s;
&.open {
opacity: 1;
z-index: 10;
}
}
.container {
position: relative;
height: 100vh;
margin: 5%;
}
.esc {
position: absolute;
top: 0;
right: 0;
color: $darkestgrey;
cursor: pointer;
padding: 20px;
border-radius: 50%;
&:hover {
color: $bluegrey;
background: $darkgrey;
}
&:active {
color: $white;
background: $main;
}
svg {
width: 32px;
height: 32px;
}
}
.content {
width: 50%;
margin: 0 auto;
display: flex;
flex-direction: column;
height: 75vh;
justify-content: center;
align-items: center;
.amountContainer {
color: $main;
display: flex;
justify-content: center;
min-height: 120px;
margin-bottom: 20px;
label, input[type=text] {
color: inherit;
display: inline-block;
vertical-align: top;
padding: 0;
}
label {
svg {
width: 85px;
height: 85px;
}
svg[data-icon='ltc'] {
margin-right: 10px;
g {
transform: scale(1.75) translate(-5px, -5px);
}
}
}
input[type=text] {
width: 100px;
font-size: 180px;
border: none;
outline: 0;
-webkit-appearance: none;
}
}
.inputContainer {
width: 100%;
display: flex;
justify-content: center;
font-size: 18px;
height: auto;
min-height: 55px;
margin-bottom: 20px;
border: 1px solid $traditionalgrey;
border-radius: 6px;
position: relative;
padding: 0 20px;
label, input[type=text] {
font-size: inherit;
}
label {
padding-top: 19px;
padding-bottom: 12px;
color: $traditionalgrey;
}
input[type=text] {
width: 100%;
border: none;
outline: 0;
-webkit-appearance: none;
height: 55px;
padding: 0 10px;
}
}
.buttonGroup {
width: 100%;
display: flex;
flex-direction: row;
border-radius: 6px;
overflow: hidden;
.button {
cursor: pointer;
height: 55px;
min-height: 55px;
text-transform: none;
font-size: 18px;
transition: opacity .2s ease-out;
background: $main;
color: $white;
border: none;
font-weight: 500;
padding: 0;
width: 100%;
text-align: center;
line-height: 55px;
&:first-child {
border-right: 1px solid lighten($main, 20%);
}
}
}
}

149
app/routes/app/components/components/Form/components/Pay/Pay.js

@ -1,149 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { FaBolt, FaChain } from 'react-icons/lib/fa'
import CurrencyIcon from 'components/CurrencyIcon'
import LoadingBolt from 'components/LoadingBolt'
import { btc } from 'utils'
import styles from './Pay.scss'
class Pay extends Component {
componentDidUpdate(prevProps) {
const { isOnchain, isLn, fetchInvoice, payment_request } = this.props
if (isOnchain) { this.amountInput.focus() }
if ((prevProps.payment_request !== payment_request) && isLn) { fetchInvoice(payment_request) }
}
render() {
const {
sendingTransaction,
invoiceAmount,
onchainAmount,
setOnchainAmount,
payment_request,
setPaymentRequest,
payInvoice,
sendCoins,
currentTicker,
currency,
crypto,
isOnchain,
isLn
} = this.props
const payClicked = () => {
if (!isOnchain && !isLn) { return }
if (isOnchain) { sendCoins({ value: onchainAmount, addr: payment_request, currency, rate: currentTicker.price_usd }) }
if (isLn) { payInvoice(payment_request) }
}
const calculateAmount = value => (currency === 'usd' ? btc.satoshisToUsd(value, currentTicker.price_usd) : btc.satoshisToBtc(value))
return (
<div>
{
sendingTransaction ?
<LoadingBolt />
:
null
}
<div className={styles.container}>
<section className={`${styles.amountContainer} ${isLn ? styles.ln : ''}`}>
<label htmlFor='amount'>
<CurrencyIcon currency={currency} crypto={crypto} />
</label>
<input
type='text'
ref={input => this.amountInput = input} // eslint-disable-line
size=''
style={
isLn ?
{ width: '75%', fontSize: '85px' }
:
{ width: `${onchainAmount.length > 1 ? (onchainAmount.length * 15) - 5 : 25}%`, fontSize: `${190 - (onchainAmount.length ** 2)}px` }
}
value={isLn ? calculateAmount(invoiceAmount) : onchainAmount}
onChange={event => setOnchainAmount(event.target.value)}
id='amount'
readOnly={isLn}
/>
</section>
<div className={styles.inputContainer}>
<div className={styles.info}>
{(() => {
if (isOnchain) {
return (
<span>{`You're about to send ${onchainAmount} ${currency.toUpperCase()} on-chain which should take around 10 minutes`}</span>
)
} else if (isLn) {
return (
<span>{`You're about to send ${calculateAmount(invoiceAmount)} ${currency.toUpperCase()} over the Lightning Network which will be instant`}</span> // eslint-disable-line
)
}
return null
})()}
</div>
<aside className={styles.paymentIcon}>
{(() => {
if (isOnchain) {
return (
<i>
<span>on-chain</span>
<FaChain />
</i>
)
} else if (isLn) {
return (
<i>
<span>lightning network</span>
<FaBolt />
</i>
)
}
return null
})()}
</aside>
<section className={styles.input}>
<input
type='text'
placeholder='Payment request or bitcoin address'
value={payment_request}
onChange={event => setPaymentRequest(event.target.value)}
id='paymentRequest'
/>
</section>
</div>
<section className={styles.buttonGroup}>
<div className={styles.button} onClick={payClicked}>
Pay
</div>
</section>
</div>
</div>
)
}
}
Pay.propTypes = {
sendingTransaction: PropTypes.bool.isRequired,
invoiceAmount: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
]).isRequired,
onchainAmount: PropTypes.string.isRequired,
setOnchainAmount: PropTypes.func.isRequired,
payment_request: PropTypes.string.isRequired,
setPaymentRequest: PropTypes.func.isRequired,
fetchInvoice: PropTypes.func.isRequired,
payInvoice: PropTypes.func.isRequired,
sendCoins: PropTypes.func.isRequired,
currentTicker: PropTypes.object.isRequired,
currency: PropTypes.string.isRequired,
crypto: PropTypes.string.isRequired,
isOnchain: PropTypes.bool.isRequired,
isLn: PropTypes.bool.isRequired
}
export default Pay

3
app/routes/app/components/components/Form/components/Pay/index.js

@ -1,3 +0,0 @@
import Pay from './Pay'
export default Pay

68
app/routes/app/components/components/Form/components/Request/Request.js

@ -1,68 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import CurrencyIcon from 'components/CurrencyIcon'
import styles from './Request.scss'
const Request = ({
amount,
setAmount,
setMessage,
createInvoice,
message,
currentTicker,
currency,
crypto,
close
}) => {
const requestClicked = () => {
createInvoice(amount, message, currency, currentTicker.price_usd)
close()
}
return (
<div className={styles.container}>
<section className={styles.amountContainer}>
<label htmlFor='amount'>
<CurrencyIcon currency={currency} crypto={crypto} />
</label>
<input
type='text'
size=''
style={{ width: `${amount.length > 1 ? (amount.length * 15) - 5 : 25}%`, fontSize: `${190 - (amount.length ** 2)}px` }}
value={amount}
onChange={event => setAmount(event.target.value)}
id='amount'
/>
</section>
<section className={styles.inputContainer}>
<label htmlFor='paymentRequest'>Request:</label>
<input
type='text'
placeholder='Dinner, Rent, etc'
value={message}
onChange={event => setMessage(event.target.value)}
id='paymentRequest'
/>
</section>
<section className={styles.buttonGroup}>
<div className={styles.button} onClick={requestClicked}>
Request
</div>
</section>
</div>
)
}
Request.propTypes = {
amount: PropTypes.string.isRequired,
setAmount: PropTypes.func.isRequired,
setMessage: PropTypes.func.isRequired,
createInvoice: PropTypes.func.isRequired,
message: PropTypes.string.isRequired,
currentTicker: PropTypes.object.isRequired,
currency: PropTypes.string.isRequired,
crypto: PropTypes.string.isRequired,
close: PropTypes.func.isRequired
}
export default Request

3
app/routes/app/components/components/Form/components/Request/index.js

@ -1,3 +0,0 @@
import Request from './Request'
export default Request

11
app/routes/app/components/components/Nav.js

@ -9,7 +9,7 @@ import CurrencyIcon from 'components/CurrencyIcon'
import { btc, usd } from 'utils'
import styles from './Nav.scss'
const Nav = ({ ticker, balance, setCurrency, formClicked, currentTicker }) => (
const Nav = ({ ticker, balance, setCurrency, currentTicker, openPayForm, openRequestForm }) => (
<nav className={styles.nav}>
<ul className={styles.info}>
<li className={`${styles.currencies} ${styles.link}`}>
@ -72,10 +72,10 @@ const Nav = ({ ticker, balance, setCurrency, formClicked, currentTicker }) => (
</li>
</ul>
<div className={styles.buttons}>
<div className={styles.button} onClick={() => formClicked('pay')}>
<div className={styles.button} onClick={openPayForm}>
<span>Pay</span>
</div>
<div className={styles.button} onClick={() => formClicked('request')}>
<div className={styles.button} onClick={openRequestForm}>
<span>Request</span>
</div>
</div>
@ -86,8 +86,9 @@ Nav.propTypes = {
ticker: PropTypes.object.isRequired,
balance: PropTypes.object.isRequired,
setCurrency: PropTypes.func.isRequired,
formClicked: PropTypes.func.isRequired,
currentTicker: PropTypes.object.isRequired
currentTicker: PropTypes.object.isRequired,
openPayForm: PropTypes.func.isRequired,
openRequestForm: PropTypes.func.isRequired
}
export default Nav

142
app/routes/app/containers/AppContainer.js

@ -1,41 +1,43 @@
import { connect } from 'react-redux'
import { fetchTicker, setCurrency, tickerSelectors } from 'reducers/ticker'
import { fetchBalance } from 'reducers/balance'
import { fetchInfo } from 'reducers/info'
import { createInvoice, fetchInvoice } from 'reducers/invoice'
import { hideModal } from 'reducers/modal'
import { payInvoice } from 'reducers/payment'
import { showModal, hideModal } from 'reducers/modal'
import { setFormType } from 'reducers/form'
import { setPayAmount, setPayInput, payFormSelectors } from 'reducers/payform'
import { setRequestAmount, setRequestMemo } from 'reducers/requestform'
import { sendCoins } from 'reducers/transaction'
import { fetchChannels } from 'reducers/channels'
import {
setForm,
setPaymentType,
setAmount,
setOnchainAmount,
setMessage,
setPubkey,
setPaymentRequest,
formSelectors
} from 'reducers/form'
import { payInvoice } from 'reducers/payment'
import { createInvoice, fetchInvoice } from 'reducers/invoice'
import App from '../components/App'
const mapDispatchToProps = {
fetchTicker,
setCurrency,
fetchBalance,
fetchInfo,
setAmount,
setOnchainAmount,
setMessage,
setPubkey,
setPaymentRequest,
setForm,
setPaymentType,
createInvoice,
showModal,
hideModal,
payInvoice,
setFormType,
setPayAmount,
setPayInput,
setRequestAmount,
setRequestMemo,
sendCoins,
fetchChannels,
payInvoice,
createInvoice,
fetchInvoice
}
@ -44,13 +46,99 @@ const mapStateToProps = state => ({
balance: state.balance,
payment: state.payment,
transaction: state.transaction,
form: state.form,
payform: state.payform,
requestform: state.requestform,
invoice: state.invoice,
modal: state.modal,
currentTicker: tickerSelectors.currentTicker(state),
isOnchain: formSelectors.isOnchain(state),
isLn: formSelectors.isLn(state)
isOnchain: payFormSelectors.isOnchain(state),
isLn: payFormSelectors.isLn(state),
currentAmount: payFormSelectors.currentAmount(state),
inputCaption: payFormSelectors.inputCaption(state),
showPayLoadingScreen: payFormSelectors.showPayLoadingScreen(state)
})
export default connect(mapStateToProps, mapDispatchToProps)(App)
const mergeProps = (stateProps, dispatchProps, ownProps) => {
const payFormProps = {
payform: stateProps.payform,
currency: stateProps.ticker.currency,
crypto: stateProps.ticker.crypto,
isOnchain: stateProps.isOnchain,
isLn: stateProps.isLn,
currentAmount: stateProps.currentAmount,
inputCaption: stateProps.inputCaption,
showPayLoadingScreen: stateProps.showPayLoadingScreen,
setPayAmount: dispatchProps.setPayAmount,
setPayInput: dispatchProps.setPayInput,
fetchInvoice: dispatchProps.fetchInvoice,
onPaySubmit: () => {
if (!stateProps.isOnchain && !stateProps.isLn) { return }
if (stateProps.isOnchain) {
dispatchProps.sendCoins({
value: stateProps.payform.amount,
addr: stateProps.payform.payInput,
currency: stateProps.ticker.currency,
rate: stateProps.currentTicker.price_usd
})
}
if (stateProps.isLn) {
dispatchProps.payInvoice(stateProps.payform.payInput)
}
}
}
const requestFormProps = {
requestform: stateProps.requestform,
currency: stateProps.ticker.currency,
crypto: stateProps.ticker.crypto,
setRequestAmount: dispatchProps.setRequestAmount,
setRequestMemo: dispatchProps.setRequestMemo,
onRequestSubmit: () => (
dispatchProps.createInvoice(
stateProps.requestform.amount,
stateProps.requestform.memo,
stateProps.ticker.currency,
stateProps.currentTicker.price_usd
)
)
}
const formProps = (formType) => {
if (!formType) { return {} }
if (formType === 'PAY_FORM') { return payFormProps }
if (formType === 'REQUEST_FORM') { return requestFormProps }
return {}
}
return {
...stateProps,
...dispatchProps,
...ownProps,
// Props to pass to the pay form
formProps: formProps(stateProps.form.formType),
// action to open the pay form
openPayForm: () => dispatchProps.setFormType('PAY_FORM'),
// action to open the request form
openRequestForm: () => dispatchProps.setFormType('REQUEST_FORM'),
// action to close form
closeForm: () => dispatchProps.setFormType(null)
}
}
export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(App)

150
app/utils/bech32.js

@ -0,0 +1,150 @@
// Using bech32 here just without the 90 char length: https://github.com/bitcoinjs/bech32/blob/master/index.js
/* eslint-disable */
'use strict'
let ALPHABET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'
// pre-compute lookup table
let ALPHABET_MAP = {}
for (let z = 0; z < ALPHABET.length; z++) {
let x = ALPHABET.charAt(z)
if (ALPHABET_MAP[x] !== undefined) throw new TypeError(x + ' is ambiguous')
ALPHABET_MAP[x] = z
}
function polymodStep (pre) {
let b = pre >> 25
return ((pre & 0x1FFFFFF) << 5) ^
(-((b >> 0) & 1) & 0x3b6a57b2) ^
(-((b >> 1) & 1) & 0x26508e6d) ^
(-((b >> 2) & 1) & 0x1ea119fa) ^
(-((b >> 3) & 1) & 0x3d4233dd) ^
(-((b >> 4) & 1) & 0x2a1462b3)
}
function prefixChk (prefix) {
let chk = 1
for (let i = 0; i < prefix.length; ++i) {
let c = prefix.charCodeAt(i)
if (c < 33 || c > 126) throw new Error('Invalid prefix (' + prefix + ')')
chk = polymodStep(chk) ^ (c >> 5)
}
chk = polymodStep(chk)
for (let i = 0; i < prefix.length; ++i) {
let v = prefix.charCodeAt(i)
chk = polymodStep(chk) ^ (v & 0x1f)
}
return chk
}
function encode (prefix, words) {
// too long?
if ((prefix.length + 7 + words.length) > 90) throw new TypeError('Exceeds Bech32 maximum length')
prefix = prefix.toLowerCase()
// determine chk mod
let chk = prefixChk(prefix)
let result = prefix + '1'
for (let i = 0; i < words.length; ++i) {
let x = words[i]
if ((x >> 5) !== 0) throw new Error('Non 5-bit word')
chk = polymodStep(chk) ^ x
result += ALPHABET.charAt(x)
}
for (let i = 0; i < 6; ++i) {
chk = polymodStep(chk)
}
chk ^= 1
for (let i = 0; i < 6; ++i) {
let v = (chk >> ((5 - i) * 5)) & 0x1f
result += ALPHABET.charAt(v)
}
return result
}
function decode (str) {
if (str.length < 8) throw new TypeError(str + ' too short')
// LN payment requests can be longer than 90 chars
// if (str.length > 90) throw new TypeError(str + ' too long')
// don't allow mixed case
let lowered = str.toLowerCase()
let uppered = str.toUpperCase()
if (str !== lowered && str !== uppered) throw new Error('Mixed-case string ' + str)
str = lowered
let split = str.lastIndexOf('1')
if (split === 0) throw new Error('Missing prefix for ' + str)
let prefix = str.slice(0, split)
let wordChars = str.slice(split + 1)
if (wordChars.length < 6) throw new Error('Data too short')
let chk = prefixChk(prefix)
let words = []
for (let i = 0; i < wordChars.length; ++i) {
let c = wordChars.charAt(i)
let v = ALPHABET_MAP[c]
if (v === undefined) throw new Error('Unknown character ' + c)
chk = polymodStep(chk) ^ v
// not in the checksum?
if (i + 6 >= wordChars.length) continue
words.push(v)
}
if (chk !== 1) throw new Error('Invalid checksum for ' + str)
return { prefix, words }
}
function convert (data, inBits, outBits, pad) {
let value = 0
let bits = 0
let maxV = (1 << outBits) - 1
let result = []
for (let i = 0; i < data.length; ++i) {
value = (value << inBits) | data[i]
bits += inBits
while (bits >= outBits) {
bits -= outBits
result.push((value >> bits) & maxV)
}
}
if (pad) {
if (bits > 0) {
result.push((value << (outBits - bits)) & maxV)
}
} else {
if (bits >= inBits) throw new Error('Excess padding')
if ((value << (outBits - bits)) & maxV) throw new Error('Non-zero padding')
}
return result
}
function toWords (bytes) {
return convert(bytes, 8, 5, true)
}
function fromWords (words) {
return convert(words, 5, 8, false)
}
export default {
decode,
encode,
toWords,
fromWords
}

4
app/utils/index.js

@ -1,7 +1,9 @@
import btc from './btc'
import usd from './usd'
import bech32 from './bech32'
export default {
btc,
usd
usd,
bech32
}

2
test/components/Channels.spec.js

@ -1,4 +1,4 @@
import React from 'react';
import React from 'react'
import { shallow } from 'enzyme'
import Channels from '../../app/components/Channels'

60
test/components/Form.spec.js

@ -0,0 +1,60 @@
import React from 'react'
import { shallow } from 'enzyme'
import Form from '../../app/components/Form'
import PayForm from '../../app/components/Form/PayForm'
import RequestForm from '../../app/components/Form/RequestForm'
const payFormProps = {
payform: {},
currency: 'BTC',
crypto: 'BTC',
isOnchain: false,
isLn: false,
currentAmount: '0',
inputCaption: '',
showPayLoadingScreen: false,
setPayAmount: () => {},
setPayInput: () => {},
fetchInvoice: () => {},
onPaySubmit: () => {}
}
const requestFormProps = {
requestform: {},
currency: '',
crypto: '',
setRequestAmount: () => {},
setRequestMemo: () => {},
onRequestSubmit: () => {}
}
const defaultProps = {
formType: '',
formProps: {},
closeForm: () => {}
}
describe('Form', () => {
describe('should show pay form when formType is PAY_FORM', () => {
const props = { ...defaultProps, formType: 'PAY_FORM', formProps: payFormProps }
const el = shallow(<Form {...props} />)
it('should contain PayForm', () => {
expect(el.find(PayForm)).toHaveLength(1)
})
})
describe('should show request form when formType is REQUEST_FORM', () => {
const props = { ...defaultProps, formType: 'REQUEST_FORM', formProps: requestFormProps }
const el = shallow(<Form {...props} />)
it('should contain RequestForm', () => {
expect(el.find(RequestForm)).toHaveLength(1)
})
})
})

78
test/reducers/__snapshots__/form.spec.js.snap

@ -1,85 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`reducers formReducer should correctly resetForm 1`] = `
exports[`reducers formReducer should correctly setFormType 1`] = `
Object {
"amount": "0",
"formType": "pay",
"message": "",
"modalOpen": false,
"onchainAmount": "0",
"payment_request": "",
"pubkey": "",
}
`;
exports[`reducers formReducer should correctly setAmount 1`] = `
Object {
"amount": 1,
"formType": "pay",
"message": "",
"modalOpen": false,
"onchainAmount": "0",
"payment_request": "",
"pubkey": "",
}
`;
exports[`reducers formReducer should correctly setForm 1`] = `
Object {
"amount": "0",
"formType": "foo",
"message": "",
"modalOpen": true,
"onchainAmount": "0",
"payment_request": "",
"pubkey": "",
}
`;
exports[`reducers formReducer should correctly setMessage 1`] = `
Object {
"amount": "0",
"formType": "pay",
"message": "foo",
"modalOpen": false,
"onchainAmount": "0",
"payment_request": "",
"pubkey": "",
}
`;
exports[`reducers formReducer should correctly setPaymentRequest 1`] = `
Object {
"amount": "0",
"formType": "pay",
"message": "",
"modalOpen": false,
"onchainAmount": "0",
"payment_request": "foo",
"pubkey": "",
}
`;
exports[`reducers formReducer should correctly setPubkey 1`] = `
Object {
"amount": "0",
"formType": "pay",
"message": "",
"modalOpen": false,
"onchainAmount": "0",
"payment_request": "",
"pubkey": "foo",
"formType": "FOO",
}
`;
exports[`reducers formReducer should handle initial state 1`] = `
Object {
"amount": "0",
"formType": "pay",
"message": "",
"modalOpen": false,
"onchainAmount": "0",
"payment_request": "",
"pubkey": "",
"formType": null,
}
`;

4
test/reducers/__snapshots__/invoice.spec.js.snap

@ -66,7 +66,9 @@ exports[`reducers invoiceReducer should correctly receiveFormInvoice 1`] = `
Object {
"data": Object {},
"formInvoice": Object {
"payreq": "foo",
"amount": "0",
"payreq": "",
"r_hash": "",
},
"invoice": null,
"invoiceLoading": false,

62
test/reducers/form.spec.js

@ -1,64 +1,26 @@
import formReducer, {
SET_FORM,
SET_AMOUNT,
SET_MESSAGE,
SET_PUBKEY,
SET_PAYMENT_REQUEST,
RESET_FORM
SET_FORM_TYPE
} from '../../app/reducers/form'
// describe('reducers', () => {
// describe('formReducer', () => {
// }
// }
describe('reducers', () => {
describe('formReducer', () => {
it('should handle initial state', () => {
expect(formReducer(undefined, {})).toMatchSnapshot()
})
it('should have SET_FORM', () => {
expect(SET_FORM).toEqual('SET_FORM')
})
it('should have SET_AMOUNT', () => {
expect(SET_AMOUNT).toEqual('SET_AMOUNT')
})
it('should have SET_MESSAGE', () => {
expect(SET_MESSAGE).toEqual('SET_MESSAGE')
})
it('should have SET_PUBKEY', () => {
expect(SET_PUBKEY).toEqual('SET_PUBKEY')
})
it('should have SET_PAYMENT_REQUEST', () => {
expect(SET_PAYMENT_REQUEST).toEqual('SET_PAYMENT_REQUEST')
})
it('should have RESET_FORM', () => {
expect(RESET_FORM).toEqual('RESET_FORM')
})
it('should correctly setForm', () => {
expect(formReducer(undefined, { type: SET_FORM, modalOpen: true, formType: 'foo' })).toMatchSnapshot()
})
it('should correctly setAmount', () => {
expect(formReducer(undefined, { type: SET_AMOUNT, amount: 1 })).toMatchSnapshot()
})
it('should correctly setMessage', () => {
expect(formReducer(undefined, { type: SET_MESSAGE, message: 'foo' })).toMatchSnapshot()
})
it('should correctly setPubkey', () => {
expect(formReducer(undefined, { type: SET_PUBKEY, pubkey: 'foo' })).toMatchSnapshot()
})
it('should correctly setPaymentRequest', () => {
expect(formReducer(undefined, { type: SET_PAYMENT_REQUEST, payment_request: 'foo' })).toMatchSnapshot()
it('should have SET_FORM_TYPE', () => {
expect(SET_FORM_TYPE).toEqual('SET_FORM_TYPE')
})
it('should correctly resetForm', () => {
expect(formReducer(undefined, { type: RESET_FORM })).toMatchSnapshot()
it('should correctly setFormType', () => {
expect(formReducer(undefined, { type: SET_FORM_TYPE, formType: 'FOO' })).toMatchSnapshot()
})
})
})

Loading…
Cancel
Save