diff --git a/LightningCustodianWallet.test.js b/LightningCustodianWallet.test.js index 82f6ab47..0903baee 100644 --- a/LightningCustodianWallet.test.js +++ b/LightningCustodianWallet.test.js @@ -247,4 +247,69 @@ describe('LightningCustodianWallet', () => { assert.equal(lOld.balance - oldBalance, 1); assert.equal(lNew.balance, 0); }); + + it('can pay free amount (tip) invoice', async function() { + jasmine.DEFAULT_TIMEOUT_INTERVAL = 100 * 1000; + if (!process.env.BLITZHUB) { + console.error('process.env.BLITZHUB not set, skipped'); + return; + } + + // fetchig invoice from tippin.me : + + const api = new Frisbee({ + baseURI: 'https://tippin.me', + }); + const res = await api.post('/lndreq/newinvoice.php', { + headers: { + Origin: 'https://tippin.me', + 'Accept-Encoding': 'gzip, deflate, br', + 'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8', + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + Accept: 'application/json, text/javascript, */*; q=0.01', + }, + body: 'userid=1188&username=overtorment&istaco=0&customAmnt=0&customMemo=', + }); + + let json; + let invoice; + if (res && res.body && (json = JSON.parse(res.body)) && json.message) { + invoice = json.message; + } else { + throw new Error('tippin.me problem: ' + JSON.stringify(res)); + } + + // --> use to pay specific invoice + // invoice = + // 'lnbc1pwrp35spp5z62nvj8yw6luq7ns4a8utpwn2qkkdwdt0ludwm54wjeazk2xv5wsdpu235hqurfdcsx7an9wf6x7undv4h8ggpgw35hqurfdchx6eff9p6nzvfc8q5scqzysxqyz5vqj8xq6wz6dezmunw6qxleuw67ensjnt3fldltrmmkvzurge0dczpn94fkwwh7hkh5wqrhsvfegtvhswn252hn6uw5kx99dyumz4v5n9sp337py2'; + + let l2 = new LightningCustodianWallet(); + l2.setSecret(process.env.BLITZHUB); + await l2.authorize(); + await l2.fetchTransactions(); + await l2.fetchBalance(); + let oldBalance = +l2.balance; + let txLen = l2.transactions_raw.length; + + let decoded = await l2.decodeInvoice(invoice); + assert.ok(decoded.payment_hash); + assert.ok(decoded.description); + assert.equal(+decoded.num_satoshis, 0); + + await l2.checkRouteInvoice(invoice); + + let start = +new Date(); + await l2.payInvoice(invoice, 3); + let end = +new Date(); + if ((end - start) / 1000 > 9) { + console.warn('payInvoice took', (end - start) / 1000, 'sec'); + } + + await l2.fetchTransactions(); + assert.equal(l2.transactions_raw.length, txLen + 1); + // transactions became more after paying an invoice + + await l2.fetchBalance(); + assert.equal(oldBalance - l2.balance, 3); + }); }); diff --git a/class/lightning-custodian-wallet.js b/class/lightning-custodian-wallet.js index 723deb05..9c0cacf7 100644 --- a/class/lightning-custodian-wallet.js +++ b/class/lightning-custodian-wallet.js @@ -102,9 +102,9 @@ export class LightningCustodianWallet extends LegacyWallet { this.secret = 'lndhub://' + json.login + ':' + json.password; } - async payInvoice(invoice) { + async payInvoice(invoice, freeAmount = 0) { let response = await this._api.post('/payinvoice', { - body: { invoice: invoice }, + body: { invoice: invoice, amount: freeAmount }, headers: { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'application/json', diff --git a/prompt.js b/prompt.js index a09338f7..366962bb 100644 --- a/prompt.js +++ b/prompt.js @@ -1,6 +1,6 @@ import prompt from 'react-native-prompt-android'; -module.exports = (title, text, isCancelable = true) => { +module.exports = (title, text, isCancelable = true, type = 'secure-text') => { return new Promise((resolve, reject) => { const buttons = isCancelable ? [ @@ -30,7 +30,7 @@ module.exports = (title, text, isCancelable = true) => { ]; prompt(title, text, buttons, { - type: 'secure-text', + type: type, cancelable: isCancelable, }); }); diff --git a/screen/lnd/scanLndInvoice.js b/screen/lnd/scanLndInvoice.js index ba29afb8..8bbd89d8 100644 --- a/screen/lnd/scanLndInvoice.js +++ b/screen/lnd/scanLndInvoice.js @@ -10,6 +10,7 @@ let BlueApp = require('../../BlueApp'); let currency = require('../../currency'); let EV = require('../../events'); let loc = require('../../loc'); +let prompt = require('../../prompt'); const { width } = Dimensions.get('window'); export default class ScanLndInvoice extends React.Component { @@ -92,6 +93,14 @@ export default class ScanLndInvoice extends React.Component { let decoded; try { decoded = await w.decodeInvoice(data); + let freeAmount = 0; + while (+decoded.num_satoshis === 0) { + freeAmount = await prompt('This is free amount invoice', 'How many satoshis do you want to tip?', false, 'numeric'); + freeAmount = parseInt(freeAmount); + if (!isNaN(freeAmount) && freeAmount > 0) { + decoded.num_satoshis = freeAmount; + } + } let expiresIn = (decoded.timestamp * 1 + decoded.expiry * 1) * 1000; // ms if (+new Date() > expiresIn) { @@ -105,6 +114,7 @@ export default class ScanLndInvoice extends React.Component { invoice: data, decoded, expiresIn, + freeAmount, destination: data, }); } catch (Err) { @@ -143,7 +153,7 @@ export default class ScanLndInvoice extends React.Component { let start = +new Date(); let end; try { - await fromWallet.payInvoice(this.state.invoice); + await fromWallet.payInvoice(this.state.invoice, this.state.freeAmount); end = +new Date(); } catch (Err) { console.log(Err.message); @@ -170,7 +180,24 @@ export default class ScanLndInvoice extends React.Component { render() { return ( - + { + if (this.state.freeAmount) { + // must ask user again about the amount + let freeAmount = await prompt('This is free amount invoice', 'How many satoshis do you want to tip?', false, 'numeric'); + freeAmount = parseInt(freeAmount); + if (!isNaN(freeAmount) && freeAmount > 0) { + let decoded = this.state.decoded; + decoded.num_satoshis = freeAmount; + this.setState({ decoded, freeAmount }); + Keyboard.dismiss(); + } + } else { + Keyboard.dismiss(); + } + }} + accessible={false} + > {this.state.hasOwnProperty('decoded') &&