Browse Source

Lnd WIP (#32)

FIX: HD wallet send
WIP: lnd wallets add/import/remove works
localNotifications
Igor Korsakov 6 years ago
committed by GitHub
parent
commit
bd71479e75
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 95
      BlueComponents.js
  2. 8
      HDWallet.test.js
  3. 143
      LightningCustodianWallet.test.js
  4. 4
      class/app-storage.js
  5. 6
      class/hd-segwit-p2sh-wallet.js
  6. 9
      class/legacy-wallet.js
  7. 449
      class/lightning-custodian-wallet.js
  8. 22
      currency.js
  9. 51
      package-lock.json
  10. 1
      package.json
  11. 152
      screen/lnd/manageFunds.js
  12. 240
      screen/lnd/scanLndInvoice.js
  13. 6
      screen/send/details.js
  14. 12
      screen/wallets.js
  15. 25
      screen/wallets/add.js
  16. 21
      screen/wallets/import.js
  17. 154
      screen/wallets/list.js

95
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 <Text {...this.props} style={{ color: BlueApp.settings.foregroundColor }} />;
return (
<Text
{...this.props}
style={Object.assign(
{
color: BlueApp.settings.foregroundColor,
},
// eslint-disable-next-line
this.props.style,
)}
/>
);
}
}
export class BlueTextCentered extends Component {
@ -599,6 +609,27 @@ export class BlueTransactionPendingIcon extends Component {
}
}
export class BlueTransactionOnchainIcon extends Component {
render() {
return (
<View {...this.props} style={stylesBlueIcon.container}>
<View style={stylesBlueIcon.boxIncomming}>
<View style={stylesBlueIcon.ballIncomming}>
<Icon
{...this.props}
name="link"
size={16}
type="font-awesome"
color="#37c0a1"
iconStyle={{ left: 0, top: 7, transform: [{ rotate: '-45deg' }] }}
/>
</View>
</View>
</View>
);
}
}
export class BlueTransactionOutgoingIcon extends Component {
render() {
return (
@ -728,6 +759,66 @@ export class BlueSendButtonIcon extends Component {
}
}
export class ManageFundsBigButton extends Component {
render() {
return (
<TouchableOpacity
{...this.props}
style={{
flex: 1,
position: 'absolute',
bottom: 30,
left: (width - 190) / 2,
}}
>
<View>
<View
style={{
flex: 1,
flexDirection: 'row',
width: 190,
height: 40,
position: 'relative',
backgroundColor: '#ccddf9',
borderBottomRightRadius: 15,
borderBottomLeftRadius: 15,
borderTopRightRadius: 15,
borderTopLeftRadius: 15,
}}
>
<View
style={{
width: 30,
height: 30,
left: 20,
top: 5,
borderBottomLeftRadius: 15,
backgroundColor: 'transparent',
transform: [{ rotate: '90deg' }],
}}
>
<Icon {...this.props} name="link" size={16} type="font-awesome" color="#2f5fb3" iconStyle={{ left: 0, top: 0 }} />
</View>
<Text
style={{
color: '#2f5fb3',
fontSize: (isIpad && 10) || 16,
fontWeight: '500',
left: 25,
top: 12,
backgroundColor: 'transparent',
position: 'relative',
}}
>
manage funds
</Text>
</View>
</View>
</TouchableOpacity>
);
}
}
export class BluePlusIconDimmed extends Component {
render() {
return (

8
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);

143
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&currency=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');
}
});
});

4
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);

6
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;

9
class/legacy-wallet.js

@ -397,10 +397,15 @@ export class LegacyWallet extends AbstractWallet {
}
getLatestTransactionTime() {
if (this.getTransactions().length === 0) {
return 0;
}
let max = 0;
for (let tx of this.getTransactions()) {
return tx.received;
max = Math.max(new Date(tx.received) * 1, max);
}
return 0;
return new Date(max).toString();
}
getRandomBlockcypherToken() {

449
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.<void>}
*/
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 fetchTransactions() {
return [];
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 getTransaction() {}
async fetchTransactions() {
// TODO: iterate over all available pages
const limit = 10;
let queryRes = '';
let offset = 0;
queryRes += '?limit=' + limit;
queryRes += '&offset=' + offset;
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 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.<Object>}
*/
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);
}
async getInfo() {}
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 } ]
*/

22
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;

51
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",

1
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",

152
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 <BlueLoading />;
}
return (
<SafeBlueArea forceInset={{ horizontal: 'always' }} style={{ flex: 1 }}>
<BlueSpacingVariable />
<BlueHeaderDefaultSub leftText={'manage funds'} onClose={() => this.props.navigation.goBack()} />
<BlueCard>
{(() => {
if (this.state.isRefill) {
return (
<View>
<Dropdown
label="Choose a source wallet"
data={data}
onChangeText={async value => {
/** @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');
}
}}
/>
</View>
);
} else {
return (
<View>
<ListItem
titleStyle={{ color: BlueApp.settings.foregroundColor }}
component={TouchableOpacity}
onPress={a => {
this.setState({ isRefill: true });
}}
title={'Refill'}
/>
<ListItem
titleStyle={{ color: BlueApp.settings.foregroundColor }}
component={TouchableOpacity}
onPress={a => {
alert('Coming soon');
}}
title={'Withdraw'}
/>
</View>
);
}
})()}
<View />
</BlueCard>
</SafeBlueArea>
);
}
}
ManageFunds.propTypes = {
navigation: PropTypes.shape({
goBack: PropTypes.function,
navigate: PropTypes.function,
state: PropTypes.shape({
params: PropTypes.shape({
fromSecret: PropTypes.string,
}),
}),
}),
};

240
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 (
<View style={{ flex: 1, paddingTop: 20 }}>
<ActivityIndicator />
</View>
);
}
if (this.state.isPaying) {
return (
<SafeBlueArea forceInset={{ horizontal: 'always' }} style={{ flex: 1 }}>
<BlueSpacingVariable />
<BlueHeaderDefaultSub leftText={'Pay invoice'} onClose={() => this.props.navigation.goBack()} />
<BlueSpacing20 />
<Text style={{ textAlign: 'center', fontSize: 50, fontWeight: '700', color: '#2f5fb3' }}>
{currency.satoshiToLocalCurrency(this.state.decoded.num_satoshis)}
</Text>
<Text style={{ textAlign: 'center', fontSize: 25, fontWeight: '600', color: '#d4d4d4' }}>
{currency.satoshiToBTC(this.state.decoded.num_satoshis)}
</Text>
<BlueSpacing20 />
<BlueCard>
<BlueFormInput value={this.state.decoded.destination} />
<BlueFormInput value={this.state.decoded.description} />
<Text style={{ color: '#81868e', fontSize: 12, left: 20, top: 10 }}>Expires in: {this.state.expiresIn}</Text>
</BlueCard>
<BlueSpacing20 />
{(() => {
if (this.state.isPayingInProgress) {
return (
<View>
<ActivityIndicator />
</View>
);
} else {
return (
<BlueButton
icon={{
name: 'bolt',
type: 'font-awesome',
color: BlueApp.settings.buttonTextColor,
}}
title={'Pay'}
buttonStyle={{ width: 150, left: (width - 150) / 2 - 20 }}
onPress={() => {
this.pay();
}}
/>
);
}
})()}
</SafeBlueArea>
);
}
const { hasCameraPermission } = this.state;
if (hasCameraPermission === null) {
return <View />;
} else if (hasCameraPermission === false) {
return <Text>No access to camera</Text>;
} else {
return (
<View style={{ flex: 1 }}>
<Camera style={{ flex: 1 }} type={this.state.type} onBarCodeRead={ret => this.onBarCodeRead(ret)}>
<View
style={{
flex: 1,
backgroundColor: 'transparent',
flexDirection: 'row',
}}
>
<TouchableOpacity
style={{
flex: 0.2,
alignSelf: 'flex-end',
alignItems: 'center',
}}
onPress={() => {
this.setState({
type: this.state.type === Camera.Constants.Type.back ? Camera.Constants.Type.front : Camera.Constants.Type.back,
});
}}
>
<Button style={{ fontSize: 18, marginBottom: 10 }} title="Go back" onPress={() => this.props.navigation.goBack()} />
</TouchableOpacity>
</View>
</Camera>
</View>
);
}
}
}
ScanLndInvoice.propTypes = {
navigation: PropTypes.shape({
goBack: PropTypes.function,
state: PropTypes.shape({
params: PropTypes.shape({
fromSecret: PropTypes.string,
}),
}),
}),
};

6
screen/send/details.js

@ -29,9 +29,12 @@ export default class SendDetails extends Component {
constructor(props) {
super(props);
console.log('props.navigation.state.params=', props.navigation.state.params);
let startTime = Date.now();
let address;
if (props.navigation.state.params) address = props.navigation.state.params.address;
let memo = false;
if (props.navigation.state.params) memo = props.navigation.state.params.memo;
let fromAddress;
if (props.navigation.state.params) fromAddress = props.navigation.state.params.fromAddress;
let fromSecret;
@ -52,6 +55,7 @@ export default class SendDetails extends Component {
let endTime2 = Date.now();
console.log('getAddress() took', (endTime2 - startTime2) / 1000, 'sec');
console.log({ memo });
this.state = {
errorMessage: false,
@ -61,6 +65,7 @@ export default class SendDetails extends Component {
isLoading: true,
address: address,
amount: '',
memo,
fee: '',
};
@ -252,6 +257,7 @@ SendDetails.propTypes = {
address: PropTypes.string,
fromAddress: PropTypes.string,
fromSecret: PropTypes.string,
memo: PropTypes.string,
}),
}),
}),

12
screen/wallets.js

@ -17,6 +17,9 @@ import sendDetails from './send/details';
import sendScanQrAddress from './send/scanQrAddress';
import sendCreate from './send/create';
import ManageFunds from './lnd/manageFunds';
import ScanLndInvoice from './lnd/scanLndInvoice';
const WalletsNavigator = StackNavigator(
{
WalletsList: {
@ -67,6 +70,15 @@ const WalletsNavigator = StackNavigator(
CreateTransaction: {
screen: sendCreate,
},
// LND:
ManageFunds: {
screen: ManageFunds,
},
ScanLndInvoice: {
screen: ScanLndInvoice,
},
},
{
headerMode: 'none',

25
screen/wallets/add.js

@ -20,6 +20,7 @@ import { RadioGroup, RadioButton } from 'react-native-flexi-radio-button';
import PropTypes from 'prop-types';
import { HDSegwitP2SHWallet } from '../../class/hd-segwit-p2sh-wallet';
import { LightningCustodianWallet } from '../../class/lightning-custodian-wallet';
let EV = require('../../events');
let A = require('../../analytics');
/** @type {AppStorage} */
@ -160,14 +161,28 @@ export default class WalletsAdd extends Component {
width: width / 1.5,
}}
onPress={() => {
if (this.state.activeLightning) {
return alert(loc.wallets.add.coming_soon);
}
this.props.navigation.goBack();
setTimeout(async () => {
let w;
if (this.state.selectedIndex === 1) {
if (this.state.activeLightning) {
// lightning was selected
return alert('Coming soon');
// eslint-disable-next-line
for (let t of BlueApp.getWallets()) {
if (t.type === new LightningCustodianWallet().type) {
// already exist
return alert('Only 1 Ligthning wallet allowed for now');
}
}
w = new LightningCustodianWallet();
w.setLabel(this.state.label || w.getTypeReadable());
await w.createAccount();
await w.authorize();
} else if (this.state.selectedIndex === 1) {
// btc was selected
// index 1 radio - segwit single address
w = new SegwitP2SHWallet();
w.setLabel(this.state.label || loc.wallets.add.label_new_segwit);

21
screen/wallets/import.js

@ -20,6 +20,7 @@ import {
BlueHeaderDefaultSub,
} from '../../BlueComponents';
import PropTypes from 'prop-types';
import { LightningCustodianWallet } from '../../class/lightning-custodian-wallet';
let EV = require('../../events');
let A = require('../../analytics');
/** @type {AppStorage} */
@ -60,6 +61,26 @@ export default class WalletsImport extends Component {
async importMnemonic(text) {
try {
// is it lightning custodian?
if (text.indexOf('blitzhub://') !== -1) {
// yep its lnd
for (let t of BlueApp.getWallets()) {
if (t.type === new LightningCustodianWallet().type) {
// already exist
return alert('Only 1 Ligthning wallet allowed for now');
}
}
let lnd = new LightningCustodianWallet();
lnd.setSecret(text);
await lnd.authorize();
await lnd.fetchTransactions();
await lnd.fetchBalance();
return this._saveWallet(lnd);
}
// trying other wallet types
let segwitWallet = new SegwitP2SHWallet();
segwitWallet.setSecret(text);
if (segwitWallet.getAddress()) {

154
screen/wallets/list.js

@ -1,6 +1,9 @@
import React, { Component } from 'react';
import { View, Dimensions, Text, ListView } from 'react-native';
import { View, TouchableOpacity, Dimensions, Text, ListView } from 'react-native';
import {
BlueText,
BlueTransactionOnchainIcon,
ManageFundsBigButton,
BlueLoading,
SafeBlueArea,
WalletsCarousel,
@ -15,7 +18,9 @@ import {
BlueHeaderDefaultMain,
is,
} from '../../BlueComponents';
import { Icon } from 'react-native-elements';
import PropTypes from 'prop-types';
import { LightningCustodianWallet } from '../../class/lightning-custodian-wallet';
const BigNumber = require('bignumber.js');
let EV = require('../../events');
let A = require('../../analytics');
@ -83,21 +88,38 @@ export default class WalletsList extends Component {
}
setTimeout(() => {
console.log('refreshFunction()');
let showSend = false;
let showReceive = false;
let showManageFundsBig = false;
let showManageFundsSmallButton = false;
let wallets = BlueApp.getWallets();
let wallet = wallets[this.lastSnappedTo || 0];
if (wallet) {
showSend = wallet.allowSend();
showReceive = wallet.allowReceive();
}
let showRereshButton = (BlueApp.getWallets().length > 0 && true) || false;
if (wallet && wallet.type === new LightningCustodianWallet().type && !showSend) {
showManageFundsBig = true;
showManageFundsSmallButton = false;
showRereshButton = false;
}
if (wallet && wallet.type === new LightningCustodianWallet().type && wallet.getBalance() > 0) {
showRereshButton = false;
showManageFundsSmallButton = true;
}
this.setState({
isLoading: false,
isTransactionsLoading: false,
showReceiveButton: showReceive,
showSendButton: showSend,
showRereshButton: (BlueApp.getWallets().length > 0 && true) || false,
showManageFundsBigButton: showManageFundsBig,
showManageFundsSmallButton,
showRereshButton,
dataSource: ds.cloneWithRows(BlueApp.getTransactions(this.lastSnappedTo || 0)),
});
}, 1);
@ -130,6 +152,8 @@ export default class WalletsList extends Component {
this.setState({
isLoading: false,
showReceiveButton: false,
showManageFundsBigButton: false,
showManageFundsSmallButton: false,
showSendButton: false,
showRereshButton: false,
// TODO: погуглить че это за ебала ds.cloneWithRows, можно ли быстрее сделать прогрузку транзакций на экран
@ -141,19 +165,38 @@ export default class WalletsList extends Component {
let showSend = false;
let showReceive = false;
let showManageFundsBig = false;
let wallets = BlueApp.getWallets();
let wallet = wallets[this.lastSnappedTo || 0];
if (wallet) {
showSend = wallet.allowSend();
showReceive = wallet.allowReceive();
}
console.log({ showSend });
let showRereshButton = true;
let showManageFundsSmallButton = true;
if (wallet && wallet.type === new LightningCustodianWallet().type && !showSend) {
showManageFundsBig = true;
showRereshButton = false;
showManageFundsSmallButton = false;
}
if (wallet && wallet.type === new LightningCustodianWallet().type) {
showRereshButton = false;
} else {
showManageFundsSmallButton = false;
}
console.log({ showManageFundsBig });
setTimeout(
() =>
this.setState({
showReceiveButton: showReceive,
showManageFundsBigButton: showManageFundsBig,
showManageFundsSmallButton,
showSendButton: showSend,
showRereshButton: true,
showRereshButton,
}),
50,
); // just to animate it, no real function
@ -163,6 +206,15 @@ export default class WalletsList extends Component {
this.lazyRefreshWallet(index);
}
isLightning() {
let w = BlueApp.getWallets()[this.lastSnappedTo || 0];
if (w && w.type === new LightningCustodianWallet().type) {
return true;
}
return false;
}
/**
* Decides whether wallet with such index shoud be refreshed,
* refreshes if yes and redraws the screen
@ -190,8 +242,11 @@ export default class WalletsList extends Component {
this.refreshFunction();
didRefresh = true;
} else if (wallets[index].timeToRefreshTransaction()) {
console.log('got TXs with low confirmations, refreshing');
console.log(wallets[index].getLabel(), 'thinks its time to refresh TXs');
await wallets[index].fetchTransactions();
if (wallets[index].fetchPendingTransactions) {
await wallets[index].fetchPendingTransactions();
}
this.refreshFunction();
didRefresh = true;
} else {
@ -229,6 +284,37 @@ export default class WalletsList extends Component {
}}
/>
{(() => {
if (this.state.showManageFundsSmallButton) {
return (
<TouchableOpacity
style={{ alignSelf: 'flex-end', right: 10, flexDirection: 'row' }}
onPress={() => {
let walletIndex = this.lastSnappedTo || 0;
let c = 0;
for (let w of BlueApp.getWallets()) {
if (c++ === walletIndex) {
console.log('navigating to secret ', w.getSecret());
navigate('ManageFunds', { fromSecret: w.getSecret() });
}
}
}}
>
<BlueText style={{ fontWeight: '600', fontSize: 16 }}>Manage funds</BlueText>
<Icon
style={{ position: 'relative' }}
name="link"
type="font-awesome"
size={14}
color={BlueApp.settings.foregroundColor}
iconStyle={{ left: 5, transform: [{ rotate: '90deg' }] }}
/>
</TouchableOpacity>
);
}
})()}
{(() => {
if (this.state.isTransactionsLoading) {
return <BlueLoading />;
@ -272,7 +358,9 @@ export default class WalletsList extends Component {
textAlign: 'center',
}}
>
{loc.wallets.list.empty_txs1}
{(this.isLightning() &&
'Lightning wallet should be used for your daily\ntransactions. Fees are unfairly cheap and\nspeed is blazing fast.') ||
loc.wallets.list.empty_txs1}
</Text>
<Text
style={{
@ -281,7 +369,8 @@ export default class WalletsList extends Component {
textAlign: 'center',
}}
>
{loc.wallets.list.empty_txs2}
{(this.isLightning() && '\nTo start using it tap on "manage funds"\nand topup your balance') ||
loc.wallets.list.empty_txs2}
</Text>
</View>
);
@ -301,6 +390,22 @@ export default class WalletsList extends Component {
return (
<BlueListItem
avatar={(() => {
if (rowData.category && rowData.category === 'receive') {
// is it lightning onchain tx?
if (rowData.confirmations < 3) {
return (
<View style={{ width: 25 }}>
<BlueTransactionPendingIcon />
</View>
);
} else {
return (
<View style={{ width: 25 }}>
<BlueTransactionOnchainIcon />
</View>
);
}
}
if (!rowData.confirmations) {
return (
<View style={{ width: 25 }}>
@ -324,12 +429,15 @@ export default class WalletsList extends Component {
title={loc.transactionTimeToReadable(rowData.received)}
subtitle={
(rowData.confirmations < 7 ? loc.transactions.list.conf + ': ' + rowData.confirmations + ' ' : '') +
this.txMemo(rowData.hash)
this.txMemo(rowData.hash) +
(rowData.memo || '')
}
onPress={() => {
navigate('TransactionDetails', {
hash: rowData.hash,
});
if (rowData.hash) {
navigate('TransactionDetails', {
hash: rowData.hash,
});
}
}}
badge={{
value: 3,
@ -395,7 +503,31 @@ export default class WalletsList extends Component {
let c = 0;
for (let w of BlueApp.getWallets()) {
if (c++ === walletIndex) {
navigate('SendDetails', { fromAddress: w.getAddress(), fromSecret: w.getSecret() });
if (w.type === new LightningCustodianWallet().type) {
navigate('ScanLndInvoice', { fromSecret: w.getSecret() });
} else {
navigate('SendDetails', { fromAddress: w.getAddress(), fromSecret: w.getSecret() });
}
}
}
}}
/>
);
}
})()}
{(() => {
if (this.state.showManageFundsBigButton) {
return (
<ManageFundsBigButton
onPress={() => {
let walletIndex = this.lastSnappedTo || 0;
let c = 0;
for (let w of BlueApp.getWallets()) {
if (c++ === walletIndex) {
console.log('navigating to secret ', w.getSecret());
navigate('ManageFunds', { fromSecret: w.getSecret() });
}
}
}}

Loading…
Cancel
Save