Tom Kirkpatrick
6 years ago
16 changed files with 618 additions and 134 deletions
@ -0,0 +1,71 @@ |
|||
import React from 'react' |
|||
import PropTypes from 'prop-types' |
|||
import { asField } from 'informed' |
|||
import { isOnchain, isLn } from 'lib/utils/crypto' |
|||
import TextArea from 'components/UI/TextArea' |
|||
import FormFieldMessage from 'components/UI/FormFieldMessage' |
|||
|
|||
/** |
|||
* @render react |
|||
* @name LightningInvoiceInput |
|||
* @example |
|||
* <LightningInvoiceInput |
|||
network="testnet" |
|||
field="testnet" |
|||
id="testnet" |
|||
validateOnChange |
|||
validateOnBlur /> |
|||
*/ |
|||
class LightningInvoiceInput extends React.Component { |
|||
static displayName = 'LightningInvoiceInput' |
|||
|
|||
static propTypes = { |
|||
required: PropTypes.bool, |
|||
chain: PropTypes.oneOf(['bitcoin', 'litecoin']), |
|||
network: PropTypes.oneOf(['mainnet', 'testnet', 'regtest']) |
|||
} |
|||
|
|||
static defaultProps = { |
|||
required: false |
|||
} |
|||
|
|||
validate = value => { |
|||
const { network, chain, required } = this.props |
|||
if (required && (!value || value.trim() === '')) { |
|||
return 'This is a required field' |
|||
} |
|||
if (value && !isLn(value, chain, network) && !isOnchain(value, chain, network)) { |
|||
return 'Not a valid address.' |
|||
} |
|||
} |
|||
|
|||
render() { |
|||
return ( |
|||
<InformedTextArea |
|||
placeholder="Paste a Lightning Payment Request or Bitcoin Address here" |
|||
rows={5} |
|||
{...this.props} |
|||
validate={this.validate} |
|||
/> |
|||
) |
|||
} |
|||
} |
|||
|
|||
const InformedTextArea = asField(({ fieldState, fieldApi, ...props }) => { |
|||
const { value } = fieldState |
|||
const { chain, network, ...rest } = props |
|||
return ( |
|||
<React.Fragment> |
|||
<TextArea {...rest} /> |
|||
{value && |
|||
!fieldState.error && ( |
|||
<FormFieldMessage variant="success" mt={2}> |
|||
Valid {isLn(value, chain, network) ? 'lightning' : chain} address{' '} |
|||
{network !== 'mainnet' && `(${network})`} |
|||
</FormFieldMessage> |
|||
)} |
|||
</React.Fragment> |
|||
) |
|||
}) |
|||
|
|||
export default LightningInvoiceInput |
@ -0,0 +1,73 @@ |
|||
import bitcoin from 'bitcoinjs-lib' |
|||
import bech32 from 'lib/utils/bech32' |
|||
|
|||
/** |
|||
* Test to see if a string is a valid on-chain address. |
|||
* @param {String} input string to check. |
|||
* @param {String} [network='mainnet'] network to check (mainnet, testnet). |
|||
* @return {Boolean} boolean indicating wether the address is a valid on-chain address. |
|||
*/ |
|||
export const isOnchain = (input, chain = 'bitcoin', network = 'mainnet') => { |
|||
if (chain !== 'bitcoin') { |
|||
// TODO: Implement address checking for litecoin.
|
|||
return true |
|||
} |
|||
try { |
|||
bitcoin.address.toOutputScript( |
|||
input, |
|||
network === 'mainnet' ? bitcoin.networks.bitcoin : bitcoin.networks.testnet |
|||
) |
|||
return true |
|||
} catch (e) { |
|||
return false |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Test to see if a string is a valid lightning address. |
|||
* @param {String} input string to check. |
|||
* @param {String} [network='bitcoin'] chain to check (bitcoin, litecoin). |
|||
* @param {String} [network='mainnet'] network to check (mainnet, testnet, regtest). |
|||
* @return {Boolean} boolean indicating wether the address is a lightning address. |
|||
*/ |
|||
export const isLn = (input, chain = 'bitcoin', network = 'mainnet') => { |
|||
let prefix = 'ln' |
|||
// Prefixes come from SLIP-0173
|
|||
// See https://github.com/satoshilabs/slips/blob/master/slip-0173.md
|
|||
if (chain === 'bitcoin') { |
|||
switch (network) { |
|||
case 'mainnet': |
|||
prefix = 'lnbc' |
|||
break |
|||
case 'testnet': |
|||
prefix = 'lntb' |
|||
break |
|||
case 'regtest': |
|||
prefix = 'lnbcrt' |
|||
break |
|||
} |
|||
} else if (chain === 'litecoin') { |
|||
switch (network) { |
|||
case 'mainnet': |
|||
prefix = 'lnltc' |
|||
break |
|||
case 'testnet': |
|||
prefix = 'lntltc' |
|||
break |
|||
case 'regtest': |
|||
prefix = 'lnrltc' |
|||
break |
|||
} |
|||
} |
|||
|
|||
if (!input.startsWith(prefix)) { |
|||
return false |
|||
} |
|||
|
|||
try { |
|||
bech32.decode(input) |
|||
return true |
|||
} catch (e) { |
|||
return false |
|||
} |
|||
} |
@ -0,0 +1,21 @@ |
|||
import React from 'react' |
|||
import { Form } from 'informed' |
|||
import renderer from 'react-test-renderer' |
|||
import { dark } from 'themes' |
|||
import { ThemeProvider } from 'styled-components' |
|||
import { LightningInvoiceInput } from 'components/UI' |
|||
|
|||
describe('component.UI.LightningInvoiceInput', () => { |
|||
it('should render correctly', () => { |
|||
const tree = renderer |
|||
.create( |
|||
<ThemeProvider theme={dark}> |
|||
<Form> |
|||
<LightningInvoiceInput field="name" chain="bitcoin" network="mainnet" theme={dark} /> |
|||
</Form> |
|||
</ThemeProvider> |
|||
) |
|||
.toJSON() |
|||
expect(tree).toMatchSnapshot() |
|||
}) |
|||
}) |
@ -0,0 +1,60 @@ |
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP |
|||
|
|||
exports[`component.UI.LightningInvoiceInput should render correctly 1`] = ` |
|||
.c0 { |
|||
display: -webkit-box; |
|||
display: -webkit-flex; |
|||
display: -ms-flexbox; |
|||
display: flex; |
|||
-webkit-flex-direction: column; |
|||
-ms-flex-direction: column; |
|||
flex-direction: column; |
|||
} |
|||
|
|||
.c1 { |
|||
padding: 16px; |
|||
width: 100%; |
|||
font-size: 13px; |
|||
color: #fff; |
|||
background-color: transparent; |
|||
color: #fff; |
|||
background-color: transparent; |
|||
font-family: "Roboto Light",Roboto,system-ui,sans-serif; |
|||
font-weight: light; |
|||
border: 1px solid; |
|||
border: 1px solid; |
|||
border-color: #959595; |
|||
border-radius: 5px; |
|||
outline: none; |
|||
} |
|||
|
|||
.c1:not([readOnly]):not([disabled]):focus { |
|||
border: 1px solid #fd9800; |
|||
} |
|||
|
|||
<form |
|||
onReset={[Function]} |
|||
onSubmit={[Function]} |
|||
> |
|||
<div |
|||
className="c0" |
|||
> |
|||
<textarea |
|||
className="c1" |
|||
color="primaryText" |
|||
fontFamily="sans" |
|||
fontSize="m" |
|||
fontWeight="light" |
|||
onBlur={[Function]} |
|||
onChange={[Function]} |
|||
onFocus={[Function]} |
|||
opacity={null} |
|||
placeholder="Paste a Lightning Payment Request or Bitcoin Address here" |
|||
required={false} |
|||
rows={5} |
|||
value="" |
|||
width={1} |
|||
/> |
|||
</div> |
|||
</form> |
|||
`; |
@ -0,0 +1,100 @@ |
|||
/* eslint-disable max-len */ |
|||
import { isLn, isOnchain } from 'lib/utils/crypto' |
|||
|
|||
const VALID_BITCOIN_MAINNET_LN = |
|||
'lnbc10u1pduey89pp57gt0mqvh9gv4m5kkxmy9a0a46ha5jlzr3mcfcz2fx8tzu63vpjksdq8w3jhxaqcqzystfg0drarrx89nvpegwykvfr4fypvwz2d9ktcr6tj5s08f0nn8gdjnv74y9amksk3rjw7englhjrsev70k77vwf603qh2pr4tnqeue6qp5n92gy' |
|||
const VALID_BITCOIN_TESTNET_LN = |
|||
'lntb10u1pdue0gxpp5uljstna5aenp3yku3ft4wn8y63qfqdgqfh4cqqxz8z58undqp8hqdqqcqzysxqyz5vqqwaeuuh0fy52tqx6rrq6kya4lwm6v523wyqe9nesd5a3mszcq7j4e9mv8rd2vhmp7ycxswtktvs8gqq8lu5awjwfevnvfc4rzp8fmacpp4h27e' |
|||
|
|||
const VALID_LITECOIN_MAINNET_LN = |
|||
'lnltc241pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66859t2d55efrxdlgqg9hdqskfstdmyssdw4fjc8qdl522ct885pqk7acn2aczh0jeht0xhuhnkmm3h0qsrxedlwm9x86787zzn4qwwwcpjkl3t2' |
|||
const VALID_LITECOIN_TESTNET_LN = |
|||
'lntltc241pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66m2eq2fx9uctzkmj30meaghyskkgsd6geap5qg9j2ae444z24a4p8xg3a6g73p8l7d689vtrlgzj0wyx2h6atq8dfty7wmkt4frx9g9sp730h5a' |
|||
|
|||
describe('Crypto.isLn', () => { |
|||
describe('Bitcoin', () => { |
|||
describe('Mainnet', () => { |
|||
it('should pass with a valid invoice ', () => { |
|||
expect(isLn(VALID_BITCOIN_MAINNET_LN, 'bitcoin', 'mainnet')).toBeTruthy() |
|||
}) |
|||
it('should fail with an invalid invoice ', () => { |
|||
expect(isLn(VALID_BITCOIN_TESTNET_LN, 'bitcoin', 'mainnet')).toBeFalsy() |
|||
}) |
|||
}) |
|||
describe('Testnet', () => { |
|||
it('should pass with a valid invoice', () => { |
|||
expect(isLn(VALID_BITCOIN_TESTNET_LN, 'bitcoin', 'testnet')).toBeTruthy() |
|||
}) |
|||
it('should fail with an invalid invoice ', () => { |
|||
expect(isLn(VALID_BITCOIN_MAINNET_LN, 'bitcoin', 'testnet')).toBeFalsy() |
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
describe('Litecoin', () => { |
|||
describe('Mainnet', () => { |
|||
it('should pass with a valid invoice ', () => { |
|||
expect(isLn(VALID_LITECOIN_MAINNET_LN, 'litecoin', 'mainnet')).toBeTruthy() |
|||
}) |
|||
it('should fail with an invalid invoice ', () => { |
|||
expect(isLn(VALID_LITECOIN_TESTNET_LN, 'litecoin', 'mainnet')).toBeFalsy() |
|||
}) |
|||
}) |
|||
describe('Testnet', () => { |
|||
it('should pass with a valid invoice', () => { |
|||
expect(isLn(VALID_LITECOIN_TESTNET_LN, 'litecoin', 'testnet')).toBeTruthy() |
|||
}) |
|||
it('should fail with an invalid invoice ', () => { |
|||
expect(isLn(VALID_LITECOIN_MAINNET_LN, 'litecoin', 'testnet')).toBeFalsy() |
|||
}) |
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
const VALID_BITCOIN_MAINNET = '3QJmV3qfvL9SuYo34YihAf3sRCW3qSinyC' |
|||
const VALID_BITCOIN_TESTNET = '2NBMEX8USTXf5uPiW16QYX2AETytrJBCK52' |
|||
|
|||
const VALID_LITECOIN_MAINNET = 'LW9Q7QU8dgaGBerVkxbmVfurr2E1cFudrn' |
|||
const VALID_LITECOIN_TESTNET = '2MvCEgZGPnwA5HmZ3GDXB4EteMZzxgS54it' |
|||
|
|||
describe('Crypto.isOnchain', () => { |
|||
describe('Bitcoin', () => { |
|||
describe('Mainnet', () => { |
|||
it('should pass with a valid address ', () => { |
|||
expect(isOnchain(VALID_BITCOIN_MAINNET, 'bitcoin', 'mainnet')).toBeTruthy() |
|||
}) |
|||
it('should fail with an invalid address ', () => { |
|||
expect(isOnchain(VALID_BITCOIN_TESTNET, 'bitcoin', 'mainnet')).toBeFalsy() |
|||
}) |
|||
}) |
|||
describe('Testnet', () => { |
|||
it('should pass with a valid address', () => { |
|||
expect(isOnchain(VALID_BITCOIN_TESTNET, 'bitcoin', 'testnet')).toBeTruthy() |
|||
}) |
|||
it('should fail with an invalid address ', () => { |
|||
expect(isOnchain(VALID_BITCOIN_MAINNET, 'bitcoin', 'testnet')).toBeFalsy() |
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
describe('Litecoin', () => { |
|||
describe('Mainnet', () => { |
|||
it('should pass with a valid address ', () => { |
|||
expect(isOnchain(VALID_LITECOIN_MAINNET, 'litecoin', 'mainnet')).toBeTruthy() |
|||
}) |
|||
// FIXME: TWe don't yet fully support litecoin, so this check always returns true for litecoin addresses
|
|||
it('should pass with an invalid address ', () => { |
|||
expect(isOnchain(VALID_LITECOIN_TESTNET, 'litecoin', 'mainnet')).toBeTruthy() |
|||
}) |
|||
}) |
|||
describe('Testnet', () => { |
|||
it('should pass with a valid address', () => { |
|||
expect(isOnchain(VALID_LITECOIN_TESTNET, 'litecoin', 'testnet')).toBeTruthy() |
|||
}) |
|||
// FIXME: TWe don't yet fully support litecoin, so this check always returns true for litecoin addresses
|
|||
it('should pass with an invalid address ', () => { |
|||
expect(isOnchain(VALID_LITECOIN_MAINNET, 'litecoin', 'testnet')).toBeTruthy() |
|||
}) |
|||
}) |
|||
}) |
|||
}) |
Loading…
Reference in new issue