Igor Korsakov
6 years ago
committed by
GitHub
17 changed files with 1342 additions and 56 deletions
@ -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'); |
|||
} |
|||
}); |
|||
}); |
|||
|
@ -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 } ] |
|||
|
|||
*/ |
|||
|
@ -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, |
|||
}), |
|||
}), |
|||
}), |
|||
}; |
@ -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, |
|||
}), |
|||
}), |
|||
}), |
|||
}; |
Loading…
Reference in new issue