Browse Source

Merge pull request #1 from LN-Zap/master

keep my fork up to date with ln-zap version
renovate/lint-staged-8.x
Jason Tarre 8 years ago
committed by GitHub
parent
commit
ab1b848f0d
  1. 40
      README.md
  2. 41
      app/api/index.js
  3. 23
      app/components/CryptoIcon/CryptoIcon.js
  4. 3
      app/components/CryptoIcon/index.js
  5. 17
      app/components/CurrencyIcon/CurrencyIcon.js
  6. 3
      app/components/CurrencyIcon/index.js
  7. 9
      app/lnd/config/index.js
  8. 1281
      app/lnd/config/rpc.proto
  9. 7
      app/lnd/index.js
  10. 14
      app/lnd/lib/lightning.js
  11. 10
      app/lnd/methods/channelbalance.js
  12. 10
      app/lnd/methods/channels.js
  13. 20
      app/lnd/methods/closechannel.js
  14. 10
      app/lnd/methods/connectpeer.js
  15. 10
      app/lnd/methods/createinvoice.js
  16. 10
      app/lnd/methods/disconnectpeer.js
  17. 147
      app/lnd/methods/index.js
  18. 10
      app/lnd/methods/info.js
  19. 12
      app/lnd/methods/invoice.js
  20. 10
      app/lnd/methods/invoices.js
  21. 10
      app/lnd/methods/newaddress.js
  22. 19
      app/lnd/methods/openchannel.js
  23. 10
      app/lnd/methods/payinvoice.js
  24. 10
      app/lnd/methods/payments.js
  25. 10
      app/lnd/methods/peers.js
  26. 10
      app/lnd/methods/pendingchannels.js
  27. 10
      app/lnd/methods/sendcoins.js
  28. 10
      app/lnd/methods/walletbalance.js
  29. 16
      app/lnd/push/closechannel.js
  30. 16
      app/lnd/push/openchannel.js
  31. 38
      app/lnd/utils/index.js
  32. 10
      app/main.dev.js
  33. 54
      app/reducers/activity.js
  34. 53
      app/reducers/address.js
  35. 19
      app/reducers/balance.js
  36. 110
      app/reducers/channels.js
  37. 4
      app/reducers/index.js
  38. 33
      app/reducers/info.js
  39. 69
      app/reducers/invoice.js
  40. 62
      app/reducers/ipc.js
  41. 41
      app/reducers/payment.js
  42. 68
      app/reducers/peers.js
  43. 76
      app/reducers/ticker.js
  44. 8
      app/routes/activity/components/Activity.js
  45. 27
      app/routes/activity/components/components/Invoices.js
  46. 13
      app/routes/activity/components/components/Invoices.scss
  47. 35
      app/routes/activity/components/components/Payments.js
  48. 11
      app/routes/activity/components/components/Payments.scss
  49. 5
      app/routes/activity/containers/ActivityContainer.js
  50. 20
      app/routes/app/components/App.js
  51. 31
      app/routes/app/components/components/Form/Form.js
  52. 12
      app/routes/app/components/components/Form/Form.scss
  53. 45
      app/routes/app/components/components/Nav.js
  54. 23
      app/routes/app/components/components/Nav.scss
  55. 20
      app/routes/app/components/components/Socket.js
  56. 6
      app/routes/app/containers/AppContainer.js
  57. 34
      app/routes/wallet/components/Wallet.js
  58. 28
      app/routes/wallet/components/Wallet.scss
  59. 37
      app/routes/wallet/components/components/Channels/Channels.js
  60. 11
      app/routes/wallet/components/components/Channels/components/Channel/Channel.js
  61. 31
      app/routes/wallet/components/components/Channels/components/ChannelForm/ChannelForm.js
  62. 9
      app/routes/wallet/components/components/Channels/components/ChannelForm/ChannelForm.scss
  63. 15
      app/routes/wallet/components/components/Channels/components/ChannelModal/ChannelModal.js
  64. 14
      app/routes/wallet/components/components/Channels/components/ClosedPendingChannel/ClosedPendingChannel.js
  65. 14
      app/routes/wallet/components/components/Channels/components/OpenPendingChannel/OpenPendingChannel.js
  66. 5
      app/routes/wallet/components/components/Peers/components/PeerForm/PeerForm.js
  67. 7
      app/routes/wallet/components/components/Peers/components/PeerModal/PeerModal.js
  68. 17
      app/routes/wallet/containers/WalletContainer.js
  69. 15
      app/store/configureStore.dev.js
  70. 3
      app/utils/btc.js
  71. 5
      app/utils/usd.js
  72. 8
      package.json
  73. 6
      resources/litecoin.svg
  74. BIN
      resources/zap_2.icns
  75. BIN
      resources/zap_2.ico
  76. 55
      test/api/index.spec.js
  77. 6
      test/reducers/__snapshots__/channels.spec.js.snap
  78. 30
      test/reducers/__snapshots__/ticker.spec.js.snap
  79. 21
      test/reducers/ticker.spec.js
  80. 30
      test/utils/usd.spec.js
  81. 208
      yarn.lock

40
README.md

@ -18,10 +18,9 @@ Join us on [slack](https://join.slack.com/t/zaphq/shared_invite/MjI2MTY4NTcwMDUy
* **An up and running BTCD**
* **An up and running LND** - see [install.md](https://github.com/lightningnetwork/lnd/blob/master/docs/INSTALL.md)
* **Zap Node.js** - see [Zap Node.js](https://github.com/LN-Zap/zap-nodejs)
* **Node.js version >= 7 and npm version >= 4.**
*For now Zap assumes you are running BTCD, LND and Zap Node.js (will change soon).*
*For now Zap assumes you are running BTCD, LND*
## Install
@ -32,11 +31,46 @@ After installing the above requirements, clone the repo via git:
git clone https://github.com/LN-Zap/zap-desktop.git
```
After the repo is cloned, you'll want to generate a Node.js compatible cert
```bash
# For Linux
$ cd ~/.lnd
# For Mac
$ cd ~/Library/Application\ Support/Lnd
# For Windows
$ cd \Users\{your_user_name}\AppData\Local\Lnd
# Then generate the cert
$ openssl ecparam -genkey -name prime256v1 -out tls.key
$ openssl req -new -sha256 -key tls.key -out csr.csr -subj '/CN=localhost/O=lnd'
$ openssl req -x509 -sha256 -days 3650 -key tls.key -in csr.csr -out tls.cert
$ rm csr.csr
```
Once you've created the Node.js compatible cert, paste the path to your cert in app/lnd/config/index.js:
```bash
// Cert will be located depending on your machine
// Mac OS X: /Users/{your_user_name}/Library/Application Support/Lnd/tls.cert
// Linux: ~/.lnd/tls.cert
// Windows: C:\Users\{your_user_name}\AppData\Local\Lnd\tls.cert
export default {
...,
cert: '/path/to/cert/tls.cert'
}
```
And then install dependencies with yarn
```bash
$ cd zap-desktop
$ yarn
# For Mac & Linux
$ ./node_modules/.bin/electron-rebuild
# For Windows
$ .\node_modules\.bin\electron-rebuild.cmd
```
Then to start it:
@ -61,7 +95,7 @@ Please see the [contributing guide](https://github.com/LN-Zap/zap-desktop/blob/m
Join us on [slack](https://join.slack.com/t/zaphq/shared_invite/MjI2MTY4NTcwMDUyLTE1MDI2OTA0ODAtNTRjMTY4YTNjNA) before tackling a todo to avoid duplicate work. This list will be updated daily to show what todos are being worked on
### Refactor
- [ ] Move Node.js proxy to [ipcRenderer](https://electron.atom.io/docs/api/ipc-renderer/) (roasbeef recommendation)
- [x] Move Node.js proxy to ipcRenderer. [Done](https://github.com/LN-Zap/zap-desktop/pull/4)
- [ ] Use two package.json [structure](https://github.com/electron-userland/electron-builder/wiki/Two-package.json-Structure)
- [ ] General refactor (I know this TODO sucks but the code is a bit sloppy still)

41
app/api/index.js

@ -1,39 +1,7 @@
import axios from 'axios'
export function callApi(endpoint, method = 'get', data = null) {
const BASE_URL = 'http://localhost:3000/api/'
let payload
if (data) {
payload = {
headers: {
'Content-Type': 'application/json'
},
method,
data,
url: `${BASE_URL}${endpoint}`
}
} else {
payload = {
headers: {
'Content-Type': 'application/json'
},
method,
url: `${BASE_URL}${endpoint}`
}
}
return axios(payload)
.then(response => response.data)
.catch(error => error)
}
export function callApis(endpoints) {
return axios.all(endpoints.map(endpoint => callApi(endpoint)))
}
export function requestTicker() {
const BASE_URL = 'https://api.coinmarketcap.com/v1/ticker/bitcoin/'
export function requestTicker(id) {
const BASE_URL = `https://api.coinmarketcap.com/v1/ticker/${id}/`
return axios({
method: 'get',
url: BASE_URL
@ -41,3 +9,8 @@ export function requestTicker() {
.then(response => response.data)
.catch(error => error)
}
export function requestTickers(ids) {
return axios.all(ids.map(id => requestTicker(id)))
.then(axios.spread((btcTicker, ltcTicker) => ({ btcTicker: btcTicker[0], ltcTicker: ltcTicker[0] })))
}

23
app/components/CryptoIcon/CryptoIcon.js

@ -0,0 +1,23 @@
import React from 'react'
import PropTypes from 'prop-types'
import path from 'path'
import { FaBitcoin } from 'react-icons/lib/fa'
import Isvg from 'react-inlinesvg'
const CryptoIcon = ({ currency, styles }) => {
switch (currency) {
case 'btc':
return <FaBitcoin style={styles} />
case 'ltc':
return <Isvg style={styles} src={path.join(__dirname, '..', 'resources/litecoin.svg')} />
default:
return <span />
}
}
CryptoIcon.propTypes = {
currency: PropTypes.string.isRequired,
styles: PropTypes.object
}
export default CryptoIcon

3
app/components/CryptoIcon/index.js

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

17
app/components/CurrencyIcon/CurrencyIcon.js

@ -0,0 +1,17 @@
import React from 'react'
import PropTypes from 'prop-types'
import { FaDollar } from 'react-icons/lib/fa'
import CryptoIcon from '../CryptoIcon'
const CurrencyIcon = ({ currency, crypto, styles }) => (currency === 'usd' ?
<FaDollar style={styles} />
:
<CryptoIcon styles={styles} currency={crypto} />)
CurrencyIcon.propTypes = {
currency: PropTypes.string.isRequired,
crypto: PropTypes.string.isRequired,
styles: PropTypes.object
}
export default CurrencyIcon

3
app/components/CurrencyIcon/index.js

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

9
app/lnd/config/index.js

@ -0,0 +1,9 @@
// Cert will be located depending on your machine
// Mac OS X: /Users/user/Library/Application Support/Lnd/tls.cert
// Linux: ~/.lnd/tls.cert
// Windows: TODO find out where cert is located for windows machine
export default {
lightningRpc: `${__dirname}/rpc.proto`,
lightningHost: 'localhost:10009',
cert: '/Users/jmow/Library/Application Support/Lnd/tls.cert'
}

1281
app/lnd/config/rpc.proto

File diff suppressed because it is too large

7
app/lnd/index.js

@ -0,0 +1,7 @@
import config from './config'
import lightning from './lib/lightning'
import methods from './methods'
const lnd = lightning(config.lightningRpc, config.lightningHost)
export default (event, msg, data) => methods(lnd, event, msg, data)

14
app/lnd/lib/lightning.js

@ -0,0 +1,14 @@
import fs from 'fs'
import grpc from 'grpc'
import config from '../config'
module.exports = (path, host) => {
process.env.GRPC_SSL_CIPHER_SUITES = 'HIGH+ECDSA'
const rpc = grpc.load(path)
const lndCert = fs.readFileSync(config.cert)
const credentials = grpc.credentials.createSsl(lndCert)
return new rpc.lnrpc.Lightning(host, credentials)
}

10
app/lnd/methods/channelbalance.js

@ -0,0 +1,10 @@
// LND Get Channel Balance
export default function channelbalance(lnd) {
return new Promise((resolve, reject) => {
lnd.channelBalance({}, (err, data) => {
if (err) { reject(err) }
resolve(data)
})
})
}

10
app/lnd/methods/channels.js

@ -0,0 +1,10 @@
// LND List Channels
export default function channels(lnd) {
return new Promise((resolve, reject) => {
lnd.listChannels({}, (err, data) => {
if (err) { reject(err) }
resolve(data)
})
})
}

20
app/lnd/methods/closechannel.js

@ -0,0 +1,20 @@
import bitcore from 'bitcore-lib'
import pushclosechannel from '../push/closechannel'
const BufferUtil = bitcore.util.buffer
export default function closechannel(lnd, event, payload) {
const tx = payload.channel_point.funding_txid.match(/.{2}/g).reverse().join('')
const res = {
channel_point: {
funding_txid: BufferUtil.hexToBuffer(tx),
output_index: Number(payload.channel_point.output_index)
}
}
return new Promise((resolve, reject) =>
pushclosechannel(lnd, event, res)
.then(data => resolve(data))
.catch(error => reject(error))
)
}

10
app/lnd/methods/connectpeer.js

@ -0,0 +1,10 @@
// LND Connect to a peer
export default function connectpeer(lnd, { pubkey, host }) {
return new Promise((resolve, reject) => {
lnd.connectPeer({ addr: { pubkey, host }, perm: true }, (err, data) => {
if (err) { reject(err) }
resolve(data)
})
})
}

10
app/lnd/methods/createinvoice.js

@ -0,0 +1,10 @@
// LND Create an invoice
export default function createInvoice(lnd, { memo, value }) {
return new Promise((resolve, reject) => {
lnd.addInvoice({ memo, value }, (err, data) => {
if (err) { reject(err) }
resolve(data)
})
})
}

10
app/lnd/methods/disconnectpeer.js

@ -0,0 +1,10 @@
// LND Disconnect from a peer
export default function disconnectpeer(lnd, { pubkey }) {
return new Promise((resolve, reject) => {
lnd.disconnectPeer({ pub_key: pubkey }, (err, data) => {
if (err) { reject(err) }
resolve(data)
})
})
}

147
app/lnd/methods/index.js

@ -0,0 +1,147 @@
/* eslint no-console: 0 */ // --> OFF
import channelbalance from './channelbalance'
import channels from './channels'
import closechannel from './closechannel'
import connectpeer from './connectpeer'
import createinvoice from './createinvoice'
import disconnectpeer from './disconnectpeer'
import info from './info'
import invoice from './invoice'
import invoices from './invoices'
import newaddress from './newaddress'
import openchannel from './openchannel'
import payinvoice from './payinvoice'
import payments from './payments'
import peers from './peers'
import pendingchannels from './pendingchannels'
import sendcoins from './sendcoins'
import walletbalance from './walletbalance'
export default function (lnd, event, msg, data) {
switch (msg) {
case 'info':
info(lnd)
.then((infoData) => {
event.sender.send('receiveInfo', infoData)
event.sender.send('receiveCryptocurrency', infoData.chains[0])
})
.catch(error => console.log('info error: ', error))
break
case 'newaddress':
// Data looks like { address: '' }
newaddress(lnd, data.type)
.then(({ address }) => event.sender.send('receiveAddress', address))
.catch(error => console.log('newaddress error: ', error))
break
case 'peers':
// Data looks like { peers: [] }
peers(lnd)
.then(peersData => event.sender.send('receivePeers', peersData))
.catch(error => console.log('peers error: ', error))
break
case 'channels':
// Data looks like
// [ { channels: [] }, { total_limbo_balance: 0, pending_open_channels: [], pending_closing_channels: [], pending_force_closing_channels: [] } ]
Promise.all([channels, pendingchannels].map(func => func(lnd)))
.then(channelsData =>
event.sender.send('receiveChannels', { channels: channelsData[0].channels, pendingChannels: channelsData[1] })
)
.catch(error => console.log('channels error: ', error))
break
case 'payments':
// Data looks like { payments: [] }
payments(lnd)
.then(paymentsData => event.sender.send('receivePayments', paymentsData))
.catch(error => console.log('payments error: ', error))
break
case 'invoices':
// Data looks like { invoices: [] }
invoices(lnd)
.then(invoicesData => event.sender.send('receiveInvoices', invoicesData))
.catch(error => console.log('invoices error: ', error))
break
case 'invoice':
// Data looks like { invoices: [] }
invoice(data.payreq)
.then(invoiceData => event.sender.send('receiveInvoice', invoiceData))
.catch(error => console.log('invoice error: ', error))
break
case 'balance':
// Balance looks like [ { balance: '129477456' }, { balance: '243914' } ]
Promise.all([walletbalance, channelbalance].map(func => func(lnd)))
.then(balance => event.sender.send('receiveBalance', { walletBalance: balance[0].balance, channelBalance: balance[1].balance }))
.catch(error => console.log('balance error: ', error))
break
case 'createInvoice':
// Invoice looks like { r_hash: Buffer, payment_request: '' }
// { memo, value } = data
createinvoice(lnd, data)
.then(newinvoice =>
event.sender.send(
'createdInvoice',
Object.assign(newinvoice, { memo: data.memo, value: data.value, r_hash: new Buffer(newinvoice.r_hash, 'hex').toString('hex') })
)
)
.catch(error => console.log('createInvoice error: ', error))
break
case 'sendPayment':
// Payment looks like { payment_preimage: Buffer, payment_route: Object }
// { paymentRequest } = data
payinvoice(lnd, data)
.then(({ payment_route }) => event.sender.send('paymentSuccessful', Object.assign(data, { payment_route })))
.catch(error => console.log('payinvoice error: ', error))
break
case 'sendCoins':
// Transaction looks like { txid: String }
// { addr, amount } = data
sendcoins(lnd, data)
.then((transaction) => {
console.log('transaction: ', transaction)
event.sender.send('sendSuccessful', { transaction })
})
.catch(error => console.log('sendcoins error: ', error))
break
case 'openChannel':
// Response is empty. Streaming updates on channel status and updates
// { pubkey, localamt, pushamt } = data
openchannel(lnd, event, data)
.then((channel) => {
console.log('CHANNEL: ', channel)
event.sender.send('channelSuccessful', { channel })
})
.catch(error => console.log('openChannel error: ', error))
break
case 'closeChannel':
// Response is empty. Streaming updates on channel status and updates
// { channel_point, force } = data
closechannel(lnd, event, data)
.then((result) => {
console.log('CLOSE CHANNEL: ', result)
event.sender.send('closeChannelSuccessful')
})
.catch(error => console.log('closeChannel error: ', error))
break
case 'connectPeer':
// Returns a peer_id. Pass the pubkey, host and peer_id so we can add a new peer to the list
// { pubkey, host } = data
connectpeer(lnd, data)
.then(({ peer_id }) => {
console.log('peer_id: ', peer_id)
event.sender.send('connectSuccess', { pub_key: data.pubkey, address: data.host, peer_id })
})
.catch(error => console.log('connectPeer error: ', error))
break
case 'disconnectPeer':
// Empty response. Pass back pubkey on success to remove it from the peers list
// { pubkey } = data
disconnectpeer(lnd, data)
.then(() => {
console.log('pubkey: ', data.pubkey)
event.sender.send('disconnectSuccess', { pubkey: data.pubkey })
})
.catch(error => console.log('disconnectPeer error: ', error))
break
default:
}
}

10
app/lnd/methods/info.js

@ -0,0 +1,10 @@
// LND Get Info
export default function info(lnd) {
return new Promise((resolve, reject) => {
lnd.getInfo({}, (err, data) => {
if (err) { reject(err) }
resolve(data)
})
})
}

12
app/lnd/methods/invoice.js

@ -0,0 +1,12 @@
import { decodeInvoice } from '../utils'
// LND Get Invoice
export default function invoice(payreq) {
return new Promise((resolve, reject) => {
try {
resolve(decodeInvoice(payreq))
} catch (error) {
reject(error)
}
})
}

10
app/lnd/methods/invoices.js

@ -0,0 +1,10 @@
// LND Get Invoices
export default function invoices(lnd) {
return new Promise((resolve, reject) => {
lnd.listInvoices({}, (err, data) => {
if (err) { reject(err) }
resolve(data)
})
})
}

10
app/lnd/methods/newaddress.js

@ -0,0 +1,10 @@
// LND Generate New Address
export default function info(lnd, type) {
return new Promise((resolve, reject) => {
lnd.newAddress({ type }, (err, data) => {
if (err) { reject(err) }
resolve(data)
})
})
}

19
app/lnd/methods/openchannel.js

@ -0,0 +1,19 @@
import bitcore from 'bitcore-lib'
import pushopenchannel from '../push/openchannel'
const BufferUtil = bitcore.util.buffer
export default function openchannel(lnd, event, payload) {
const { pubkey, localamt, pushamt } = payload
const res = {
node_pubkey: BufferUtil.hexToBuffer(pubkey),
local_funding_amount: Number(localamt),
push_sat: Number(pushamt)
}
return new Promise((resolve, reject) =>
pushopenchannel(lnd, event, res)
.then(data => resolve(data))
.catch(error => reject(error))
)
}

10
app/lnd/methods/payinvoice.js

@ -0,0 +1,10 @@
// LND Pay an invoice
export default function payinvoice(lnd, { paymentRequest }) {
return new Promise((resolve, reject) => {
lnd.sendPaymentSync({ payment_request: paymentRequest }, (err, data) => {
if (err) { reject(err) }
resolve(data)
})
})
}

10
app/lnd/methods/payments.js

@ -0,0 +1,10 @@
// LND Get Payments
export default function payments(lnd) {
return new Promise((resolve, reject) => {
lnd.listPayments({}, (err, data) => {
if (err) { reject(err) }
resolve(data)
})
})
}

10
app/lnd/methods/peers.js

@ -0,0 +1,10 @@
// LND List Peers
export default function peers(lnd) {
return new Promise((resolve, reject) => {
lnd.listPeers({}, (err, data) => {
if (err) { reject(err) }
resolve(data)
})
})
}

10
app/lnd/methods/pendingchannels.js

@ -0,0 +1,10 @@
// LND Get Pending Channels
export default function channels(lnd) {
return new Promise((resolve, reject) => {
lnd.pendingChannels({}, (err, data) => {
if (err) { reject(err) }
resolve(data)
})
})
}

10
app/lnd/methods/sendcoins.js

@ -0,0 +1,10 @@
// LND send coins on chain
export default function sendcoins(lnd, { addr, amount }) {
return new Promise((resolve, reject) => {
lnd.sendCoins({ addr, amount }, (err, data) => {
if (err) { reject(err) }
resolve(data)
})
})
}

10
app/lnd/methods/walletbalance.js

@ -0,0 +1,10 @@
// LND Get Wallet Balance
export default function walletbalance(lnd) {
return new Promise((resolve, reject) => {
lnd.walletBalance({}, (err, data) => {
if (err) { reject(err) }
resolve(data)
})
})
}

16
app/lnd/push/closechannel.js

@ -0,0 +1,16 @@
export default function pushclosechannel(lnd, event, payload) {
return new Promise((resolve, reject) => {
try {
const call = lnd.closeChannel(payload)
call.on('data', data => event.sender.send('pushclosechannelupdated', { data }))
call.on('end', () => event.sender.send('pushclosechannelend'))
call.on('error', error => event.sender.send('pushclosechannelerror', { error }))
call.on('status', status => event.sender.send('pushclosechannelstatus', { status }))
resolve(null, payload)
} catch (error) {
reject(error, null)
}
})
}

16
app/lnd/push/openchannel.js

@ -0,0 +1,16 @@
export default function pushopenchannel(lnd, event, payload) {
return new Promise((resolve, reject) => {
try {
const call = lnd.openChannel(payload)
call.on('data', data => event.sender.send('pushchannelupdated', { data }))
call.on('end', () => event.sender.send('pushchannelend'))
call.on('error', error => event.sender.send('pushchannelerror', { error }))
call.on('status', status => event.sender.send('pushchannelstatus', { status }))
resolve(null, payload)
} catch (error) {
reject(error, null)
}
})
}

38
app/lnd/utils/index.js

@ -0,0 +1,38 @@
import zbase32 from 'zbase32'
function convertBigEndianBufferToLong(longBuffer) {
let longValue = 0
const byteArray = Buffer.from(longBuffer).swap64()
for (let i = byteArray.length - 1; i >= 0; i -= 1) {
longValue = (longValue * 256) + byteArray[i]
}
return longValue
}
export function decodeInvoice(payreq) {
const payreqBase32 = zbase32.decode(payreq)
const bufferHexRotated = Buffer.from(payreqBase32).toString('hex')
const bufferHex = bufferHexRotated.substr(bufferHexRotated.length - 1, bufferHexRotated.length)
+ bufferHexRotated.substr(0, bufferHexRotated.length - 1)
const buffer = Buffer.from(bufferHex, 'hex')
const paymentHashBuffer = buffer.slice(33, 65)
const paymentHashHex = paymentHashBuffer.toString('hex')
const valueBuffer = buffer.slice(65, 73)
const amount = convertBigEndianBufferToLong(valueBuffer)
return {
payreq,
amount,
r_hash: paymentHashHex
}
}
export default {
decodeInvoice
}

10
app/main.dev.js

@ -11,8 +11,9 @@
*
* @flow
*/
import { app, BrowserWindow } from 'electron'
import { app, BrowserWindow, ipcMain } from 'electron'
import MenuBuilder from './menu'
import lnd from './lnd'
let mainWindow = null;
@ -28,7 +29,7 @@ if (process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true')
require('module').globalPaths.push(p);
// set icon
app.dock.setIcon(`${path.join(__dirname, '..', 'resources')}/zap_2.png`)
// app.dock.setIcon(`${path.join(__dirname, '..', 'resources')}/zap_2.png`)
}
const installExtensions = async () => {
@ -90,3 +91,8 @@ app.on('ready', async () => {
const menuBuilder = new MenuBuilder(mainWindow);
menuBuilder.buildMenu();
});
ipcMain.on('lnd', (event, { msg, data }) => {
lnd(event, msg, data)
})

54
app/reducers/activity.js

@ -1,54 +0,0 @@
import { callApis } from '../api'
// ------------------------------------
// Constants
// ------------------------------------
export const GET_ACTIVITY = 'GET_ACTIVITY'
export const RECEIVE_ACTIVITY = 'RECEIVE_ACTIVITY'
// ------------------------------------
// Actions
// ------------------------------------
export function getActivity() {
return {
type: GET_ACTIVITY
}
}
export function receiveActvity(data) {
return {
type: RECEIVE_ACTIVITY,
payments: data[0].data.payments.reverse(),
invoices: data[1].data.invoices.reverse()
}
}
export const fetchActivity = () => async (dispatch) => {
dispatch(getActivity())
const activity = await callApis(['payments', 'invoices'])
dispatch(receiveActvity(activity))
}
// ------------------------------------
// Action Handlers
// ------------------------------------
const ACTION_HANDLERS = {
[GET_ACTIVITY]: state => ({ ...state, activityLoading: true }),
[RECEIVE_ACTIVITY]: (state, { payments, invoices }) => (
{ ...state, activityLoading: false, payments, invoices }
)
}
// ------------------------------------
// Reducer
// ------------------------------------
const initialState = {
activityLoading: false,
payments: [],
invoices: []
}
export default function activityReducer(state = initialState, action) {
const handler = ACTION_HANDLERS[action.type]
return handler ? handler(state, action) : state
}

53
app/reducers/address.js

@ -0,0 +1,53 @@
import { ipcRenderer } from 'electron'
// ------------------------------------
// Constants
// ------------------------------------
export const GET_ADDRESS = 'GET_ADDRESS'
export const RECEIVE_ADDRESS = 'RECEIVE_ADDRESS'
// LND expects types to be sent as int, so this object will allow mapping from string to int
const addressTypes = {
p2wkh: 0,
np2wkh: 1,
p2pkh: 2
}
// ------------------------------------
// Actions
// ------------------------------------
export function getAddress() {
return {
type: GET_ADDRESS
}
}
// Send IPC event for getinfo
export const newAddress = type => async (dispatch) => {
dispatch(getAddress())
ipcRenderer.send('lnd', { msg: 'newaddress', data: { type: addressTypes[type] } })
}
// Receive IPC event for info
export const receiveAddress = (event, address) => dispatch => dispatch({ type: RECEIVE_ADDRESS, address })
// ------------------------------------
// Action Handlers
// ------------------------------------
const ACTION_HANDLERS = {
[GET_ADDRESS]: state => ({ ...state, addressLoading: true }),
[RECEIVE_ADDRESS]: (state, { address }) => ({ ...state, addressLoading: false, address })
}
// ------------------------------------
// Reducer
// ------------------------------------
const initialState = {
addressLoading: false,
address: ''
}
export default function addressReducer(state = initialState, action) {
const handler = ACTION_HANDLERS[action.type]
return handler ? handler(state, action) : state
}

19
app/reducers/balance.js

@ -1,4 +1,4 @@
import { callApis } from '../api'
import { ipcRenderer } from 'electron'
// ------------------------------------
// Constants
// ------------------------------------
@ -14,20 +14,17 @@ export function getBalance() {
}
}
export function receiveBalance(data) {
return {
type: RECEIVE_BALANCE,
walletBalance: data[0].data.balance,
channelBalance: data[1].data.balance
}
}
// Send IPC event for balance
export const fetchBalance = () => async (dispatch) => {
dispatch(getBalance())
const balance = await callApis(['wallet_balance', 'channel_balance'])
dispatch(receiveBalance(balance))
ipcRenderer.send('lnd', { msg: 'balance' })
}
// Receive IPC event for balance
export const receiveBalance = (event, { walletBalance, channelBalance }) => dispatch => (
dispatch({ type: RECEIVE_BALANCE, walletBalance, channelBalance })
)
// ------------------------------------
// Action Handlers
// ------------------------------------

110
app/reducers/channels.js

@ -1,5 +1,5 @@
import { createSelector } from 'reselect'
import { callApi, callApis } from '../api'
import { ipcRenderer } from 'electron'
// ------------------------------------
// Constants
// ------------------------------------
@ -14,6 +14,10 @@ export const OPENING_CHANNEL = 'OPENING_CHANNEL'
export const OPENING_SUCCESSFUL = 'OPENING_SUCCESSFUL'
export const OPENING_FAILURE = 'OPENING_FAILURE'
export const CLOSING_CHANNEL = 'CLOSING_CHANNEL'
export const CLOSING_SUCCESSFUL = 'CLOSING_SUCCESSFUL'
export const CLOSING_FAILURE = 'CLOSING_FAILURE'
// ------------------------------------
// Actions
// ------------------------------------
@ -38,17 +42,15 @@ export function getChannels() {
}
}
export function receiveChannels(channels) {
export function openingChannel() {
return {
type: RECEIVE_CHANNELS,
channels: channels[0].data.channels,
pendingChannels: channels[1].data
type: OPENING_CHANNEL
}
}
export function openingChannel() {
export function closingChannel() {
return {
type: OPENING_CHANNEL
type: CLOSING_CHANNEL
}
}
@ -64,24 +66,90 @@ export function openingFailure() {
}
}
// Send IPC event for peers
export const fetchChannels = () => async (dispatch) => {
dispatch(getChannels())
const channels = await callApis(['channels', 'pending_channels'])
dispatch(receiveChannels(channels))
ipcRenderer.send('lnd', { msg: 'channels' })
}
export const openChannel = ({ pubkey, localamt, pushamt }) => async (dispatch) => {
const payload = { pubkey, localamt, pushamt }
// Receive IPC event for channels
export const receiveChannels = (event, { channels, pendingChannels }) => dispatch => dispatch({ type: RECEIVE_CHANNELS, channels, pendingChannels })
// Send IPC event for opening a channel
export const openChannel = ({ pubkey, localamt, pushamt }) => (dispatch) => {
dispatch(openingChannel())
const channel = await callApi('addchannel', 'post', payload)
ipcRenderer.send('lnd', { msg: 'openChannel', data: { pubkey, localamt, pushamt } })
}
if (channel.data) {
dispatch(openingSuccessful())
} else {
dispatch(openingFailure())
}
// TODO: Decide how to handle streamed updates for channels
// Receive IPC event for openChannel
export const channelSuccessful = () => (dispatch) => {
dispatch(fetchChannels())
}
// Receive IPC event for updated channel
export const pushchannelupdated = () => (dispatch) => {
dispatch(fetchChannels())
}
// Receive IPC event for channel end
export const pushchannelend = () => (dispatch) => {
dispatch(fetchChannels())
}
// Receive IPC event for channel error
export const pushchannelerror = () => (dispatch) => {
dispatch(fetchChannels())
}
// Receive IPC event for channel status
export const pushchannelstatus = () => (dispatch) => {
dispatch(fetchChannels())
}
// Send IPC event for opening a channel
export const closeChannel = ({ channel_point }) => (dispatch) => {
dispatch(closingChannel())
const channelPoint = channel_point.split(':')
ipcRenderer.send(
'lnd',
{
msg: 'closeChannel',
data: {
channel_point: {
funding_txid: channelPoint[0],
output_index: channelPoint[1]
},
force: true
}
}
)
}
// TODO: Decide how to handle streamed updates for closing channels
// Receive IPC event for closeChannel
export const closeChannelSuccessful = () => (dispatch) => {
dispatch(fetchChannels())
}
// Receive IPC event for updated closing channel
export const pushclosechannelupdated = () => (dispatch) => {
dispatch(fetchChannels())
}
// Receive IPC event for closing channel end
export const pushclosechannelend = () => (dispatch) => {
dispatch(fetchChannels())
}
// Receive IPC event for closing channel error
export const pushclosechannelerror = () => (dispatch) => {
dispatch(fetchChannels())
}
return channel
// Receive IPC event for closing channel status
export const pushclosechannelstatus = () => (dispatch) => {
dispatch(fetchChannels())
}
// ------------------------------------
@ -99,7 +167,8 @@ const ACTION_HANDLERS = {
{ ...state, channelsLoading: false, channels, pendingChannels }
),
[OPENING_CHANNEL]: state => ({ ...state, openingChannel: true })
[OPENING_CHANNEL]: state => ({ ...state, openingChannel: true }),
[CLOSING_CHANNEL]: state => ({ ...state, closingChannel: true })
}
const channelsSelectors = {}
@ -145,7 +214,8 @@ const initialState = {
local_amt: '',
push_amt: ''
},
openingChannel: false
openingChannel: false,
closingChannel: false
}
export default function channelsReducer(state = initialState, action) {

4
app/reducers/index.js

@ -9,7 +9,7 @@ import peers from './peers'
import channels from './channels'
import form from './form'
import invoice from './invoice'
import activity from './activity'
import address from './address'
const rootReducer = combineReducers({
router,
@ -21,7 +21,7 @@ const rootReducer = combineReducers({
channels,
form,
invoice,
activity
address
})
export default rootReducer

33
app/reducers/info.js

@ -1,4 +1,5 @@
import { callApi } from '../api'
import { createSelector } from 'reselect'
import { ipcRenderer } from 'electron'
// ------------------------------------
// Constants
// ------------------------------------
@ -14,19 +15,15 @@ export function getInfo() {
}
}
export function receiveInfo(data) {
return {
type: RECEIVE_INFO,
data
}
}
// Send IPC event for getinfo
export const fetchInfo = () => async (dispatch) => {
dispatch(getInfo())
const info = await callApi('info')
dispatch(receiveInfo(info.data))
ipcRenderer.send('lnd', { msg: 'info' })
}
// Receive IPC event for info
export const receiveInfo = (event, data) => dispatch => dispatch({ type: RECEIVE_INFO, data })
// ------------------------------------
// Action Handlers
// ------------------------------------
@ -43,6 +40,22 @@ const initialState = {
data: {}
}
// Selectors
const infoSelectors = {}
const testnetSelector = state => state.info.data.testnet
infoSelectors.isTestnet = createSelector(
testnetSelector,
isTestnet => (!!isTestnet)
)
infoSelectors.explorerLinkBase = createSelector(
infoSelectors.isTestnet,
isTestnet => (isTestnet ? 'https://testnet.smartbit.com.au' : 'https://smartbit.com.au')
)
export { infoSelectors }
export default function infoReducer(state = initialState, action) {
const handler = ACTION_HANDLERS[action.type]

69
app/reducers/invoice.js

@ -1,5 +1,5 @@
import { createSelector } from 'reselect'
import { callApi } from '../api'
import { ipcRenderer } from 'electron'
import { btc, usd } from '../utils'
// ------------------------------------
// Constants
@ -50,82 +50,51 @@ export function receiveInvoice(invoice) {
}
}
export function receiveFormInvoice(formInvoice) {
return {
type: RECEIVE_FORM_INVOICE,
formInvoice
}
}
export function getInvoices() {
return {
type: GET_INVOICES
}
}
export function receiveInvoices(data) {
return {
type: RECEIVE_INVOICES,
invoices: data.invoices.reverse()
}
}
export function sendInvoice() {
return {
type: SEND_INVOICE
}
}
export function invoiceSuccessful(invoice) {
return {
type: INVOICE_SUCCESSFUL,
invoice
}
}
export function invoiceFailed() {
return {
type: INVOICE_FAILED
}
}
export const fetchInvoice = payreq => async (dispatch) => {
// Send IPC event for a specific invoice
export const fetchInvoice = payreq => (dispatch) => {
dispatch(getInvoice())
const invoice = await callApi(`invoice/${payreq}`, 'get')
if (invoice) {
dispatch(receiveFormInvoice(invoice.data))
return true
}
dispatch(invoiceFailed())
return false
ipcRenderer.send('lnd', { msg: 'invoice', data: { payreq } })
}
export const fetchInvoices = () => async (dispatch) => {
dispatch(getInvoice())
const invoices = await callApi('invoices')
if (invoices) {
dispatch(receiveInvoices(invoices.data))
} else {
dispatch(invoiceFailed())
}
// Receive IPC event for form invoice
export const receiveFormInvoice = (event, formInvoice) => dispatch => dispatch({ type: RECEIVE_FORM_INVOICE, formInvoice })
return invoices
// Send IPC event for invoices
export const fetchInvoices = () => (dispatch) => {
dispatch(getInvoices())
ipcRenderer.send('lnd', { msg: 'invoices' })
}
export const createInvoice = (amount, memo, currency, rate) => async (dispatch) => {
const value = currency === 'btc' ? btc.btcToSatoshis(amount) : btc.btcToSatoshis(usd.usdToBtc(amount, rate))
// Receive IPC event for invoices
export const receiveInvoices = (event, { invoices }) => dispatch => dispatch({ type: RECEIVE_INVOICES, invoices })
// Send IPC event for creating an invoice
export const createInvoice = (amount, memo, currency, rate) => (dispatch) => {
const value = currency === 'btc' ? btc.btcToSatoshis(amount) : btc.btcToSatoshis(usd.usdToBtc(amount, rate))
dispatch(sendInvoice())
const invoice = await callApi('addinvoice', 'post', { value, memo })
if (invoice) {
dispatch(invoiceSuccessful({ memo, value, payment_request: invoice.data.payment_request }))
} else {
dispatch(invoiceFailed())
}
return invoice
ipcRenderer.send('lnd', { msg: 'createInvoice', data: { value, memo } })
}
// Receive IPC event for newly created invoice
export const createdInvoice = (event, invoice) => dispatch => dispatch({ type: INVOICE_SUCCESSFUL, invoice })
// ------------------------------------
// Action Handlers
// ------------------------------------

62
app/reducers/ipc.js

@ -0,0 +1,62 @@
import createIpc from 'redux-electron-ipc'
import { receiveInfo } from './info'
import { receiveAddress } from './address'
import { receiveCryptocurrency } from './ticker'
import { receivePeers, connectSuccess, disconnectSuccess } from './peers'
import {
receiveChannels,
channelSuccessful,
pushchannelupdated,
pushchannelend,
pushchannelerror,
pushchannelstatus,
closeChannelSuccessful,
pushclosechannelupdated,
pushclosechannelend,
pushclosechannelerror,
pushclosechannelstatus
} from './channels'
import { receivePayments, paymentSuccessful } from './payment'
import { receiveInvoices, createdInvoice, receiveFormInvoice } from './invoice'
import { receiveBalance } from './balance'
// Import all receiving IPC event handlers and pass them into createIpc
const ipc = createIpc({
receiveInfo,
receivePeers,
receiveChannels,
receivePayments,
receiveInvoices,
receiveInvoice: receiveFormInvoice,
createdInvoice,
receiveBalance,
paymentSuccessful,
channelSuccessful,
pushchannelupdated,
pushchannelend,
pushchannelerror,
pushchannelstatus,
closeChannelSuccessful,
pushclosechannelupdated,
pushclosechannelend,
pushclosechannelerror,
pushclosechannelstatus,
connectSuccess,
disconnectSuccess,
receiveAddress,
receiveCryptocurrency
})
export default ipc

41
app/reducers/payment.js

@ -1,5 +1,5 @@
import { createSelector } from 'reselect'
import { callApi } from '../api'
import { ipcRenderer } from 'electron'
// ------------------------------------
// Constants
@ -29,13 +29,6 @@ export function getPayments() {
}
}
export function receivePayments(data) {
return {
type: RECEIVE_PAYMENTS,
payments: data.payments.reverse()
}
}
export function sendPayment() {
return {
type: SEND_PAYMENT
@ -55,32 +48,24 @@ export function paymentFailed() {
}
}
export const fetchPayments = () => async (dispatch) => {
// Send IPC event for payments
export const fetchPayments = () => (dispatch) => {
dispatch(getPayments())
const payments = await callApi('payments')
if (payments) {
dispatch(receivePayments(payments.data))
} else {
dispatch(paymentFailed())
}
return payments
ipcRenderer.send('lnd', { msg: 'payments' })
}
export const payInvoice = payment_request => async (dispatch) => {
dispatch(sendPayment())
const payment = await callApi('sendpayment', 'post', { payment_request })
if (payment) {
dispatch(fetchPayments())
} else {
dispatch(paymentFailed())
}
// Receive IPC event for payments
export const receivePayments = (event, { payments }) => dispatch => dispatch({ type: RECEIVE_PAYMENTS, payments })
return payment
export const payInvoice = paymentRequest => (dispatch) => {
dispatch(sendPayment())
ipcRenderer.send('lnd', { msg: 'sendPayment', data: { paymentRequest } })
}
// Receive IPC event for successful payment
// TODO: Add payment to state, not a total re-fetch
export const paymentSuccessful = () => fetchPayments()
// ------------------------------------
// Action Handlers

68
app/reducers/peers.js

@ -1,5 +1,5 @@
import { createSelector } from 'reselect'
import { callApi } from '../api'
import { ipcRenderer } from 'electron'
// ------------------------------------
// Constants
// ------------------------------------
@ -27,13 +27,6 @@ export function connectPeer() {
}
}
export function connectSuccess(peer) {
return {
type: CONNECT_SUCCESS,
peer
}
}
export function connectFailure() {
return {
type: CONNECT_FAILURE
@ -46,13 +39,6 @@ export function disconnectPeer() {
}
}
export function disconnectSuccess(pubkey) {
return {
type: DISCONNECT_SUCCESS,
pubkey
}
}
export function disconnectFailure() {
return {
type: DISCONNECT_FAILURE
@ -79,53 +65,47 @@ export function getPeers() {
}
}
export function receivePeers({ peers }) {
return {
type: RECEIVE_PEERS,
peers
}
}
// Send IPC event for peers
export const fetchPeers = () => async (dispatch) => {
dispatch(getPeers())
const peers = await callApi('peers')
dispatch(receivePeers(peers.data))
ipcRenderer.send('lnd', { msg: 'peers' })
}
export const connectRequest = ({ pubkey, host }) => async (dispatch) => {
dispatch(connectPeer())
const success = await callApi('connect', 'post', { pubkey, host })
if (success.data) {
dispatch(connectSuccess({ pub_key: pubkey, address: host, peer_id: success.data.peer_id }))
} else {
dispatch(connectFailure())
}
// Receive IPC event for peers
export const receivePeers = (event, { peers }) => dispatch => dispatch({ type: RECEIVE_PEERS, peers })
return success
// Send IPC event for connecting to a peer
export const connectRequest = ({ pubkey, host }) => (dispatch) => {
dispatch(connectPeer())
ipcRenderer.send('lnd', { msg: 'connectPeer', data: { pubkey, host } })
}
export const disconnectRequest = ({ pubkey }) => async (dispatch) => {
dispatch(disconnectPeer())
const success = await callApi('disconnect', 'post', { pubkey })
if (success) {
dispatch(disconnectSuccess(pubkey))
} else {
dispatch(disconnectFailure())
}
// Send IPC receive for successfully connecting to a peer
export const connectSuccess = (event, peer) => dispatch => dispatch({ type: CONNECT_SUCCESS, peer })
return success
// Send IPC send for disconnecting from a peer
export const disconnectRequest = ({ pubkey }) => (dispatch) => {
dispatch(disconnectPeer())
ipcRenderer.send('lnd', { msg: 'disconnectPeer', data: { pubkey } })
}
// Send IPC receive for successfully disconnecting from a peer
export const disconnectSuccess = (event, { pubkey }) => dispatch => dispatch({ type: DISCONNECT_SUCCESS, pubkey })
// ------------------------------------
// Action Handlers
// ------------------------------------
const ACTION_HANDLERS = {
[DISCONNECT_PEER]: state => ({ ...state, disconnecting: true }),
[DISCONNECT_SUCCESS]: (state, { pubkey }) => ({ ...state, disconnecting: false, peers: state.peers.filter(peer => peer.pub_key !== pubkey) }),
[DISCONNECT_SUCCESS]: (state, { pubkey }) => (
{ ...state, disconnecting: false, peer: null, peers: state.peers.filter(peer => peer.pub_key !== pubkey) }
),
[DISCONNECT_FAILURE]: state => ({ ...state, disconnecting: false }),
[CONNECT_PEER]: state => ({ ...state, connecting: true }),
[CONNECT_SUCCESS]: (state, { peer }) => ({ ...state, connecting: false, peers: [...state.peers, peer] }),
[CONNECT_SUCCESS]: (state, { peer }) => (
{ ...state, connecting: false, peerForm: { pubkey: '', host: '', isOpen: false }, peers: [...state.peers, peer] }
),
[CONNECT_FAILURE]: state => ({ ...state, connecting: false }),
[SET_PEER_FORM]: (state, { form }) => ({ ...state, peerForm: Object.assign({}, state.peerForm, form) }),

76
app/reducers/ticker.js

@ -1,10 +1,18 @@
import { requestTicker } from '../api'
import { createSelector } from 'reselect'
import { requestTickers } from '../api'
// ------------------------------------
// Constants
// ------------------------------------
export const SET_CURRENCY = 'SET_CURRENCY'
export const GET_TICKER = 'GET_TICKER'
export const RECIEVE_TICKER = 'RECIEVE_TICKER'
export const SET_CRYPTO = 'SET_CRYPTO'
export const GET_TICKERS = 'GET_TICKERS'
export const RECIEVE_TICKERS = 'RECIEVE_TICKERS'
// Map for crypto names to crypto tickers
const cryptoTickers = {
bitcoin: 'btc',
litecoin: 'ltc'
}
// ------------------------------------
// Actions
@ -16,46 +24,78 @@ export function setCurrency(currency) {
}
}
export function getTicker() {
export function setCrypto(crypto) {
return {
type: SET_CRYPTO,
crypto
}
}
export function getTickers() {
return {
type: GET_TICKER
type: GET_TICKERS
}
}
export function recieveTicker(ticker) {
export function recieveTickers({ btcTicker, ltcTicker }) {
return {
type: RECIEVE_TICKER,
ticker
type: RECIEVE_TICKERS,
btcTicker,
ltcTicker
}
}
export const fetchTicker = () => async (dispatch) => {
dispatch(getTicker())
const ticker = await requestTicker()
dispatch(recieveTicker(ticker))
dispatch(getTickers())
const tickers = await requestTickers(['bitcoin', 'litecoin'])
dispatch(recieveTickers(tickers))
return ticker
return tickers
}
// Receive IPC event for receiveCryptocurrency
export const receiveCryptocurrency = (event, currency) => (dispatch) => {
dispatch({ type: SET_CURRENCY, currency: cryptoTickers[currency] })
dispatch({ type: SET_CRYPTO, crypto: cryptoTickers[currency] })
}
// ------------------------------------
// Action Handlers
// ------------------------------------
const ACTION_HANDLERS = {
[SET_CURRENCY]: (state, { currency }) => ({ ...state, currency }),
[GET_TICKER]: state => ({ ...state, tickerLoading: true }),
[RECIEVE_TICKER]: (state, { ticker }) => (
{ ...state, tickerLoading: false, btcTicker: ticker[0] }
[SET_CRYPTO]: (state, { crypto }) => ({ ...state, crypto }),
[GET_TICKERS]: state => ({ ...state, tickerLoading: true }),
[RECIEVE_TICKERS]: (state, { btcTicker, ltcTicker }) => (
{ ...state, tickerLoading: false, btcTicker, ltcTicker }
)
}
// Selectors
const tickerSelectors = {}
const cryptoSelector = state => state.ticker.crypto
const bitcoinTickerSelector = state => state.ticker.btcTicker
const litecoinTickerSelector = state => state.ticker.ltcTicker
tickerSelectors.currentTicker = createSelector(
cryptoSelector,
bitcoinTickerSelector,
litecoinTickerSelector,
(crypto, btcTicker, ltcTicker) => (crypto === 'btc' ? btcTicker : ltcTicker)
)
export { tickerSelectors }
// ------------------------------------
// Reducer
// ------------------------------------
const initialState = {
tickerLoading: false,
currency: 'btc',
crypto: 'btc',
btcTicker: null
currency: '',
crypto: '',
btcTicker: null,
ltcTicker: null
}
export default function tickerReducer(state = initialState, action) {

8
app/routes/activity/components/Activity.js

@ -31,7 +31,8 @@ class Activity extends Component {
setPayment,
setInvoice,
paymentModalOpen,
invoiceModalOpen
invoiceModalOpen,
currentTicker
} = this.props
if (invoiceLoading || paymentLoading) { return <div>Loading...</div> }
@ -75,6 +76,7 @@ class Activity extends Component {
ticker={ticker}
setPayment={setPayment}
paymentModalOpen={paymentModalOpen}
currentTicker={currentTicker}
/>
:
<Invoices
@ -83,6 +85,7 @@ class Activity extends Component {
ticker={ticker}
setInvoice={setInvoice}
invoiceModalOpen={invoiceModalOpen}
currentTicker={currentTicker}
/>
}
</div>
@ -103,7 +106,8 @@ Activity.propTypes = {
setPayment: PropTypes.func.isRequired,
setInvoice: PropTypes.func.isRequired,
paymentModalOpen: PropTypes.bool.isRequired,
invoiceModalOpen: PropTypes.bool.isRequired
invoiceModalOpen: PropTypes.bool.isRequired,
currentTicker: PropTypes.object.isRequired
}
export default Activity

27
app/routes/activity/components/components/Invoices.js

@ -2,10 +2,10 @@ import React from 'react'
import PropTypes from 'prop-types'
import Moment from 'react-moment'
import 'moment-timezone'
import { FaBitcoin, FaDollar } from 'react-icons/lib/fa'
import { MdCheck } from 'react-icons/lib/md'
import QRCode from 'qrcode.react'
import Modal from './Modal'
import CurrencyIcon from '../../../../components/CurrencyIcon'
import { btc } from '../../../../utils'
import styles from './Invoices.scss'
@ -14,7 +14,8 @@ const Invoices = ({
invoices,
ticker,
setInvoice,
invoiceModalOpen
invoiceModalOpen,
currentTicker
}) => (
<div>
<Modal isOpen={invoiceModalOpen} resetObject={setInvoice}>
@ -23,18 +24,13 @@ const Invoices = ({
<div className={styles.invoiceModal}>
<h3>{invoice.memo}</h3>
<h1>
{
ticker.currency === 'btc' ?
<FaBitcoin style={{ verticalAlign: 'top' }} />
:
<FaDollar style={{ verticalAlign: 'top' }} />
}
<CurrencyIcon currency={ticker.currency} crypto={ticker.crypto} styles={{ verticalAlign: 'top' }} />
<span className={styles.value}>
{
ticker.currency === 'btc' ?
btc.satoshisToBtc(invoice.value)
ticker.currency === 'usd' ?
btc.satoshisToUsd(invoice.value, currentTicker.price_usd)
:
btc.satoshisToUsd(invoice.value, ticker.btcTicker.price_usd)
btc.satoshisToBtc(invoice.value)
}
</span>
</h1>
@ -88,10 +84,10 @@ const Invoices = ({
<div className={styles.right}>
<div className={invoiceItem.settled ? styles.settled : null}>
{
ticker.currency === 'btc' ?
btc.satoshisToBtc(invoiceItem.value)
ticker.currency === 'usd' ?
btc.satoshisToUsd(invoiceItem.value, currentTicker.price_usd)
:
btc.satoshisToUsd(invoiceItem.value, ticker.btcTicker.price_usd)
btc.satoshisToBtc(invoiceItem.value)
}
</div>
</div>
@ -107,7 +103,8 @@ Invoices.propTypes = {
invoices: PropTypes.array.isRequired,
ticker: PropTypes.object.isRequired,
setInvoice: PropTypes.func.isRequired,
invoiceModalOpen: PropTypes.bool.isRequired
invoiceModalOpen: PropTypes.bool.isRequired,
currentTicker: PropTypes.object.isRequired
}
export default Invoices

13
app/routes/activity/components/components/Invoices.scss

@ -17,7 +17,18 @@
margin: 20px 20px 60px 0;
svg {
font-size: 20px;
font-size: 30px;
vertical-align: top;
}
span svg[data-icon='ltc'] {
width: 30px;
height: 30px;
vertical-align: top;
g {
transform: scale(1.75) translate(-5px, -5px);
}
}
.value {

35
app/routes/activity/components/components/Payments.js

@ -2,8 +2,8 @@ import React from 'react'
import PropTypes from 'prop-types'
import Moment from 'react-moment'
import 'moment-timezone'
import { FaBitcoin, FaDollar } from 'react-icons/lib/fa'
import Modal from './Modal'
import CurrencyIcon from '../../../../components/CurrencyIcon'
import { btc } from '../../../../utils'
import styles from './Payments.scss'
@ -12,7 +12,8 @@ const Payments = ({
payments,
ticker,
setPayment,
paymentModalOpen
paymentModalOpen,
currentTicker
}) => (
<div>
<Modal isOpen={paymentModalOpen} resetObject={setPayment}>
@ -21,18 +22,13 @@ const Payments = ({
<div className={styles.paymentModal}>
<h3>{payment.payment_hash}</h3>
<h1>
{
ticker.currency === 'btc' ?
<FaBitcoin style={{ verticalAlign: 'top' }} />
:
<FaDollar style={{ verticalAlign: 'top' }} />
}
<CurrencyIcon currency={ticker.currency} crypto={ticker.crypto} styles={{ verticalAlign: 'top' }} />
<span className={styles.value}>
{
ticker.currency === 'btc' ?
btc.satoshisToBtc(payment.value)
ticker.currency === 'usd' ?
btc.satoshisToUsd(payment.value, currentTicker.price_usd)
:
btc.satoshisToUsd(payment.value, ticker.btcTicker.price_usd)
btc.satoshisToBtc(payment.value)
}
</span>
</h1>
@ -72,26 +68,26 @@ const Payments = ({
</div>
<div className={styles.center}>
<div className={styles.date}>
<Moment format='MMMM Do'>{paymentItem.creation_date * 1000}</Moment>
<Moment format='MMM Do'>{paymentItem.creation_date * 1000}</Moment>
</div>
</div>
<div className={styles.right}>
<span className={styles.fee}>
{
ticker.currency === 'btc' ?
btc.satoshisToBtc(paymentItem.fee)
ticker.currency === 'usd' ?
btc.satoshisToUsd(paymentItem.fee, currentTicker.price_usd)
:
btc.satoshisToUsd(paymentItem.fee, ticker.btcTicker.price_usd)
btc.satoshisToBtc(paymentItem.fee)
}
</span>
</div>
<div className={styles.right}>
<span className={styles.value}>
{
ticker.currency === 'btc' ?
btc.satoshisToBtc(paymentItem.value)
ticker.currency === 'usd' ?
btc.satoshisToUsd(paymentItem.value, currentTicker.price_usd)
:
btc.satoshisToUsd(paymentItem.value, ticker.btcTicker.price_usd)
btc.satoshisToBtc(paymentItem.value)
}
</span>
</div>
@ -107,7 +103,8 @@ Payments.propTypes = {
payments: PropTypes.array.isRequired,
ticker: PropTypes.object.isRequired,
setPayment: PropTypes.func.isRequired,
paymentModalOpen: PropTypes.bool.isRequired
paymentModalOpen: PropTypes.bool.isRequired,
currentTicker: PropTypes.object.isRequired
}
export default Payments

11
app/routes/activity/components/components/Payments.scss

@ -10,6 +10,17 @@
svg {
font-size: 30px;
vertical-align: top;
}
span svg[data-icon='ltc'] {
width: 30px;
height: 30px;
vertical-align: top;
g {
transform: scale(1.75) translate(-5px, -5px);
}
}
.value {

5
app/routes/activity/containers/ActivityContainer.js

@ -1,4 +1,5 @@
import { connect } from 'react-redux'
import { tickerSelectors } from '../../../reducers/ticker'
import {
fetchInvoices,
searchInvoices,
@ -31,7 +32,9 @@ const mapStateToProps = state => ({
ticker: state.ticker,
paymentModalOpen: paymentSelectors.paymentModalOpen(state),
invoiceModalOpen: invoiceSelectors.invoiceModalOpen(state)
invoiceModalOpen: invoiceSelectors.invoiceModalOpen(state),
currentTicker: tickerSelectors.currentTicker(state)
})
export default connect(mapStateToProps, mapDispatchToProps)(Activity)

20
app/routes/app/components/App.js

@ -2,20 +2,15 @@ import React, { Component } from 'react'
import PropTypes from 'prop-types'
import Form from './components/Form'
import Nav from './components/Nav'
import Socket from './components/Socket'
import styles from './App.scss'
export const CHANNEL_DATA = 'CHANNEL_DATA'
export const CHANNEL_END = 'CHANNEL_END'
export const CHANNEL_ERROR = 'CHANNEL_ERROR'
export const CHANNEL_STATUS = 'CHANNEL_STATUS'
class App extends Component {
componentWillMount() {
const { fetchTicker, fetchBalance } = this.props
const { fetchTicker, fetchBalance, fetchInfo } = this.props
fetchTicker()
fetchBalance()
fetchInfo()
}
render() {
@ -34,11 +29,13 @@ class App extends Component {
setForm,
createInvoice,
payInvoice,
fetchChannels,
fetchInvoice,
currentTicker,
children
} = this.props
if (!currentTicker) { return <div>Loading...</div> }
return (
<div>
<Form
@ -56,18 +53,18 @@ class App extends Component {
payInvoice={payInvoice}
fetchInvoice={fetchInvoice}
formInvoice={formInvoice}
currentTicker={currentTicker}
/>
<Nav
ticker={ticker}
balance={balance}
setCurrency={setCurrency}
formClicked={formType => setForm({ modalOpen: true, formType })}
currentTicker={currentTicker}
/>
<div className={styles.content}>
{children}
</div>
<Socket fetchChannels={fetchChannels} />
</div>
)
}
@ -90,8 +87,9 @@ App.propTypes = {
setForm: PropTypes.func.isRequired,
createInvoice: PropTypes.func.isRequired,
payInvoice: PropTypes.func.isRequired,
fetchChannels: PropTypes.func.isRequired,
fetchInvoice: PropTypes.func.isRequired,
fetchInfo: PropTypes.func.isRequired,
currentTicker: PropTypes.object,
children: PropTypes.object.isRequired
}

31
app/routes/app/components/components/Form/Form.js

@ -1,7 +1,7 @@
import React from 'react'
import PropTypes from 'prop-types'
import { FaDollar, FaBitcoin } from 'react-icons/lib/fa'
import { MdClose } from 'react-icons/lib/md'
import CurrencyIcon from '../../../../../components/CurrencyIcon'
import { btc } from '../../../../../utils'
import styles from './Form.scss'
@ -10,26 +10,23 @@ const Form = ({
setAmount,
setMessage,
setPaymentRequest,
ticker: { currency, btcTicker },
ticker: { currency, crypto },
isOpen,
close,
createInvoice,
payInvoice,
fetchInvoice,
formInvoice
formInvoice,
currentTicker
}) => {
const requestClicked = () => {
createInvoice(amount, message, currency, btcTicker.price_usd)
.then((success) => {
if (success) { close() }
})
createInvoice(amount, message, currency, currentTicker.price_usd)
close()
}
const payClicked = () => {
payInvoice(payment_request)
.then((success) => {
if (success) { close() }
})
close()
}
const paymentRequestOnChange = (payreq) => {
@ -37,7 +34,7 @@ const Form = ({
if (payreq.length === 124) { fetchInvoice(payreq) }
}
const calculateAmount = value => (currency === 'btc' ? btc.satoshisToBtc(value) : btc.satoshisToUsd(value, btcTicker.price_usd))
const calculateAmount = value => (currency === 'usd' ? btc.satoshisToUsd(value, currentTicker.price_usd) : btc.satoshisToBtc(value))
return (
<div className={`${styles.formContainer} ${isOpen ? styles.open : ''}`}>
@ -48,19 +45,14 @@ const Form = ({
<div className={styles.content}>
<section className={styles.amountContainer}>
<label htmlFor='amount'>
{
currency === 'btc' ?
<FaBitcoin />
:
<FaDollar />
}
<CurrencyIcon currency={currency} crypto={crypto} />
</label>
<input
type='text'
size=''
style={
formType === 'pay' ?
{ width: '75%', fontSize: '100px' }
{ width: '75%', fontSize: '85px' }
:
{ width: `${amount.length > 1 ? (amount.length * 15) - 5 : 25}%`, fontSize: `${190 - (amount.length ** 2)}px` }
}
@ -125,7 +117,8 @@ Form.propTypes = {
createInvoice: PropTypes.func.isRequired,
payInvoice: PropTypes.func.isRequired,
fetchInvoice: PropTypes.func.isRequired,
formInvoice: PropTypes.object.isRequired
formInvoice: PropTypes.object.isRequired,
currentTicker: PropTypes.object.isRequired
}
export default Form

12
app/routes/app/components/components/Form/Form.scss

@ -73,8 +73,16 @@
label {
svg {
width: 100px;
height: 100px;
width: 85px;
height: 85px;
}
svg[data-icon='ltc'] {
margin-right: 10px;
g {
transform: scale(1.75) translate(-5px, -5px);
}
}
}

45
app/routes/app/components/components/Nav.js

@ -2,21 +2,23 @@ import React from 'react'
import PropTypes from 'prop-types'
import { NavLink } from 'react-router-dom'
import ReactSVG from 'react-svg'
import { MdAccountBalanceWallet, MdSettings } from 'react-icons/lib/md'
import { FaClockO, FaBitcoin, FaDollar } from 'react-icons/lib/fa'
import { btc } from '../../../../utils'
import { MdAccountBalanceWallet } from 'react-icons/lib/md'
import { FaClockO, FaDollar } from 'react-icons/lib/fa'
import CryptoIcon from '../../../../components/CryptoIcon'
import CurrencyIcon from '../../../../components/CurrencyIcon'
import { btc, usd } from '../../../../utils'
import styles from './Nav.scss'
const Nav = ({ ticker, balance, setCurrency, formClicked }) => (
const Nav = ({ ticker, balance, setCurrency, formClicked, currentTicker }) => (
<nav className={styles.nav}>
<ul className={styles.info}>
<li className={`${styles.currencies} ${styles.link}`}>
<span
data-hint={ticker.btcTicker ? ticker.btcTicker.price_usd : null}
className={`${styles.currency} ${ticker.currency === 'btc' ? styles.active : ''} hint--bottom`}
onClick={() => setCurrency('btc')}
data-hint={currentTicker ? usd.formatUsd(currentTicker.price_usd) : null}
className={`${styles.currency} ${ticker.currency === ticker.crypto ? styles.active : ''} hint--bottom`}
onClick={() => setCurrency(ticker.crypto)}
>
<FaBitcoin />
<CryptoIcon currency={ticker.crypto} />
</span>
<span
className={`${styles.currency} ${ticker.currency === 'usd' ? styles.active : ''}`}
@ -27,26 +29,24 @@ const Nav = ({ ticker, balance, setCurrency, formClicked }) => (
</li>
<li className={`${styles.balance} ${styles.link}`}>
<p data-hint='Wallet balance' className='hint--bottom-left'>
<span>{ticker.currency === 'btc' ? <FaBitcoin /> : <FaDollar />}</span>
<span><CurrencyIcon currency={ticker.currency} crypto={ticker.crypto} /></span>
<span>
{
ticker.currency === 'btc' ?
btc.satoshisToBtc(balance.walletBalance)
ticker.currency === 'usd' ?
btc.satoshisToUsd(balance.walletBalance, currentTicker.price_usd)
:
btc.satoshisToUsd(balance.walletBalance, ticker.btcTicker.price_usd)
btc.satoshisToBtc(balance.walletBalance)
}
</span>
</p>
<p data-hint='Channel balance' className='hint--bottom-left'>
<span>
{ticker.currency === 'btc' ? <FaBitcoin /> : <FaDollar />}
</span>
<span><CurrencyIcon currency={ticker.currency} crypto={ticker.crypto} /></span>
<span>
{
ticker.currency === 'btc' ?
btc.satoshisToBtc(balance.channelBalance)
ticker.currency === 'usd' ?
btc.satoshisToUsd(balance.channelBalance, currentTicker.price_usd)
:
btc.satoshisToUsd(balance.channelBalance, ticker.btcTicker.price_usd)
btc.satoshisToBtc(balance.channelBalance)
}
</span>
</p>
@ -70,12 +70,6 @@ const Nav = ({ ticker, balance, setCurrency, formClicked }) => (
<span>Wallet</span>
</NavLink>
</li>
<li>
<NavLink to='/settings' activeClassName={styles.active} className={styles.link}>
<MdSettings />
<span>Settings</span>
</NavLink>
</li>
</ul>
<div className={styles.buttons}>
<div className={styles.button} onClick={() => formClicked('pay')}>
@ -92,7 +86,8 @@ Nav.propTypes = {
ticker: PropTypes.object.isRequired,
balance: PropTypes.object.isRequired,
setCurrency: PropTypes.func.isRequired,
formClicked: PropTypes.func.isRequired
formClicked: PropTypes.func.isRequired,
currentTicker: PropTypes.object.isRequired
}
export default Nav

23
app/routes/app/components/components/Nav.scss

@ -31,6 +31,20 @@
&.active {
color: $main;
}
span {
display: inline-block;
vertical-align: middle;
svg[data-icon='ltc'] {
width: 24px;
height: 28px;
g {
transform: scale(1.75) translate(-5px, -5px);
}
}
}
}
.logo {
@ -61,6 +75,15 @@
span {
display: inline-block;
vertical-align: top;
svg[data-icon='ltc'] {
width: 10px;
height: 10px;
g {
transform: scale(1.75) translate(-5px, -5px);
}
}
}
}
}

20
app/routes/app/components/components/Socket.js

@ -1,20 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import Websocket from 'react-websocket'
const Socket = ({ fetchChannels }) => {
const onMessage = () => {
// TODO: Assumes only socket relationship is with channels. Actually flesh out socket logic
fetchChannels()
}
return (
<Websocket debug url='ws://localhost:3000/' onMessage={onMessage} />
)
}
Socket.propTypes = {
fetchChannels: PropTypes.func.isRequired
}
export default Socket

6
app/routes/app/containers/AppContainer.js

@ -1,6 +1,6 @@
import { connect } from 'react-redux'
import App from '../components/App'
import { fetchTicker, setCurrency } from '../../../reducers/ticker'
import { fetchTicker, setCurrency, tickerSelectors } from '../../../reducers/ticker'
import { fetchBalance } from '../../../reducers/balance'
import { fetchInfo } from '../../../reducers/info'
import { createInvoice, fetchInvoice } from '../../../reducers/invoice'
@ -35,7 +35,9 @@ const mapStateToProps = state => ({
balance: state.balance,
payment: state.payment,
form: state.form,
invoice: state.invoice
invoice: state.invoice,
currentTicker: tickerSelectors.currentTicker(state)
})
export default connect(mapStateToProps, mapDispatchToProps)(App)

34
app/routes/wallet/components/Wallet.js

@ -7,16 +7,17 @@ import styles from './Wallet.scss'
class Wallet extends Component {
componentWillMount() {
const { fetchInfo, fetchPeers, fetchChannels } = this.props
const { fetchPeers, fetchChannels, newAddress } = this.props
fetchInfo()
fetchPeers()
fetchChannels()
newAddress('p2pkh')
}
render() {
const {
info,
address: { address },
ticker,
peers: { peersLoading, peers, peer, peerForm },
channels: { channelsLoading, channels, channel, channelForm, pendingChannels },
@ -29,14 +30,26 @@ class Wallet extends Component {
connectRequest,
disconnectRequest,
allChannels,
openChannel
openChannel,
closeChannel,
currentTicker,
explorerLinkBase
} = this.props
return (
<div className={styles.wallet}>
<section className={styles.header}>
<ReactSVG path='../resources/zap_2.svg' />
<h1>{info.data.identity_pubkey}</h1>
<section className={styles.walletInfo}>
<ReactSVG path='../resources/zap_2.svg' />
<h1 data-hint='Node identity public key' className='hint--top'>{info.data.identity_pubkey}</h1>
<h4 className={`${styles.address} hint--top`} data-hint='Wallet address'>
<input
type='text'
value={address}
readOnly
/>
</h4>
</section>
</section>
<section className={styles.walletData}>
<Peers
@ -63,6 +76,9 @@ class Wallet extends Component {
channelForm={channelForm}
setChannelForm={setChannelForm}
openChannel={openChannel}
closeChannel={closeChannel}
currentTicker={currentTicker}
explorerLinkBase={explorerLinkBase}
/>
</section>
</div>
@ -71,7 +87,6 @@ class Wallet extends Component {
}
Wallet.propTypes = {
fetchInfo: PropTypes.func.isRequired,
fetchPeers: PropTypes.func.isRequired,
fetchChannels: PropTypes.func.isRequired,
info: PropTypes.object.isRequired,
@ -87,7 +102,12 @@ Wallet.propTypes = {
connectRequest: PropTypes.func.isRequired,
disconnectRequest: PropTypes.func.isRequired,
allChannels: PropTypes.array.isRequired,
openChannel: PropTypes.func.isRequired
openChannel: PropTypes.func.isRequired,
closeChannel: PropTypes.func.isRequired,
newAddress: PropTypes.func.isRequired,
address: PropTypes.object.isRequired,
currentTicker: PropTypes.object.isRequired,
explorerLinkBase: PropTypes.string.isRequired
}

28
app/routes/wallet/components/Wallet.scss

@ -7,18 +7,42 @@
.header {
background: $white;
padding: 80px 30px;
text-align: center;
border-bottom: 1px solid $darkgrey;
text-align: center;
.walletInfo {
width: 75%;
margin: 0 auto;
}
svg {
width: 100px;
height: 100px;
text-align: center;
}
h1 {
color: $black;
font-size: 20px;
margin: 20px 0;
font-weight: bold;
color: $black;
text-align: center;
}
.address {
text-align: center;
margin-top: 10px;
input[type=text] {
width: 50%;
text-align: center;
font-size: 14px;
font-weight: 200;
border-radius: 7px;
background: $lightgrey;
border: 1px solid $main;
padding: 10px;
}
}
}

37
app/routes/wallet/components/components/Channels/Channels.js

@ -18,11 +18,20 @@ const Channels = ({
channelForm,
setChannelForm,
allChannels,
openChannel
openChannel,
closeChannel,
currentTicker,
explorerLinkBase
}) => (
<div className={styles.channels}>
<ChannelModal isOpen={channelModalOpen} resetChannel={setChannel} channel={modalChannel} />
<ChannelForm form={channelForm} setForm={setChannelForm} ticker={ticker} peers={peers} openChannel={openChannel} />
<ChannelModal
isOpen={channelModalOpen}
resetChannel={setChannel}
channel={modalChannel}
explorerLinkBase={explorerLinkBase}
closeChannel={closeChannel}
/>
<ChannelForm form={channelForm} setForm={setChannelForm} ticker={ticker} peers={peers} openChannel={openChannel} currentTicker={currentTicker} />
<div className={styles.header}>
<h3>Channels</h3>
<div
@ -39,11 +48,23 @@ const Channels = ({
allChannels.map((channel, index) => {
if (Object.prototype.hasOwnProperty.call(channel, 'blocks_till_open')) {
return (
<OpenPendingChannel key={index} channel={channel} ticker={ticker} />
<OpenPendingChannel
key={index}
channel={channel}
ticker={ticker}
currentTicker={currentTicker}
explorerLinkBase={explorerLinkBase}
/>
)
} else if (Object.prototype.hasOwnProperty.call(channel, 'closing_txid')) {
return (
<ClosedPendingChannel key={index} channel={channel} ticker={ticker} />
<ClosedPendingChannel
key={index}
channel={channel}
ticker={ticker}
currentTicker={currentTicker}
explorerLinkBase={explorerLinkBase}
/>
)
}
return (
@ -52,6 +73,7 @@ const Channels = ({
ticker={ticker}
channel={channel}
setChannel={setChannel}
currentTicker={currentTicker}
/>
)
})
@ -72,7 +94,10 @@ Channels.propTypes = {
channelForm: PropTypes.object.isRequired,
setChannelForm: PropTypes.func.isRequired,
allChannels: PropTypes.array.isRequired,
openChannel: PropTypes.func.isRequired
openChannel: PropTypes.func.isRequired,
closeChannel: PropTypes.func.isRequired,
currentTicker: PropTypes.object.isRequired,
explorerLinkBase: PropTypes.string.isRequired
}
export default Channels

11
app/routes/wallet/components/components/Channels/components/Channel/Channel.js

@ -3,7 +3,7 @@ import PropTypes from 'prop-types'
import { btc } from '../../../../../../../utils'
import styles from './Channel.scss'
const Channel = ({ ticker, channel, setChannel }) => (
const Channel = ({ ticker, channel, setChannel, currentTicker }) => (
<li className={styles.channel} onClick={() => setChannel(channel)}>
<h1 className={styles.status}>Status: Open</h1>
<div className={styles.left}>
@ -24,7 +24,7 @@ const Channel = ({ ticker, channel, setChannel }) => (
ticker.currency === 'btc' ?
btc.satoshisToBtc(channel.capacity)
:
btc.satoshisToUsd(channel.capacity, ticker.btcTicker.price_usd)
btc.satoshisToUsd(channel.capacity, currentTicker.price_usd)
}
</h2>
</section>
@ -35,7 +35,7 @@ const Channel = ({ ticker, channel, setChannel }) => (
ticker.currency === 'btc' ?
btc.satoshisToBtc(channel.local_balance)
:
btc.satoshisToUsd(channel.local_balance, ticker.btcTicker.price_usd)
btc.satoshisToUsd(channel.local_balance, currentTicker.price_usd)
}
</h4>
<span>Local</span>
@ -46,7 +46,7 @@ const Channel = ({ ticker, channel, setChannel }) => (
ticker.currency === 'btc' ?
btc.satoshisToBtc(channel.remote_balance)
:
btc.satoshisToUsd(channel.remote_balance, ticker.btcTicker.price_usd)
btc.satoshisToUsd(channel.remote_balance, currentTicker.price_usd)
}
</h4>
<span>Remote</span>
@ -59,7 +59,8 @@ const Channel = ({ ticker, channel, setChannel }) => (
Channel.propTypes = {
ticker: PropTypes.object.isRequired,
channel: PropTypes.object.isRequired,
setChannel: PropTypes.func.isRequired
setChannel: PropTypes.func.isRequired,
currentTicker: PropTypes.object.isRequired
}
export default Channel

31
app/routes/wallet/components/components/Channels/components/ChannelForm/ChannelForm.js

@ -1,20 +1,20 @@
import React from 'react'
import PropTypes from 'prop-types'
import ReactModal from 'react-modal'
import { FaUser, FaBitcoin, FaDollar } from 'react-icons/lib/fa'
import { FaUser } from 'react-icons/lib/fa'
import CurrencyIcon from '../../../../../../../components/CurrencyIcon'
import { usd, btc } from '../../../../../../../utils'
import styles from './ChannelForm.scss'
const ChannelForm = ({ form, setForm, ticker, peers, openChannel }) => {
const ChannelForm = ({ form, setForm, ticker, peers, openChannel, currentTicker }) => {
const submitClicked = () => {
const { node_key, local_amt, push_amt } = form
const localamt = ticker.currency === 'btc' ? btc.btcToSatoshis(local_amt) : btc.btcToSatoshis(usd.usdToBtc(local_amt, ticker.btcTicker.price_usd))
const pushamt = ticker.currency === 'btc' ? btc.btcToSatoshis(push_amt) : btc.btcToSatoshis(usd.usdToBtc(push_amt, ticker.btcTicker.price_usd))
const localamt = ticker.currency === 'usd' ? btc.btcToSatoshis(usd.usdToBtc(local_amt, currentTicker.price_usd)) : btc.btcToSatoshis(local_amt)
const pushamt = ticker.currency === 'usd' ? btc.btcToSatoshis(usd.usdToBtc(push_amt, currentTicker.price_usd)) : btc.btcToSatoshis(push_amt)
openChannel({ pubkey: node_key, localamt, pushamt }).then((channel) => {
if (channel.data) { setForm({ isOpen: false }) }
})
openChannel({ pubkey: node_key, localamt, pushamt })
setForm({ isOpen: false })
}
const customStyles = {
@ -60,12 +60,7 @@ const ChannelForm = ({ form, setForm, ticker, peers, openChannel }) => {
</section>
<section className={styles.local}>
<label htmlFor='localamount'>
{
ticker.currency === 'btc' ?
<FaBitcoin />
:
<FaDollar />
}
<CurrencyIcon currency={ticker.currency} crypto={ticker.crypto} />
</label>
<input
type='text'
@ -78,12 +73,7 @@ const ChannelForm = ({ form, setForm, ticker, peers, openChannel }) => {
</section>
<section className={styles.push}>
<label htmlFor='pushamount'>
{
ticker.currency === 'btc' ?
<FaBitcoin />
:
<FaDollar />
}
<CurrencyIcon currency={ticker.currency} crypto={ticker.crypto} />
</label>
<input
type='text'
@ -130,7 +120,8 @@ ChannelForm.propTypes = {
setForm: PropTypes.func.isRequired,
ticker: PropTypes.object.isRequired,
peers: PropTypes.array.isRequired,
openChannel: PropTypes.func.isRequired
openChannel: PropTypes.func.isRequired,
currentTicker: PropTypes.object.isRequired
}
export default ChannelForm

9
app/routes/wallet/components/components/Channels/components/ChannelForm/ChannelForm.scss

@ -27,6 +27,15 @@
padding-top: 19px;
padding-bottom: 12px;
color: $traditionalgrey;
svg[data-icon='ltc'] {
width: 18px;
height: 16px;
g {
transform: scale(1.75) translate(-5px, -5px);
}
}
}
input[type=text] {

15
app/routes/wallet/components/components/Channels/components/ChannelModal/ChannelModal.js

@ -4,7 +4,7 @@ import PropTypes from 'prop-types'
import ReactModal from 'react-modal'
import styles from './ChannelModal.scss'
const ChannelModal = ({ isOpen, resetChannel, channel }) => {
const ChannelModal = ({ isOpen, resetChannel, channel, explorerLinkBase, closeChannel }) => {
const customStyles = {
overlay: {
cursor: 'pointer',
@ -21,6 +21,11 @@ const ChannelModal = ({ isOpen, resetChannel, channel }) => {
}
}
const closeChannelClicked = () => {
closeChannel({ channel_point: channel.channel_point })
resetChannel(null)
}
return (
<ReactModal
isOpen={isOpen}
@ -39,7 +44,7 @@ const ChannelModal = ({ isOpen, resetChannel, channel }) => {
<h2
data-hint='Channel point'
className='hint--top-left'
onClick={() => shell.openExternal(`https://testnet.smartbit.com.au/tx/${channel.channel_point.split(':')[0]}`)}
onClick={() => shell.openExternal(`${explorerLinkBase}/tx/${channel.channel_point.split(':')[0]}`)}
>
{channel.channel_point}
</h2>
@ -71,7 +76,7 @@ const ChannelModal = ({ isOpen, resetChannel, channel }) => {
<dd>{channel.num_updates}</dd>
</dl>
</div>
<div className={styles.close}>
<div className={styles.close} onClick={closeChannelClicked}>
<div>Close channel</div>
</div>
<footer className={styles.active}>
@ -88,7 +93,9 @@ const ChannelModal = ({ isOpen, resetChannel, channel }) => {
ChannelModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
resetChannel: PropTypes.func.isRequired,
channel: PropTypes.object
channel: PropTypes.object,
explorerLinkBase: PropTypes.string.isRequired,
closeChannel: PropTypes.func.isRequired
}
export default ChannelModal

14
app/routes/wallet/components/components/Channels/components/ClosedPendingChannel/ClosedPendingChannel.js

@ -4,8 +4,8 @@ import PropTypes from 'prop-types'
import { btc } from '../../../../../../../utils'
import styles from './ClosedPendingChannel.scss'
const ClosedPendingChannel = ({ ticker, channel: { channel, closing_txid } }) => (
<li className={styles.channel} onClick={() => shell.openExternal(`https://testnet.smartbit.com.au/tx/${closing_txid}`)}>
const ClosedPendingChannel = ({ ticker, channel: { channel, closing_txid }, currentTicker, explorerLinkBase }) => (
<li className={styles.channel} onClick={() => shell.openExternal(`${explorerLinkBase}/tx/${closing_txid}`)}>
<h1 className={styles.closing}>Status: Closing</h1>
<div className={styles.left}>
<section className={styles.remotePubkey}>
@ -25,7 +25,7 @@ const ClosedPendingChannel = ({ ticker, channel: { channel, closing_txid } }) =>
ticker.currency === 'btc' ?
btc.satoshisToBtc(channel.capacity)
:
btc.satoshisToUsd(channel.capacity, ticker.btcTicker.price_usd)
btc.satoshisToUsd(channel.capacity, currentTicker.price_usd)
}
</h2>
</section>
@ -36,7 +36,7 @@ const ClosedPendingChannel = ({ ticker, channel: { channel, closing_txid } }) =>
ticker.currency === 'btc' ?
btc.satoshisToBtc(channel.local_balance)
:
btc.satoshisToUsd(channel.local_balance, ticker.btcTicker.price_usd)
btc.satoshisToUsd(channel.local_balance, currentTicker.price_usd)
}
</h4>
<span>Local</span>
@ -47,7 +47,7 @@ const ClosedPendingChannel = ({ ticker, channel: { channel, closing_txid } }) =>
ticker.currency === 'btc' ?
btc.satoshisToBtc(channel.remote_balance)
:
btc.satoshisToUsd(channel.remote_balance, ticker.btcTicker.price_usd)
btc.satoshisToUsd(channel.remote_balance, currentTicker.price_usd)
}
</h4>
<span>Remote</span>
@ -59,7 +59,9 @@ const ClosedPendingChannel = ({ ticker, channel: { channel, closing_txid } }) =>
ClosedPendingChannel.propTypes = {
ticker: PropTypes.object.isRequired,
channel: PropTypes.object.isRequired
channel: PropTypes.object.isRequired,
currentTicker: PropTypes.object.isRequired,
explorerLinkBase: PropTypes.string.isRequired
}
export default ClosedPendingChannel

14
app/routes/wallet/components/components/Channels/components/OpenPendingChannel/OpenPendingChannel.js

@ -4,8 +4,8 @@ import PropTypes from 'prop-types'
import { btc } from '../../../../../../../utils'
import styles from './OpenPendingChannel.scss'
const OpenPendingChannel = ({ ticker, channel: { channel } }) => (
<li className={styles.channel} onClick={() => shell.openExternal(`https://testnet.smartbit.com.au/tx/${channel.channel_point.split(':')[0]}`)}>
const OpenPendingChannel = ({ ticker, channel: { channel }, currentTicker, explorerLinkBase }) => (
<li className={styles.channel} onClick={() => shell.openExternal(`${explorerLinkBase}/tx/${channel.channel_point.split(':')[0]}`)}>
<h1 className={styles.pending}>Status: Pending</h1>
<div className={styles.left}>
<section className={styles.remotePubkey}>
@ -25,7 +25,7 @@ const OpenPendingChannel = ({ ticker, channel: { channel } }) => (
ticker.currency === 'btc' ?
btc.satoshisToBtc(channel.capacity)
:
btc.satoshisToUsd(channel.capacity, ticker.btcTicker.price_usd)
btc.satoshisToUsd(channel.capacity, currentTicker.price_usd)
}
</h2>
</section>
@ -36,7 +36,7 @@ const OpenPendingChannel = ({ ticker, channel: { channel } }) => (
ticker.currency === 'btc' ?
btc.satoshisToBtc(channel.local_balance)
:
btc.satoshisToUsd(channel.local_balance, ticker.btcTicker.price_usd)
btc.satoshisToUsd(channel.local_balance, currentTicker.price_usd)
}
</h4>
<span>Local</span>
@ -47,7 +47,7 @@ const OpenPendingChannel = ({ ticker, channel: { channel } }) => (
ticker.currency === 'btc' ?
btc.satoshisToBtc(channel.remote_balance)
:
btc.satoshisToUsd(channel.remote_balance, ticker.btcTicker.price_usd)
btc.satoshisToUsd(channel.remote_balance, currentTicker.price_usd)
}
</h4>
<span>Remote</span>
@ -59,7 +59,9 @@ const OpenPendingChannel = ({ ticker, channel: { channel } }) => (
OpenPendingChannel.propTypes = {
ticker: PropTypes.object.isRequired,
channel: PropTypes.object.isRequired
channel: PropTypes.object.isRequired,
currentTicker: PropTypes.object.isRequired,
explorerLinkBase: PropTypes.string.isRequired
}
export default OpenPendingChannel

5
app/routes/wallet/components/components/Peers/components/PeerForm/PeerForm.js

@ -6,10 +6,7 @@ import styles from './PeerForm.scss'
const PeerForm = ({ form, setForm, connect }) => {
const submit = () => {
const { pubkey, host } = form
connect({ pubkey, host }).then((success) => {
if (success.data) { setForm({ isOpen: false }) }
})
connect({ pubkey, host })
}
const customStyles = {

7
app/routes/wallet/components/components/Peers/components/PeerModal/PeerModal.js

@ -4,11 +4,6 @@ import ReactModal from 'react-modal'
import styles from './PeerModal.scss'
const PeerModal = ({ isOpen, resetPeer, peer, disconnect }) => {
const disconnectClicked = () => {
disconnect({ pubkey: peer.pub_key })
.then(success => (success ? resetPeer(null) : null))
}
const customStyles = {
overlay: {
cursor: 'pointer',
@ -55,7 +50,7 @@ const PeerModal = ({ isOpen, resetPeer, peer, disconnect }) => {
<dd>{peer.bytes_sent}</dd>
</dl>
</div>
<div className={styles.close} onClick={disconnectClicked}>
<div className={styles.close} onClick={() => disconnect({ pubkey: peer.pub_key })}>
<div>Disconnect peer</div>
</div>
</div>

17
app/routes/wallet/containers/WalletContainer.js

@ -1,5 +1,7 @@
import { connect } from 'react-redux'
import { fetchInfo } from '../../../reducers/info'
import { infoSelectors } from '../../../reducers/info'
import { newAddress } from '../../../reducers/address'
import { tickerSelectors } from '../../../reducers/ticker'
import {
fetchPeers,
setPeer,
@ -14,12 +16,13 @@ import {
setChannel,
channelsSelectors,
setChannelForm,
openChannel
openChannel,
closeChannel
} from '../../../reducers/channels'
import Wallet from '../components/Wallet'
const mapDispatchToProps = {
fetchInfo,
newAddress,
fetchPeers,
setPeer,
@ -30,6 +33,7 @@ const mapDispatchToProps = {
fetchPendingChannels,
setChannel,
openChannel,
closeChannel,
setPeerForm,
setChannelForm
@ -37,6 +41,7 @@ const mapDispatchToProps = {
const mapStateToProps = state => ({
info: state.info,
address: state.address,
ticker: state.ticker,
peers: state.peers,
@ -45,7 +50,11 @@ const mapStateToProps = state => ({
allChannels: channelsSelectors.allChannels(state),
peerModalOpen: peersSelectors.peerModalOpen(state),
channelModalOpen: channelsSelectors.channelModalOpen(state)
channelModalOpen: channelsSelectors.channelModalOpen(state),
currentTicker: tickerSelectors.currentTicker(state),
explorerLinkBase: infoSelectors.explorerLinkBase(state)
})
export default connect(mapStateToProps, mapDispatchToProps)(Wallet)

15
app/store/configureStore.dev.js

@ -1,9 +1,10 @@
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import { createHashHistory } from 'history';
import { routerMiddleware, routerActions } from 'react-router-redux';
import { createLogger } from 'redux-logger';
import rootReducer from '../reducers';
import { createStore, applyMiddleware, compose } from 'redux'
import thunk from 'redux-thunk'
import { createHashHistory } from 'history'
import { routerMiddleware, routerActions } from 'react-router-redux'
import { createLogger } from 'redux-logger'
import rootReducer from '../reducers'
import ipc from '../reducers/ipc'
const history = createHashHistory();
@ -41,7 +42,7 @@ const configureStore = (initialState?: counterStateType) => {
/* eslint-enable no-underscore-dangle */
// Apply Middleware & Compose Enhancers
enhancers.push(applyMiddleware(...middleware));
enhancers.push(applyMiddleware(...middleware, ipc));
const enhancer = composeEnhancers(...enhancers);
// Create Store

3
app/utils/btc.js

@ -13,7 +13,8 @@ export function satoshisToBtc(satoshis) {
}
export function btcToUsd(btc, price) {
return parseFloat((btc * price).toFixed(2)).toLocaleString('en')
const amount = parseFloat(btc * price).toFixed(2)
return (btc > 0 && amount <= 0) ? '< 0.01' : amount.toLocaleString('en')
}
export function satoshisToUsd(satoshis, price) {

5
app/utils/usd.js

@ -1,3 +1,7 @@
export function formatUsd(usd) {
return `$${(+usd).toFixed(2)}`
}
export function usdToBtc(usd, rate) {
if (usd === undefined || usd === null || usd === '') return null
@ -5,5 +9,6 @@ export function usdToBtc(usd, rate) {
}
export default {
formatUsd,
usdToBtc
}

8
package.json

@ -137,6 +137,7 @@
"electron": "^1.6.10",
"electron-builder": "^19.8.0",
"electron-devtools-installer": "^2.2.0",
"electron-rebuild": "^1.6.0",
"enzyme": "^2.9.1",
"enzyme-to-json": "^1.5.1",
"eslint": "^4.4.1",
@ -183,9 +184,11 @@
},
"dependencies": {
"axios": "^0.16.2",
"bitcore-lib": "^0.14.0",
"devtron": "^1.4.0",
"electron-debug": "^1.2.0",
"font-awesome": "^4.7.0",
"grpc": "^1.4.1",
"history": "^4.6.3",
"moment-timezone": "^0.5.13",
"prop-types": "^15.5.10",
@ -194,6 +197,7 @@
"react-addons-css-transition-group": "^15.6.0",
"react-dom": "^15.6.1",
"react-hot-loader": "3.0.0-beta.6",
"react-inlinesvg": "^0.6.2",
"react-modal": "^2.2.2",
"react-moment": "^0.6.0",
"react-redux": "^5.0.5",
@ -204,11 +208,13 @@
"react-svg-morph": "^0.1.10",
"react-websocket": "^1.1.7",
"redux": "^3.7.1",
"redux-electron-ipc": "^1.1.10",
"redux-thunk": "^2.2.0",
"reselect": "^3.0.1",
"satoshi-bitcoin": "^1.0.4",
"source-map-support": "^0.4.15",
"xtend": "^4.0.1"
"xtend": "^4.0.1",
"zbase32": "^0.0.2"
},
"devEngines": {
"node": ">=7.x",

6
resources/litecoin.svg

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 40 40" data-icon="ltc">
<g>
<path d="M12.29 28.04l1.29-5.52-1.58.67.63-2.85 1.64-.68L16.52 10h5.23l-1.52 7.14 2.09-.74-.58 2.7-2.05.8-.9 4.34h8.1l-.99 3.8z">
</path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 263 B

BIN
resources/zap_2.icns

Binary file not shown.

BIN
resources/zap_2.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

55
test/api/index.spec.js

@ -1,55 +0,0 @@
import { callApi } from '../../app/api'
describe('API', () => {
describe('getinfo', () => {
it('is synced to the chain', async () => {
const info = await callApi('info')
expect(info.data.synced_to_chain).toEqual(true)
})
it('only supports 1 chain at a time', async () => {
const info = await callApi('info')
expect(info.data.chains.length).toEqual(1)
})
})
describe('balance', () => {
it('returns wallet balance', async () => {
const wallet_balances = await callApi('wallet_balance')
expect(typeof (wallet_balances.data.balance)).toEqual('string')
})
it('returns channel balance', async () => {
const channel_balances = await callApi('channel_balance')
expect(typeof (channel_balances.data.balance)).toEqual('string')
})
})
describe('peers', () => {
it('peers is an array', async () => {
const peers = await callApi('peers')
expect(Array.isArray(peers.data.peers)).toEqual(true)
})
})
describe('channels', () => {
it('channels is an array', async () => {
const channels = await callApi('channels')
expect(Array.isArray(channels.data.channels)).toEqual(true)
})
})
describe('invoices', () => {
it('invoices is an array', async () => {
const invoices = await callApi('invoices')
expect(Array.isArray(invoices.data.invoices)).toEqual(true)
})
})
describe('payments', () => {
it('payments is an array', async () => {
const payments = await callApi('payments')
expect(Array.isArray(payments.data.payments)).toEqual(true)
})
})
})

6
test/reducers/__snapshots__/channels.spec.js.snap

@ -11,6 +11,7 @@ Object {
},
"channels": Array [],
"channelsLoading": true,
"closingChannel": false,
"openingChannel": false,
"pendingChannels": Object {
"pending_closing_channels": Array [],
@ -32,6 +33,7 @@ Object {
},
"channels": Array [],
"channelsLoading": false,
"closingChannel": false,
"openingChannel": true,
"pendingChannels": Object {
"pending_closing_channels": Array [],
@ -56,6 +58,7 @@ Object {
2,
],
"channelsLoading": false,
"closingChannel": false,
"openingChannel": false,
"pendingChannels": Array [
3,
@ -75,6 +78,7 @@ Object {
},
"channels": Array [],
"channelsLoading": false,
"closingChannel": false,
"openingChannel": false,
"pendingChannels": Object {
"pending_closing_channels": Array [],
@ -96,6 +100,7 @@ Object {
},
"channels": Array [],
"channelsLoading": false,
"closingChannel": false,
"openingChannel": false,
"pendingChannels": Object {
"pending_closing_channels": Array [],
@ -117,6 +122,7 @@ Object {
},
"channels": Array [],
"channelsLoading": false,
"closingChannel": false,
"openingChannel": false,
"pendingChannels": Object {
"pending_closing_channels": Array [],

30
test/reducers/__snapshots__/ticker.spec.js.snap

@ -3,17 +3,29 @@
exports[`reducers tickerReducer should correctly getTicker 1`] = `
Object {
"btcTicker": null,
"crypto": "btc",
"currency": "btc",
"crypto": "",
"currency": "",
"ltcTicker": null,
"tickerLoading": true,
}
`;
exports[`reducers tickerReducer should correctly receiveTicker 1`] = `
Object {
"btcTicker": "f",
"crypto": "btc",
"currency": "btc",
"btcTicker": undefined,
"crypto": "",
"currency": "",
"ltcTicker": undefined,
"tickerLoading": false,
}
`;
exports[`reducers tickerReducer should correctly setCrypto 1`] = `
Object {
"btcTicker": null,
"crypto": "foo",
"currency": "",
"ltcTicker": null,
"tickerLoading": false,
}
`;
@ -21,8 +33,9 @@ Object {
exports[`reducers tickerReducer should correctly setCurrency 1`] = `
Object {
"btcTicker": null,
"crypto": "btc",
"crypto": "",
"currency": "foo",
"ltcTicker": null,
"tickerLoading": false,
}
`;
@ -30,8 +43,9 @@ Object {
exports[`reducers tickerReducer should handle initial state 1`] = `
Object {
"btcTicker": null,
"crypto": "btc",
"currency": "btc",
"crypto": "",
"currency": "",
"ltcTicker": null,
"tickerLoading": false,
}
`;

21
test/reducers/ticker.spec.js

@ -1,7 +1,8 @@
import tickerReducer, {
SET_CURRENCY,
GET_TICKER,
RECIEVE_TICKER
SET_CRYPTO,
GET_TICKERS,
RECIEVE_TICKERS
} from '../../app/reducers/ticker'
describe('reducers', () => {
@ -14,24 +15,32 @@ describe('reducers', () => {
expect(SET_CURRENCY).toEqual('SET_CURRENCY')
})
it('should have SET_CRYPTO', () => {
expect(SET_CRYPTO).toEqual('SET_CRYPTO')
})
it('should have GET_TICKER', () => {
expect(GET_TICKER).toEqual('GET_TICKER')
expect(GET_TICKERS).toEqual('GET_TICKERS')
})
it('should have RECIEVE_TICKER', () => {
expect(RECIEVE_TICKER).toEqual('RECIEVE_TICKER')
expect(RECIEVE_TICKERS).toEqual('RECIEVE_TICKERS')
})
it('should correctly setCurrency', () => {
expect(tickerReducer(undefined, { type: SET_CURRENCY, currency: 'foo' })).toMatchSnapshot()
})
it('should correctly setCrypto', () => {
expect(tickerReducer(undefined, { type: SET_CRYPTO, crypto: 'foo' })).toMatchSnapshot()
})
it('should correctly getTicker', () => {
expect(tickerReducer(undefined, { type: GET_TICKER })).toMatchSnapshot()
expect(tickerReducer(undefined, { type: GET_TICKERS })).toMatchSnapshot()
})
it('should correctly receiveTicker', () => {
expect(tickerReducer(undefined, { type: RECIEVE_TICKER, ticker: 'foo' })).toMatchSnapshot()
expect(tickerReducer(undefined, { type: RECIEVE_TICKERS, ticker: 'foo' })).toMatchSnapshot()
})
})
})

30
test/utils/usd.spec.js

@ -0,0 +1,30 @@
import {
formatUsd,
usdToBtc
} from '../../app/utils/usd'
describe('usd', () => {
describe('formatUsd', () => {
it('should handle a string value', () => {
expect(formatUsd('42.0')).toBe('$42.00')
})
it('should handle a numerical value', () => {
expect(formatUsd(42.0)).toBe('$42.00')
})
it('should round to two decimal places', () => {
expect(formatUsd('42.416')).toBe('$42.42')
})
})
describe('usdToBtc', () => {
it('should convert USD to BTC using rate', () => {
expect(usdToBtc(1, 50)).toBe('0.02000000')
})
it('should round to eight decimal places', () => {
expect(usdToBtc(2, 3)).toBe('0.66666667')
})
})
})

208
yarn.lock

@ -246,6 +246,10 @@ argparse@^1.0.7:
dependencies:
sprintf-js "~1.0.2"
arguejs@^0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/arguejs/-/arguejs-0.2.3.tgz#b6f939f5fe0e3cd1f3f93e2aa9262424bf312af7"
aria-query@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-0.7.0.tgz#4af10a1e61573ddea0cf3b99b51c52c05b424d24"
@ -322,6 +326,13 @@ asar-integrity@0.1.1:
bluebird-lst "^1.0.2"
fs-extra-p "^4.3.0"
ascli@~1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/ascli/-/ascli-1.0.1.tgz#bcfa5974a62f18e81cabaeb49732ab4a88f906bc"
dependencies:
colour "~0.7.1"
optjs "~3.2.2"
asn1.js@^4.0.0:
version "4.9.1"
resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.9.1.tgz#48ba240b45a9280e94748990ba597d216617fd40"
@ -1474,6 +1485,17 @@ binary-extensions@^1.0.0:
buffers "~0.1.1"
chainsaw "~0.1.0"
bitcore-lib@^0.14.0:
version "0.14.0"
resolved "https://registry.yarnpkg.com/bitcore-lib/-/bitcore-lib-0.14.0.tgz#21cb2359fe7b997a3b7b773eb7d7275ae37d644e"
dependencies:
bn.js "=2.0.4"
bs58 "=2.0.0"
buffer-compare "=1.0.0"
elliptic "=3.0.3"
inherits "=2.0.1"
lodash "=3.10.1"
bl@^1.0.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.1.tgz#cac328f7bee45730d404b692203fcb590e172d5e"
@ -1496,6 +1518,14 @@ bluebird@^3.0.5, bluebird@^3.4.7, bluebird@^3.5.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.0.tgz#791420d7f551eea2897453a8a77653f96606d67c"
bn.js@=2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-2.0.4.tgz#220a7cd677f7f1bfa93627ff4193776fe7819480"
bn.js@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-2.2.0.tgz#12162bc2ae71fc40a5626c33438f3a875cd37625"
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
version "4.11.6"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.6.tgz#53344adb14617a13f6e8dd2ce28905d1c0ba3215"
@ -1623,6 +1653,10 @@ browserslist@^1.1.1, browserslist@^1.1.3, browserslist@^1.3.6, browserslist@^1.5
caniuse-db "^1.0.30000639"
electron-to-chromium "^1.2.7"
bs58@=2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/bs58/-/bs58-2.0.0.tgz#72b713bed223a0ac518bbda0e3ce3f4817f39eb5"
bser@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/bser/-/bser-1.0.2.tgz#381116970b2a6deea5646dd15dd7278444b56169"
@ -1635,6 +1669,10 @@ bser@^2.0.0:
dependencies:
node-int64 "^0.4.0"
buffer-compare@=1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/buffer-compare/-/buffer-compare-1.0.0.tgz#acaa7a966e98eee9fae14b31c39a5f158fb3c4a2"
buffer-crc32@^0.2.1:
version "0.2.13"
resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
@ -1667,6 +1705,12 @@ builtin-status-codes@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
bytebuffer@~5:
version "5.0.1"
resolved "https://registry.yarnpkg.com/bytebuffer/-/bytebuffer-5.0.1.tgz#582eea4b1a873b6d020a48d58df85f0bba6cfddd"
dependencies:
long "~3"
bytes@2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.3.0.tgz#d5b680a165b6201739acb611542aabc2d8ceb070"
@ -1703,7 +1747,7 @@ camelcase@^1.0.2:
version "1.2.1"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39"
camelcase@^2.0.0:
camelcase@^2.0.0, camelcase@^2.0.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f"
@ -1873,6 +1917,10 @@ cli-cursor@^2.1.0:
dependencies:
restore-cursor "^2.0.0"
cli-spinners@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-1.0.0.tgz#ef987ed3d48391ac3dab9180b406a742180d6e6a"
cli-width@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.1.0.tgz#b234ca209b29ef66fc518d9b98d5847b00edf00a"
@ -1885,7 +1933,7 @@ cliui@^2.1.0:
right-align "^0.1.1"
wordwrap "0.0.2"
cliui@^3.2.0:
cliui@^3.0.3, cliui@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d"
dependencies:
@ -1990,6 +2038,10 @@ colors@^1.1.2, colors@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63"
colour@~0.7.1:
version "0.7.1"
resolved "https://registry.yarnpkg.com/colour/-/colour-0.7.1.tgz#9cb169917ec5d12c0736d3e8685746df1cadf778"
combined-stream@^1.0.5, combined-stream@~1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009"
@ -2476,7 +2528,7 @@ debug@2.6.7:
dependencies:
ms "2.0.0"
debug@2.6.8, debug@^2.1.1, debug@^2.1.3, debug@^2.2.0, debug@^2.4.5, debug@^2.6.0, debug@^2.6.1, debug@^2.6.3, debug@^2.6.6, debug@^2.6.8:
debug@2.6.8, debug@^2.1.1, debug@^2.1.3, debug@^2.2.0, debug@^2.4.5, debug@^2.5.1, debug@^2.6.0, debug@^2.6.1, debug@^2.6.3, debug@^2.6.6, debug@^2.6.8:
version "2.6.8"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc"
dependencies:
@ -2918,6 +2970,20 @@ electron-publish@19.8.0:
fs-extra-p "^4.3.0"
mime "^1.3.6"
electron-rebuild@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/electron-rebuild/-/electron-rebuild-1.6.0.tgz#e8d26f4d8e9fe5388df35864b3658e5cfd4dcb7e"
dependencies:
colors "^1.1.2"
debug "^2.6.3"
fs-extra "^3.0.1"
node-abi "^2.0.0"
node-gyp "^3.6.0"
ora "^1.2.0"
rimraf "^2.6.1"
spawn-rx "^2.0.10"
yargs "^7.0.2"
electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.11:
version "1.3.14"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.14.tgz#64af0f9efd3c3c6acd57d71f83b49ca7ee9c4b43"
@ -2930,6 +2996,15 @@ electron@^1.6.10:
electron-download "^3.0.1"
extract-zip "^1.0.3"
elliptic@=3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-3.0.3.tgz#865c9b420bfbe55006b9f969f97a0d2c44966595"
dependencies:
bn.js "^2.0.0"
brorand "^1.0.1"
hash.js "^1.0.0"
inherits "^2.0.1"
elliptic@^6.0.0:
version "6.4.0"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df"
@ -4024,6 +4099,16 @@ growly@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
grpc@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/grpc/-/grpc-1.4.1.tgz#3ee4a8346a613f2823928c9f8f99081b6368ec7c"
dependencies:
arguejs "^0.2.3"
lodash "^4.15.0"
nan "^2.0.0"
node-pre-gyp "^0.6.35"
protobufjs "^5.0.0"
gulp-util@^3.0.4:
version "3.0.8"
resolved "https://registry.yarnpkg.com/gulp-util/-/gulp-util-3.0.8.tgz#0054e1e744502e27c04c187c3ecc505dd54bbb4f"
@ -4316,6 +4401,14 @@ http-signature@~1.1.0:
jsprim "^1.2.2"
sshpk "^1.7.0"
httpplease@^0.16:
version "0.16.4"
resolved "https://registry.yarnpkg.com/httpplease/-/httpplease-0.16.4.tgz#d382ebe230ef5079080b4e9ffebf316a9e75c0da"
dependencies:
urllite "~0.5.0"
xmlhttprequest "*"
xtend "~3.0.0"
https-browserify@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-0.0.1.tgz#3f91365cabe60b77ed0ebba24b454e3e09d95a82"
@ -4393,7 +4486,7 @@ inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, i
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
inherits@2.0.1:
inherits@2.0.1, inherits@=2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1"
@ -5504,6 +5597,10 @@ lodash.uniq@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
lodash@=3.10.1:
version "3.10.1"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
lodash@^4.0.0, lodash@^4.1.0, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.1, lodash@^4.6.1, lodash@^4.8.0, lodash@~4.17.4:
version "4.17.4"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
@ -5518,6 +5615,10 @@ lolex@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/lolex/-/lolex-1.6.0.tgz#3a9a0283452a47d7439e72731b9e07d7386e49f6"
long@~3:
version "3.2.0"
resolved "https://registry.yarnpkg.com/long/-/long-3.2.0.tgz#d821b7138ca1cb581c172990ef14db200b5c474b"
longest@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"
@ -5789,7 +5890,7 @@ mute-stream@0.0.7:
version "0.0.7"
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
nan@^2.3.0, nan@^2.3.2:
nan@^2.0.0, nan@^2.3.0, nan@^2.3.2:
version "2.6.2"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.6.2.tgz#e4ff34e6c95fdfb5aecc08de6596f43605a7db45"
@ -5821,6 +5922,10 @@ no-case@^2.2.0:
dependencies:
lower-case "^1.1.1"
node-abi@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.1.0.tgz#50ad834affcf17440e12bfc5f9ba0946f572d10c"
node-emoji@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.5.1.tgz#fd918e412769bf8c448051238233840b2aff16a1"
@ -5838,7 +5943,7 @@ node-forge@0.6.33:
version "0.6.33"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.6.33.tgz#463811879f573d45155ad6a9f43dc296e8e85ebc"
node-gyp@^3.3.1:
node-gyp@^3.3.1, node-gyp@^3.6.0:
version "3.6.2"
resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.6.2.tgz#9bfbe54562286284838e750eac05295853fa1c60"
dependencies:
@ -5925,7 +6030,7 @@ node-notifier@^5.0.2:
shellwords "^0.1.0"
which "^1.2.12"
node-pre-gyp@^0.6.29:
node-pre-gyp@^0.6.29, node-pre-gyp@^0.6.35:
version "0.6.36"
resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.36.tgz#db604112cb74e0d477554e9b505b17abddfab786"
dependencies:
@ -6137,7 +6242,7 @@ on-headers@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7"
once@^1.3.0, once@^1.3.3, once@^1.4.0:
once@^1.3.0, once@^1.3.3, once@^1.4, once@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
dependencies:
@ -6182,6 +6287,19 @@ optionator@^0.8.1, optionator@^0.8.2:
type-check "~0.3.2"
wordwrap "~1.0.0"
optjs@~3.2.2:
version "3.2.2"
resolved "https://registry.yarnpkg.com/optjs/-/optjs-3.2.2.tgz#69a6ce89c442a44403141ad2f9b370bd5bb6f4ee"
ora@^1.2.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/ora/-/ora-1.3.0.tgz#80078dd2b92a934af66a3ad72a5b910694ede51a"
dependencies:
chalk "^1.1.1"
cli-cursor "^2.1.0"
cli-spinners "^1.0.0"
log-symbols "^1.0.2"
original@>=0.0.5:
version "1.0.0"
resolved "https://registry.yarnpkg.com/original/-/original-1.0.0.tgz#9147f93fa1696d04be61e01bd50baeaca656bd3b"
@ -6828,6 +6946,15 @@ prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.5.8:
fbjs "^0.8.9"
loose-envify "^1.3.1"
protobufjs@^5.0.0:
version "5.0.2"
resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-5.0.2.tgz#59748d7dcf03d2db22c13da9feb024e16ab80c91"
dependencies:
ascli "~1"
bytebuffer "~5"
glob "^7.0.5"
yargs "^3.10.0"
proxy-addr@~1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.4.tgz#27e545f6960a44a627d9b44467e35c1b6b4ce2f3"
@ -6980,6 +7107,13 @@ react-hot-loader@3.0.0-beta.6:
redbox-react "^1.2.5"
source-map "^0.4.4"
react-inlinesvg@^0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/react-inlinesvg/-/react-inlinesvg-0.6.2.tgz#cad181824652934f6235ff2b09e39b8c86344aab"
dependencies:
httpplease "^0.16"
once "^1.4"
react-modal@^2.2.2:
version "2.2.4"
resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-2.2.4.tgz#a32483c3555bd7677f09bca65d82f51da3abcbc0"
@ -7210,6 +7344,10 @@ reduce-function-call@^1.0.1:
dependencies:
balanced-match "^0.4.2"
redux-electron-ipc@^1.1.10:
version "1.1.10"
resolved "https://registry.yarnpkg.com/redux-electron-ipc/-/redux-electron-ipc-1.1.10.tgz#0e4de0ae30eb8571209f24e75149007e965e65d1"
redux-logger@^3.0.6:
version "3.0.6"
resolved "https://registry.yarnpkg.com/redux-logger/-/redux-logger-3.0.6.tgz#f7555966f3098f3c88604c449cf0baf5778274bf"
@ -7479,6 +7617,12 @@ rx@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/rx/-/rx-4.1.0.tgz#a5f13ff79ef3b740fe30aa803fb09f98805d4782"
rxjs@^5.1.1:
version "5.4.3"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.4.3.tgz#0758cddee6033d68e0fd53676f0f3596ce3d483f"
dependencies:
symbol-observable "^1.0.1"
safe-buffer@^5.0.1, safe-buffer@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.0.tgz#fe4c8460397f9eaaaa58e73be46273408a45e223"
@ -7793,6 +7937,14 @@ spawn-command@^0.0.2-1:
version "0.0.2"
resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2.tgz#9544e1a43ca045f8531aac1a48cb29bdae62338e"
spawn-rx@^2.0.10:
version "2.0.11"
resolved "https://registry.yarnpkg.com/spawn-rx/-/spawn-rx-2.0.11.tgz#65451ad65662801daea75549832a782de0048dbf"
dependencies:
debug "^2.5.1"
lodash.assign "^4.2.0"
rxjs "^5.1.1"
spdx-correct@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40"
@ -8228,7 +8380,7 @@ svgpath@^2.1.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/svgpath/-/svgpath-2.2.1.tgz#0834bb67c89a76472b2bd06cc101fa7b517b222c"
symbol-observable@^1.0.3:
symbol-observable@^1.0.1, symbol-observable@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.4.tgz#29bf615d4aa7121bdd898b22d4b3f9bc4e2aa03d"
@ -8636,6 +8788,12 @@ url@^0.11.0, url@~0.11.0:
punycode "1.3.2"
querystring "0.2.0"
urllite@~0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/urllite/-/urllite-0.5.0.tgz#1b7bb9ca3fb0db9520de113466bbcf7cc341451a"
dependencies:
xtend "~4.0.0"
utf8-byte-length@^1.0.1:
version "1.0.4"
resolved "https://registry.yarnpkg.com/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz#f45f150c4c66eee968186505ab93fcbb8ad6bf61"
@ -8948,6 +9106,10 @@ window-size@0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d"
window-size@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.4.tgz#f8e1aa1ee5a53ec5bf151ffa09742a6ad7697876"
window-size@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.2.0.tgz#b4315bb4214a3d7058ebeee892e13fa24d98b075"
@ -9023,7 +9185,11 @@ xmldom@0.1.x:
version "0.1.27"
resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9"
"xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1:
xmlhttprequest@*:
version "1.8.0"
resolved "https://registry.yarnpkg.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz#67fe075c5c24fef39f9d65f5f7b7fe75171968fc"
"xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.0, xtend@~4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
@ -9033,7 +9199,11 @@ xtend@~2.1.1:
dependencies:
object-keys "~0.4.0"
y18n@^3.2.1:
xtend@~3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-3.0.0.tgz#5cce7407baf642cba7becda568111c493f59665a"
y18n@^3.2.0, y18n@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
@ -9070,6 +9240,18 @@ yargs@^1.2.6:
version "1.3.3"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-1.3.3.tgz#054de8b61f22eefdb7207059eaef9d6b83fb931a"
yargs@^3.10.0:
version "3.32.0"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.32.0.tgz#03088e9ebf9e756b69751611d2a5ef591482c995"
dependencies:
camelcase "^2.0.1"
cliui "^3.0.3"
decamelize "^1.1.1"
os-locale "^1.4.0"
string-width "^1.0.1"
window-size "^0.1.4"
y18n "^3.2.0"
yargs@^3.5.4, yargs@~3.10.0:
version "3.10.0"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1"
@ -9158,6 +9340,10 @@ yauzl@2.4.1:
dependencies:
fd-slicer "~1.0.1"
zbase32@^0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/zbase32/-/zbase32-0.0.2.tgz#169c6f2130a6c27a84247017538b56826a54b283"
zip-stream@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-1.1.1.tgz#5216b48bbb4d2651f64d5c6e6f09eb4a7399d557"

Loading…
Cancel
Save