diff --git a/BlueComponents.js b/BlueComponents.js index bd9f46362..62055cc91 100644 --- a/BlueComponents.js +++ b/BlueComponents.js @@ -37,7 +37,6 @@ export class BlueButton extends Component { marginTop: 20, borderWidth: 0.7, borderColor: 'transparent', - borderLeftColor: 'transparent', }} buttonStyle={Object.assign( { @@ -184,7 +183,18 @@ export class BlueCard extends Component { export class BlueText extends Component { render() { - return ; + return ( + + ); } } export class BlueTextCentered extends Component { @@ -599,6 +609,27 @@ export class BlueTransactionPendingIcon extends Component { } } +export class BlueTransactionOnchainIcon extends Component { + render() { + return ( + + + + + + + + ); + } +} + export class BlueTransactionOutgoingIcon extends Component { render() { return ( @@ -728,6 +759,66 @@ export class BlueSendButtonIcon extends Component { } } +export class ManageFundsBigButton extends Component { + render() { + return ( + + + + + + + + manage funds + + + + + ); + } +} + export class BluePlusIconDimmed extends Component { render() { return ( diff --git a/HDWallet.test.js b/HDWallet.test.js index 3fd59ea37..a991df4c5 100644 --- a/HDWallet.test.js +++ b/HDWallet.test.js @@ -69,9 +69,9 @@ it('can generate Segwit HD (BIP49)', async () => { assert.ok(hd2.validateMnemonic()); }); -it('HD (BIP49)can create TX', async () => { +it('HD (BIP49) can create TX', async () => { if (!process.env.HD_MNEMONIC) { - console.log('process.env.HD_MNEMONIC not set, skipped'); + console.warn('process.env.HD_MNEMONIC not set, skipped'); return; } jasmine.DEFAULT_TIMEOUT_INTERVAL = 90 * 1000; @@ -85,7 +85,7 @@ it('HD (BIP49)can create TX', async () => { let txhex = hd.createTx(hd.utxo, 0.000014, 0.000001, '3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK'); assert.equal( txhex, - '01000000000102ee7a13faf14dd004c6fa403c3073fbb6e0d7389ffa45e879fd96b5e21fd8989d00000000171600142f18e8406c9d210f30c901b24e5feeae78784eb7ffffffff22cde2709a2774a008fd0513e94edde4fdc71195ce0fd408e524df10f386fb67000000001716001468dde644410cc789d91a7f36b823f38369755a1cffffffff02780500000000000017a914a3a65daca3064280ae072b9d6773c027b30abace87dc0500000000000017a914850f4dbc255654de2c12c6f6d79cf9cb756cad038702473044022025e2a280e77691804ef3aa8039dceb5b7e454fb97edd2088f32858e86115bb030220553c21f7c9026a833ad9582a119cd6b24227fc45ed84fd18115ae71e5a8975f5012102edd141c5a27a726dda66be10a38b0fd3ccbb40e7c380034aaa43a1656d5f4dd60247304402207c9b7b0b7767e7bb37388fbfb865402ca58d2d7b88a7110244fc5d7881ae3cce022037874f10db854df4bfdc9ef2b02a9e2919a238eac6aad82bd82e528585084e3b0121030db3c49461a5e539e97bab62ab2b8f88151d1c2376493cf73ef1d02ef60637fd00000000', + '010000000001029d98d81fe2b596fd79e845fa9f38d7e0b6fb73303c40fac604d04df1fa137aee00000000171600142f18e8406c9d210f30c901b24e5feeae78784eb7ffffffff67fb86f310df24e508d40fce9511c7fde4dd4ee91305fd08a074279a70e2cd22000000001716001468dde644410cc789d91a7f36b823f38369755a1cffffffff02780500000000000017a914a3a65daca3064280ae072b9d6773c027b30abace87dc0500000000000017a914850f4dbc255654de2c12c6f6d79cf9cb756cad038702483045022100dc8390a9fd34c31259fa47f9fc182f20d991110ecfd5b58af1cf542fe8de257a022004c2d110da7b8c4127675beccc63b46fd65c706951f090fd381fa3b21d3c5c08012102edd141c5a27a726dda66be10a38b0fd3ccbb40e7c380034aaa43a1656d5f4dd60247304402207c0aef8313d55e72474247daad955979f62e56d1cbac5f2d14b8b022c6ce112602205d9aa3804f04624b12ab8a5ab0214b529c531c2f71c27c6f18aba6502a6ea0a80121030db3c49461a5e539e97bab62ab2b8f88151d1c2376493cf73ef1d02ef60637fd00000000', ); let bitcoin = require('bitcoinjs-lib'); @@ -124,7 +124,7 @@ it('Segwit HD (BIP49) can fetch UTXO', async function() { let hd = new HDSegwitP2SHWallet(); hd.usedAddresses = ['1Ez69SnzzmePmZX3WpEzMKTrcBF2gpNQ55', '1BiTCHeYzJNMxBLFCMkwYXNdFEdPJP53ZV']; // hacking internals await hd.fetchUtxo(); - assert.equal(hd.utxo.length, 8); + assert.equal(hd.utxo.length, 9); assert.ok(hd.utxo[0].confirmations); assert.ok(hd.utxo[0].txid); assert.ok(hd.utxo[0].vout); diff --git a/LightningCustodianWallet.test.js b/LightningCustodianWallet.test.js index 931b31dc0..20b7ba62f 100644 --- a/LightningCustodianWallet.test.js +++ b/LightningCustodianWallet.test.js @@ -1,12 +1,143 @@ -/* global it */ +/* global it, describe, jasmine */ +import Frisbee from 'frisbee'; import { LightningCustodianWallet } from './class'; let assert = require('assert'); -it('can generate auth secret', () => { +describe('LightningCustodianWallet', () => { let l1 = new LightningCustodianWallet(); - let l2 = new LightningCustodianWallet(); - l1.generate(); - l2.generate(); - assert.ok(l1.getSecret() !== l2.getSecret(), 'generated credentials should not be the same'); + it('can create, auth and getbtc', async () => { + jasmine.DEFAULT_TIMEOUT_INTERVAL = 100 * 1000; + assert.ok(l1.refill_addressess.length === 0); + assert.ok(l1._refresh_token_created_ts === 0); + assert.ok(l1._access_token_created_ts === 0); + l1.balance = 'FAKE'; + + await l1.createAccount(); + await l1.authorize(); + await l1.fetchBtcAddress(); + await l1.fetchBalance(); + await l1.fetchInfo(); + await l1.fetchTransactions(); + await l1.fetchPendingTransactions(); + + assert.ok(l1.access_token); + assert.ok(l1.refresh_token); + assert.ok(l1._refresh_token_created_ts > 0); + assert.ok(l1._access_token_created_ts > 0); + assert.ok(l1.refill_addressess.length > 0); + assert.ok(l1.balance === 0); + assert.ok(l1.info_raw); + assert.ok(l1.pending_transactions_raw.length === 0); + assert.ok(l1.transactions_raw.length === 0); + assert.ok(l1.transactions_raw.length === l1.getTransactions().length); + }); + + it('can refresh token', async () => { + jasmine.DEFAULT_TIMEOUT_INTERVAL = 100 * 1000; + let oldRefreshToken = l1.refresh_token; + let oldAccessToken = l1.access_token; + await l1.refreshAcessToken(); + assert.ok(oldRefreshToken !== l1.refresh_token); + assert.ok(oldAccessToken !== l1.access_token); + assert.ok(l1.access_token); + assert.ok(l1.refresh_token); + }); + + it('can use existing login/pass', async () => { + jasmine.DEFAULT_TIMEOUT_INTERVAL = 100 * 1000; + if (!process.env.BLITZHUB) { + console.error('process.env.BLITZHUB not set, skipped'); + return; + } + let l2 = new LightningCustodianWallet(); + l2.setSecret(process.env.BLITZHUB); + await l2.authorize(); + await l2.fetchPendingTransactions(); + await l2.fetchTransactions(); + assert.ok(l2.pending_transactions_raw.length === 0); + assert.ok(l2.transactions_raw.length > 0); + assert.ok(l2.transactions_raw.length === l2.getTransactions().length); + await l2.fetchBalance(); + assert.ok(l2.getBalance() > 0); + }); + + it('can decode & check invoice', async () => { + if (!process.env.BLITZHUB) { + console.error('process.env.BLITZHUB not set, skipped'); + return; + } + jasmine.DEFAULT_TIMEOUT_INTERVAL = 30 * 1000; + let l2 = new LightningCustodianWallet(); + l2.setSecret(process.env.BLITZHUB); + await l2.authorize(); + + let invoice = + 'lnbc1u1pdcqpt3pp5ltuevvq2g69kdrzcegrs9gfqjer45rwjc0w736qjl92yvwtxhn6qdp8dp6kuerjv4j9xct5daeks6tnyp3xc6t50f582cscqp2zrkghzl535xjav52ns0rpskcn20takzdr2e02wn4xqretlgdemg596acq5qtfqhjk4jpr7jk8qfuuka2k0lfwjsk9mchwhxcgxzj3tsp09gfpy'; + let decoded = await l2.decodeInvoice(invoice); + + assert.ok(decoded.payment_hash); + assert.ok(decoded.description); + assert.ok(decoded.num_satoshis); + + await l2.checkRouteInvoice(invoice); + + // checking that bad invoice cant be decoded + invoice = 'gsom'; + let error = false; + try { + await l2.decodeInvoice(invoice); + } catch (Err) { + error = true; + } + assert.ok(error); + }); + + it('can pay invoice', async () => { + jasmine.DEFAULT_TIMEOUT_INTERVAL = 100 * 1000; + if (!process.env.BLITZHUB) { + console.error('process.env.BLITZHUB not set, skipped'); + return; + } + if (!process.env.STRIKE) { + console.error('process.env.STRIKE not set, skipped'); + return; + } + + const api = new Frisbee({ + baseURI: 'https://api.strike.acinq.co', + }); + + api.auth(process.env.STRIKE + ':'); + + const res = await api.post('/api/v1/charges', { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: 'amount=1¤cy=btc&description=acceptance+test', + }); + + if (!res.body || !res.body.payment_request) { + throw new Error('Strike problem: ' + JSON.stringify(res)); + } + + let invoice = res.body.payment_request; + + let l2 = new LightningCustodianWallet(); + l2.setSecret(process.env.BLITZHUB); + await l2.authorize(); + + let decoded = await l2.decodeInvoice(invoice); + assert.ok(decoded.payment_hash); + assert.ok(decoded.description); + + await l2.checkRouteInvoice(invoice); + + let start = +new Date(); + await l2.payInvoice(invoice); + let end = +new Date(); + if ((end - start) / 1000 > 9) { + console.warn('payInvoice took', (end - start) / 1000, 'sec'); + } + }); }); diff --git a/class/app-storage.js b/class/app-storage.js index f4d822c7c..7eeb3ec15 100644 --- a/class/app-storage.js +++ b/class/app-storage.js @@ -8,6 +8,7 @@ import { SegwitP2SHWallet, SegwitBech32Wallet, } from './'; +import { LightningCustodianWallet } from './lightning-custodian-wallet'; let encryption = require('../encryption'); export class AppStorage { @@ -147,6 +148,9 @@ export class AppStorage { case new HDLegacyBreadwalletWallet().type: unserializedWallet = HDLegacyBreadwalletWallet.fromJson(key); break; + case new LightningCustodianWallet().type: + unserializedWallet = LightningCustodianWallet.fromJson(key); + break; case 'legacy': default: unserializedWallet = LegacyWallet.fromJson(key); diff --git a/class/hd-segwit-p2sh-wallet.js b/class/hd-segwit-p2sh-wallet.js index d83ad7968..1ed59289d 100644 --- a/class/hd-segwit-p2sh-wallet.js +++ b/class/hd-segwit-p2sh-wallet.js @@ -22,6 +22,10 @@ export class HDSegwitP2SHWallet extends AbstractHDWallet { return 'HD SegWit (BIP49 P2SH)'; } + allowSend() { + return this.getBalance() > 0; + } + generate() { let c = 32; let totalhex = ''; @@ -303,7 +307,7 @@ export class HDSegwitP2SHWallet extends AbstractHDWallet { for (let unspent of json.unspent_outputs) { // a lil transform for signer module - unspent.txid = unspent.tx_hash; + unspent.txid = unspent.tx_hash_big_endian; unspent.vout = unspent.tx_output_n; unspent.amount = unspent.value; diff --git a/class/legacy-wallet.js b/class/legacy-wallet.js index c9a65338c..0edf8bd30 100644 --- a/class/legacy-wallet.js +++ b/class/legacy-wallet.js @@ -397,10 +397,15 @@ export class LegacyWallet extends AbstractWallet { } getLatestTransactionTime() { - for (let tx of this.getTransactions()) { - return tx.received; + if (this.getTransactions().length === 0) { + return 0; } - return 0; + let max = 0; + for (let tx of this.getTransactions()) { + max = Math.max(new Date(tx.received) * 1, max); + } + + return new Date(max).toString(); } getRandomBlockcypherToken() { diff --git a/class/lightning-custodian-wallet.js b/class/lightning-custodian-wallet.js index 4aed4b54b..69401db0a 100644 --- a/class/lightning-custodian-wallet.js +++ b/class/lightning-custodian-wallet.js @@ -1,55 +1,464 @@ import { LegacyWallet } from './legacy-wallet'; import Frisbee from 'frisbee'; +let BigNumber = require('bignumber.js'); export class LightningCustodianWallet extends LegacyWallet { constructor() { super(); + this.init(); this.type = 'lightningCustodianWallet'; - this.pendingTransactions = []; - this.token = false; - this.tokenRefreshedOn = 0; + this.refresh_token = ''; + this.access_token = ''; + this._refresh_token_created_ts = 0; + this._access_token_created_ts = 0; + this.refill_addressess = []; + this.pending_transactions_raw = []; + this.info_raw = false; + } + + getAddress() { + return ''; + } + + timeToRefreshBalance() { + // blitzhub calls are cheap, so why not refresh constantly + return true; + } + + timeToRefreshTransaction() { + // blitzhub calls are cheap, so why not refresh the list constantly + return true; + } + + static fromJson(param) { + let obj = super.fromJson(param); + obj.init(); + return obj; + } + + init() { this._api = new Frisbee({ - baseURI: 'https://api.blockcypher.com/v1/btc/main/addrs/', + baseURI: 'https://api.blitzhub.io/', }); } + accessTokenExpired() { + return (+new Date() - this._access_token_created_ts) / 1000 >= 3600 * 2; // 2h + } + + refreshTokenExpired() { + return (+new Date() - this._refresh_token_created_ts) / 1000 >= 3600 * 24 * 7; // 7d + } + + generate() { + // nop + } + getTypeReadable() { return 'Lightning (custodian)'; } - async createAccount() {} + async createAccount() { + let response = await this._api.post('/create', { + body: { partnerid: 'bluewallet', test: true }, + headers: { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'application/json' }, + }); + let json = response.body; + if (typeof json === 'undefined') { + throw new Error('API failure: ' + response.err + ' ' + JSON.stringify(response.body)); + } - async authorize() {} + if (json && json.error) { + throw new Error('API error: ' + json.message + ' (code ' + json.code + ')'); + } - async getToken() {} + if (!json.login || !json.password) { + throw new Error('API unexpected response: ' + JSON.stringify(response.body)); + } - async getBtcAddress() {} + this.secret = 'blitzhub://' + json.login + ':' + json.password; - async newBtcAddress() {} + console.log(response.body); + } - async getPendngBalance() {} + async payInvoice(invoice) { + let response = await this._api.post('/payinvoice', { + body: { invoice: invoice }, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Content-Type': 'application/json', + Authorization: 'Bearer' + ' ' + this.access_token, + }, + }); + let json = response.body; + if (typeof json === 'undefined') { + throw new Error('API failure: ' + response.err + ' ' + JSON.stringify(response.originalResponse)); + } - async decodeInvoice() {} + if (json && json.error) { + throw new Error('API error: ' + json.message + ' (code ' + json.code + ')'); + } - async checkRoute() {} + console.log(response.body); - async payInvoice() {} + this.last_paid_invoice_result = json; - async sendCoins() {} + if (json.payment_preimage) { + return true; + } else { + return false; + } + } + + async checkRouteInvoice(invoice) { + let response = await this._api.get('/checkrouteinvoice?invoice=' + invoice, { + headers: { + 'Access-Control-Allow-Origin': '*', + 'Content-Type': 'application/json', + Authorization: 'Bearer' + ' ' + this.access_token, + }, + }); + + let json = response.body; + if (typeof json === 'undefined') { + throw new Error('API failure: ' + response.err + ' ' + JSON.stringify(response.body)); + } + + if (json && json.error) { + throw new Error('API error: ' + json.message + ' (code ' + json.code + ')'); + } + + console.log(json); + } + + /** + * Uses login & pass stored in `this.secret` to authorize + * and set internal `access_token` & `refresh_token` + * + * @return {Promise.} + */ + async authorize() { + let login = this.secret.replace('blitzhub://', '').split(':')[0]; + let password = this.secret.replace('blitzhub://', '').split(':')[1]; + console.log('auth uses login:pass', login, password); + let response = await this._api.post('/auth?type=auth', { + body: { login: login, password: password }, + headers: { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'application/json' }, + }); + + let json = response.body; + if (typeof json === 'undefined') { + throw new Error('API failure: ' + response.err + ' ' + JSON.stringify(response.body)); + } + + if (json && json.error) { + throw new Error('API error: ' + json.message + ' (code ' + json.code + ')'); + } + + if (!json.access_token || !json.refresh_token) { + throw new Error('API unexpected response: ' + JSON.stringify(response.body)); + } + + this.refresh_token = json.refresh_token; + this.access_token = json.access_token; + this._refresh_token_created_ts = +new Date(); + this._access_token_created_ts = +new Date(); + + console.log(json); + } + + async checkLogin() { + if (this.accessTokenExpired() && this.refreshTokenExpired()) { + // all tokens expired, only option is to login with login and password + return this.authorize(); + } + + if (this.accessTokenExpired()) { + // only access token expired, so only refreshing it + let refreshedOk = true; + try { + await this.refreshAcessToken(); + } catch (Err) { + refreshedOk = false; + } + + if (!refreshedOk) { + // something went wrong, lets try to login regularly + return this.authorize(); + } + } + } + + async refreshAcessToken() { + let response = await this._api.post('/auth?type=refresh_token', { + body: { refresh_token: this.refresh_token }, + headers: { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'application/json' }, + }); + + let json = response.body; + if (typeof json === 'undefined') { + throw new Error('API failure: ' + response.err + ' ' + JSON.stringify(response.body)); + } + + if (json && json.error) { + throw new Error('API error: ' + json.message + ' (code ' + json.code + ')'); + } + + if (!json.access_token || !json.refresh_token) { + throw new Error('API unexpected response: ' + JSON.stringify(response.body)); + } + + this.refresh_token = json.refresh_token; + this.access_token = json.access_token; + this._refresh_token_created_ts = +new Date(); + this._access_token_created_ts = +new Date(); + + console.log(json); + } + + async fetchBtcAddress() { + let response = await this._api.get('/getbtc', { + headers: { + 'Access-Control-Allow-Origin': '*', + 'Content-Type': 'application/json', + Authorization: 'Bearer' + ' ' + this.access_token, + }, + }); + + let json = response.body; + if (typeof json === 'undefined') { + throw new Error('API failure: ' + response.err + ' ' + JSON.stringify(response.body)); + } + + if (json && json.error) { + throw new Error('API error: ' + json.message + ' (code ' + json.code + ')'); + } + + this.refill_addressess = []; + + for (let arr of json) { + this.refill_addressess.push(arr.address); + } + + console.log(json); + } getTransactions() { - return []; + let txs = []; + this.pending_transactions_raw = this.pending_transactions_raw || []; + this.transactions_raw = this.transactions_raw || []; + txs = txs.concat(this.pending_transactions_raw, this.transactions_raw.slice().reverse()); // slice so array is cloned + // transforming to how wallets/list screen expects it + for (let tx of txs) { + tx.value = tx.amount * 100000000; + tx.received = new Date(tx.time * 1000).toString(); + tx.memo = 'Refill'; + } + return txs; + } + + async fetchPendingTransactions() { + let response = await this._api.get('/getpending', { + headers: { + 'Access-Control-Allow-Origin': '*', + 'Content-Type': 'application/json', + Authorization: 'Bearer' + ' ' + this.access_token, + }, + }); + + let json = response.body; + if (typeof json === 'undefined') { + throw new Error('API failure: ' + response.err + ' ' + JSON.stringify(response.body)); + } + + if (json && json.error) { + throw new Error('API error: ' + json.message + ' (code ' + json.code + ')'); + } + + this.pending_transactions_raw = json; + + console.log(json); } async fetchTransactions() { - return []; - } + // TODO: iterate over all available pages + const limit = 10; + let queryRes = ''; + let offset = 0; + queryRes += '?limit=' + limit; + queryRes += '&offset=' + offset; - async getTransaction() {} + let response = await this._api.get('/gettxs' + queryRes, { + headers: { + 'Access-Control-Allow-Origin': '*', + 'Content-Type': 'application/json', + Authorization: 'Bearer' + ' ' + this.access_token, + }, + }); + + let json = response.body; + if (typeof json === 'undefined') { + throw new Error('API failure: ' + response.err + ' ' + JSON.stringify(response.body)); + } + + if (json && json.error) { + throw new Error('API error: ' + json.message + ' (code ' + json.code + ')'); + } + + if (typeof json.btc_txs === 'undefined' || typeof json.paid_invoices === 'undefined' || typeof json.sended_coins === 'undefined') { + throw new Error('API unexpected response: ' + JSON.stringify(response.body)); + } + + this.transactions_raw = [].concat(json.btc_txs || [], json.paid_invoices || [], json.sended_coins || []); + + console.log(json); + } getBalance() { - return 0; + return new BigNumber(this.balance).div(100000000).toString(10); } - async getInfo() {} + async fetchBalance() { + await this.checkLogin(); + + let response = await this._api.get('/balance', { + headers: { + 'Access-Control-Allow-Origin': '*', + 'Content-Type': 'application/json', + Authorization: 'Bearer' + ' ' + this.access_token, + }, + }); + + let json = response.body; + if (typeof json === 'undefined') { + throw new Error('API failure: ' + response.err + ' ' + JSON.stringify(response.body)); + } + + if (json && json.error) { + throw new Error('API error: ' + json.message + ' (code ' + json.code + ')'); + } + + if (!json.BTC || typeof json.BTC.AvailableBalance === 'undefined') { + throw new Error('API unexpected response: ' + JSON.stringify(response.body)); + } + + this.balance_raw = json; + this.balance = json.BTC.AvailableBalance; + this._lastBalanceFetch = +new Date(); + + console.log(json); + } + + /** + * Example return: + * { destination: '03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f', + * payment_hash: 'faf996300a468b668c58ca0702a12096475a0dd2c3dde8e812f954463966bcf4', + * num_satoshisnum_satoshis: '100', + * timestamp: '1535116657', + * expiry: '3600', + * description: 'hundredSatoshis blitzhub', + * description_hash: '', + * fallback_addr: '', + * cltv_expiry: '10', + * route_hints: [] } + * + * @param invoice BOLT invoice string + * @return {Promise.} + */ + async decodeInvoice(invoice) { + await this.checkLogin(); + + let response = await this._api.get('/decodeinvoice?invoice=' + invoice, { + headers: { + 'Access-Control-Allow-Origin': '*', + 'Content-Type': 'application/json', + Authorization: 'Bearer' + ' ' + this.access_token, + }, + }); + + let json = response.body; + if (typeof json === 'undefined') { + throw new Error('API failure: ' + response.err + ' ' + JSON.stringify(response.body)); + } + + if (json && json.error) { + throw new Error('API error: ' + json.message + ' (code ' + json.code + ')'); + } + + if (!json.payment_hash) { + throw new Error('API unexpected response: ' + JSON.stringify(response.body)); + } + + console.log(json); + + return (this.decoded_invoice_raw = json); + } + + async fetchInfo() { + let response = await this._api.get('/getinfo', { + headers: { + 'Access-Control-Allow-Origin': '*', + 'Content-Type': 'application/json', + Authorization: 'Bearer' + ' ' + this.access_token, + }, + }); + + let json = response.body; + if (typeof json === 'undefined') { + throw new Error('API failure: ' + response.err + ' ' + JSON.stringify(response.body)); + } + + if (json && json.error) { + throw new Error('API error: ' + json.message + ' (code ' + json.code + ')'); + } + + if (!json.identity_pubkey) { + throw new Error('API unexpected response: ' + JSON.stringify(response.body)); + } + + this.info_raw = json; + + console.log(json); + } + + allowReceive() { + return false; + } } + +/* + + + +pending tx: + + [ { amount: 0.00078061, + account: '521172', + address: '3F9seBGCJZQ4WJJHwGhrxeGXCGbrm5SNpF', + category: 'receive', + confirmations: 0, + blockhash: '', + blockindex: 0, + blocktime: 0, + txid: '28a74277e47c2d772ee8a40464209c90dce084f3b5de38a2f41b14c79e3bfc62', + walletconflicts: [], + time: 1535024434, + timereceived: 1535024434 } ] + + +tx: + + [ { amount: 0.00078061, + account: '521172', + address: '3F9seBGCJZQ4WJJHwGhrxeGXCGbrm5SNpF', + category: 'receive', + confirmations: 5, + blockhash: '0000000000000000000edf18e9ece18e449c6d8eed1f729946b3531c32ee9f57', + blockindex: 693, + blocktime: 1535024914, + txid: '28a74277e47c2d772ee8a40464209c90dce084f3b5de38a2f41b14c79e3bfc62', + walletconflicts: [], + time: 1535024434, + timereceived: 1535024434 } ] + + */ diff --git a/currency.js b/currency.js index c7a5d3e74..ed7a37a9d 100644 --- a/currency.js +++ b/currency.js @@ -1,6 +1,7 @@ import Frisbee from 'frisbee'; import { AsyncStorage } from 'react-native'; import { AppStorage } from './class'; +let BigNumber = require('bignumber.js'); let lang = {}; // let btcusd = 6500; // default @@ -52,6 +53,27 @@ async function startUpdater() { return updateExchangeRate(); } +function satoshiToLocalCurrency(satoshi) { + if (!lang[STRUCT.BTC_USD]) return satoshi; + + let b = new BigNumber(satoshi); + b = b + .div(100000000) + .mul(lang[STRUCT.BTC_USD]) + .toString(10); + b = parseFloat(b).toFixed(2); + + return '$' + b; +} + +function satoshiToBTC(satoshi) { + let b = new BigNumber(satoshi); + b = b.div(100000000); + return b.toString(10) + ' BTC'; +} + module.exports.updateExchangeRate = updateExchangeRate; module.exports.startUpdater = startUpdater; module.exports.STRUCT = STRUCT; +module.exports.satoshiToLocalCurrency = satoshiToLocalCurrency; +module.exports.satoshiToBTC = satoshiToBTC; diff --git a/package-lock.json b/package-lock.json index 88d21be5c..df6cee380 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10304,6 +10304,52 @@ "resolved": "https://registry.npmjs.org/react-native-maps/-/react-native-maps-0.17.1.tgz", "integrity": "sha1-qyI2NB/ZhNrIhkICrlUzG8Ji9gw=" }, + "react-native-material-buttons": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/react-native-material-buttons/-/react-native-material-buttons-0.5.0.tgz", + "integrity": "sha1-qys+P8P1AMpxP1Hp11l4r/YCFSo=", + "requires": { + "prop-types": "15.6.1", + "react-native-material-ripple": "0.7.5" + }, + "dependencies": { + "react-native-material-ripple": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/react-native-material-ripple/-/react-native-material-ripple-0.7.5.tgz", + "integrity": "sha1-4q9REGgFMvFK6jw6Q4JHvi/+9lk=", + "requires": { + "prop-types": "15.6.1" + } + } + } + }, + "react-native-material-dropdown": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/react-native-material-dropdown/-/react-native-material-dropdown-0.11.1.tgz", + "integrity": "sha1-wP5DSo5heUHvkQukTS8HyPN1hP4=", + "requires": { + "prop-types": "15.6.1", + "react-native-material-buttons": "0.5.0", + "react-native-material-ripple": "0.8.0", + "react-native-material-textfield": "0.12.0" + } + }, + "react-native-material-ripple": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/react-native-material-ripple/-/react-native-material-ripple-0.8.0.tgz", + "integrity": "sha1-uMJOb96iryoh6EaLH0CzVIMBni8=", + "requires": { + "prop-types": "15.6.1" + } + }, + "react-native-material-textfield": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/react-native-material-textfield/-/react-native-material-textfield-0.12.0.tgz", + "integrity": "sha1-P7oZ12q4n2cFLIHgghUvwkPYKj8=", + "requires": { + "prop-types": "15.6.1" + } + }, "react-native-qrcode": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/react-native-qrcode/-/react-native-qrcode-0.2.6.tgz", @@ -10437,11 +10483,6 @@ } } }, - "react-native-simple-radio-button": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/react-native-simple-radio-button/-/react-native-simple-radio-button-2.7.2.tgz", - "integrity": "sha512-BdlllHsC/gYJtxPJ2tshDWN8CzmlGg1G9uB+Lu4FRGvGkwhvMtJ/uNShMbvxu134xosH/feri6HQgLGlIT202Q==" - }, "react-native-snap-carousel": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/react-native-snap-carousel/-/react-native-snap-carousel-3.7.2.tgz", diff --git a/package.json b/package.json index 90f7e7329..b07b39a0d 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "react-native-elements": "^0.18.5", "react-native-flexi-radio-button": "^0.2.2", "react-native-level-fs": "^3.0.0", + "react-native-material-dropdown": "^0.11.1", "react-native-qrcode": "^0.2.6", "react-native-snap-carousel": "^3.7.2", "react-navigation": "^1.0.0-beta.23", diff --git a/screen/lnd/manageFunds.js b/screen/lnd/manageFunds.js new file mode 100644 index 000000000..d7f4cee50 --- /dev/null +++ b/screen/lnd/manageFunds.js @@ -0,0 +1,152 @@ +/* global alert */ +import React, { Component } from 'react'; +import { TouchableOpacity, View } from 'react-native'; +import { Dropdown } from 'react-native-material-dropdown'; +import { BlueSpacingVariable, BlueLoading, SafeBlueArea, BlueCard, BlueHeaderDefaultSub } from '../../BlueComponents'; +import { ListItem } from 'react-native-elements'; +import PropTypes from 'prop-types'; +import { LightningCustodianWallet } from '../../class/lightning-custodian-wallet'; +/** @type {AppStorage} */ +let BlueApp = require('../../BlueApp'); + +let data = []; + +export default class ManageFunds extends Component { + static navigationOptions = { + tabBarVisible: false, + }; + + constructor(props) { + super(props); + let fromSecret; + if (props.navigation.state.params.fromSecret) fromSecret = props.navigation.state.params.fromSecret; + let fromWallet = false; + + for (let w of BlueApp.getWallets()) { + if (w.getSecret() === fromSecret) { + fromWallet = w; + break; + } + } + + if (fromWallet) { + console.log(fromWallet.type); + } + + this.state = { + fromWallet, + fromSecret, + isLoading: true, + }; + } + + async componentDidMount() { + data = []; + for (let c = 0; c < BlueApp.getWallets().length; c++) { + let w = BlueApp.getWallets()[c]; + if (w.type !== new LightningCustodianWallet().type) { + data.push({ + value: c, + label: w.getLabel() + ' (' + w.getBalance() + ' BTC)', + }); + } + } + + this.setState({ + isLoading: false, + }); + } + + render() { + if (this.state.isLoading) { + return ; + } + + return ( + + + this.props.navigation.goBack()} /> + + + {(() => { + if (this.state.isRefill) { + return ( + + { + /** @type {LightningCustodianWallet} */ + let fromWallet = this.state.fromWallet; + let toAddress = false; + if (fromWallet.refill_addressess.length > 0) { + toAddress = fromWallet.refill_addressess[0]; + } else { + try { + await fromWallet.fetchBtcAddress(); + toAddress = fromWallet.refill_addressess[0]; + } catch (Err) { + return alert(Err.message); + } + } + + let wallet = BlueApp.getWallets()[value]; + if (wallet) { + console.log(wallet.getSecret()); + setTimeout(() => { + console.log({ toAddress }); + this.props.navigation.navigate('SendDetails', { + memo: 'Refill Lightning wallet balance', + fromSecret: wallet.getSecret(), + address: toAddress, + }); + }, 750); + } else { + return alert('Internal error'); + } + }} + /> + + ); + } else { + return ( + + { + this.setState({ isRefill: true }); + }} + title={'Refill'} + /> + { + alert('Coming soon'); + }} + title={'Withdraw'} + /> + + ); + } + })()} + + + + + ); + } +} + +ManageFunds.propTypes = { + navigation: PropTypes.shape({ + goBack: PropTypes.function, + navigate: PropTypes.function, + state: PropTypes.shape({ + params: PropTypes.shape({ + fromSecret: PropTypes.string, + }), + }), + }), +}; diff --git a/screen/lnd/scanLndInvoice.js b/screen/lnd/scanLndInvoice.js new file mode 100644 index 000000000..6a669d33d --- /dev/null +++ b/screen/lnd/scanLndInvoice.js @@ -0,0 +1,240 @@ +/* global alert */ +import React from 'react'; +import { Text, Dimensions, ActivityIndicator, Button, View, TouchableOpacity } from 'react-native'; +import { Camera, Permissions } from 'expo'; +import PropTypes from 'prop-types'; +import { + BlueSpacingVariable, + BlueFormInput, + BlueSpacing20, + BlueButton, + SafeBlueArea, + BlueCard, + BlueHeaderDefaultSub, +} from '../../BlueComponents'; +/** @type {AppStorage} */ +let BlueApp = require('../../BlueApp'); +let currency = require('../../currency'); +const { width } = Dimensions.get('window'); + +export default class ScanLndInvoice extends React.Component { + static navigationOptions = { + tabBarVisible: false, + }; + + state = { + isLoading: false, + hasCameraPermission: null, + type: Camera.Constants.Type.back, + }; + + constructor(props) { + super(props); + let fromSecret; + if (props.navigation.state.params.fromSecret) fromSecret = props.navigation.state.params.fromSecret; + let fromWallet = {}; + + for (let w of BlueApp.getWallets()) { + if (w.getSecret() === fromSecret) { + fromWallet = w; + break; + } + } + + this.state = { + fromWallet, + fromSecret, + }; + } + + async onBarCodeRead(ret) { + if (this.ignoreRead) return; + this.ignoreRead = true; + setTimeout(() => { + this.ignoreRead = false; + }, 6000); + + if (!this.state.fromWallet) { + alert('Error: cant find source wallet (this should never happen)'); + return this.props.navigation.goBack(); + } + + ret.data = ret.data.replace('LIGHTNING:', ''); + console.log(ret.data); + + /** + * @type {LightningCustodianWallet} + */ + let w = this.state.fromWallet; + let decoded = false; + try { + decoded = await w.decodeInvoice(ret.data); + + let expiresIn = (decoded.timestamp * 1 + decoded.expiry * 1) * 1000; // ms + if (+new Date() > expiresIn) { + expiresIn = 'expired'; + } else { + expiresIn = Math.round((expiresIn - +new Date()) / (60 * 1000)) + ' min'; + } + + this.setState({ + isPaying: true, + invoice: ret.data, + decoded, + expiresIn, + }); + } catch (Err) { + alert(Err.message); + } + } // end + + async componentWillMount() { + const { status } = await Permissions.askAsync(Permissions.CAMERA); + this.setState({ + hasCameraPermission: status === 'granted', + onCameraReady: function() { + alert('onCameraReady'); + }, + barCodeTypes: [Camera.Constants.BarCodeType.qr], + }); + } + + async pay() { + let decoded = this.state.decoded; + + /** @type {LightningCustodianWallet} */ + let fromWallet = this.state.fromWallet; + + let expiresIn = (decoded.timestamp * 1 + decoded.expiry * 1) * 1000; // ms + if (+new Date() > expiresIn) { + return alert('Invoice expired'); + } + + this.setState({ + isPayingInProgress: true, + }); + + let start = +new Date(); + let end; + try { + await fromWallet.payInvoice(this.state.invoice); + end = +new Date(); + } catch (Err) { + console.log(Err.message); + return alert('Error'); + } + + console.log('payInvoice took', (end - start) / 1000, 'sec'); + + alert('Success'); + this.props.navigation.goBack(); + } + + render() { + if (this.state.isLoading) { + return ( + + + + ); + } + + if (this.state.isPaying) { + return ( + + + this.props.navigation.goBack()} /> + + + + {currency.satoshiToLocalCurrency(this.state.decoded.num_satoshis)} + + + {currency.satoshiToBTC(this.state.decoded.num_satoshis)} + + + + + + + Expires in: {this.state.expiresIn} + + + + + {(() => { + if (this.state.isPayingInProgress) { + return ( + + + + ); + } else { + return ( + { + this.pay(); + }} + /> + ); + } + })()} + + ); + } + + const { hasCameraPermission } = this.state; + if (hasCameraPermission === null) { + return ; + } else if (hasCameraPermission === false) { + return No access to camera; + } else { + return ( + + this.onBarCodeRead(ret)}> + + { + this.setState({ + type: this.state.type === Camera.Constants.Type.back ? Camera.Constants.Type.front : Camera.Constants.Type.back, + }); + }} + > +