mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2025-03-26 08:55:56 +01:00
Lnd WIP (#32)
FIX: HD wallet send WIP: lnd wallets add/import/remove works
This commit is contained in:
parent
316667f9c3
commit
bd71479e75
17 changed files with 1343 additions and 57 deletions
|
@ -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 (
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 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.<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);
|
||||
}
|
||||
|
||||
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
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
generated
51
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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
Normal file
152
screen/lnd/manageFunds.js
Normal file
|
@ -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
Normal file
240
screen/lnd/scanLndInvoice.js
Normal file
|
@ -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,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
};
|
|
@ -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,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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…
Add table
Reference in a new issue