diff --git a/CHANGELOG.md b/CHANGELOG.md index 4550558b..1d3eaa6e 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +- ui: Add a new interface for withdrawing on-chain funds + ## 0.2.5 - 2019-02-24 - Use the compact alphanumeric QR encoding mode for bech32 addresses diff --git a/client/src/intent.js b/client/src/intent.js index b78aee3d..00ecc0e2 100644 --- a/client/src/intent.js +++ b/client/src/intent.js @@ -23,6 +23,7 @@ module.exports = ({ DOM, route, conf$, scan$, urihandler$ }) => { , goNewChan$ = route('/channels/new') , goDeposit$ = route('/deposit').mapTo('bech32') .merge(click('[data-newaddr-type]').map(e => e.ownerTarget.dataset.newaddrType)) + , goWithdraw$ = route('/withdraw') // Display and confirm payment requests (from QR, lightning: URIs and manual entry) , viewPay$ = O.merge(scan$, urihandler$).map(parseUri).filter(x => !!x) @@ -69,10 +70,18 @@ module.exports = ({ DOM, route, conf$, scan$, urihandler$ }) => { .share() , openChan$ = submit('[do=open-channel]') .map(d => ({ ...d, channel_capacity_sat: toSatCapacity(d.channel_capacity_msat) })) - , fundMaxChan$ = on('[name=channel-fund-max]', 'input') - .map(e => e.target.checked) - .merge(goNewChan$.mapTo(false)) - .startWith(false) + + // Withdraw + , execWithdraw$ = submit('[do=exec-withdraw]') + .map(d => ({ ...d, amount_sat: toSatCapacity(d.amount_sat) })) + + , fundMax$ = O.merge( + on('[name=channel-fund-max]', 'input') + , on('[name=withdraw-fund-max]', 'input')) + .map(e => e.target.checked) + .merge(goNewChan$.mapTo(false)) + .merge(goWithdraw$.mapTo(false)) + .startWith(false) return { conf$, page$ , goHome$, goScan$, goSend$, goRecv$, goNode$, goLogs$, goRpc$, goDeposit$ @@ -82,7 +91,8 @@ module.exports = ({ DOM, route, conf$, scan$, urihandler$ }) => { , newInv$, amtVal$ , togExp$, togTheme$, togUnit$ , feedStart$, togFeed$ - , togChan$, updChan$, openChan$, closeChan$, fundMaxChan$ + , togChan$, updChan$, openChan$, closeChan$ + , goWithdraw$, execWithdraw$, fundMax$ , dismiss$ } } diff --git a/client/src/model.js b/client/src/model.js index b8fdfb75..a73f5729 100644 --- a/client/src/model.js +++ b/client/src/model.js @@ -22,10 +22,11 @@ const module.exports = ({ dismiss$, togExp$, togTheme$, togUnit$, page$, goHome$, goRecv$, goChan$ , amtVal$, execRpc$, execRes$, clrHist$, feedStart$: feedStart_$, togFeed$, togChan$ - , fundMaxChan$ + , fundMax$ , conf$: savedConf$ , req$$, error$, invoice$, incoming$, outgoing$, payments$, invoices$, funds$ , funded$, closed$ + , withdrawn$ , btcusd$, info$, peers$ }) => { const @@ -68,6 +69,7 @@ module.exports = ({ dismiss$, togExp$, togTheme$, togUnit$, page$, goHome$, goRe , outgoing$.map(p => [ 'success', `Sent payment of @{{${p.msatoshi}}}` ]) , funded$.map(c => [ 'success', `Opening channel for @{{${c.chan.msatoshi_total}}}, awaiting on-chain confirmation` ]) , closed$.map(c => [ 'success', `Channel ${c.chan.short_channel_id || c.chan.channel_id} is closing` ]) + , withdrawn$.map(w => [ 'success', `Withdraw sent. txid: ${w.txid}` ]) , dismiss$.mapTo(null) ) // hide "connection lost" errors when we get back online @@ -171,7 +173,7 @@ module.exports = ({ dismiss$, togExp$, togTheme$, togUnit$, page$, goHome$, goRe , info$: info$.startWith(null), peers$: peers$.startWith(null), channels$: channels$.startWith(null) , feed$: feed$.startWith(null), feedStart$, feedActive$ , amtData$, chanActive$, rpcHist$ - , fundMaxChan$ + , fundMax$ , msatusd$, btcusd$: btcusd$.startWith(null) }).shareReplay(1) } diff --git a/client/src/rpc.js b/client/src/rpc.js index e7a149bf..2199360b 100644 --- a/client/src/rpc.js +++ b/client/src/rpc.js @@ -30,6 +30,7 @@ exports.parseRes = ({ HTTP, SSE }) => { , invoice$: reply('invoice').map(r => ({ ...r.body, ...r.request.ctx })) , outgoing$: reply('pay').map(r => ({ ...r.body, ...r.request.ctx })) , newaddr$: reply('newaddr').map(r => ({ address: r.body.address, type: r.request.send.params[0] })) + , withdrawn$: reply('withdraw').map(r => ({ txid: r.body.txid })) , funded$: reply('connectfund').map(r => r.body) , closed$: reply('closeget').map(r => r.body) , execRes$: reply('console').map(r => ({ ...r.request.send, res: r.body })) @@ -43,7 +44,7 @@ exports.parseRes = ({ HTTP, SSE }) => { // RPC commands to send // NOTE: "connectfund" and "closeget" are custom rpc commands provided by the Spark server. -exports.makeReq = ({ viewPay$, confPay$, newInv$, goLogs$, goChan$, goNewChan$, goDeposit$, updChan$, openChan$, closeChan$, execRpc$ }) => O.merge( +exports.makeReq = ({ viewPay$, confPay$, newInv$, goLogs$, goChan$, goNewChan$, execWithdraw$, goDeposit$, updChan$, openChan$, closeChan$, execRpc$ }) => O.merge( viewPay$.map(bolt11 => [ 'decodepay', [ bolt11 ], { bolt11 } ]) , confPay$.map(pay => [ 'pay', [ pay.bolt11, ...(pay.custom_msat ? [ pay.custom_msat ] : []) ], pay ]) , newInv$.map(inv => [ 'invoice', [ inv.msatoshi, inv.label, inv.description, INVOICE_TTL ], inv ]) @@ -52,7 +53,7 @@ exports.makeReq = ({ viewPay$, confPay$, newInv$, goLogs$, goChan$, goNewChan$, , updChan$.mapTo( [ 'listpeers' ] ) , openChan$.map(d => [ 'connectfund', [ d.nodeuri, d.channel_capacity_sat, d.feerate ] ]) , closeChan$.map(d => [ 'closeget', [ d.peerid, d.chanid ] ]) - +, execWithdraw$.map(d => [ 'withdraw', [ d.address, d.amount_sat, d.feerate ] ]) , goDeposit$.map(type => [ 'newaddr', [ type ] ]) , timer(60000).mapTo( [ 'listinvoices', [], { bg: true } ]) diff --git a/client/src/view.js b/client/src/view.js index f1720616..379fea5e 100644 --- a/client/src/view.js +++ b/client/src/view.js @@ -6,7 +6,7 @@ import themeColors from '../theme-colors.json' const isFunc = x => typeof x == 'function' // DOM view -exports.vdom = ({ state$, goHome$, goScan$, goSend$, goRecv$, goChan$, goNewChan$, goNode$, goRpc$, payreq$, invoice$, newaddr$, logs$ }) => { +exports.vdom = ({ state$, goHome$, goScan$, goSend$, goRecv$, goChan$, goNewChan$, goWithdraw$, goNode$, goRpc$, payreq$, invoice$, newaddr$, logs$ }) => { const body$ = O.merge( // user actions goHome$.startWith(1).mapTo(views.home) @@ -15,6 +15,7 @@ exports.vdom = ({ state$, goHome$, goScan$, goSend$, goRecv$, goChan$, goNewChan , goRecv$.mapTo(views.recv) , goChan$.mapTo(views.channels) , goNewChan$.mapTo(views.newChannel) + , goWithdraw$.mapTo(views.withdraw) , goNode$.mapTo(views.nodeInfo) , goRpc$.mapTo(views.rpc) diff --git a/client/src/views/channels.js b/client/src/views/channels.js index e8534887..c62b5df0 100644 --- a/client/src/views/channels.js +++ b/client/src/views/channels.js @@ -45,7 +45,7 @@ export const channels = ({ channels, chanActive, unitf, info, conf: { expert } } ]) } -export const newChannel = ({ amtData, fundMaxChan, obalance, unitf, conf: { unit, expert } }) => { +export const newChannel = ({ amtData, fundMax, obalance, unitf, conf: { unit, expert } }) => { const availText = obalance != null ? `Available: ${unitf(obalance)}` : '' return form({ attrs: { do: 'open-channel' } }, [ @@ -55,14 +55,14 @@ export const newChannel = ({ amtData, fundMaxChan, obalance, unitf, conf: { unit name: 'nodeuri', placeholder: 'nodeid@host[:port]', required: true } })) , formGroup('Channel funding', div([ - !fundMaxChan + !fundMax ? amountField(amtData, 'channel_capacity_msat', true, availText) : div('.input-group', [ input({ attrs: { type: 'hidden', name: 'channel_capacity_msat', value: 'all' } }) , input('.form-control.form-control-lg.disabled', { attrs: { disabled: true, placeholder: availText } }) , div('.input-group-append.toggle-unit', span('.input-group-text', unit)) ]) - , fancyCheckbox('channel-fund-max', 'Fund maximum', fundMaxChan, '.btn-sm') + , fancyCheckbox('channel-fund-max', 'Fund maximum', fundMax, '.btn-sm') ])) , expert ? formGroup('Fee rate', input('.form-control.form-control-lg' diff --git a/client/src/views/node.js b/client/src/views/node.js index e2f9d56f..7f4585df 100644 --- a/client/src/views/node.js +++ b/client/src/views/node.js @@ -23,8 +23,11 @@ exports.nodeInfo = async ({ info, peers, conf: { expert } }) => { process.env.BUILD_TARGET != 'web' ? a('.btn.btn-secondary.btn-sm', { attrs: { href: 'settings.html', rel: 'external' }}, 'Server settings') : '' , ' ' , a('.btn.btn-secondary.btn-sm', { attrs: { href: '#/channels' }}, 'Channels') - , ' ' + ]) + , p('.text-center.mt-4', [ , a('.btn.btn-secondary.btn-sm', { attrs: { href: '#/deposit' }}, 'Deposit') + , ' ' + , a('.btn.btn-secondary.btn-sm', { attrs: { href: '#/withdraw' }}, 'Withdraw') ]) , expert ? yaml(info) : '' ]) diff --git a/client/src/views/onchain.js b/client/src/views/onchain.js index 5eca237b..ace6fea7 100644 --- a/client/src/views/onchain.js +++ b/client/src/views/onchain.js @@ -1,5 +1,5 @@ -import { div, img, h2, h4, small, a, button, p } from '@cycle/dom' -import { yaml, qruri } from './util' +import { div, img, h2, h4, span, a, p, button, form, input } from '@cycle/dom' +import { yaml, qruri, formGroup, amountField, fancyCheckbox } from './util' const labelType = { bech32: 'Bech32', 'p2sh-segwit': 'P2SH' } , otherType = { bech32: 'p2sh-segwit', 'p2sh-segwit': 'bech32' } @@ -28,3 +28,33 @@ export const deposit = ({ address, type }) => addrQr(address, type).then(qr => ( , p('.text-muted.small', 'Note: c-lightning does not process unconfirmed payments. You will not receive a notification for the payment, please check back once its confirmed.') , expert ? yaml({ outputs: funds && funds.outputs }) : '' ])) + + export const withdraw = ({ amtData, fundMax, obalance, unitf, conf: { unit, expert } }) => { + const availText = obalance != null ? `Available: ${unitf(obalance)}` : '' + + return form({ attrs: { do: 'exec-withdraw' } }, [ + h2('On-chain withdraw') + + , formGroup('Address', input('.form-control.form-control-lg' , { attrs: { + name: 'address', required: true } })) + + , formGroup('Withdraw Amount', div([ + !fundMax + ? amountField(amtData, 'amount_sat', true, availText) + : div('.input-group', [ + input({ attrs: { type: 'hidden', name: 'amount_sat', value: 'all' } }) + , input('.form-control.form-control-lg.disabled', { attrs: { disabled: true, placeholder: availText } }) + , div('.input-group-append.toggle-unit', span('.input-group-text', unit)) + ]) + , fancyCheckbox('withdraw-fund-max', 'Withdraw All', fundMax, '.btn-sm') + ])) + + , expert ? formGroup('Fee rate', input('.form-control.form-control-lg' + , { attrs: { type: 'text', name: 'feerate', placeholder: '(optional)' + , pattern: '[0-9]+(perk[bw])?|slow|normal|urgent' } })) : '' + + , div('.form-buttons', [ + button('.btn.btn-lg.btn-primary', { attrs: { type: 'submit' } }, 'Withdraw') + ]) + ]) + } \ No newline at end of file diff --git a/client/styl/style.styl b/client/styl/style.styl index c8e7448e..58c7b1f5 100644 --- a/client/styl/style.styl +++ b/client/styl/style.styl @@ -96,7 +96,7 @@ html .alert font-size 0.9rem font-weight 400 - word-break break-word + word-wrap break-word .btn-link cursor pointer